Skip to content

Commit

Permalink
Control _stateless using resource (and defaults)
Browse files Browse the repository at this point in the history
  • Loading branch information
mtarld committed Mar 9, 2020
1 parent 924524a commit d38dad2
Show file tree
Hide file tree
Showing 11 changed files with 52 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@
* GraphQL: Add page-based pagination (#3175)
* OpenAPI: Add PHP default values to the documentation (#2386)
* Deprecate using a validation groups generator service not implementing `ApiPlatform\Core\Bridge\Symfony\Validator\ValidationGroupsGeneratorInterface` (#3346)
* Add stateless ApiResource attribute

## 2.5.x-dev

Expand Down
11 changes: 10 additions & 1 deletion src/Annotation/ApiResource.php
Expand Up @@ -66,7 +66,8 @@
* @Attribute("subresourceOperations", type="array"),
* @Attribute("sunset", type="string"),
* @Attribute("swaggerContext", type="array"),
* @Attribute("validationGroups", type="mixed")
* @Attribute("validationGroups", type="mixed"),
* @Attribute("stateless", type="bool")
* )
*/
final class ApiResource
Expand Down Expand Up @@ -120,6 +121,7 @@ final class ApiResource
'sunset',
'swaggerContext',
'validationGroups',
'stateless',
];

/**
Expand Down Expand Up @@ -165,6 +167,13 @@ final class ApiResource
*/
public $subresourceOperations;

/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
*
* @var bool
*/
public $stateless;

/**
* @see https://api-platform.com/docs/core/performance/#setting-custom-http-cache-headers
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
Expand Down
Expand Up @@ -120,7 +120,6 @@ public function getConfigTreeBuilder()
->booleanNode('enable_entrypoint')->defaultTrue()->info('Enable the entrypoint')->end()
->booleanNode('enable_docs')->defaultTrue()->info('Enable the docs')->end()
->booleanNode('enable_profiler')->defaultTrue()->info('Enable the data collector and the WebProfilerBundle integration.')->end()
->booleanNode('enable_stateless')->defaultFalse()->info('Enable the stateless reporting integration.')->end()
->arrayNode('collection')
->addDefaultsIfNotSet()
->children()
Expand Down
1 change: 0 additions & 1 deletion src/Bridge/Symfony/Bundle/Resources/config/api.xml
Expand Up @@ -44,7 +44,6 @@
<argument>%api_platform.enable_docs%</argument>
<argument>%api_platform.graphql.graphiql.enabled%</argument>
<argument>%api_platform.graphql.graphql_playground.enabled%</argument>
<argument>%api_platform.enable_stateless%</argument>

<tag name="routing.loader" />
</service>
Expand Down
8 changes: 3 additions & 5 deletions src/Bridge/Symfony/Routing/ApiLoader.php
Expand Up @@ -56,9 +56,8 @@ final class ApiLoader extends Loader
private $graphQlPlaygroundEnabled;
private $entrypointEnabled;
private $docsEnabled;
private $statelessEnabled;

public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, array $resourceClassDirectories = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $graphqlEnabled = false, bool $entrypointEnabled = true, bool $docsEnabled = true, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, bool $statelessEnabled = false)
public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, array $resourceClassDirectories = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $graphqlEnabled = false, bool $entrypointEnabled = true, bool $docsEnabled = true, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false)
{
/** @var string[]|string $paths */
$paths = $kernel->locateResource('@ApiPlatformBundle/Resources/config/routing');
Expand All @@ -75,7 +74,6 @@ public function __construct(KernelInterface $kernel, ResourceNameCollectionFacto
$this->graphQlPlaygroundEnabled = $graphQlPlaygroundEnabled;
$this->entrypointEnabled = $entrypointEnabled;
$this->docsEnabled = $docsEnabled;
$this->statelessEnabled = $statelessEnabled;
}

/**
Expand Down Expand Up @@ -136,7 +134,7 @@ public function load($data, $type = null): RouteCollection
'collection' => $operation['collection'],
'operationId' => $operationId,
],
] + ($operation['defaults'] ?? []) + ($this->statelessEnabled ? ['_stateless' => true] : []),
] + ($operation['defaults'] ?? []) + ($resourceMetadata->getStateless() ? ['_stateless' => true] : []),
$operation['requirements'] ?? [],
$operation['options'] ?? [],
$operation['host'] ?? '',
Expand Down Expand Up @@ -233,7 +231,7 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
'_format' => null,
'_api_resource_class' => $resourceClass,
sprintf('_api_%s_operation_name', $operationType) => $operationName,
] + ($operation['defaults'] ?? []) + ($this->statelessEnabled ? ['_stateless' => true] : []),
] + ($operation['defaults'] ?? []) + ($resourceMetadata->getStateless() ? ['_stateless' => true] : []),
$operation['requirements'] ?? [],
$operation['options'] ?? [],
$operation['host'] ?? '',
Expand Down
Expand Up @@ -91,12 +91,13 @@ private function createMetadata(ApiResource $annotation, ResourceMetadata $paren
$annotation->collectionOperations ?? $this->defaults['collection_operations'] ?? null,
$attributes,
$annotation->subresourceOperations,
$annotation->graphql ?? $this->defaults['graphql'] ?? null
$annotation->graphql ?? $this->defaults['graphql'] ?? null,
$annotation->stateless ?? $this->defaults['stateless'] ?? false
);
}

$resourceMetadata = $parentResourceMetadata;
foreach (['shortName', 'description', 'iri', 'itemOperations', 'collectionOperations', 'subresourceOperations', 'graphql', 'attributes'] as $property) {
foreach (['shortName', 'description', 'iri', 'itemOperations', 'collectionOperations', 'subresourceOperations', 'graphql', 'attributes', 'stateless'] as $property) {
$resourceMetadata = $this->createWith($resourceMetadata, $property, $annotation->{$property});
}

Expand Down
Expand Up @@ -59,6 +59,7 @@ public function create(string $resourceClass): ResourceMetadata
$resource['itemOperations'] = $resource['itemOperations'] ?? $this->defaults['item_operations'] ?? null;
$resource['collectionOperations'] = $resource['collectionOperations'] ?? $this->defaults['collection_operations'] ?? null;
$resource['graphql'] = $resource['graphql'] ?? $this->defaults['graphql'] ?? null;
$resource['stateless'] = $resource['stateless'] ?? $this->defaults['stateless'] ?? null;
$resource['attributes'] = (null === $resource['attributes'] && [] === $this->defaults['attributes']) ? null : (array) $resource['attributes'] + $this->defaults['attributes'];

return $this->update($parentResourceMetadata ?: new ResourceMetadata(), $resource);
Expand All @@ -83,7 +84,7 @@ private function handleNotFound(?ResourceMetadata $parentPropertyMetadata, strin
*/
private function update(ResourceMetadata $resourceMetadata, array $metadata): ResourceMetadata
{
foreach (['shortName', 'description', 'iri', 'itemOperations', 'collectionOperations', 'subresourceOperations', 'graphql', 'attributes'] as $property) {
foreach (['shortName', 'description', 'iri', 'itemOperations', 'collectionOperations', 'subresourceOperations', 'graphql', 'attributes', 'stateless'] as $property) {
if (null === $metadata[$property] || null !== $resourceMetadata->{'get'.ucfirst($property)}()) {
continue;
}
Expand Down
23 changes: 22 additions & 1 deletion src/Metadata/Resource/ResourceMetadata.php
Expand Up @@ -29,9 +29,10 @@ final class ResourceMetadata
private $collectionOperations;
private $subresourceOperations;
private $graphql;
private $stateless;
private $attributes;

public function __construct(string $shortName = null, string $description = null, string $iri = null, array $itemOperations = null, array $collectionOperations = null, array $attributes = null, array $subresourceOperations = null, array $graphql = null)
public function __construct(string $shortName = null, string $description = null, string $iri = null, array $itemOperations = null, array $collectionOperations = null, array $attributes = null, array $subresourceOperations = null, array $graphql = null, bool $stateless = null)
{
$this->shortName = $shortName;
$this->description = $description;
Expand All @@ -40,6 +41,7 @@ public function __construct(string $shortName = null, string $description = null
$this->collectionOperations = $collectionOperations;
$this->subresourceOperations = $subresourceOperations;
$this->graphql = $graphql;
$this->stateless = $stateless;
$this->attributes = $attributes;
}

Expand Down Expand Up @@ -291,6 +293,25 @@ public function withGraphql(array $graphql): self
return $metadata;
}

/**
* Gets stateless option.
*/
public function getStateless(): ?bool
{
return $this->stateless;
}

/**
* Returns a new instance with the given stateless option.
*/
public function withStateless(bool $stateless): self
{
$metadata = clone $this;
$metadata->stateless = $stateless;

return $metadata;
}

/**
* Gets an operation attribute, optionally fallback to a resource attribute.
*
Expand Down
8 changes: 4 additions & 4 deletions tests/Bridge/Symfony/Routing/ApiLoaderTest.php
Expand Up @@ -221,12 +221,12 @@ public function testRecursiveSubresource()
public function testStatelessApiLoader()
{
$resourceMetadata = new ResourceMetadata();
$resourceMetadata = $resourceMetadata->withShortName('dummy');
$resourceMetadata = $resourceMetadata->withShortName('dummy')->withStateless(true);
$resourceMetadata = $resourceMetadata->withItemOperations([
'get' => ['method' => 'GET'],
]);

$routeCollection = $this->getApiLoaderWithResourceMetadata($resourceMetadata, false, true)->load(null);
$routeCollection = $this->getApiLoaderWithResourceMetadata($resourceMetadata, false)->load(null);

$this->assertEquals(
$this->getRoute('/dummies/{id}.{_format}', 'api_platform.action.get_item', DummyEntity::class, 'get', ['GET'], false, [], ['_stateless' => true]),
Expand All @@ -239,7 +239,7 @@ public function testStatelessApiLoader()
);
}

private function getApiLoaderWithResourceMetadata(ResourceMetadata $resourceMetadata, $recursiveSubresource = false, bool $stateless = false): ApiLoader
private function getApiLoaderWithResourceMetadata(ResourceMetadata $resourceMetadata, $recursiveSubresource = false): ApiLoader
{
$routingConfig = __DIR__.'/../../../../src/Bridge/Symfony/Bundle/Resources/config/routing';

Expand Down Expand Up @@ -312,7 +312,7 @@ private function getApiLoaderWithResourceMetadata(ResourceMetadata $resourceMeta

$subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), new UnderscorePathSegmentNameGenerator());

return new ApiLoader($kernelProphecy->reveal(), $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, $operationPathResolver, $containerProphecy->reveal(), ['jsonld' => ['application/ld+json']], [], $subresourceOperationFactory, false, true, true, false, false, $stateless);
return new ApiLoader($kernelProphecy->reveal(), $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, $operationPathResolver, $containerProphecy->reveal(), ['jsonld' => ['application/ld+json']], [], $subresourceOperationFactory, false, true, true, false, false);
}

private function getRoute(string $path, string $controller, string $resourceClass, string $operationName, array $methods, bool $collection = false, array $requirements = [], array $extraDefaults = [], array $options = [], string $host = '', array $schemes = [], string $condition = ''): Route
Expand Down
Expand Up @@ -44,6 +44,7 @@ public function testCreate($reader, $decorated, string $expectedShortName, strin
$this->assertEquals(['sub' => ['bus' => false]], $metadata->getSubresourceOperations());
$this->assertEquals(['a' => 1, 'route_prefix' => '/foobar'], $metadata->getAttributes());
$this->assertEquals(['foo' => 'bar'], $metadata->getGraphql());
$this->assertFalse($metadata->getStateless());
}

public function testCreateWithDefaults()
Expand All @@ -57,6 +58,7 @@ public function testCreateWithDefaults()
'pagination_items_per_page' => 4,
'pagination_maximum_items_per_page' => 6,
],
'stateless' => true,
];

$annotation = new ApiResource([
Expand All @@ -76,6 +78,7 @@ public function testCreateWithDefaults()
$this->assertEquals(['get', 'delete'], $metadata->getItemOperations());
$this->assertEquals(4, $metadata->getAttribute('pagination_items_per_page'));
$this->assertEquals(10, $metadata->getAttribute('pagination_maximum_items_per_page'));
$this->assertTrue($metadata->getStateless());
}

public function testCreateWithoutAttributes()
Expand All @@ -100,6 +103,7 @@ public function getCreateDependencies()
'subresourceOperations' => ['sub' => ['bus' => false]],
'attributes' => ['a' => 1, 'route_prefix' => '/foobar'],
'graphql' => ['foo' => 'bar'],
'stateless' => false,
]);

$reader = $this->prophesize(Reader::class);
Expand Down
Expand Up @@ -300,6 +300,7 @@ public function testItFallbacksToDefaultConfiguration()
'pagination_items_per_page' => 4,
'pagination_maximum_items_per_page' => 6,
],
'stateless' => true,
];
$resourceConfiguration = [
Dummy::class => [
Expand All @@ -310,6 +311,7 @@ public function testItFallbacksToDefaultConfiguration()
'attributes' => [
'pagination_maximum_items_per_page' => 10,
],
'stateless' => null,
],
];

Expand All @@ -335,5 +337,6 @@ public function getResources(): array
$this->assertEquals(['get', 'delete'], $metadata->getItemOperations());
$this->assertEquals(4, $metadata->getAttribute('pagination_items_per_page'));
$this->assertEquals(10, $metadata->getAttribute('pagination_maximum_items_per_page'));
$this->assertTrue($metadata->getStateless());
}
}

0 comments on commit d38dad2

Please sign in to comment.