Skip to content

Commit

Permalink
Merge 06ce4e7 into 2625c81
Browse files Browse the repository at this point in the history
  • Loading branch information
alanpoulain committed Nov 5, 2021
2 parents 2625c81 + 06ce4e7 commit d902f69
Show file tree
Hide file tree
Showing 33 changed files with 859 additions and 310 deletions.
74 changes: 74 additions & 0 deletions docs/adr/0004-link.md
@@ -0,0 +1,74 @@
# Link

* Status: accepted
* Deciders: @dunglas, @soyuka, @alanpoulain

Implementation: [#4536][pull/4536]

## Context and Problem Statement

The [URI Variables](0003-uri-variables.md) ADR introduces a new `UriVariable` POPO.
In GraphQL, having URI variables make no sense: this object needs either an alias or needs to be named differently.

## Considered Options

* Create a `Traverser` alias for GraphQL.
* Rename `UriVariable` to `Link`.

## Decision Outcome

We chose to rename `UriVariable` to `Link` in order to simplify the codebase.
However the `uriVariables` parameter in the REST operations will not be renamed since it makes sense to have this name.
GraphQL operations don't need to have links at the operation level, a `Link` attribute on the property will be used instead if necessary (the main use case is when an `inverseProperty` is necessary).

### Example

```php
<?php

#[Query]
#[Get('/companies/{companyId}/employees', uriVariables: [
'companyId' => new Link(
targetClass: Company::class,
identifiers: ['id'],
property: 'company'
)
])]
class Employee
{
public $id;

public Company $company;
}
```

```php
<?php

#[Query]
#[Get]
class Company
{
public $id;

/** @var Employee[] */
#[Link(inverseProperty: 'company')]
public iterable $employees;
}
```

```graphql
{
companies(id: "/companies/2") {
employees {
edges {
node {
id
}
}
}
}
}
```

[pull/4536]: https://github.com/api-platform/core/pull/4536 "Link implementation"
20 changes: 11 additions & 9 deletions src/Api/IdentifiersExtractor.php
Expand Up @@ -19,6 +19,7 @@
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
use ApiPlatform\Exception\RuntimeException;
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
Expand Down Expand Up @@ -56,18 +57,19 @@ public function getIdentifiersFromItem($item, string $operationName = null, arra
$resourceClass = $this->getResourceClass($item, true);
$operation = $context['operation'] ?? $this->resourceMetadataFactory->create($resourceClass)->getOperation($operationName);

foreach ($operation->getUriVariables() ?? [] as $parameterName => $uriVariableDefinition) {
if (1 < \count($uriVariableDefinition->getIdentifiers())) {
$links = $operation instanceof GraphQlOperation ? $operation->getLinks() : $operation->getUriVariables();
foreach ($links ?? [] as $link) {
if (1 < \count($link->getIdentifiers())) {
$compositeIdentifiers = [];
foreach ($uriVariableDefinition->getIdentifiers() as $identifier) {
$compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $uriVariableDefinition->getTargetClass() ?? $resourceClass, $identifier, $parameterName);
foreach ($link->getIdentifiers() as $identifier) {
$compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $link->getTargetClass() ?? $resourceClass, $identifier, $link->getParameterName());
}

$identifiers[($operation->getExtraProperties()['is_legacy_resource_metadata'] ?? false) ? 'id' : $parameterName] = CompositeIdentifierParser::stringify($compositeIdentifiers);
$identifiers[($operation->getExtraProperties()['is_legacy_resource_metadata'] ?? false) ? 'id' : $link->getParameterName()] = CompositeIdentifierParser::stringify($compositeIdentifiers);
continue;
}

$identifiers[$parameterName] = $this->getIdentifierValue($item, $uriVariableDefinition->getTargetClass(), $uriVariableDefinition->getIdentifiers()[0], $parameterName);
$identifiers[$link->getParameterName()] = $this->getIdentifierValue($item, $link->getTargetClass(), $link->getIdentifiers()[0], $link->getParameterName());
}

return $identifiers;
Expand Down Expand Up @@ -126,9 +128,9 @@ private function resolveIdentifierValue($identifierValue, string $parameterName)
if ($this->isResourceClass($relatedResourceClass = $this->getObjectClass($identifierValue))) {
trigger_deprecation('api-platform/core', '2.7', 'Using a resource class as identifier is deprecated, please make this identifier Stringable');
$relatedOperation = $this->resourceMetadataFactory->create($relatedResourceClass)->getOperation();
$relatedIdentifiers = $relatedOperation->getUriVariables();
if (1 === \count($relatedIdentifiers)) {
$identifierValue = $this->getIdentifierValue($identifierValue, $relatedResourceClass, current($relatedIdentifiers)->getIdentifiers()[0], $parameterName);
$relatedLinks = $relatedOperation instanceof GraphQlOperation ? $relatedOperation->getLinks() : $relatedOperation->getUriVariables();
if (1 === \count($relatedLinks)) {
$identifierValue = $this->getIdentifierValue($identifierValue, $relatedResourceClass, current($relatedLinks)->getIdentifiers()[0], $parameterName);

if ($identifierValue instanceof \Stringable || is_scalar($identifierValue) || method_exists($identifierValue, '__toString')) {
return (string) $identifierValue;
Expand Down
4 changes: 2 additions & 2 deletions src/Bridge/Doctrine/Orm/State/CollectionProvider.php
Expand Up @@ -30,7 +30,7 @@
*/
final class CollectionProvider implements ProviderInterface
{
use UriVariablesHandlerTrait;
use LinksHandlerTrait;

private $resourceMetadataCollectionFactory;
private $managerRegistry;
Expand Down Expand Up @@ -59,7 +59,7 @@ public function provide(string $resourceClass, array $identifiers = [], ?string
$queryBuilder = $repository->createQueryBuilder('o');
$queryNameGenerator = new QueryNameGenerator();

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

foreach ($this->collectionExtensions as $extension) {
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
Expand Down
4 changes: 2 additions & 2 deletions src/Bridge/Doctrine/Orm/State/ItemProvider.php
Expand Up @@ -30,7 +30,7 @@
*/
final class ItemProvider implements ProviderInterface
{
use UriVariablesHandlerTrait;
use LinksHandlerTrait;

private $resourceMetadataCollectionFactory;
private $managerRegistry;
Expand Down Expand Up @@ -64,7 +64,7 @@ public function provide(string $resourceClass, array $identifiers = [], ?string
$queryBuilder = $repository->createQueryBuilder('o');
$queryNameGenerator = new QueryNameGenerator();

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

foreach ($this->itemExtensions as $extension) {
$extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $operationName, $context);
Expand Down
111 changes: 111 additions & 0 deletions src/Bridge/Doctrine/Orm/State/LinksHandlerTrait.php
@@ -0,0 +1,111 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

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 Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\Mapping\ClassMetadata;

trait LinksHandlerTrait
{
private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, QueryNameGenerator $queryNameGenerator, array $context, string $resourceClass, ?string $operationName = null): void
{
$operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation($operationName);
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
$doctrineClassMetadata = $manager->getClassMetadata($resourceClass);
$alias = $queryBuilder->getRootAliases()[0];

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

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;
}
}

$operation = $this->resourceMetadataCollectionFactory->create($linkClass)->getOperation($operationName);
$links = $operation instanceof GraphQlOperation ? $operation->getLinks() : $operation->getUriVariables();
foreach ($links as $link) {
if ($resourceClass === $link->getTargetClass()) {
$link = $link->withInverseProperty($link->getProperty())->withTargetClass($linkClass);
foreach ($identifiers as $identifier => $value) {
$this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
}

return;
}
}

throw new RuntimeException(sprintf('The class "%s" cannot be retrieved from "%s".', $resourceClass, $linkClass));
}

if (!$links) {
return;
}

foreach ($identifiers as $identifier => $value) {
$link = $links[$identifier] ?? $links['id'];

$this->applyLink($queryBuilder, $queryNameGenerator, $doctrineClassMetadata, $alias, $link, $identifier, $value);
}
}

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

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

$expression = $queryBuilder->expr()->eq(
"{$joinAlias}.{$propertyIdentifier}",
$placeholder
);
} elseif ($property = $link->getProperty()) {
$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
);
}
$queryBuilder->andWhere($expression);
$queryBuilder->setParameter($placeholder, $value, $doctrineClassMetadata->getTypeOfField($identifier));
}
}
75 changes: 0 additions & 75 deletions src/Bridge/Doctrine/Orm/State/UriVariablesHandlerTrait.php

This file was deleted.

Expand Up @@ -30,10 +30,20 @@
</service>

<service id="api_platform.metadata.resource.metadata_collection_factory.uri_template" class="ApiPlatform\Metadata\Resource\Factory\UriTemplateResourceMetadataCollectionFactory" decorates="api_platform.metadata.resource.metadata_collection_factory" decoration-priority="500" public="false">
<argument type="service" id="api_platform.metadata.resource.link_factory" />
<argument type="service" id="api_platform.path_segment_name_generator" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory.uri_template.inner" />
</service>

<service id="api_platform.metadata.resource.metadata_collection_factory.link" class="ApiPlatform\Metadata\Resource\Factory\LinkResourceMetadataCollectionFactory" decorates="api_platform.metadata.resource.metadata_collection_factory" decoration-priority="500" public="false">
<argument type="service" id="api_platform.metadata.resource.link_factory" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory.link.inner" />
</service>

<service id="api_platform.metadata.resource.link_factory" class="ApiPlatform\Metadata\Resource\Factory\LinkFactory" public="false">
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
<argument type="service" id="api_platform.metadata.property.identifier_metadata_factory" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory.uri_template.inner" />
<argument type="service" id="api_platform.resource_class_resolver" />
</service>

<!-- must run after uri template, do not change uriTemplate neither the operation names in < 500 priorities -->
Expand Down
6 changes: 3 additions & 3 deletions src/Metadata/ApiResource.php
Expand Up @@ -42,7 +42,7 @@ final class ApiResource
*/
private $outputFormats;
/**
* @var array<string, UriVariable>|array<string, array>|string[]|string|null
* @var array<string, Link>|array<string, array>|string[]|string|null
*/
private $uriVariables;
private $routePrefix;
Expand Down Expand Up @@ -405,15 +405,15 @@ public function withOutputFormats($outputFormats): self
}

/**
* @return array<string, UriVariable>|array<string, array>|string[]|string|null
* @return array<string, Link>|array<string, array>|string[]|string|null
*/
public function getUriVariables()
{
return $this->uriVariables;
}

/**
* @param array<string, UriVariable>|array<string, array>|string[]|string|null $uriVariables
* @param array<string, Link>|array<string, array>|string[]|string|null $uriVariables
*/
public function withUriVariables($uriVariables): self
{
Expand Down

0 comments on commit d902f69

Please sign in to comment.