Skip to content

Commit 99183a7

Browse files
committed
feat: links handler
1 parent 2decc7b commit 99183a7

File tree

17 files changed

+696
-161
lines changed

17 files changed

+696
-161
lines changed

.github/workflows/ci.yml

-1
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

+4-4
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

+3
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

+4-4
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

+4-2
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

+1
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

+2
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

+95-65
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
namespace ApiPlatform\Bridge\Doctrine\Orm\State;
1515

1616
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
17-
use ApiPlatform\Exception\RuntimeException;
1817
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
1918
use ApiPlatform\Metadata\Link;
19+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyProduct;
20+
use Doctrine\ORM\Mapping\ClassMetadataInfo;
2021
use Doctrine\ORM\QueryBuilder;
2122
use Doctrine\Persistence\Mapping\ClassMetadata;
2223

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

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

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-
}
35+
// if ($linkClass = $context['linkClass'] ?? false) {
36+
// foreach ($links as $link) {
37+
// if ($linkClass === $link->getTargetClass()) {
38+
// foreach ($identifiers as $identifier => $value) {
39+
// $this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
40+
// }
41+
//
42+
// return;
43+
// }
44+
// }
45+
// }
4046

41-
return;
42-
}
47+
if (!$links) {
48+
return;
49+
}
50+
51+
$previousAlias = $alias;
52+
$previousIdentifier = end($links)->getIdentifiers()[0] ?? 'id';
53+
$expressions = [];
54+
$i = 0;
55+
56+
foreach (array_reverse($links) as $parameterName => $link) {
57+
if ($link->getExpandedValue() || !$link->getFromClass()) {
58+
++$i;
59+
continue;
4360
}
4461

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-
}
62+
$identifierProperty = $link->getIdentifiers()[0] ?? 'id';
63+
$currentAlias = $i === 0 ? $alias : $queryNameGenerator->generateJoinAlias($alias);
64+
$placeholder = $queryNameGenerator->generateParameterName($parameterName);
5365

54-
return;
55-
}
66+
if (!$link->getFromProperty() && !$link->getToProperty()) {
67+
$doctrineClassMetadata = $manager->getClassMetadata($link->getFromClass());
68+
69+
$queryBuilder->andWhere("{$currentAlias}.$identifierProperty = :$placeholder");
70+
$queryBuilder->setParameter($placeholder, $identifiers[$parameterName], $doctrineClassMetadata->getTypeOfField($identifierProperty));
71+
$previousAlias = $currentAlias;
72+
$previousIdentifier = $identifierProperty;
73+
++$i;
74+
continue;
5675
}
5776

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

61-
if (!$links) {
62-
return;
63-
}
83+
if ($relationType & ClassMetadataInfo::TO_MANY) {
84+
$nextAlias = $queryNameGenerator->generateJoinAlias($alias);
6485

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

68-
$this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
88+
$queryBuilder->setParameter($placeholder, $identifiers[$parameterName], $doctrineClassMetadata->getTypeOfField($identifierProperty));
89+
$previousAlias = $nextAlias;
90+
++$i;
91+
continue;
92+
}
93+
94+
95+
// A single-valued association path expression to an inverse side is not supported in DQL queries.
96+
if ($relationType & ClassMetadataInfo::TO_ONE && !$assocationMapping['isOwningSide']) {
97+
$queryBuilder->innerJoin("$previousAlias.".$assocationMapping['mappedBy'], $joinAlias);
98+
} else {
99+
$queryBuilder->join(
100+
$link->getFromClass(),
101+
$joinAlias,
102+
'with',
103+
"{$previousAlias}.{$previousIdentifier} = $joinAlias.{$link->getFromProperty()}"
104+
);
105+
}
106+
107+
$queryBuilder->andWhere("$joinAlias.$identifierProperty = :$placeholder");
108+
$queryBuilder->setParameter($placeholder, $identifiers[$parameterName], $doctrineClassMetadata->getTypeOfField($identifierProperty));
109+
$previousAlias = $joinAlias;
110+
$previousIdentifier = $identifierProperty;
111+
++$i;
112+
continue;
113+
}
114+
115+
$joinAlias = $queryNameGenerator->generateJoinAlias($alias);
116+
$queryBuilder->join("{$previousAlias}.{$link->getToProperty()}", $joinAlias);
117+
$queryBuilder->andWhere("$joinAlias.$identifierProperty = :$placeholder");
118+
$queryBuilder->setParameter($placeholder, $identifiers[$parameterName], $doctrineClassMetadata->getTypeOfField($identifierProperty));
119+
$previousAlias = $joinAlias;
120+
$previousIdentifier = $identifierProperty;
121+
++$i;
69122
}
70-
}
71123

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-
);
124+
if ($expressions) {
125+
$i = 0;
126+
$clause = '';
127+
foreach ($expressions as $alias => $expression) {
128+
if ($i === 0) {
129+
$clause .= "$alias IN (" . $expression;
130+
$i++;
131+
continue;
132+
}
133+
134+
$clause .= " AND $alias IN (" . $expression;
135+
$i++;
136+
}
137+
138+
$queryBuilder->andWhere($clause . str_repeat(')', $i));
107139
}
108-
$queryBuilder->andWhere($expression);
109-
$queryBuilder->setParameter($placeholder, $value, $doctrineClassMetadata->getTypeOfField($identifier));
110140
}
111141
}

0 commit comments

Comments
 (0)