Skip to content

Commit

Permalink
feat: add an ApiResource PHP 8 attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
dunglas committed Nov 26, 2020
1 parent a2f719c commit 6a7d09d
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
/tests/Fixtures/app/var/
/tests/Fixtures/app/public/bundles/
/vendor/
/Dockerfile
118 changes: 113 additions & 5 deletions src/Annotation/ApiResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,28 @@
* @Attribute("validationGroups", type="mixed"),
* )
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class ApiResource
{
use AttributesHydratorTrait;

private const PUBLIC_PROPERTIES = [
'description',
'collectionOperations',
'graphql',
'iri',
'itemOperations',
'shortName',
'subresourceOperations',
];

/**
* @internal
*
* @see \ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Configuration::addDefaultsSection
*/
public const CONFIGURABLE_DEFAULTS = [
'accessControl',
'accessControlMessage',
'attributes',
'security',
'securityMessage',
'securityPostDenormalize',
Expand Down Expand Up @@ -114,7 +124,6 @@ final class ApiResource
'paginationEnabled',
'paginationFetchJoinCollection',
'paginationItemsPerPage',
'maximumItemsPerPage',
'paginationMaximumItemsPerPage',
'paginationPartial',
'paginationViaCursor',
Expand Down Expand Up @@ -453,10 +462,109 @@ final class ApiResource
private $urlGenerationStrategy;

/**
* @param array|string $valuesOrDescription
* @param array $collectionOperations https://api-platform.com/docs/core/operations
* @param array $graphql https://api-platform.com/docs/core/graphql
* @param array $itemOperations https://api-platform.com/docs/core/operations
* @param array $subresourceOperations https://api-platform.com/docs/core/subresources
*
* @param array $cacheHeaders https://api-platform.com/docs/core/performance/#setting-custom-http-cache-headers
* @param array $denormalizationContext https://api-platform.com/docs/core/serialization/#using-serialization-groups
* @param string $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties
* @param bool $elasticsearch https://api-platform.com/docs/core/elasticsearch/
* @param bool $fetchPartial https://api-platform.com/docs/core/performance/#fetch-partial
* @param bool $forceEager https://api-platform.com/docs/core/performance/#force-eager
* @param array $formats https://api-platform.com/docs/core/content-negotiation/#configuring-formats-for-a-specific-resource-or-operation
* @param string[] $filters https://api-platform.com/docs/core/filters/#doctrine-orm-and-mongodb-odm-filters
* @param string[] $hydraContext https://api-platform.com/docs/core/extending-jsonld-context/#hydra
* @param string|false $input https://api-platform.com/docs/core/dto/#specifying-an-input-or-an-output-data-representation
* @param bool|array $mercure https://api-platform.com/docs/core/mercure
* @param bool $messenger https://api-platform.com/docs/core/messenger/#dispatching-a-resource-through-the-message-bus
* @param array $normalizationContext https://api-platform.com/docs/core/serialization/#using-serialization-groups
* @param array $openapiContext https://api-platform.com/docs/core/swagger/#using-the-openapi-and-swagger-contexts
* @param array $order https://api-platform.com/docs/core/default-order/#overriding-default-order
* @param string|false $output https://api-platform.com/docs/core/dto/#specifying-an-input-or-an-output-data-representation
* @param bool $paginationClientEnabled https://api-platform.com/docs/core/pagination/#for-a-specific-resource-1
* @param bool $paginationClientItemsPerPage https://api-platform.com/docs/core/pagination/#for-a-specific-resource-3
* @param bool $paginationClientPartial https://api-platform.com/docs/core/pagination/#for-a-specific-resource-6
* @param array $paginationViaCursor https://api-platform.com/docs/core/pagination/#cursor-based-pagination
* @param bool $paginationEnabled https://api-platform.com/docs/core/pagination/#for-a-specific-resource
* @param bool $paginationFetchJoinCollection https://api-platform.com/docs/core/pagination/#controlling-the-behavior-of-the-doctrine-orm-paginator
* @param int $paginationItemsPerPage https://api-platform.com/docs/core/pagination/#changing-the-number-of-items-per-page
* @param int $paginationMaximumItemsPerPage https://api-platform.com/docs/core/pagination/#changing-maximum-items-per-page
* @param bool $paginationPartial https://api-platform.com/docs/core/performance/#partial-pagination
* @param string $routePrefix https://api-platform.com/docs/core/operations/#prefixing-all-routes-of-all-operations
* @param string $security https://api-platform.com/docs/core/security
* @param string $securityMessage https://api-platform.com/docs/core/security/#configuring-the-access-control-error-message
* @param string $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
* @param string $securityPostDenormalizeMessage https://api-platform.com/docs/core/security/#configuring-the-access-control-error-message
* @param bool $stateless
* @param string $sunset https://api-platform.com/docs/core/deprecations/#setting-the-sunset-http-header-to-indicate-when-a-resource-or-an-operation-will-be-removed
* @param array $swaggerContext https://api-platform.com/docs/core/swagger/#using-the-openapi-and-swagger-contexts
* @param array $validationGroups https://api-platform.com/docs/core/validation/#using-validation-groups
* @param int $urlGenerationStrategy
*
* @throws InvalidArgumentException
*/
public function __construct(array $values = [])
public function __construct(
$description = null,
array $collectionOperations = [],
array $graphql = [],
string $iri = '',
array $itemOperations = [],
string $shortName = '',
array $subresourceOperations = [],

// attributes
?array $attributes = null,
?array $cacheHeaders = null,
?array $denormalizationContext = null,
?string $deprecationReason = null,
?bool $elasticsearch = null,
?bool $fetchPartial = null,
?bool $forceEager = null,
?array $formats = null,
?array $filters = null,
?array $hydraContext = null,
$input = null,
$mercure = null,
$messenger = null,
?array $normalizationContext = null,
?array $openapiContext = null,
?array $order = null,
$output = null,
?bool $paginationClientEnabled = null,
?bool $paginationClientItemsPerPage = null,
?bool $paginationClientPartial = null,
?array $paginationViaCursor = null,
?bool $paginationEnabled = null,
?bool $paginationFetchJoinCollection = null,
?int $paginationItemsPerPage = null,
?int $paginationMaximumItemsPerPage = null,
?bool $paginationPartial = null,
?string $routePrefix = null,
?string $security = null,
?string $securityMessage = null,
?string $securityPostDenormalize = null,
?string $securityPostDenormalizeMessage = null,
?bool $stateless = null,
?string $sunset = null,
?array $swaggerContext = null,
?array $validationGroups = null,
?int $urlGenerationStrategy = null
)
{
$this->hydrateAttributes($values);
if (!is_array($description)) {
foreach (self::PUBLIC_PROPERTIES as $prop) {
$this->$prop = $$prop;
}

$description = [];
foreach (array_diff(self::CONFIGURABLE_DEFAULTS, self::PUBLIC_PROPERTIES) as $attribute) {
$description[$attribute] = $$attribute;
}
}

$this->hydrateAttributes($description);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ final class AnnotationResourceMetadataFactory implements ResourceMetadataFactory
private $decorated;
private $defaults;

public function __construct(Reader $reader, ResourceMetadataFactoryInterface $decorated = null, array $defaults = [])
public function __construct(Reader $reader = null, ResourceMetadataFactoryInterface $decorated = null, array $defaults = [])
{
$this->reader = $reader;
$this->decorated = $decorated;
Expand All @@ -56,6 +56,14 @@ public function create(string $resourceClass): ResourceMetadata
return $this->handleNotFound($parentResourceMetadata, $resourceClass);
}

if (\PHP_VERSION_ID >= 80000 && $attributes = $reflectionClass->getAttributes(ApiResource::class)) {
return $this->createMetadata($attributes[0]->newInstance(), $parentResourceMetadata);
}

if (null === $this->reader) {
$this->handleNotFound($parentResourceMetadata, $resourceClass);
}

$resourceAnnotation = $this->reader->getClassAnnotation($reflectionClass, ApiResource::class);
if (!$resourceAnnotation instanceof ApiResource) {
return $this->handleNotFound($parentResourceMetadata, $resourceClass);
Expand Down
109 changes: 109 additions & 0 deletions tests/Annotation/ApiResourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Core\Api\UrlGeneratorInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Tests\Fixtures\AnnotatedClass;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyPhp8;
use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -111,6 +112,114 @@ public function testConstruct()
], $resource->attributes);
}

/**
* @requires PHP 8.0
*/
public function testConstructAttribute()
{
$resource = eval(<<<'PHP'
return new \ApiPlatform\Core\Annotation\ApiResource(
security: 'is_granted("ROLE_FOO")',
securityMessage: 'You are not foo.',
securityPostDenormalize: 'is_granted("ROLE_BAR")',
securityPostDenormalizeMessage: 'You are not bar.',
attributes: ['foo' => 'bar', 'validation_groups' => ['baz', 'qux'], 'cache_headers' => ['max_age' => 0, 'shared_max_age' => 0, 'vary' => ['Custom-Vary-1', 'Custom-Vary-2']]],
collectionOperations: ['bar' => ['foo']],
denormalizationContext: ['groups' => ['foo']],
description: 'description',
fetchPartial: true,
forceEager: false,
formats: ['foo', 'bar' => ['application/bar']],
filters: ['foo', 'bar'],
graphql: ['query' => ['normalization_context' => ['groups' => ['foo', 'bar']]]],
input: 'Foo',
iri: 'http://example.com/res',
itemOperations: ['foo' => ['bar']],
mercure: ['private' => true],
messenger: true,
normalizationContext: ['groups' => ['bar']],
order: ['foo', 'bar' => 'ASC'],
openapiContext: ['description' => 'foo'],
output: 'Bar',
paginationClientEnabled: true,
paginationClientItemsPerPage: true,
paginationClientPartial: true,
paginationEnabled: true,
paginationFetchJoinCollection: true,
paginationItemsPerPage: 42,
paginationMaximumItemsPerPage: 50,
paginationPartial: true,
routePrefix: '/foo',
shortName: 'shortName',
subresourceOperations: [],
swaggerContext: ['description' => 'bar'],
validationGroups: ['foo', 'bar'],
sunset: 'Thu, 11 Oct 2018 00:00:00 +0200',
urlGenerationStrategy: \ApiPlatform\Core\Api\UrlGeneratorInterface::ABS_PATH,
deprecationReason: 'reason',
elasticsearch: true,
hydraContext: ['hydra' => 'foo'],
paginationViaCursor: ['foo'],
stateless: true,
);
PHP
);

$this->assertSame('shortName', $resource->shortName);
$this->assertSame('description', $resource->description);
$this->assertSame('http://example.com/res', $resource->iri);
$this->assertSame(['foo' => ['bar']], $resource->itemOperations);
$this->assertSame(['bar' => ['foo']], $resource->collectionOperations);
$this->assertSame([], $resource->subresourceOperations);
$this->assertSame(['query' => ['normalization_context' => ['groups' => ['foo', 'bar']]]], $resource->graphql);
$this->assertEquals([
'security' => 'is_granted("ROLE_FOO")',
'security_message' => 'You are not foo.',
'security_post_denormalize' => 'is_granted("ROLE_BAR")',
'security_post_denormalize_message' => 'You are not bar.',
'denormalization_context' => ['groups' => ['foo']],
'fetch_partial' => true,
'foo' => 'bar',
'force_eager' => false,
'formats' => ['foo', 'bar' => ['application/bar']],
'filters' => ['foo', 'bar'],
'input' => 'Foo',
'mercure' => ['private' => true],
'messenger' => true,
'normalization_context' => ['groups' => ['bar']],
'order' => ['foo', 'bar' => 'ASC'],
'openapi_context' => ['description' => 'foo'],
'output' => 'Bar',
'pagination_client_enabled' => true,
'pagination_client_items_per_page' => true,
'pagination_client_partial' => true,
'pagination_enabled' => true,
'pagination_fetch_join_collection' => true,
'pagination_items_per_page' => 42,
'pagination_maximum_items_per_page' => 50,
'pagination_partial' => true,
'route_prefix' => '/foo',
'swagger_context' => ['description' => 'bar'],
'validation_groups' => ['baz', 'qux'],
'cache_headers' => ['max_age' => 0, 'shared_max_age' => 0, 'vary' => ['Custom-Vary-1', 'Custom-Vary-2']],
'sunset' => 'Thu, 11 Oct 2018 00:00:00 +0200',
'url_generation_strategy' => 1,
'deprecation_reason' => 'reason',
'elasticsearch' => true,
'hydra_context' => ['hydra' => 'foo'],
'pagination_via_cursor' => ['foo'],
'stateless' => true,
], $resource->attributes);
}

/**
* @requires PHP 8.0
*/
public function testUseAttribute()
{
$this->assertSame('Hey PHP 8', (new \ReflectionClass(DummyPhp8::class))->getAttributes(ApiResource::class)[0]->getArguments()['description']);
}

public function testApiResourceAnnotation()
{
$reader = new AnnotationReader();
Expand Down
26 changes: 26 additions & 0 deletions tests/Fixtures/TestBundle/Entity/DummyPhp8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\Core\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;

#[ApiResource(description: "Hey PHP 8")]
class DummyPhp8
{
/**
* @ApiProperty(identifier=true)
*/
public $id;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyPhp8;
use ApiPlatform\Core\Tests\ProphecyTrait;
use Doctrine\Common\Annotations\Reader;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -49,6 +50,17 @@ public function testCreate($reader, $decorated, string $expectedShortName, ?stri
$this->assertEquals(['foo' => 'bar'], $metadata->getGraphql());
}

/**
* @requires PHP 8.0
*/
public function testCreateAttribute()
{
$factory = new AnnotationResourceMetadataFactory();
$metadata = $factory->create(DummyPhp8::class);

$this->assertSame('Hey PHP 8', $metadata->getDescription());
}

public function testCreateWithDefaults()
{
$defaults = [
Expand Down

0 comments on commit 6a7d09d

Please sign in to comment.