Skip to content

Commit aa33674

Browse files
committed
serializer done
1 parent 1ddf2d3 commit aa33674

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1817
-239
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ jobs:
196196
- HttpCache
197197
- RamseyUuid
198198
- GraphQl
199+
- Serializer
199200
fail-fast: false
200201
steps:
201202
- name: Checkout

notes

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
First, this patch allows `stateOptions: entityClass` to work with Doctrine ODM.
2+
3+
Then, it provides a way to hook into our providers to change how links are provided to Doctrine. This really help with improving performances, it is quite complicated let me try to explain.
4+
5+
## The Problem
6+
7+
Say you have `/company/les-tilleuls/employees/soyuka`. API Platform tries to handle your links, and the algorithm tries to cover all the cases so it'd probably do something like this:
8+
9+
```sql
10+
SELECT * FROM Employee e
11+
INNER JOIN Company c ON e.company_id = c.id
12+
WHERE c.id = 'les-tilleuls' and e.id = 'soyuka'
13+
```
14+
15+
First it's not that simple as we work with doctrine, relations between multiple tables and different nature (toMany, toOne) are sometimes really tricky and API Platform does things like this:
16+
17+
```sql
18+
SELECT * FROM Employee e
19+
WHERE e.id IN (
20+
SELECT c.employee_id FROM Company c
21+
WHERE c.id = 'les-tilleuls'
22+
)
23+
```
24+
25+
Depending on the nature of the relation it can be over-complicated and probably also bad in term of query performances.
26+
27+
A solution to this is to say that, depending on business rules, we decide to use:
28+
29+
```
30+
SELECT * FROM Employee e
31+
WHERE e.company = 'les-tilleuls' and e.id = 'soyuka'
32+
```
33+
34+
## DX
35+
36+
Today you'd have to write a custom provider. Thing is, people love our filters and our pagination handling. Despite trying my best to work on that extensibility, for now, the best is to "copy paste API Platform code" (and keep our copyright thanks <3).
37+
38+
[URI Variables](https://github.com/api-platform/core/blob/main/docs/adr/0003-uri-variables.md) came with this huge refactoring and re-visiting data retrieval on subresources. This lead to a quite natural extension point where all our logic resides:
39+
40+
https://github.com/api-platform/core/blob/92a81f024541054b9322e7457b75c721261e14e0/src/Doctrine/Odm/State/ItemProvider.php#L62
41+
42+
https://github.com/api-platform/core/blob/92a81f024541054b9322e7457b75c721261e14e0/src/Doctrine/Orm/State/ItemProvider.php#L67
43+
44+
## Current implementation
45+
46+
Maybe this needs re-visiting, maybe that we need a new interface, but it'd need an ORM-specific signature... We already happen to have `ApiPlatfirm\State\Option` for this?
47+
48+
```
49+
use ApiPlatform\Doctrine\Orm\State;
50+
use Doctrine\ORM\QueryBuilder;
51+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator;
52+
use ApiPlatform\Metadata\Operation;
53+
54+
#[ApiResource(uriTemplate: '/company/{company}/employees/{employee}' stateOptions: new Options(handleLinks: [Employee::class, 'handleLinks']))]
55+
#[ORM\Entity]
56+
final class Employee {
57+
public string $id;
58+
public string $employee;
59+
60+
static function handleLinks(QueryBuilder $queryBuilder, array $identifiers, QueryNameGenerator $queryNameGenerator, array $context, string $entityClass, Operation $operation) {
61+
$alias = $queryBuilder->getRootAliases()[0];
62+
$queryBuilder->andWhere("$alias.id = :id");
63+
$queryBuilder->setParameter('id', $identifiers['employee']);
64+
}
65+
}
66+
```
67+
68+
You really have all you need in that signature, but we could also move some of them in the `$context` (entityClass and operation).
69+
70+
Let me know your thoughts.

src/Api/IdentifiersExtractor.php

Lines changed: 3 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -13,152 +13,10 @@
1313

1414
namespace ApiPlatform\Api;
1515

16-
use ApiPlatform\Exception\RuntimeException;
17-
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
18-
use ApiPlatform\Metadata\HttpOperation;
19-
use ApiPlatform\Metadata\Operation;
20-
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
21-
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
22-
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
23-
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
24-
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
25-
use Symfony\Component\PropertyAccess\PropertyAccess;
26-
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
16+
class_exists(\ApiPlatform\Metadata\IdentifiersExtractor::class);
2717

28-
/**
29-
* {@inheritdoc}
30-
*
31-
* @author Antoine Bluchet <[email protected]>
32-
*/
33-
final class IdentifiersExtractor implements IdentifiersExtractorInterface
34-
{
35-
use ResourceClassInfoTrait;
36-
private readonly PropertyAccessorInterface $propertyAccessor;
37-
38-
public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null)
18+
if (false) {
19+
final class IdentifiersExtractor extends \ApiPlatform\Metadata\IdentifiersExtractor
3920
{
40-
$this->resourceMetadataFactory = $resourceMetadataFactory;
41-
$this->resourceClassResolver = $resourceClassResolver;
42-
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
43-
}
44-
45-
/**
46-
* {@inheritdoc}
47-
*
48-
* TODO: 3.0 identifiers should be stringable?
49-
*/
50-
public function getIdentifiersFromItem(object $item, Operation $operation = null, array $context = []): array
51-
{
52-
if (!$this->isResourceClass($this->getObjectClass($item))) {
53-
return ['id' => $this->propertyAccessor->getValue($item, 'id')];
54-
}
55-
56-
if ($operation && $operation->getClass()) {
57-
return $this->getIdentifiersFromOperation($item, $operation, $context);
58-
}
59-
60-
$resourceClass = $this->getResourceClass($item, true);
61-
$operation ??= $this->resourceMetadataFactory->create($resourceClass)->getOperation(null, false, true);
62-
63-
return $this->getIdentifiersFromOperation($item, $operation, $context);
64-
}
65-
66-
private function getIdentifiersFromOperation(object $item, Operation $operation, array $context = []): array
67-
{
68-
if ($operation instanceof HttpOperation) {
69-
$links = $operation->getUriVariables();
70-
} elseif ($operation instanceof GraphQlOperation) {
71-
$links = $operation->getLinks();
72-
}
73-
74-
$identifiers = [];
75-
foreach ($links ?? [] as $link) {
76-
if (1 < (is_countable($link->getIdentifiers()) ? \count($link->getIdentifiers()) : 0)) {
77-
$compositeIdentifiers = [];
78-
foreach ($link->getIdentifiers() as $identifier) {
79-
$compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $identifier, $link->getParameterName());
80-
}
81-
82-
$identifiers[$link->getParameterName()] = CompositeIdentifierParser::stringify($compositeIdentifiers);
83-
continue;
84-
}
85-
86-
$parameterName = $link->getParameterName();
87-
$identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $link->getIdentifiers()[0], $parameterName, $link->getToProperty());
88-
}
89-
90-
return $identifiers;
91-
}
92-
93-
/**
94-
* Gets the value of the given class property.
95-
*/
96-
private function getIdentifierValue(object $item, string $class, string $property, string $parameterName, string $toProperty = null): float|bool|int|string
97-
{
98-
if ($item instanceof $class) {
99-
try {
100-
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, $property), $parameterName);
101-
} catch (NoSuchPropertyException $e) {
102-
throw new RuntimeException('Not able to retrieve identifiers.', $e->getCode(), $e);
103-
}
104-
}
105-
106-
if ($toProperty) {
107-
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$toProperty.$property"), $parameterName);
108-
}
109-
110-
$resourceClass = $this->getResourceClass($item, true);
111-
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
112-
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
113-
114-
$types = $propertyMetadata->getBuiltinTypes();
115-
if (null === ($type = $types[0] ?? null)) {
116-
continue;
117-
}
118-
119-
try {
120-
if ($type->isCollection()) {
121-
$collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
122-
123-
if (null !== $collectionValueType && $collectionValueType->getClassName() === $class) {
124-
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, sprintf('%s[0].%s', $propertyName, $property)), $parameterName);
125-
}
126-
}
127-
128-
if ($type->getClassName() === $class) {
129-
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$propertyName.$property"), $parameterName);
130-
}
131-
} catch (NoSuchPropertyException $e) {
132-
throw new RuntimeException('Not able to retrieve identifiers.', $e->getCode(), $e);
133-
}
134-
}
135-
136-
throw new RuntimeException('Not able to retrieve identifiers.');
137-
}
138-
139-
/**
140-
* TODO: in 3.0 this method just uses $identifierValue instanceof \Stringable and we remove the weird behavior.
141-
*
142-
* @param mixed|\Stringable $identifierValue
143-
*/
144-
private function resolveIdentifierValue(mixed $identifierValue, string $parameterName): float|bool|int|string
145-
{
146-
if (null === $identifierValue) {
147-
throw new RuntimeException('No identifier value found, did you forget to persist the entity?');
148-
}
149-
150-
if (\is_scalar($identifierValue)) {
151-
return $identifierValue;
152-
}
153-
154-
if ($identifierValue instanceof \Stringable) {
155-
return (string) $identifierValue;
156-
}
157-
158-
if ($identifierValue instanceof \BackedEnum) {
159-
return (string) $identifierValue->value;
160-
}
161-
162-
throw new RuntimeException(sprintf('We were not able to resolve the identifier matching parameter "%s".', $parameterName));
16321
}
16422
}

src/Api/IdentifiersExtractorInterface.php

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,10 @@
1313

1414
namespace ApiPlatform\Api;
1515

16-
use ApiPlatform\Exception\RuntimeException;
17-
use ApiPlatform\Metadata\Operation;
16+
class_alias(\ApiPlatform\Metadata\IdentifiersExtractorInterface::class, \ApiPlatform\Api\IdentifiersExtractorInterface::class);
1817

19-
/**
20-
* Extracts identifiers for a given Resource according to the retrieved Metadata.
21-
*
22-
* @author Antoine Bluchet <[email protected]>
23-
*/
24-
interface IdentifiersExtractorInterface
25-
{
26-
/**
27-
* Finds identifiers from an Item (object).
28-
*
29-
* @throws RuntimeException
30-
*/
31-
public function getIdentifiersFromItem(object $item, Operation $operation = null, array $context = []): array;
18+
if (false) {
19+
interface IdentifiersExtractorInterface extends \ApiPlatform\Metadata\IdentifiersExtractorInterface
20+
{
21+
}
3222
}

src/Api/IriConverterInterface.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
namespace ApiPlatform\Api;
1515

16-
interface IriConverterInterface extends \ApiPlatform\Metadata\IriConverterInterface
17-
{
16+
class_alias(\ApiPlatform\Metadata\IriConverterInterface::class, \ApiPlatform\Api\IriConverterInterface::class);
17+
18+
if (false) {
19+
interface IriConverterInterface extends \ApiPlatform\Metadata\IriConverterInterface
20+
{
21+
}
1822
}

src/Api/ResourceClassResolverInterface.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace ApiPlatform\Api;
1515

16-
interface ResourceClassResolverInterface extends \ApiPlatform\Metadata\ResourceClassResolverInterface
17-
{
16+
class_alias(\ApiPlatform\Metadata\ResourceClassResolverInterface::class, \ApiPlatform\Api\ResourceClassResolverInterface::class);
17+
18+
if (false) {
19+
interface ResourceClassResolverInterface extends \ApiPlatform\Metadata\ResourceClassResolverInterface {}
1820
}

src/GraphQl/Serializer/ItemNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace ApiPlatform\GraphQl\Serializer;
1515

16-
use ApiPlatform\Api\IdentifiersExtractorInterface;
16+
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
1717
use ApiPlatform\Metadata\ApiProperty;
1818
use ApiPlatform\Metadata\IriConverterInterface;
1919
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;

src/GraphQl/Tests/Fixtures/Type/Definition/DateTimeType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function parseValue($value): string
7878
/**
7979
* {@inheritdoc}
8080
*/
81-
public function parseLiteral(Node $valueNode, ?array $variables = null): string
81+
public function parseLiteral(Node $valueNode, array $variables = null): string
8282
{
8383
if ($valueNode instanceof StringValueNode && false !== \DateTime::createFromFormat(\DateTime::ATOM, $valueNode->value)) {
8484
return $valueNode->value;

src/GraphQl/composer.json

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,14 @@
2525
"api-platform/state": "*@dev || ^3.1",
2626
"symfony/property-info": "^6.1",
2727
"symfony/serializer": "^6.1",
28-
"symfony/http-foundation": "^6.1",
29-
"symfony/http-kernel": "^6.1"
28+
"webonyx/graphql-php": "^14.0 || ^15.0"
3029
},
3130
"require-dev": {
32-
"doctrine/common": "^3.2.2",
3331
"phpspec/prophecy-phpunit": "^2.0",
3432
"symfony/phpunit-bridge": "^6.1",
3533
"symfony/routing": "^6.1",
3634
"symfony/validator": "^6.1",
37-
"symfony/twig-bundle": "^6.1",
38-
"symfony/mercure-bundle": "*",
39-
"webonyx/graphql-php": "^14.0 || ^15.0"
35+
"symfony/mercure-bundle": "*"
4036
},
4137
"autoload": {
4238
"psr-4": {

src/Hydra/Serializer/CollectionFiltersNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
use ApiPlatform\Doctrine\Orm\State\Options;
1818
use ApiPlatform\Metadata\FilterInterface;
1919
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
20-
use ApiPlatform\Serializer\CacheableSupportsMethodInterface;
2120
use ApiPlatform\Metadata\ResourceClassResolverInterface;
21+
use ApiPlatform\Serializer\CacheableSupportsMethodInterface;
2222
use Psr\Container\ContainerInterface;
2323
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
2424
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;

0 commit comments

Comments
 (0)