Skip to content

Commit

Permalink
Merge pull request #1487 from soyuka/merge-2.1
Browse files Browse the repository at this point in the history
Merge 2.1
  • Loading branch information
soyuka committed Nov 10, 2017
2 parents 0febda2 + 2f113f3 commit d683d00
Show file tree
Hide file tree
Showing 15 changed files with 308 additions and 96 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -44,7 +44,7 @@ script:
- if [[ $coverage = 1 ]]; then phpdbg -qrr vendor/bin/phpunit --coverage-php build/cov/coverage-phpunit.cov; else vendor/bin/phpunit; fi
- if [[ $coverage = 1 ]]; then for f in $(find features -name '*.feature'); do FEATURE=${f//\//_} phpdbg -qrr vendor/bin/behat --format=progress --profile coverage $f || exit $?; done; else vendor/bin/behat --format=progress; fi
- if [[ $coverage = 1 ]]; then phpdbg -qrr phpcov.phar merge --clover build/logs/clover.xml build/cov; fi
- tests/Fixtures/app/console api:swagger:export > swagger.json && swagger validate swagger.json && rm swagger.json
- tests/Fixtures/app/console api:swagger:export > swagger.json && swagger-cli validate swagger.json && rm swagger.json
- if [[ $lint = 1 ]]; then php php-cs-fixer.phar fix --dry-run --diff --no-ansi; fi
- if [[ $lint = 1 ]]; then phpstan analyse -c phpstan.neon -l5 --ansi src tests; fi

Expand Down
23 changes: 23 additions & 0 deletions features/bootstrap/FeatureContext.php
Expand Up @@ -30,6 +30,7 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\EmbeddedDummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FooDummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Node;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Person;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\PersonToPet;
Expand Down Expand Up @@ -144,6 +145,28 @@ public function thereAreFooObjectsWithFakeNames(int $nb)
$this->manager->flush();
}

/**
* @Given there are :nb fooDummy objects with fake names
*/
public function thereAreFooDummyObjectsWithFakeNames($nb)
{
$names = ['Hawsepipe', 'Ephesian', 'Sthenelus', 'Separativeness', 'Balbo'];
$dummies = ['Lorem', 'Dolor', 'Dolor', 'Sit', 'Amet'];

for ($i = 0; $i < $nb; ++$i) {
$dummy = new Dummy();
$dummy->setName($dummies[$i]);

$foo = new FooDummy();
$foo->setName($names[$i]);
$foo->setDummy($dummy);

$this->manager->persist($foo);
}

$this->manager->flush();
}

/**
* @Given there is :nb dummy group objects
*/
Expand Down
60 changes: 59 additions & 1 deletion features/main/default_order.feature
Expand Up @@ -3,7 +3,7 @@ Feature: Default order
As a client software developer,
I need to be able to specify default order.

@createSchema @dropSchema
@createSchema
Scenario: Override custom order
Given there are 5 foo objects with fake names
When I send a "GET" request to "/foos?itemsPerPage=10"
Expand Down Expand Up @@ -60,3 +60,61 @@ Feature: Default order
}
}
"""

@dropSchema
Scenario: Override custom order by association
Given there are 5 fooDummy objects with fake names
When I send a "GET" request to "/foo_dummies?itemsPerPage=10"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/FooDummy",
"@id": "/foo_dummies",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/foo_dummies/5",
"@type": "FooDummy",
"id": 5,
"name": "Balbo",
"dummy": "/dummies/5"
},
{
"@id": "/foo_dummies/2",
"@type": "FooDummy",
"id": 2,
"name": "Ephesian",
"dummy": "/dummies/2"
},
{
"@id": "/foo_dummies/3",
"@type": "FooDummy",
"id": 3,
"name": "Sthenelus",
"dummy": "/dummies/3"
},
{
"@id": "/foo_dummies/1",
"@type": "FooDummy",
"id": 1,
"name": "Hawsepipe",
"dummy": "/dummies/1"
},
{
"@id": "/foo_dummies/4",
"@type": "FooDummy",
"id": 4,
"name": "Separativeness",
"dummy": "/dummies/4"
}
],
"hydra:totalItems": 5,
"hydra:view": {
"@id": "/foo_dummies?itemsPerPage=10",
"@type": "hydra:PartialCollectionView"
}
}
"""
1 change: 0 additions & 1 deletion phpstan.neon
Expand Up @@ -11,6 +11,5 @@ parameters:
- '#Call to an undefined method Doctrine\\Common\\Persistence\\Mapping\\ClassMetadata::getAssociationMappings\(\)#'

# False positives
- '#Parameter \#2 \$dqlPart of method Doctrine\\ORM\\QueryBuilder::add\(\) expects Doctrine\\ORM\\Query\\Expr\\Base, Doctrine\\ORM\\Query\\Expr\\Join\[\] given#' # Fixed in Doctrine's master
- '#Call to an undefined method Doctrine\\Common\\Persistence\\ObjectManager::getConnection\(\)#'
- '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\QueryResult(Item|Collection)ExtensionInterface::getResult\(\) invoked with 3 parameters, 1 required#'
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\EagerLoadingTrait;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use Doctrine\ORM\Query\Expr\Join;
Expand Down Expand Up @@ -119,12 +120,15 @@ private function getQueryBuilderWithNewAliases(QueryBuilder $queryBuilder, Query

//Change join aliases
foreach ($joinParts[$originAlias] as $joinPart) {
/** @var Join $joinPart */
$joinString = str_replace($aliases, $replacements, $joinPart->getJoin());
$pos = strpos($joinString, '.');
$alias = substr($joinString, 0, $pos);
$association = substr($joinString, $pos + 1);
$condition = str_replace($aliases, $replacements, $joinPart->getCondition());
$newAlias = QueryBuilderHelper::addJoinOnce($queryBuilderClone, $queryNameGenerator, $alias, $association, $joinPart->getJoinType(), $joinPart->getConditionType(), $condition);
$aliases[] = "{$joinPart->getAlias()}.";
$alias = $queryNameGenerator->generateJoinAlias($joinPart->getAlias());
$replacements[] = "$alias.";
$join = new Join($joinPart->getJoinType(), str_replace($aliases, $replacements, $joinPart->getJoin()), $alias, $joinPart->getConditionType(), str_replace($aliases, $replacements, $joinPart->getCondition()), $joinPart->getIndexBy());

$queryBuilderClone->add('join', [$join], true);
$replacements[] = "$newAlias.";
}

$queryBuilderClone->add('where', str_replace($aliases, $replacements, (string) $wherePart));
Expand Down
11 changes: 10 additions & 1 deletion src/Bridge/Doctrine/Orm/Extension/OrderExtension.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use Doctrine\ORM\QueryBuilder;
Expand Down Expand Up @@ -47,10 +48,18 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
if (null !== $defaultOrder) {
foreach ($defaultOrder as $field => $order) {
if (is_int($field)) {
// Default direction
$field = $order;
$order = 'ASC';
}
$queryBuilder->addOrderBy('o.'.$field, $order);
if (false === ($pos = strpos($field, '.'))) {
// Configure default filter with property
$field = 'o.'.$field;
} else {
$alias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, 'o', substr($field, 0, $pos));
$field = sprintf('%s.%s', $alias, substr($field, $pos + 1));
}
$queryBuilder->addOrderBy($field, $order);
}

return;
Expand Down
62 changes: 2 additions & 60 deletions src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php
Expand Up @@ -13,13 +13,12 @@

namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Filter;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Util\RequestParser;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
Expand Down Expand Up @@ -336,7 +335,7 @@ protected function addJoinsForNestedProperty(string $property, string $rootAlias
$parentAlias = $rootAlias;

foreach ($propertyParts['associations'] as $association) {
$alias = $this->addJoinOnce($queryBuilder, $queryNameGenerator, $parentAlias, $association);
$alias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $parentAlias, $association);
$parentAlias = $alias;
}

Expand All @@ -346,61 +345,4 @@ protected function addJoinsForNestedProperty(string $property, string $rootAlias

return [$alias, $propertyParts['field'], $propertyParts['associations']];
}

/**
* Get the existing join from queryBuilder DQL parts.
*
* @param QueryBuilder $queryBuilder
* @param string $alias
* @param string $association the association field
*
* @return Join|null
*/
private function getExistingJoin(QueryBuilder $queryBuilder, string $alias, string $association)
{
$parts = $queryBuilder->getDQLPart('join');

if (!isset($parts['o'])) {
return null;
}

foreach ($parts['o'] as $join) {
if (sprintf('%s.%s', $alias, $association) === $join->getJoin()) {
return $join;
}
}

return null;
}

/**
* Adds a join to the queryBuilder if none exists.
*
* @param QueryBuilder $queryBuilder
* @param QueryNameGeneratorInterface $queryNameGenerator
* @param string $alias
* @param string $association the association field
*
* @return string the new association alias
*/
protected function addJoinOnce(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $association): string
{
$join = $this->getExistingJoin($queryBuilder, $alias, $association);

if (null === $join) {
$associationAlias = $queryNameGenerator->generateJoinAlias($association);

if (true === QueryChecker::hasLeftJoin($queryBuilder)) {
$queryBuilder
->leftJoin(sprintf('%s.%s', $alias, $association), $associationAlias);
} else {
$queryBuilder
->innerJoin(sprintf('%s.%s', $alias, $association), $associationAlias);
}
} else {
$associationAlias = $join->getAlias();
}

return $associationAlias;
}
}
3 changes: 2 additions & 1 deletion src/Bridge/Doctrine/Orm/Filter/SearchFilter.php
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Filter;

use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use Doctrine\Common\Persistence\ManagerRegistry;
Expand Down Expand Up @@ -251,7 +252,7 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
$valueParameter = $queryNameGenerator->generateParameterName($association);

if ($metadata->isCollectionValuedAssociation($association)) {
$associationAlias = $this->addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $association);
$associationAlias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $association);
$associationField = 'id';
} else {
$associationAlias = $alias;
Expand Down
75 changes: 75 additions & 0 deletions src/Bridge/Doctrine/Orm/Util/QueryBuilderHelper.php
@@ -0,0 +1,75 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Util;

use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;

/**
* @author Vincent Chalamon <vincent@les-tilleuls.coop>
*
* @internal
*/
final class QueryBuilderHelper
{
private function __construct()
{
}

/**
* Adds a join to the queryBuilder if none exists.
*/
public static function addJoinOnce(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $association, string $joinType = null, string $conditionType = null, string $condition = null): string
{
$join = self::getExistingJoin($queryBuilder, $alias, $association);

if (null !== $join) {
return $join->getAlias();
}

$associationAlias = $queryNameGenerator->generateJoinAlias($association);
$query = "$alias.$association";

if (Join::LEFT_JOIN === $joinType || QueryChecker::hasLeftJoin($queryBuilder)) {
$queryBuilder->leftJoin($query, $associationAlias, $conditionType, $condition);
} else {
$queryBuilder->innerJoin($query, $associationAlias, $conditionType, $condition);
}

return $associationAlias;
}

/**
* Get the existing join from queryBuilder DQL parts.
*
* @return Join|null
*/
private static function getExistingJoin(QueryBuilder $queryBuilder, string $alias, string $association)
{
$parts = $queryBuilder->getDQLPart('join');

if (!isset($parts['o'])) {
return null;
}

foreach ($parts['o'] as $join) {
/** @var Join $join */
if (sprintf('%s.%s', $alias, $association) === $join->getJoin()) {
return $join;
}
}

return null;
}
}
11 changes: 6 additions & 5 deletions src/Bridge/Symfony/Routing/ApiLoader.php
Expand Up @@ -193,12 +193,13 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
'_format' => null,
'_api_resource_class' => $resourceClass,
sprintf('_api_%s_operation_name', $operationType) => $operationName,
],
] + ($operation['defaults'] ?? []),
$operation['requirements'] ?? [],
[],
'',
[],
[$operation['method']]
$operation['options'] ?? [],
$operation['host'] ?? '',
$operation['schemes'] ?? [],
[$operation['method']],
$operation['condition'] ?? ''
);

$routeCollection->add(RouteNameGenerator::generate($operationName, $resourceShortName, $operationType), $route);
Expand Down

0 comments on commit d683d00

Please sign in to comment.