Skip to content

Commit

Permalink
Merge 818185a into 06d8433
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Sep 1, 2016
2 parents 06d8433 + 818185a commit 226c840
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 50 deletions.
7 changes: 6 additions & 1 deletion src/Bridge/Doctrine/Orm/CollectionDataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
use ApiPlatform\Core\Exception\RuntimeException;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\ORM\Query;

/**
* Collection data provider for the Doctrine ORM.
Expand Down Expand Up @@ -67,6 +68,10 @@ public function getCollection(string $resourceClass, string $operationName = nul
}
}

return $queryBuilder->getQuery()->getResult();
$query = $queryBuilder->getQuery();
//forces doctrine to not lazy load relations, see http://stackoverflow.com/questions/9848747/primary-key-of-owning-side-as-a-join-column
$query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true);

return $query->getResult();
}
}
61 changes: 54 additions & 7 deletions src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\QueryBuilder;

Expand All @@ -23,6 +25,15 @@
*/
final class EagerLoadingExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
private $propertyNameCollectionFactory;
private $propertyMetadataFactory;

public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory)
{
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
}

/**
* {@inheritdoc}
*/
Expand All @@ -39,23 +50,59 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
$this->joinRelations($queryBuilder, $resourceClass);
}

public function getMetadataProperties(string $resourceClass): array
{
$properties = [];

foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
$properties[$property] = $this->propertyMetadataFactory->create($resourceClass, $property);
}

return $properties;
}

/**
* Left joins relations to eager load.
*
* @param QueryBuilder $queryBuilder
* @param string $resourceClass
*/
private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass)
private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, string $originAlias = 'o', string &$relationAlias = 'a')
{
$classMetaData = $queryBuilder->getEntityManager()->getClassMetadata($resourceClass);
$classMetadata = $queryBuilder->getEntityManager()->getClassMetadata($resourceClass);
$j = 0;

foreach ($classMetadata->getAssociationNames() as $i => $association) {
$mapping = $classMetadata->associationMappings[$association];
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association);

if (ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch'] || false === $propertyMetadata->isReadableLink()) {
continue;
}

foreach ($classMetaData->getAssociationNames() as $i => $association) {
$mapping = $classMetaData->associationMappings[$association];
$method = false === $mapping['joinColumns'][0]['nullable'] ? 'innerJoin' : 'leftJoin';

if (ClassMetadataInfo::FETCH_EAGER === $mapping['fetch']) {
$queryBuilder->leftJoin('o.'.$association, 'a'.$i);
$queryBuilder->addSelect('a'.$i);
$associationAlias = $relationAlias.$i;
$queryBuilder->{$method}($originAlias.'.'.$association, $associationAlias);
$select = [];
$targetClassMetadata = $queryBuilder->getEntityManager()->getClassMetadata($mapping['targetEntity']);

foreach ($this->getMetadataProperties($mapping['targetEntity']) as $property => $propertyMetadata) {
if (true === $propertyMetadata->isIdentifier()) {
$select[] = $property;
continue;
}

//the field test allows to add methods to a Resource which do not reflect real database fields
if (true === $targetClassMetadata->hasField($property) && true === $propertyMetadata->isReadable()) {
$select[] = $property;
}
}

$queryBuilder->addSelect(sprintf('partial %s.{%s}', $associationAlias, implode(',', $select)));

$relationAlias = $relationAlias.++$j;
$this->joinRelations($queryBuilder, $mapping['targetEntity'], $associationAlias, $relationAlias);
}
}
}
52 changes: 50 additions & 2 deletions src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,7 @@ protected function addJoinsForNestedProperty(string $property, string $rootAlias
$parentAlias = $rootAlias;

foreach ($propertyParts['associations'] as $association) {
$alias = $queryNameGenerator->generateJoinAlias($association);
$queryBuilder->leftJoin(sprintf('%s.%s', $parentAlias, $association), $alias);
$alias = $this->addJoinOnce($queryBuilder, $queryNameGenerator, $parentAlias, $association);
$parentAlias = $alias;
}

Expand All @@ -207,4 +206,53 @@ protected function addJoinsForNestedProperty(string $property, string $rootAlias

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

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

if (!isset($parts[$alias])) {
return;
}

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

/**
* Adds a join to the queryBuilder if none exists.
*
* @param QueryBuilder $queryBuilder
* @param QueryNameGenerator $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);
$queryBuilder
->join(sprintf('%s.%s', $alias, $association), $associationAlias);
} else {
$associationAlias = $join->getAlias();
}

return $associationAlias;
}
}
44 changes: 19 additions & 25 deletions src/Bridge/Doctrine/Orm/Filter/SearchFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,8 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
if ($this->isPropertyNested($property)) {
$propertyParts = $this->splitPropertyParts($property);

$parentAlias = $alias;

foreach ($propertyParts['associations'] as $association) {
$alias = $queryNameGenerator->generateJoinAlias($association);
$queryBuilder->join(sprintf('%s.%s', $parentAlias, $association), $alias);
$parentAlias = $alias;
$alias = $this->addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $association);
}

$field = $propertyParts['field'];
Expand Down Expand Up @@ -159,11 +155,9 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
$values = array_map([$this, 'getIdFromValue'], $values);

$association = $field;
$associationAlias = $queryNameGenerator->generateJoinAlias($association);
$valueParameter = $queryNameGenerator->generateParameterName($association);

$queryBuilder
->join(sprintf('%s.%s', $alias, $association), $associationAlias);
$associationAlias = $this->addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $association);

if (1 === count($values)) {
$queryBuilder
Expand Down Expand Up @@ -337,21 +331,21 @@ private function getIdFromValue(string $value)
return $value;
}

/**
* Normalize the values array.
*
* @param array $values
*
* @return array
*/
private function normalizeValues(array $values) : array
{
foreach ($values as $key => $value) {
if (!is_int($key) || !is_string($value)) {
unset($values[$key]);
}
}

return array_values($values);
}
/**
* Normalize the values array.
*
* @param array $values
*
* @return array
*/
private function normalizeValues(array $values) : array
{
foreach ($values as $key => $value) {
if (!is_int($key) || !is_string($value)) {
unset($values[$key]);
}
}

return array_values($values);
}
}
3 changes: 3 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
<!-- Doctrine Query extensions -->

<service id="api_platform.doctrine.orm.query_extension.eager_loading" class="ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\EagerLoadingExtension" public="false">
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
<argument type="service" id="api_platform.metadata.property.metadata_factory" />

<tag name="api_platform.doctrine.orm.query_extension.item" priority="64" />
<tag name="api_platform.doctrine.orm.query_extension.collection" priority="64" />
</service>
Expand Down
2 changes: 2 additions & 0 deletions tests/Bridge/Doctrine/Orm/CollectionDataProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Doctrine\Common\Persistence\ObjectRepository;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Prophecy\Argument;

Expand All @@ -32,6 +33,7 @@ class CollectionDataProviderTest extends \PHPUnit_Framework_TestCase
public function testGetCollection()
{
$queryProphecy = $this->prophesize(AbstractQuery::class);
$queryProphecy->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true)->shouldBeCalled();
$queryProphecy->getResult()->willReturn([])->shouldBeCalled();

$queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
Expand Down
Loading

0 comments on commit 226c840

Please sign in to comment.