Skip to content

Commit 3f4bd56

Browse files
committed
feat: add ApiResource PHP 8 attribute
1 parent a2f719c commit 3f4bd56

File tree

6 files changed

+270
-6
lines changed

6 files changed

+270
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
/tests/Fixtures/app/var/
1212
/tests/Fixtures/app/public/bundles/
1313
/vendor/
14+
/Dockerfile

src/Annotation/ApiResource.php

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,28 @@
7171
* @Attribute("validationGroups", type="mixed"),
7272
* )
7373
*/
74+
#[\Attribute(\Attribute::TARGET_CLASS)]
7475
final class ApiResource
7576
{
7677
use AttributesHydratorTrait;
7778

79+
private const PUBLIC_PROPERTIES = [
80+
'description',
81+
'collectionOperations',
82+
'graphql',
83+
'iri',
84+
'itemOperations',
85+
'shortName',
86+
'subresourceOperations',
87+
];
88+
7889
/**
7990
* @internal
8091
*
8192
* @see \ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Configuration::addDefaultsSection
8293
*/
8394
public const CONFIGURABLE_DEFAULTS = [
84-
'accessControl',
85-
'accessControlMessage',
95+
'attributes',
8696
'security',
8797
'securityMessage',
8898
'securityPostDenormalize',
@@ -114,7 +124,6 @@ final class ApiResource
114124
'paginationEnabled',
115125
'paginationFetchJoinCollection',
116126
'paginationItemsPerPage',
117-
'maximumItemsPerPage',
118127
'paginationMaximumItemsPerPage',
119128
'paginationPartial',
120129
'paginationViaCursor',
@@ -453,10 +462,109 @@ final class ApiResource
453462
private $urlGenerationStrategy;
454463

455464
/**
465+
* @param array|string $valuesOrDescription
466+
* @param array $collectionOperations https://api-platform.com/docs/core/operations
467+
* @param array $graphql https://api-platform.com/docs/core/graphql
468+
* @param array $itemOperations https://api-platform.com/docs/core/operations
469+
* @param array $subresourceOperations https://api-platform.com/docs/core/subresources
470+
*
471+
* @param array $cacheHeaders https://api-platform.com/docs/core/performance/#setting-custom-http-cache-headers
472+
* @param array $denormalizationContext https://api-platform.com/docs/core/serialization/#using-serialization-groups
473+
* @param string $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties
474+
* @param bool $elasticsearch https://api-platform.com/docs/core/elasticsearch/
475+
* @param bool $fetchPartial https://api-platform.com/docs/core/performance/#fetch-partial
476+
* @param bool $forceEager https://api-platform.com/docs/core/performance/#force-eager
477+
* @param array $formats https://api-platform.com/docs/core/content-negotiation/#configuring-formats-for-a-specific-resource-or-operation
478+
* @param string[] $filters https://api-platform.com/docs/core/filters/#doctrine-orm-and-mongodb-odm-filters
479+
* @param string[] $hydraContext https://api-platform.com/docs/core/extending-jsonld-context/#hydra
480+
* @param string|false $input https://api-platform.com/docs/core/dto/#specifying-an-input-or-an-output-data-representation
481+
* @param bool|array $mercure https://api-platform.com/docs/core/mercure
482+
* @param bool $messenger https://api-platform.com/docs/core/messenger/#dispatching-a-resource-through-the-message-bus
483+
* @param array $normalizationContext https://api-platform.com/docs/core/serialization/#using-serialization-groups
484+
* @param array $openapiContext https://api-platform.com/docs/core/swagger/#using-the-openapi-and-swagger-contexts
485+
* @param array $order https://api-platform.com/docs/core/default-order/#overriding-default-order
486+
* @param string|false $output https://api-platform.com/docs/core/dto/#specifying-an-input-or-an-output-data-representation
487+
* @param bool $paginationClientEnabled https://api-platform.com/docs/core/pagination/#for-a-specific-resource-1
488+
* @param bool $paginationClientItemsPerPage https://api-platform.com/docs/core/pagination/#for-a-specific-resource-3
489+
* @param bool $paginationClientPartial https://api-platform.com/docs/core/pagination/#for-a-specific-resource-6
490+
* @param array $paginationViaCursor https://api-platform.com/docs/core/pagination/#cursor-based-pagination
491+
* @param bool $paginationEnabled https://api-platform.com/docs/core/pagination/#for-a-specific-resource
492+
* @param bool $paginationFetchJoinCollection https://api-platform.com/docs/core/pagination/#controlling-the-behavior-of-the-doctrine-orm-paginator
493+
* @param int $paginationItemsPerPage https://api-platform.com/docs/core/pagination/#changing-the-number-of-items-per-page
494+
* @param int $paginationMaximumItemsPerPage https://api-platform.com/docs/core/pagination/#changing-maximum-items-per-page
495+
* @param bool $paginationPartial https://api-platform.com/docs/core/performance/#partial-pagination
496+
* @param string $routePrefix https://api-platform.com/docs/core/operations/#prefixing-all-routes-of-all-operations
497+
* @param string $security https://api-platform.com/docs/core/security
498+
* @param string $securityMessage https://api-platform.com/docs/core/security/#configuring-the-access-control-error-message
499+
* @param string $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
500+
* @param string $securityPostDenormalizeMessage https://api-platform.com/docs/core/security/#configuring-the-access-control-error-message
501+
* @param bool $stateless
502+
* @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
503+
* @param array $swaggerContext https://api-platform.com/docs/core/swagger/#using-the-openapi-and-swagger-contexts
504+
* @param array $validationGroups https://api-platform.com/docs/core/validation/#using-validation-groups
505+
* @param int $urlGenerationStrategy
506+
*
456507
* @throws InvalidArgumentException
457508
*/
458-
public function __construct(array $values = [])
509+
public function __construct(
510+
$description = null,
511+
array $collectionOperations = [],
512+
array $graphql = [],
513+
string $iri = '',
514+
array $itemOperations = [],
515+
string $shortName = '',
516+
array $subresourceOperations = [],
517+
518+
// attributes
519+
?array $attributes = null,
520+
?array $cacheHeaders = null,
521+
?array $denormalizationContext = null,
522+
?string $deprecationReason = null,
523+
?bool $elasticsearch = null,
524+
?bool $fetchPartial = null,
525+
?bool $forceEager = null,
526+
?array $formats = null,
527+
?array $filters = null,
528+
?array $hydraContext = null,
529+
$input = null,
530+
$mercure = null,
531+
$messenger = null,
532+
?array $normalizationContext = null,
533+
?array $openapiContext = null,
534+
?array $order = null,
535+
$output = null,
536+
?bool $paginationClientEnabled = null,
537+
?bool $paginationClientItemsPerPage = null,
538+
?bool $paginationClientPartial = null,
539+
?array $paginationViaCursor = null,
540+
?bool $paginationEnabled = null,
541+
?bool $paginationFetchJoinCollection = null,
542+
?int $paginationItemsPerPage = null,
543+
?int $paginationMaximumItemsPerPage = null,
544+
?bool $paginationPartial = null,
545+
?string $routePrefix = null,
546+
?string $security = null,
547+
?string $securityMessage = null,
548+
?string $securityPostDenormalize = null,
549+
?string $securityPostDenormalizeMessage = null,
550+
?bool $stateless = null,
551+
?string $sunset = null,
552+
?array $swaggerContext = null,
553+
?array $validationGroups = null,
554+
?int $urlGenerationStrategy = null
555+
)
459556
{
460-
$this->hydrateAttributes($values);
557+
if (!is_array($description)) {
558+
foreach (self::PUBLIC_PROPERTIES as $prop) {
559+
$this->$prop = $$prop;
560+
}
561+
562+
$description = [];
563+
foreach (array_diff(self::CONFIGURABLE_DEFAULTS, self::PUBLIC_PROPERTIES) as $attribute) {
564+
$description[$attribute] = $$attribute;
565+
}
566+
}
567+
568+
$this->hydrateAttributes($description);
461569
}
462570
}

src/Metadata/Resource/Factory/AnnotationResourceMetadataFactory.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ final class AnnotationResourceMetadataFactory implements ResourceMetadataFactory
2929
private $decorated;
3030
private $defaults;
3131

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

59+
if (\PHP_VERSION_ID >= 80000 && $attributes = $reflectionClass->getAttributes(ApiResource::class)) {
60+
return $this->createMetadata($attributes[0]->newInstance(), $parentResourceMetadata);
61+
}
62+
63+
if (null === $this->reader) {
64+
$this->handleNotFound($parentResourceMetadata, $resourceClass);
65+
}
66+
5967
$resourceAnnotation = $this->reader->getClassAnnotation($reflectionClass, ApiResource::class);
6068
if (!$resourceAnnotation instanceof ApiResource) {
6169
return $this->handleNotFound($parentResourceMetadata, $resourceClass);

tests/Annotation/ApiResourceTest.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Core\Api\UrlGeneratorInterface;
1818
use ApiPlatform\Core\Exception\InvalidArgumentException;
1919
use ApiPlatform\Core\Tests\Fixtures\AnnotatedClass;
20+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyPhp8;
2021
use Doctrine\Common\Annotations\AnnotationReader;
2122
use PHPUnit\Framework\TestCase;
2223

@@ -111,6 +112,114 @@ public function testConstruct()
111112
], $resource->attributes);
112113
}
113114

115+
/**
116+
* @requires PHP 8.0
117+
*/
118+
public function testConstructAttribute()
119+
{
120+
$resource = eval(<<<'PHP'
121+
return new \ApiPlatform\Core\Annotation\ApiResource(
122+
security: 'is_granted("ROLE_FOO")',
123+
securityMessage: 'You are not foo.',
124+
securityPostDenormalize: 'is_granted("ROLE_BAR")',
125+
securityPostDenormalizeMessage: 'You are not bar.',
126+
attributes: ['foo' => 'bar', 'validation_groups' => ['baz', 'qux'], 'cache_headers' => ['max_age' => 0, 'shared_max_age' => 0, 'vary' => ['Custom-Vary-1', 'Custom-Vary-2']]],
127+
collectionOperations: ['bar' => ['foo']],
128+
denormalizationContext: ['groups' => ['foo']],
129+
description: 'description',
130+
fetchPartial: true,
131+
forceEager: false,
132+
formats: ['foo', 'bar' => ['application/bar']],
133+
filters: ['foo', 'bar'],
134+
graphql: ['query' => ['normalization_context' => ['groups' => ['foo', 'bar']]]],
135+
input: 'Foo',
136+
iri: 'http://example.com/res',
137+
itemOperations: ['foo' => ['bar']],
138+
mercure: ['private' => true],
139+
messenger: true,
140+
normalizationContext: ['groups' => ['bar']],
141+
order: ['foo', 'bar' => 'ASC'],
142+
openapiContext: ['description' => 'foo'],
143+
output: 'Bar',
144+
paginationClientEnabled: true,
145+
paginationClientItemsPerPage: true,
146+
paginationClientPartial: true,
147+
paginationEnabled: true,
148+
paginationFetchJoinCollection: true,
149+
paginationItemsPerPage: 42,
150+
paginationMaximumItemsPerPage: 50,
151+
paginationPartial: true,
152+
routePrefix: '/foo',
153+
shortName: 'shortName',
154+
subresourceOperations: [],
155+
swaggerContext: ['description' => 'bar'],
156+
validationGroups: ['foo', 'bar'],
157+
sunset: 'Thu, 11 Oct 2018 00:00:00 +0200',
158+
urlGenerationStrategy: \ApiPlatform\Core\Api\UrlGeneratorInterface::ABS_PATH,
159+
deprecationReason: 'reason',
160+
elasticsearch: true,
161+
hydraContext: ['hydra' => 'foo'],
162+
paginationViaCursor: ['foo'],
163+
stateless: true,
164+
);
165+
PHP
166+
);
167+
168+
$this->assertSame('shortName', $resource->shortName);
169+
$this->assertSame('description', $resource->description);
170+
$this->assertSame('http://example.com/res', $resource->iri);
171+
$this->assertSame(['foo' => ['bar']], $resource->itemOperations);
172+
$this->assertSame(['bar' => ['foo']], $resource->collectionOperations);
173+
$this->assertSame([], $resource->subresourceOperations);
174+
$this->assertSame(['query' => ['normalization_context' => ['groups' => ['foo', 'bar']]]], $resource->graphql);
175+
$this->assertEquals([
176+
'security' => 'is_granted("ROLE_FOO")',
177+
'security_message' => 'You are not foo.',
178+
'security_post_denormalize' => 'is_granted("ROLE_BAR")',
179+
'security_post_denormalize_message' => 'You are not bar.',
180+
'denormalization_context' => ['groups' => ['foo']],
181+
'fetch_partial' => true,
182+
'foo' => 'bar',
183+
'force_eager' => false,
184+
'formats' => ['foo', 'bar' => ['application/bar']],
185+
'filters' => ['foo', 'bar'],
186+
'input' => 'Foo',
187+
'mercure' => ['private' => true],
188+
'messenger' => true,
189+
'normalization_context' => ['groups' => ['bar']],
190+
'order' => ['foo', 'bar' => 'ASC'],
191+
'openapi_context' => ['description' => 'foo'],
192+
'output' => 'Bar',
193+
'pagination_client_enabled' => true,
194+
'pagination_client_items_per_page' => true,
195+
'pagination_client_partial' => true,
196+
'pagination_enabled' => true,
197+
'pagination_fetch_join_collection' => true,
198+
'pagination_items_per_page' => 42,
199+
'pagination_maximum_items_per_page' => 50,
200+
'pagination_partial' => true,
201+
'route_prefix' => '/foo',
202+
'swagger_context' => ['description' => 'bar'],
203+
'validation_groups' => ['baz', 'qux'],
204+
'cache_headers' => ['max_age' => 0, 'shared_max_age' => 0, 'vary' => ['Custom-Vary-1', 'Custom-Vary-2']],
205+
'sunset' => 'Thu, 11 Oct 2018 00:00:00 +0200',
206+
'url_generation_strategy' => 1,
207+
'deprecation_reason' => 'reason',
208+
'elasticsearch' => true,
209+
'hydra_context' => ['hydra' => 'foo'],
210+
'pagination_via_cursor' => ['foo'],
211+
'stateless' => true,
212+
], $resource->attributes);
213+
}
214+
215+
/**
216+
* @requires PHP 8.0
217+
*/
218+
public function testUseAttribute()
219+
{
220+
$this->assertSame('Hey PHP 8', (new \ReflectionClass(DummyPhp8::class))->getAttributes(ApiResource::class)[0]->getArguments()['description']);
221+
}
222+
114223
public function testApiResourceAnnotation()
115224
{
116225
$reader = new AnnotationReader();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiProperty;
17+
use ApiPlatform\Core\Annotation\ApiResource;
18+
19+
#[ApiResource(description: "Hey PHP 8")]
20+
class DummyPhp8
21+
{
22+
/**
23+
* @ApiProperty(identifier=true)
24+
*/
25+
public $id;
26+
}

tests/Metadata/Resource/Factory/AnnotationResourceMetadataFactoryTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2020
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
2121
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
22+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyPhp8;
2223
use ApiPlatform\Core\Tests\ProphecyTrait;
2324
use Doctrine\Common\Annotations\Reader;
2425
use PHPUnit\Framework\TestCase;
@@ -49,6 +50,17 @@ public function testCreate($reader, $decorated, string $expectedShortName, ?stri
4950
$this->assertEquals(['foo' => 'bar'], $metadata->getGraphql());
5051
}
5152

53+
/**
54+
* @requires PHP 8.0
55+
*/
56+
public function testCreateAttribute()
57+
{
58+
$factory = new AnnotationResourceMetadataFactory();
59+
$metadata = $factory->create(DummyPhp8::class);
60+
61+
$this->assertSame('Hey PHP 8', $metadata->getDescription());
62+
}
63+
5264
public function testCreateWithDefaults()
5365
{
5466
$defaults = [

0 commit comments

Comments
 (0)