forked from api-platform/core
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
54 changed files
with
1,817 additions
and
239 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -196,6 +196,7 @@ jobs: | |
- HttpCache | ||
- RamseyUuid | ||
- GraphQl | ||
- Serializer | ||
fail-fast: false | ||
steps: | ||
- name: Checkout | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
First, this patch allows `stateOptions: entityClass` to work with Doctrine ODM. | ||
|
||
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. | ||
|
||
## The Problem | ||
|
||
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: | ||
|
||
```sql | ||
SELECT * FROM Employee e | ||
INNER JOIN Company c ON e.company_id = c.id | ||
WHERE c.id = 'les-tilleuls' and e.id = 'soyuka' | ||
``` | ||
|
||
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: | ||
|
||
```sql | ||
SELECT * FROM Employee e | ||
WHERE e.id IN ( | ||
SELECT c.employee_id FROM Company c | ||
WHERE c.id = 'les-tilleuls' | ||
) | ||
``` | ||
|
||
Depending on the nature of the relation it can be over-complicated and probably also bad in term of query performances. | ||
|
||
A solution to this is to say that, depending on business rules, we decide to use: | ||
|
||
``` | ||
SELECT * FROM Employee e | ||
WHERE e.company = 'les-tilleuls' and e.id = 'soyuka' | ||
``` | ||
|
||
## DX | ||
|
||
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). | ||
|
||
[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: | ||
|
||
https://github.com/api-platform/core/blob/92a81f024541054b9322e7457b75c721261e14e0/src/Doctrine/Odm/State/ItemProvider.php#L62 | ||
|
||
https://github.com/api-platform/core/blob/92a81f024541054b9322e7457b75c721261e14e0/src/Doctrine/Orm/State/ItemProvider.php#L67 | ||
|
||
## Current implementation | ||
|
||
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? | ||
|
||
``` | ||
use ApiPlatform\Doctrine\Orm\State; | ||
use Doctrine\ORM\QueryBuilder; | ||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator; | ||
use ApiPlatform\Metadata\Operation; | ||
|
||
#[ApiResource(uriTemplate: '/company/{company}/employees/{employee}' stateOptions: new Options(handleLinks: [Employee::class, 'handleLinks']))] | ||
#[ORM\Entity] | ||
final class Employee { | ||
public string $id; | ||
public string $employee; | ||
|
||
static function handleLinks(QueryBuilder $queryBuilder, array $identifiers, QueryNameGenerator $queryNameGenerator, array $context, string $entityClass, Operation $operation) { | ||
$alias = $queryBuilder->getRootAliases()[0]; | ||
$queryBuilder->andWhere("$alias.id = :id"); | ||
$queryBuilder->setParameter('id', $identifiers['employee']); | ||
} | ||
} | ||
``` | ||
|
||
You really have all you need in that signature, but we could also move some of them in the `$context` (entityClass and operation). | ||
|
||
Let me know your thoughts. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,152 +13,10 @@ | |
|
||
namespace ApiPlatform\Api; | ||
|
||
use ApiPlatform\Exception\RuntimeException; | ||
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; | ||
use ApiPlatform\Metadata\HttpOperation; | ||
use ApiPlatform\Metadata\Operation; | ||
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; | ||
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; | ||
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; | ||
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; | ||
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; | ||
use Symfony\Component\PropertyAccess\PropertyAccess; | ||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; | ||
class_exists(\ApiPlatform\Metadata\IdentifiersExtractor::class); | ||
|
||
/** | ||
* {@inheritdoc} | ||
* | ||
* @author Antoine Bluchet <[email protected]> | ||
*/ | ||
final class IdentifiersExtractor implements IdentifiersExtractorInterface | ||
{ | ||
use ResourceClassInfoTrait; | ||
private readonly PropertyAccessorInterface $propertyAccessor; | ||
|
||
public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null) | ||
if (false) { | ||
final class IdentifiersExtractor extends \ApiPlatform\Metadata\IdentifiersExtractor | ||
{ | ||
$this->resourceMetadataFactory = $resourceMetadataFactory; | ||
$this->resourceClassResolver = $resourceClassResolver; | ||
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
* | ||
* TODO: 3.0 identifiers should be stringable? | ||
*/ | ||
public function getIdentifiersFromItem(object $item, Operation $operation = null, array $context = []): array | ||
{ | ||
if (!$this->isResourceClass($this->getObjectClass($item))) { | ||
return ['id' => $this->propertyAccessor->getValue($item, 'id')]; | ||
} | ||
|
||
if ($operation && $operation->getClass()) { | ||
return $this->getIdentifiersFromOperation($item, $operation, $context); | ||
} | ||
|
||
$resourceClass = $this->getResourceClass($item, true); | ||
$operation ??= $this->resourceMetadataFactory->create($resourceClass)->getOperation(null, false, true); | ||
|
||
return $this->getIdentifiersFromOperation($item, $operation, $context); | ||
} | ||
|
||
private function getIdentifiersFromOperation(object $item, Operation $operation, array $context = []): array | ||
{ | ||
if ($operation instanceof HttpOperation) { | ||
$links = $operation->getUriVariables(); | ||
} elseif ($operation instanceof GraphQlOperation) { | ||
$links = $operation->getLinks(); | ||
} | ||
|
||
$identifiers = []; | ||
foreach ($links ?? [] as $link) { | ||
if (1 < (is_countable($link->getIdentifiers()) ? \count($link->getIdentifiers()) : 0)) { | ||
$compositeIdentifiers = []; | ||
foreach ($link->getIdentifiers() as $identifier) { | ||
$compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $identifier, $link->getParameterName()); | ||
} | ||
|
||
$identifiers[$link->getParameterName()] = CompositeIdentifierParser::stringify($compositeIdentifiers); | ||
continue; | ||
} | ||
|
||
$parameterName = $link->getParameterName(); | ||
$identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $link->getIdentifiers()[0], $parameterName, $link->getToProperty()); | ||
} | ||
|
||
return $identifiers; | ||
} | ||
|
||
/** | ||
* Gets the value of the given class property. | ||
*/ | ||
private function getIdentifierValue(object $item, string $class, string $property, string $parameterName, string $toProperty = null): float|bool|int|string | ||
{ | ||
if ($item instanceof $class) { | ||
try { | ||
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, $property), $parameterName); | ||
} catch (NoSuchPropertyException $e) { | ||
throw new RuntimeException('Not able to retrieve identifiers.', $e->getCode(), $e); | ||
} | ||
} | ||
|
||
if ($toProperty) { | ||
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$toProperty.$property"), $parameterName); | ||
} | ||
|
||
$resourceClass = $this->getResourceClass($item, true); | ||
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) { | ||
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); | ||
|
||
$types = $propertyMetadata->getBuiltinTypes(); | ||
if (null === ($type = $types[0] ?? null)) { | ||
continue; | ||
} | ||
|
||
try { | ||
if ($type->isCollection()) { | ||
$collectionValueType = $type->getCollectionValueTypes()[0] ?? null; | ||
|
||
if (null !== $collectionValueType && $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); | ||
} | ||
} catch (NoSuchPropertyException $e) { | ||
throw new RuntimeException('Not able to retrieve identifiers.', $e->getCode(), $e); | ||
} | ||
} | ||
|
||
throw new RuntimeException('Not able to retrieve identifiers.'); | ||
} | ||
|
||
/** | ||
* TODO: in 3.0 this method just uses $identifierValue instanceof \Stringable and we remove the weird behavior. | ||
* | ||
* @param mixed|\Stringable $identifierValue | ||
*/ | ||
private function resolveIdentifierValue(mixed $identifierValue, string $parameterName): float|bool|int|string | ||
{ | ||
if (null === $identifierValue) { | ||
throw new RuntimeException('No identifier value found, did you forget to persist the entity?'); | ||
} | ||
|
||
if (\is_scalar($identifierValue)) { | ||
return $identifierValue; | ||
} | ||
|
||
if ($identifierValue instanceof \Stringable) { | ||
return (string) $identifierValue; | ||
} | ||
|
||
if ($identifierValue instanceof \BackedEnum) { | ||
return (string) $identifierValue->value; | ||
} | ||
|
||
throw new RuntimeException(sprintf('We were not able to resolve the identifier matching parameter "%s".', $parameterName)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,20 +13,10 @@ | |
|
||
namespace ApiPlatform\Api; | ||
|
||
use ApiPlatform\Exception\RuntimeException; | ||
use ApiPlatform\Metadata\Operation; | ||
class_alias(\ApiPlatform\Metadata\IdentifiersExtractorInterface::class, \ApiPlatform\Api\IdentifiersExtractorInterface::class); | ||
|
||
/** | ||
* Extracts identifiers for a given Resource according to the retrieved Metadata. | ||
* | ||
* @author Antoine Bluchet <[email protected]> | ||
*/ | ||
interface IdentifiersExtractorInterface | ||
{ | ||
/** | ||
* Finds identifiers from an Item (object). | ||
* | ||
* @throws RuntimeException | ||
*/ | ||
public function getIdentifiersFromItem(object $item, Operation $operation = null, array $context = []): array; | ||
if (false) { | ||
interface IdentifiersExtractorInterface extends \ApiPlatform\Metadata\IdentifiersExtractorInterface | ||
{ | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.