Skip to content

Commit

Permalink
feat: links handler
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Nov 8, 2021
1 parent 2decc7b commit 99183a7
Show file tree
Hide file tree
Showing 17 changed files with 696 additions and 161 deletions.
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,6 @@ jobs:
run: |
mkdir -p build/logs/behat
vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=default --no-interaction
continue-on-error: true
- name: Upload test artifacts
if: always()
uses: actions/upload-artifact@v1
Expand Down
8 changes: 4 additions & 4 deletions docs/adr/0003-uri-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ We will use a POPO to define URI variables, for now these options are available:
uriVariables: [
'companyId' => new UriVariable(
targetClass: Company::class,
inverseProperty: null,
targetProperty: null,
property: 'company'
identifiers: ['id'],
compositeIdentifier: true,
Expand All @@ -53,7 +53,7 @@ Where `uriVariables` keys are the URI template's variable names. Its value is a

- `targetClass` is the PHP FQDN of the class this value belongs to
- `property` represents the property, the URI Variable is mapped to in the current class
- `inverseProperty` represents the property, the URI Variable is mapped to in the related class and is not available in the current class
- `targetProperty` represents the property, the URI Variable is mapped to in the related class and is not available in the current class
- `identifiers` are the properties of the targetClass to which we map the URI variable
- `compositeIdentifier` is used to match a single variable to multiple identifiers (`ida=1;idb=2` to `class::ida` and `class::idb`)

Expand Down Expand Up @@ -122,7 +122,7 @@ class Company {
}
```

Note that the above is a shortcut for: `new UriVariable(targetClass: Employee::class, inverseProperty: 'company')`
Note that the above is a shortcut for: `new UriVariable(targetClass: Employee::class, targetProperty: 'company')`

Corresponding DQL:

Expand Down Expand Up @@ -259,7 +259,7 @@ class Employee {
#[ApiResource("/employees/{employeeId}/company", uriVariables: [
'employeeId' => new UriVariable(
targetClass: Employee::class,
inverseProperty: 'company'
targetProperty: 'company'
property: null,
identifiers: ['id'],
compositeIdentifier: true
Expand Down
3 changes: 3 additions & 0 deletions features/main/subresource.feature
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ Feature: Subresource support
}
"""

@createSchema
Scenario: Get the subresource relation item
Given there is a dummy object with a fourth level relation
When I send a "GET" request to "/dummies/1/related_dummies/2"
Then the response status code should be 200
And the response should be in JSON
Expand Down Expand Up @@ -299,6 +301,7 @@ Feature: Subresource support
}
"""

@createSchema
Scenario: Get offers subresource from aggregate offers subresource
Given I have a product with offers
When I send a "GET" request to "/dummy_products/2/offers/1/offers"
Expand Down
8 changes: 4 additions & 4 deletions src/Api/IdentifiersExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,13 @@ private function getIdentifierValue($item, string $class, string $property, stri
continue;
}

if ($type->getClassName() === $class) {
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$propertyName.$property"), $parameterName);
}

if ($type->isCollection() && ($collectionValueType = $type->getCollectionValueType()) && $collectionValueType->getClassName() === $class) {
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, sprintf('%s[0].%s', $propertyName, $property)), $parameterName);
}

if ($type->getClassName() === $class) {
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$propertyName.$property"), $parameterName);
}
}

throw new RuntimeException('Not able to retrieve identifiers.');
Expand Down
6 changes: 4 additions & 2 deletions src/Api/UriVariablesConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Api;

use ApiPlatform\Exception\InvalidUriVariableException;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use Symfony\Component\PropertyInfo\Type;
Expand Down Expand Up @@ -46,10 +47,11 @@ public function convert(array $uriVariables, string $class, array $context = [])
{
$operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($class)->getOperation();
$context = $context + ['operation' => $operation];
$uriVariablesDefinition = $operation->getUriVariables() ?? [];
$uriVariablesDefinitions = $operation->getUriVariables() ?? [];

foreach ($uriVariables as $parameterName => $value) {
if ([] === $types = $this->getIdentifierTypes($uriVariablesDefinition[$parameterName]->getFromClass() ?? $class, $uriVariablesDefinition[$parameterName]->getIdentifiers() ?? [$parameterName])) {
$uriVariableDefinition = $uriVariablesDefinitions[$parameterName] ?? $uriVariablesDefinitions['id'] ?? new Link();
if ([] === $types = $this->getIdentifierTypes($uriVariableDefinition->getFromClass() ?? $class, $uriVariableDefinition->getIdentifiers() ?? [$parameterName])) {
continue;
}

Expand Down
1 change: 1 addition & 0 deletions src/Bridge/Doctrine/Orm/State/CollectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public function provide(string $resourceClass, array $identifiers = [], ?string

$this->handleLinks($queryBuilder, $identifiers, $queryNameGenerator, $context, $resourceClass, $operationName);

// dd($queryBuilder->getQuery());
foreach ($this->collectionExtensions as $extension) {
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);

Expand Down
2 changes: 2 additions & 0 deletions src/Bridge/Doctrine/Orm/State/ItemProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\State\ProviderInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
Expand Down Expand Up @@ -56,6 +57,7 @@ public function provide(string $resourceClass, array $identifiers = [], ?string
return $manager->getReference($resourceClass, $identifiers);
}

/** @var EntityRepository $repository */
$repository = $manager->getRepository($resourceClass);
if (!method_exists($repository, 'createQueryBuilder')) {
throw new RuntimeException('The repository class must have a "createQueryBuilder" method.');
Expand Down
160 changes: 95 additions & 65 deletions src/Bridge/Doctrine/Orm/State/LinksHandlerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
namespace ApiPlatform\Bridge\Doctrine\Orm\State;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
use ApiPlatform\Exception\RuntimeException;
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyProduct;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\Mapping\ClassMetadata;

Expand All @@ -31,81 +32,110 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que

$links = $operation instanceof GraphQlOperation ? $operation->getLinks() : $operation->getUriVariables();

if ($linkClass = $context['linkClass'] ?? false) {
foreach ($links as $link) {
if ($linkClass === $link->getFromClass()) {
foreach ($identifiers as $identifier => $value) {
$this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
}
// if ($linkClass = $context['linkClass'] ?? false) {
// foreach ($links as $link) {
// if ($linkClass === $link->getTargetClass()) {
// foreach ($identifiers as $identifier => $value) {
// $this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
// }
//
// return;
// }
// }
// }

return;
}
if (!$links) {
return;
}

$previousAlias = $alias;
$previousIdentifier = end($links)->getIdentifiers()[0] ?? 'id';
$expressions = [];
$i = 0;

foreach (array_reverse($links) as $parameterName => $link) {
if ($link->getExpandedValue() || !$link->getFromClass()) {
++$i;
continue;
}

$operation = $this->resourceMetadataCollectionFactory->create($linkClass)->getOperation($operationName);
$links = $operation instanceof GraphQlOperation ? $operation->getLinks() : $operation->getUriVariables();
foreach ($links as $link) {
if ($resourceClass === $link->getFromClass()) {
$link = $link->withFromProperty($link->getToProperty())->withFromClass($linkClass);
foreach ($identifiers as $identifier => $value) {
$this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
}
$identifierProperty = $link->getIdentifiers()[0] ?? 'id';
$currentAlias = $i === 0 ? $alias : $queryNameGenerator->generateJoinAlias($alias);
$placeholder = $queryNameGenerator->generateParameterName($parameterName);

return;
}
if (!$link->getFromProperty() && !$link->getToProperty()) {
$doctrineClassMetadata = $manager->getClassMetadata($link->getFromClass());

$queryBuilder->andWhere("{$currentAlias}.$identifierProperty = :$placeholder");
$queryBuilder->setParameter($placeholder, $identifiers[$parameterName], $doctrineClassMetadata->getTypeOfField($identifierProperty));
$previousAlias = $currentAlias;
$previousIdentifier = $identifierProperty;
++$i;
continue;
}

throw new RuntimeException(sprintf('The class "%s" cannot be retrieved from "%s".', $resourceClass, $linkClass));
}
if ($link->getFromProperty()) {
$doctrineClassMetadata = $manager->getClassMetadata($link->getFromClass());
$joinAlias = $queryNameGenerator->generateJoinAlias('m');
$assocationMapping = $doctrineClassMetadata->getAssociationMappings()[$link->getFromProperty()];
$relationType = $assocationMapping['type'];

if (!$links) {
return;
}
if ($relationType & ClassMetadataInfo::TO_MANY) {
$nextAlias = $queryNameGenerator->generateJoinAlias($alias);

foreach ($identifiers as $identifier => $value) {
$link = $links[$identifier] ?? $links['id'];
$expressions["$previousAlias.$previousIdentifier"] = "SELECT $joinAlias.{$previousIdentifier} FROM {$link->getFromClass()} $nextAlias INNER JOIN $nextAlias.{$link->getFromProperty()} $joinAlias WHERE $nextAlias.{$identifierProperty} = :$placeholder";

$this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
$queryBuilder->setParameter($placeholder, $identifiers[$parameterName], $doctrineClassMetadata->getTypeOfField($identifierProperty));
$previousAlias = $nextAlias;
++$i;
continue;
}


// A single-valued association path expression to an inverse side is not supported in DQL queries.
if ($relationType & ClassMetadataInfo::TO_ONE && !$assocationMapping['isOwningSide']) {
$queryBuilder->innerJoin("$previousAlias.".$assocationMapping['mappedBy'], $joinAlias);
} else {
$queryBuilder->join(
$link->getFromClass(),
$joinAlias,
'with',
"{$previousAlias}.{$previousIdentifier} = $joinAlias.{$link->getFromProperty()}"
);
}

$queryBuilder->andWhere("$joinAlias.$identifierProperty = :$placeholder");
$queryBuilder->setParameter($placeholder, $identifiers[$parameterName], $doctrineClassMetadata->getTypeOfField($identifierProperty));
$previousAlias = $joinAlias;
$previousIdentifier = $identifierProperty;
++$i;
continue;
}

$joinAlias = $queryNameGenerator->generateJoinAlias($alias);
$queryBuilder->join("{$previousAlias}.{$link->getToProperty()}", $joinAlias);
$queryBuilder->andWhere("$joinAlias.$identifierProperty = :$placeholder");
$queryBuilder->setParameter($placeholder, $identifiers[$parameterName], $doctrineClassMetadata->getTypeOfField($identifierProperty));
$previousAlias = $joinAlias;
$previousIdentifier = $identifierProperty;
++$i;
}
}

private function applyLink(QueryBuilder $queryBuilder, QueryNameGenerator $queryNameGenerator, ClassMetadata $doctrineClassMetadata, string $alias, Link $link, string $identifier, $value)
{
$placeholder = ':id_'.$identifier;
if ($fromProperty = $link->getFromProperty()) {
$propertyIdentifier = $link->getIdentifiers()[0];
$joinAlias = $queryNameGenerator->generateJoinAlias($fromProperty);

$queryBuilder->join(
$link->getFromClass(),
$joinAlias,
'with',
"$alias.$propertyIdentifier = $joinAlias.$fromProperty"
);

$expression = $queryBuilder->expr()->eq(
"{$joinAlias}.{$propertyIdentifier}",
$placeholder
);
} elseif ($property = $link->getToProperty()) {
$propertyIdentifier = $link->getIdentifiers()[0];
$joinAlias = $queryNameGenerator->generateJoinAlias($property);

$queryBuilder->join(
"$alias.$property",
$joinAlias,
);

$expression = $queryBuilder->expr()->eq(
"{$joinAlias}.{$propertyIdentifier}",
$placeholder
);
} else {
$expression = $queryBuilder->expr()->eq(
"{$alias}.{$identifier}", $placeholder
);
if ($expressions) {
$i = 0;
$clause = '';
foreach ($expressions as $alias => $expression) {
if ($i === 0) {
$clause .= "$alias IN (" . $expression;
$i++;
continue;
}

$clause .= " AND $alias IN (" . $expression;
$i++;
}

$queryBuilder->andWhere($clause . str_repeat(')', $i));
}
$queryBuilder->andWhere($expression);
$queryBuilder->setParameter($placeholder, $value, $doctrineClassMetadata->getTypeOfField($identifier));
}
}
Loading

0 comments on commit 99183a7

Please sign in to comment.