Skip to content

Commit

Permalink
feat(metadata): crud on subresource
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Sep 9, 2022
1 parent 37bc691 commit 21f1f67
Show file tree
Hide file tree
Showing 17 changed files with 437 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

## 3.0.0

* Metadata: CRUD on subresource with experimental write support (#4932)
* Symfony: 6.1 compatibility and remove 4.4 and 5.4 support (#4851)
* Symfony: removed the $exceptionOnNoToken parameter in `ResourceAccessChecker::__construct()` (#4905)
* Symfony: use conventional service names for Doctrine state providers and processors (#4859)
Expand Down
32 changes: 32 additions & 0 deletions features/main/sub_resource.feature
Original file line number Diff line number Diff line change
Expand Up @@ -561,3 +561,35 @@ Feature: Sub-resource support
"foo": null
}
"""

@!mongodb
@createSchema
Scenario: The generated crud should allow us to interact with the SubresourceEmployee
Given I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/subresource_organizations" with body:
"""
{
"name": "Les Tilleuls"
}
"""
Then the response status code should be 201
Given I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/subresource_organizations/1/subresource_employees" with body:
"""
{
"name": "soyuka"
}
"""
Then the response status code should be 201
And I send a "GET" request to "/subresource_organizations/1/subresource_employees/1"
Then the response status code should be 200
And I send a "GET" request to "/subresource_organizations/1/subresource_employees"
Then the response status code should be 200
Given I add "Content-Type" header equal to "application/ld+json"
And I send a "PUT" request to "/subresource_organizations/1/subresource_employees/1" with body:
"""
{
"name": "ok"
}
"""
Then the response status code should be 200
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\State\CreateProvider;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
Expand Down Expand Up @@ -130,8 +131,9 @@ private function buildResourceOperations(array $attributes, string $resourceClas
// Loop again and set default operations if none where found
foreach ($resources as $index => $resource) {
$operations = [];
foreach ($resource->getOperations() ?? [new Get(), new GetCollection(), new Post(), new Put(), new Patch(), new Delete()] as $i => $operation) {
[$key, $operation] = $this->getOperationWithDefaults($resource, $operation);

foreach ($resource->getOperations() ?? $this->getDefaultHttpOperations($resource) as $i => $operation) {
[$key, $operation] = $this->getOperationWithDefaults($resource, $operation, $resource->getOperations() ? false : true);
$operations[$key] = $operation;
}

Expand Down Expand Up @@ -162,7 +164,7 @@ private function buildResourceOperations(array $attributes, string $resourceClas
return $resources;
}

private function getOperationWithDefaults(ApiResource $resource, Operation $operation): array
private function getOperationWithDefaults(ApiResource $resource, Operation $operation, bool $generated = false): array
{
// Inherit from resource defaults
foreach (get_class_methods($resource) as $methodName) {
Expand All @@ -181,7 +183,11 @@ private function getOperationWithDefaults(ApiResource $resource, Operation $oper
$operation = $operation->{'with'.substr($methodName, 3)}($value);
}

$operation = $operation->withExtraProperties(array_merge($resource->getExtraProperties(), $operation->getExtraProperties()));
$operation = $operation->withExtraProperties(array_merge(
$resource->getExtraProperties(),
$operation->getExtraProperties(),
$generated ? ['generated_operation' => true] : []
));

// Add global defaults attributes to the operation
$operation = $this->addGlobalDefaults($operation);
Expand Down Expand Up @@ -312,4 +318,14 @@ private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource

return $resource->withGraphQlOperations($graphQlOperations);
}

private function getDefaultHttpOperations($resource): iterable
{
$post = new Post();
if ($resource->getUriTemplate() && !$resource->getProvider()) {
$post = $post->withProvider(CreateProvider::class);
}

return [new Get(), new GetCollection(), $post, new Put(), new Patch(), new Delete()];
}
}
19 changes: 18 additions & 1 deletion src/Metadata/Resource/Factory/LinkFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Metadata\Resource\Factory;

use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\Exception\RuntimeException;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Operation;
Expand All @@ -24,12 +25,28 @@
/**
* @internal
*/
final class LinkFactory implements LinkFactoryInterface
final class LinkFactory implements LinkFactoryInterface, PropertyLinkFactoryInterface
{
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceClassResolverInterface $resourceClassResolver)
{
}

/**
* {@inheritdoc}
*/
public function createLinkFromProperty(ApiResource|Operation $operation, string $property): Link
{
$metadata = $this->propertyMetadataFactory->create($resourceClass = $operation->getClass(), $property);
$relationClass = $this->getPropertyClassType($metadata->getBuiltinTypes());
if (!$relationClass) {
throw new RuntimeException(sprintf('We could not find a class matching the uriVariable "%s" on "%s".', $property, $resourceClass));
}

$identifiers = $this->resourceClassResolver->isResourceClass($relationClass) ? $this->getIdentifiersFromResourceClass($relationClass) : ['id'];

return new Link(fromClass: $relationClass, toProperty: $property, identifiers: $identifiers, parameterName: $property);
}

/**
* {@inheritdoc}
*/
Expand Down
29 changes: 29 additions & 0 deletions src/Metadata/Resource/Factory/PropertyLinkFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?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\Metadata\Resource\Factory;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Operation;

/**
* @internal
*/
interface PropertyLinkFactoryInterface
{
/**
* Create a link for a given property.
*/
public function createLinkFromProperty(ApiResource|Operation $operation, string $property): Link;
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ public function create(string $resourceClass): ResourceMetadataCollection
/** @var HttpOperation */
$operation = $this->configureUriVariables($operation);

if ($operation->getUriTemplate()) {
if (
$operation->getUriTemplate()
&& !($operation->getExtraProperties()['generated_operation'] ?? false)
) {
$operation = $operation->withExtraProperties($operation->getExtraProperties() + ['user_defined_uri_template' => true]);
if (!$operation->getName()) {
$operation = $operation->withName($key);
Expand Down Expand Up @@ -90,12 +93,15 @@ public function create(string $resourceClass): ResourceMetadataCollection

private function generateUriTemplate(HttpOperation $operation): string
{
$uriTemplate = sprintf('/%s', $this->pathSegmentNameGenerator->getSegmentName($operation->getShortName()));
$uriTemplate = $operation->getUriTemplate() ?? sprintf('/%s', $this->pathSegmentNameGenerator->getSegmentName($operation->getShortName()));
$uriVariables = $operation->getUriVariables() ?? [];

if ($parameters = array_keys($uriVariables)) {
foreach ($parameters as $parameterName) {
$uriTemplate .= sprintf('/{%s}', $parameterName);
$part = sprintf('/{%s}', $parameterName);
if (false === strpos($uriTemplate, $part)) {
$uriTemplate .= sprintf('/{%s}', $parameterName);
}
}
}

Expand All @@ -107,7 +113,10 @@ private function configureUriVariables(ApiResource|HttpOperation $operation): Ap
// We will generate the collection route, don't initialize variables here
if ($operation instanceof HttpOperation && (
[] === $operation->getUriVariables() ||
($operation instanceof CollectionOperationInterface && null === $operation->getUriTemplate())
(
$operation instanceof CollectionOperationInterface
&& null === $operation->getUriTemplate()
)
)) {
if (null === $operation->getUriVariables()) {
return $operation;
Expand Down Expand Up @@ -152,7 +161,24 @@ private function configureUriVariables(ApiResource|HttpOperation $operation): Ap
return $operation->withUriVariables($newUriVariables);
}

return $operation;
// When an operation is generated we need to find properties matching it's uri variables
if (!($operation->getExtraProperties()['generated_operation'] ?? false) || !$this->linkFactory instanceof PropertyLinkFactoryInterface) {
return $operation;
}

$diff = array_diff($variables, array_keys($uriVariables));
if (0 === \count($diff)) {
return $operation;
}

// We generated this operation but there're some missing identifiers
$uriVariables = HttpOperation::METHOD_POST === $operation->getMethod() || $operation instanceof CollectionOperationInterface ? [] : $operation->getUriVariables();

foreach ($diff as $key) {
$uriVariables[$key] = $this->linkFactory->createLinkFromProperty($operation, $key);
}

return $operation->withUriVariables($uriVariables);
}

private function normalizeUriVariables(ApiResource|HttpOperation $operation): ApiResource|HttpOperation
Expand Down
66 changes: 66 additions & 0 deletions src/State/CreateProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?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\State;

use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Post;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

/**
* An ItemProvider for POST operations on generated subresources.
*
* @see ApiPlatform\Tests\Fixtures\TestBundle\Entity\SubresourceEmployee
*
* @author Antoine Bluchet <soyuka@gmail.com>
*
* @experimental
*/
final class CreateProvider implements ProviderInterface
{
public function __construct(private ProviderInterface $decorated, private ?PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object
{
if (!$uriVariables || !$operation instanceof HttpOperation || null !== $operation->getController()) {
return $this->decorated->provide($operation, $uriVariables, $context);
}

$operationUriVariables = $operation->getUriVariables();
$relationClass = current($operationUriVariables)->getFromClass();
$key = key($operationUriVariables);
$relationUriVariables = [];

foreach ($operationUriVariables as $parameterName => $value) {
if ($key === $parameterName) {
$relationUriVariables['id'] = new Link(identifiers: $value->getIdentifiers(), fromClass: $value->getFromClass(), parameterName: $key);
continue;
}

$relationUriVariables[$parameterName] = $value;
}

$relation = $this->decorated->provide(new Get(uriVariables: $relationUriVariables, class: $relationClass), $uriVariables);
$resource = new ($operation->getClass());
$this->propertyAccessor->setValue($resource, $key, $relation);

return $resource;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
<tag name="api_platform.state_provider" priority="-100" />
</service>
<service id="api_platform.doctrine_mongodb.odm.state.item_provider" alias="ApiPlatform\Doctrine\Odm\State\ItemProvider" />
<service id="api_platform.state.item_provider" alias="ApiPlatform\Doctrine\Odm\State\ItemProvider" />

<service id="api_platform.doctrine.odm.metadata.resource.metadata_collection_factory" class="ApiPlatform\Doctrine\Odm\Metadata\Resource\DoctrineMongoDbOdmResourceCollectionMetadataFactory" decorates="api_platform.metadata.resource.metadata_collection_factory" decoration-priority="40">
<argument type="service" id="doctrine_mongodb" />
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/Resources/config/doctrine_orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
<tag name="api_platform.state_provider" priority="-100" />
</service>
<service id="api_platform.doctrine.orm.state.item_provider" alias="ApiPlatform\Doctrine\Orm\State\ItemProvider" />
<service id="api_platform.state.item_provider" alias="ApiPlatform\Doctrine\Orm\State\ItemProvider" />

<service id="api_platform.doctrine.orm.search_filter" class="ApiPlatform\Doctrine\Orm\Filter\SearchFilter" public="false" abstract="true">
<argument type="service" id="doctrine" />
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/Resources/config/elasticsearch.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@

<tag name="api_platform.state_provider" priority="-100" />
</service>
<service id="api_platform.state.item_provider" alias="ApiPlatform\Elasticsearch\State\ItemProvider" />

<service id="ApiPlatform\Elasticsearch\State\CollectionProvider" class="ApiPlatform\Elasticsearch\State\CollectionProvider" public="false">
<argument type="service" id="api_platform.elasticsearch.client" />
Expand Down
8 changes: 8 additions & 0 deletions src/Symfony/Bundle/Resources/config/state.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,13 @@
<argument>%api_platform.collection.pagination.partial_parameter_name%</argument>
</service>
<service id="ApiPlatform\State\Pagination\PaginationOptions" alias="api_platform.pagination_options" />

<service id="ApiPlatform\State\CreateProvider" class="ApiPlatform\State\CreateProvider">
<argument type="service" id="api_platform.state.item_provider" />

<tag name="api_platform.state_provider" />
</service>
<service id="api_platform.state_provider.create" alias="ApiPlatform\State\CreateProvider" />

</services>
</container>
5 changes: 2 additions & 3 deletions tests/Fixtures/TestBundle/Entity/Company.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@
#[GetCollection]
#[Get]
#[Post]
#[ApiResource(
#[Get(
uriTemplate: '/employees/{employeeId}/rooms/{roomId}/company/{companyId}',
uriVariables: [
'employeeId' => ['from_class' => Employee::class, 'from_property' => 'company'],
],
)]
#[Get]
#[ApiResource(
#[Get(
uriTemplate: '/employees/{employeeId}/company',
uriVariables: [
'employeeId' => ['from_class' => Employee::class, 'from_property' => 'company'],
Expand Down

0 comments on commit 21f1f67

Please sign in to comment.