Skip to content

Commit 051abbb

Browse files
committed
feat: links handler
1 parent 2decc7b commit 051abbb

File tree

21 files changed

+680
-168
lines changed

21 files changed

+680
-168
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,6 @@ jobs:
858858
run: |
859859
mkdir -p build/logs/behat
860860
vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=default --no-interaction
861-
continue-on-error: true
862861
- name: Upload test artifacts
863862
if: always()
864863
uses: actions/upload-artifact@v1

docs/adr/0003-uri-variables.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ We will use a POPO to define URI variables, for now these options are available:
3737
uriVariables: [
3838
'companyId' => new UriVariable(
3939
targetClass: Company::class,
40-
inverseProperty: null,
40+
targetProperty: null,
4141
property: 'company'
4242
identifiers: ['id'],
4343
compositeIdentifier: true,
@@ -53,7 +53,7 @@ Where `uriVariables` keys are the URI template's variable names. Its value is a
5353

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

@@ -122,7 +122,7 @@ class Company {
122122
}
123123
```
124124

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

127127
Corresponding DQL:
128128

@@ -259,7 +259,7 @@ class Employee {
259259
#[ApiResource("/employees/{employeeId}/company", uriVariables: [
260260
'employeeId' => new UriVariable(
261261
targetClass: Employee::class,
262-
inverseProperty: 'company'
262+
targetProperty: 'company'
263263
property: null,
264264
identifiers: ['id'],
265265
compositeIdentifier: true

features/main/subresource.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,9 @@ Feature: Subresource support
223223
}
224224
"""
225225

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

304+
@createSchema
302305
Scenario: Get offers subresource from aggregate offers subresource
303306
Given I have a product with offers
304307
When I send a "GET" request to "/dummy_products/2/offers/1/offers"

src/Api/IdentifiersExtractor.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,13 @@ private function getIdentifierValue($item, string $class, string $property, stri
9494
continue;
9595
}
9696

97-
if ($type->getClassName() === $class) {
98-
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$propertyName.$property"), $parameterName);
99-
}
100-
10197
if ($type->isCollection() && ($collectionValueType = $type->getCollectionValueType()) && $collectionValueType->getClassName() === $class) {
10298
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, sprintf('%s[0].%s', $propertyName, $property)), $parameterName);
10399
}
100+
101+
if ($type->getClassName() === $class) {
102+
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$propertyName.$property"), $parameterName);
103+
}
104104
}
105105

106106
throw new RuntimeException('Not able to retrieve identifiers.');

src/Api/UriVariablesConverter.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\Api;
1515

1616
use ApiPlatform\Exception\InvalidUriVariableException;
17+
use ApiPlatform\Metadata\Link;
1718
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
1819
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
1920
use Symfony\Component\PropertyInfo\Type;
@@ -46,10 +47,11 @@ public function convert(array $uriVariables, string $class, array $context = [])
4647
{
4748
$operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($class)->getOperation();
4849
$context = $context + ['operation' => $operation];
49-
$uriVariablesDefinition = $operation->getUriVariables() ?? [];
50+
$uriVariablesDefinitions = $operation->getUriVariables() ?? [];
5051

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

src/Bridge/Doctrine/Orm/State/CollectionProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public function provide(string $resourceClass, array $identifiers = [], ?string
6161

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

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

src/Bridge/Doctrine/Orm/State/ItemProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2121
use ApiPlatform\State\ProviderInterface;
2222
use Doctrine\ORM\EntityManagerInterface;
23+
use Doctrine\ORM\EntityRepository;
2324
use Doctrine\Persistence\ManagerRegistry;
2425

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

60+
/** @var EntityRepository $repository */
5961
$repository = $manager->getRepository($resourceClass);
6062
if (!method_exists($repository, 'createQueryBuilder')) {
6163
throw new RuntimeException('The repository class must have a "createQueryBuilder" method.');

src/Bridge/Doctrine/Orm/State/LinksHandlerTrait.php

Lines changed: 100 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
1717
use ApiPlatform\Exception\RuntimeException;
1818
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
19-
use ApiPlatform\Metadata\Link;
19+
use Doctrine\ORM\Mapping\ClassMetadataInfo;
2020
use Doctrine\ORM\QueryBuilder;
21-
use Doctrine\Persistence\Mapping\ClassMetadata;
2221

2322
trait LinksHandlerTrait
2423
{
@@ -31,81 +30,117 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que
3130

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

34-
if ($linkClass = $context['linkClass'] ?? false) {
35-
foreach ($links as $link) {
36-
if ($linkClass === $link->getFromClass()) {
37-
foreach ($identifiers as $identifier => $value) {
38-
$this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
39-
}
33+
// if ($linkClass = $context['linkClass'] ?? false) {
34+
// foreach ($links as $link) {
35+
// if ($linkClass === $link->getTargetClass()) {
36+
// foreach ($identifiers as $identifier => $value) {
37+
// $this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
38+
// }
39+
//
40+
// return;
41+
// }
42+
// }
43+
// }
4044

41-
return;
42-
}
45+
if (!$links) {
46+
return;
47+
}
48+
49+
$previousAlias = $alias;
50+
$previousIdentifiers = end($links)->getIdentifiers();
51+
$expressions = [];
52+
$identifiers = array_reverse($identifiers);
53+
54+
foreach (array_reverse($links) as $parameterName => $link) {
55+
if ($link->getExpandedValue() || !$link->getFromClass()) {
56+
continue;
4357
}
4458

45-
$operation = $this->resourceMetadataCollectionFactory->create($linkClass)->getOperation($operationName);
46-
$links = $operation instanceof GraphQlOperation ? $operation->getLinks() : $operation->getUriVariables();
47-
foreach ($links as $link) {
48-
if ($resourceClass === $link->getFromClass()) {
49-
$link = $link->withFromProperty($link->getToProperty())->withFromClass($linkClass);
50-
foreach ($identifiers as $identifier => $value) {
51-
$this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
52-
}
59+
$identifierProperties = $link->getIdentifiers();
60+
$currentAlias = $queryNameGenerator->generateJoinAlias($alias);
5361

54-
return;
62+
if ($link->getFromClass() === $resourceClass) {
63+
$currentAlias = $alias;
64+
}
65+
66+
if (!$link->getFromProperty() && !$link->getToProperty()) {
67+
$doctrineClassMetadata = $manager->getClassMetadata($link->getFromClass());
68+
69+
foreach ($identifierProperties as $identifierProperty) {
70+
$placeholder = $queryNameGenerator->generateParameterName($identifierProperty);
71+
$queryBuilder->andWhere("{$currentAlias}.$identifierProperty = :$placeholder");
72+
$queryBuilder->setParameter($placeholder, array_shift($identifiers), $doctrineClassMetadata->getTypeOfField($identifierProperty));
5573
}
74+
75+
$previousAlias = $currentAlias;
76+
$previousIdentifiers = $identifierProperties;
77+
continue;
5678
}
5779

58-
throw new RuntimeException(sprintf('The class "%s" cannot be retrieved from "%s".', $resourceClass, $linkClass));
59-
}
80+
if (1 < \count($previousIdentifiers) || 1 < \count($identifierProperties)) {
81+
throw new RuntimeException('Composite identifiers on a relation can not be handled automatically, implement your own query.');
82+
}
6083

61-
if (!$links) {
62-
return;
63-
}
84+
$previousIdentifier = $previousIdentifiers[0];
85+
$identifierProperty = $identifierProperties[0];
86+
$placeholder = $queryNameGenerator->generateParameterName($identifierProperty);
87+
88+
if ($link->getFromProperty()) {
89+
$doctrineClassMetadata = $manager->getClassMetadata($link->getFromClass());
90+
$joinAlias = $queryNameGenerator->generateJoinAlias('m');
91+
$assocationMapping = $doctrineClassMetadata->getAssociationMappings()[$link->getFromProperty()];
92+
$relationType = $assocationMapping['type'];
93+
94+
if ($relationType & ClassMetadataInfo::TO_MANY) {
95+
$nextAlias = $queryNameGenerator->generateJoinAlias($alias);
96+
$expressions["$previousAlias.$previousIdentifier"] = "SELECT $joinAlias.{$previousIdentifier} FROM {$link->getFromClass()} $nextAlias INNER JOIN $nextAlias.{$link->getFromProperty()} $joinAlias WHERE $nextAlias.{$identifierProperty} = :$placeholder";
97+
$queryBuilder->setParameter($placeholder, array_shift($identifiers), $doctrineClassMetadata->getTypeOfField($identifierProperty));
98+
$previousAlias = $nextAlias;
99+
continue;
100+
}
64101

65-
foreach ($identifiers as $identifier => $value) {
66-
$link = $links[$identifier] ?? $links['id'];
102+
// A single-valued association path expression to an inverse side is not supported in DQL queries.
103+
if ($relationType & ClassMetadataInfo::TO_ONE && !$assocationMapping['isOwningSide']) {
104+
$queryBuilder->innerJoin("$previousAlias.".$assocationMapping['mappedBy'], $joinAlias);
105+
} else {
106+
$queryBuilder->join(
107+
$link->getFromClass(),
108+
$joinAlias,
109+
'with',
110+
"{$previousAlias}.{$previousIdentifier} = $joinAlias.{$link->getFromProperty()}"
111+
);
112+
}
113+
114+
$queryBuilder->andWhere("$joinAlias.$identifierProperty = :$placeholder");
115+
$queryBuilder->setParameter($placeholder, array_shift($identifiers), $doctrineClassMetadata->getTypeOfField($identifierProperty));
116+
$previousAlias = $joinAlias;
117+
$previousIdentifier = $identifierProperty;
118+
continue;
119+
}
67120

68-
$this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
121+
$joinAlias = $queryNameGenerator->generateJoinAlias($alias);
122+
$queryBuilder->join("{$previousAlias}.{$link->getToProperty()}", $joinAlias);
123+
$queryBuilder->andWhere("$joinAlias.$identifierProperty = :$placeholder");
124+
$queryBuilder->setParameter($placeholder, array_shift($identifiers), $doctrineClassMetadata->getTypeOfField($identifierProperty));
125+
$previousAlias = $joinAlias;
126+
$previousIdentifier = $identifierProperty;
69127
}
70-
}
71128

72-
private function applyLink(QueryBuilder $queryBuilder, QueryNameGenerator $queryNameGenerator, ClassMetadata $doctrineClassMetadata, string $alias, Link $link, string $identifier, $value)
73-
{
74-
$placeholder = ':id_'.$identifier;
75-
if ($fromProperty = $link->getFromProperty()) {
76-
$propertyIdentifier = $link->getIdentifiers()[0];
77-
$joinAlias = $queryNameGenerator->generateJoinAlias($fromProperty);
78-
79-
$queryBuilder->join(
80-
$link->getFromClass(),
81-
$joinAlias,
82-
'with',
83-
"$alias.$propertyIdentifier = $joinAlias.$fromProperty"
84-
);
85-
86-
$expression = $queryBuilder->expr()->eq(
87-
"{$joinAlias}.{$propertyIdentifier}",
88-
$placeholder
89-
);
90-
} elseif ($property = $link->getToProperty()) {
91-
$propertyIdentifier = $link->getIdentifiers()[0];
92-
$joinAlias = $queryNameGenerator->generateJoinAlias($property);
93-
94-
$queryBuilder->join(
95-
"$alias.$property",
96-
$joinAlias,
97-
);
98-
99-
$expression = $queryBuilder->expr()->eq(
100-
"{$joinAlias}.{$propertyIdentifier}",
101-
$placeholder
102-
);
103-
} else {
104-
$expression = $queryBuilder->expr()->eq(
105-
"{$alias}.{$identifier}", $placeholder
106-
);
129+
if ($expressions) {
130+
$i = 0;
131+
$clause = '';
132+
foreach ($expressions as $alias => $expression) {
133+
if (0 === $i) {
134+
$clause .= "$alias IN (".$expression;
135+
++$i;
136+
continue;
137+
}
138+
139+
$clause .= " AND $alias IN (".$expression;
140+
++$i;
141+
}
142+
143+
$queryBuilder->andWhere($clause.str_repeat(')', $i));
107144
}
108-
$queryBuilder->andWhere($expression);
109-
$queryBuilder->setParameter($placeholder, $value, $doctrineClassMetadata->getTypeOfField($identifier));
110145
}
111146
}

0 commit comments

Comments
 (0)