Skip to content

Commit

Permalink
Offer post, put, delete, patch subresource routes
Browse files Browse the repository at this point in the history
  • Loading branch information
torreytsui committed Mar 14, 2019
1 parent cce3b94 commit 99f8ad2
Show file tree
Hide file tree
Showing 8 changed files with 1,228 additions and 34 deletions.
42 changes: 42 additions & 0 deletions features/main/subresource.feature
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,48 @@ Feature: Subresource support
}
"""

Scenario: Post the subresource relation item
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/dummies/1/related_dummies" with body:
"""
{
"@context": "/contexts/RelatedDummy",
"@type": "https://schema.org/Product",
"name": null,
"symfony": "symfony",
"dummyDate": "2019-01-01",
"thirdLevel": "/third_levels/1",
"relatedToDummyFriend": [],
"dummyBoolean": null,
"embeddedDummy": [],
"age": null
}
"""
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/RelatedDummy",
"@id": "/related_dummies/3",
"@type": "https://schema.org/Product",
"id": 3,
"name": null,
"symfony": "symfony",
"dummyDate": "2019-01-01T00:00:00+00:00",
"thirdLevel": {
"@id": "/third_levels/1",
"@type": "ThirdLevel",
"fourthLevel": "/fourth_levels/1"
},
"relatedToDummyFriend": [],
"dummyBoolean": null,
"embeddedDummy": [],
"age": null
}
"""

Scenario: Get the subresource relation item
When I send a "GET" request to "/dummies/1/related_dummies/2"
And the response status code should be 200
Expand Down
5 changes: 5 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@
<service id="api_platform.action.put_item" alias="api_platform.action.placeholder" public="true" />
<service id="api_platform.action.delete_item" alias="api_platform.action.placeholder" public="true" />
<service id="api_platform.action.get_subresource" alias="api_platform.action.placeholder" public="true" />
<service id="api_platform.action.post_subresource" alias="api_platform.action.placeholder" public="true" />
<service id="api_platform.action.put_subresource" alias="api_platform.action.placeholder" public="true" />
<service id="api_platform.action.delete_subresource" alias="api_platform.action.placeholder" public="true" />
<service id="api_platform.action.patch_subresource" alias="api_platform.action.placeholder" public="true" />

<service id="api_platform.action.entrypoint" class="ApiPlatform\Core\Action\EntrypointAction" public="true">
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
Expand Down Expand Up @@ -287,6 +291,7 @@
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
<argument type="service" id="api_platform.path_segment_name_generator" />
<argument>%api_platform.formats%</argument>
</service>

<service id="api_platform.subresource_operation_factory.cached" class="ApiPlatform\Core\Operation\Factory\CachedSubresourceOperationFactory" decorates="api_platform.subresource_operation_factory" decoration-priority="-10" public="false">
Expand Down
42 changes: 25 additions & 17 deletions src/Bridge/Symfony/Routing/ApiLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,9 @@ public function load($data, $type = null): RouteCollection
}

foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $operation) {
if (null === $controller = $operation['controller'] ?? null) {
$controller = self::DEFAULT_ACTION_PATTERN.'get_subresource';
$this->assertOperationMethod($resourceClass, $operationId, $operation);

if (!$this->container->has($controller)) {
throw new RuntimeException(sprintf('There is no builtin action for the %s %s operation. You need to define the controller yourself.', OperationType::SUBRESOURCE, 'GET'));
}
}
$controller = $this->resolveOperationController($operation, OperationType::SUBRESOURCE);

$routeCollection->add($operation['route_name'], new Route(
$operation['path'],
Expand All @@ -135,7 +131,7 @@ public function load($data, $type = null): RouteCollection
$operation['options'] ?? [],
$operation['host'] ?? '',
$operation['schemes'] ?? [],
['GET'],
[$operation['method']],
$operation['condition'] ?? ''
));
}
Expand Down Expand Up @@ -189,17 +185,9 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
return;
}

if (!isset($operation['method'])) {
throw new RuntimeException(sprintf('Either a "route_name" or a "method" operation attribute must exist for the operation "%s" of the resource "%s".', $operationName, $resourceClass));
}

if (null === $controller = $operation['controller'] ?? null) {
$controller = sprintf('%s%s_%s', self::DEFAULT_ACTION_PATTERN, strtolower($operation['method']), $operationType);
$this->assertOperationMethod($resourceClass, $operationName, $operation);

if (!$this->container->has($controller)) {
throw new RuntimeException(sprintf('There is no builtin action for the %s %s operation. You need to define the controller yourself.', $operationType, $operation['method']));
}
}
$controller = $this->resolveOperationController($operation, $operationType);

$path = trim(trim($resourceMetadata->getAttribute('route_prefix', '')), '/');
$path .= $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
Expand All @@ -222,4 +210,24 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas

$routeCollection->add(RouteNameGenerator::generate($operationName, $resourceShortName, $operationType), $route);
}

private function assertOperationMethod(string $resourceClass, string $operationName, array $operation)
{
if (!isset($operation['method'])) {
throw new RuntimeException(sprintf('Either a "route_name" or a "method" operation attribute must exist for the operation "%s" of the resource "%s".', $operationName, $resourceClass));
}
}

private function resolveOperationController(array $operation, string $operationType): string
{
if (null === $controller = $operation['controller'] ?? null) {
$controller = sprintf('%s%s_%s', self::DEFAULT_ACTION_PATTERN, strtolower($operation['method']), $operationType);

if (!$this->container->has($controller)) {
throw new RuntimeException(sprintf('There is no builtin action for the %s %s operation. You need to define the controller yourself.', $operationType, $operation['method']));
}
}

return $controller;
}
}
33 changes: 22 additions & 11 deletions src/Metadata/Resource/Factory/OperationResourceMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,19 @@ public function __construct(ResourceMetadataFactoryInterface $decorated, array $
}

/**
* {@inheritdoc}
* @internal
*/
public function create(string $resourceClass): ResourceMetadata
public static function populateOperations(string $resourceClass, ResourceMetadata $resourceMetadata, array $formats): ResourceMetadata
{
$resourceMetadata = $this->decorated->create($resourceClass);
$isAbstract = (new \ReflectionClass($resourceClass))->isAbstract();

$collectionOperations = $resourceMetadata->getCollectionOperations();
if (null === $collectionOperations) {
$resourceMetadata = $resourceMetadata->withCollectionOperations($this->createOperations(
$resourceMetadata = $resourceMetadata->withCollectionOperations(static::createOperations(
$isAbstract ? ['GET'] : ['GET', 'POST']
));
} else {
$resourceMetadata = $this->normalize(true, $resourceMetadata, $collectionOperations);
$resourceMetadata = static::normalize(true, $resourceMetadata, $collectionOperations, $formats);
}

$itemOperations = $resourceMetadata->getItemOperations();
Expand All @@ -72,16 +71,28 @@ public function create(string $resourceClass): ResourceMetadata
if (!$isAbstract) {
$methods[] = 'PUT';

if (isset($this->formats['jsonapi'])) {
if (isset($formats['jsonapi'])) {
$methods[] = 'PATCH';
}
}

$resourceMetadata = $resourceMetadata->withItemOperations($this->createOperations($methods));
$resourceMetadata = $resourceMetadata->withItemOperations(static::createOperations($methods));
} else {
$resourceMetadata = $this->normalize(false, $resourceMetadata, $itemOperations);
$resourceMetadata = static::normalize(false, $resourceMetadata, $itemOperations, $formats);
}

return $resourceMetadata;
}

/**
* {@inheritdoc}
*/
public function create(string $resourceClass): ResourceMetadata
{
$resourceMetadata = $this->decorated->create($resourceClass);
$formats = $this->formats;
$resourceMetadata = self::populateOperations($resourceClass, $resourceMetadata, $formats);

$graphql = $resourceMetadata->getGraphql();
if (null === $graphql) {
$resourceMetadata = $resourceMetadata->withGraphql(['query' => [], 'delete' => [], 'update' => [], 'create' => []]);
Expand All @@ -92,7 +103,7 @@ public function create(string $resourceClass): ResourceMetadata
return $resourceMetadata;
}

private function createOperations(array $methods): array
private static function createOperations(array $methods): array
{
$operations = [];
foreach ($methods as $method) {
Expand All @@ -102,7 +113,7 @@ private function createOperations(array $methods): array
return $operations;
}

private function normalize(bool $collection, ResourceMetadata $resourceMetadata, array $operations): ResourceMetadata
private static function normalize(bool $collection, ResourceMetadata $resourceMetadata, array $operations, array $formats): ResourceMetadata
{
$newOperations = [];
foreach ($operations as $operationName => $operation) {
Expand All @@ -116,7 +127,7 @@ private function normalize(bool $collection, ResourceMetadata $resourceMetadata,
if ($collection) {
$supported = isset(self::SUPPORTED_COLLECTION_OPERATION_METHODS[$upperOperationName]);
} else {
$supported = isset(self::SUPPORTED_ITEM_OPERATION_METHODS[$upperOperationName]) || (isset($this->formats['jsonapi']) && 'PATCH' === $upperOperationName);
$supported = isset(self::SUPPORTED_ITEM_OPERATION_METHODS[$upperOperationName]) || (isset($formats['jsonapi']) && 'PATCH' === $upperOperationName);
}

if (!isset($operation['method']) && !isset($operation['route_name'])) {
Expand Down
Loading

0 comments on commit 99f8ad2

Please sign in to comment.