Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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#'
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Loading