From b3b5da5cd4222df15e468132f01f830e86ef8974 Mon Sep 17 00:00:00 2001 From: W0rma Date: Tue, 22 Oct 2024 11:11:27 +0200 Subject: [PATCH 1/9] Add attributes to make the code work even if doctrine/annotations is not installed --- .../AbstractSegmentedRepresentation.php | 7 +++++++ src/Representation/CollectionRepresentation.php | 3 +++ src/Representation/OffsetRepresentation.php | 9 +++++++++ src/Representation/PaginatedRepresentation.php | 13 +++++++++++++ src/Representation/RouteAwareRepresentation.php | 4 ++++ src/Representation/VndErrorRepresentation.php | 10 ++++++++++ 6 files changed, 46 insertions(+) diff --git a/src/Representation/AbstractSegmentedRepresentation.php b/src/Representation/AbstractSegmentedRepresentation.php index f0e67d92..d824b027 100644 --- a/src/Representation/AbstractSegmentedRepresentation.php +++ b/src/Representation/AbstractSegmentedRepresentation.php @@ -9,6 +9,7 @@ /** * @Serializer\ExclusionPolicy("all") */ +#[Serializer\ExclusionPolicy('all')] abstract class AbstractSegmentedRepresentation extends RouteAwareRepresentation { /** @@ -18,6 +19,9 @@ abstract class AbstractSegmentedRepresentation extends RouteAwareRepresentation * * @var int */ + #[Serializer\Expose] + #[Serializer\Type('integer')] + #[Serializer\XmlAttribute] private $limit; /** @@ -27,6 +31,9 @@ abstract class AbstractSegmentedRepresentation extends RouteAwareRepresentation * * @var int */ + #[Serializer\Expose] + #[Serializer\Type('integer')] + #[Serializer\XmlAttribute] private $total; /** diff --git a/src/Representation/CollectionRepresentation.php b/src/Representation/CollectionRepresentation.php index 29537f32..be677eee 100644 --- a/src/Representation/CollectionRepresentation.php +++ b/src/Representation/CollectionRepresentation.php @@ -16,6 +16,9 @@ * embedded = @Hateoas\Embedded("expr(object.getResources())") * ) */ +#[Serializer\ExclusionPolicy('all')] +#[Serializer\XmlRoot('collection')] +#[Hateoas\Relation(name: 'items', embedded: new Hateoas\Embedded(content: 'expr(object.getResources())'))] class CollectionRepresentation { /** diff --git a/src/Representation/OffsetRepresentation.php b/src/Representation/OffsetRepresentation.php index bf2847b7..6fa68e32 100644 --- a/src/Representation/OffsetRepresentation.php +++ b/src/Representation/OffsetRepresentation.php @@ -54,6 +54,13 @@ * ) * ) */ +#[Serializer\ExclusionPolicy('all')] +#[Serializer\XmlRoot('collection')] +#[Serializer\AccessorOrder(order: 'custom', custom: ['offset', 'limit', 'total'])] +#[Hateoas\Relation(name: 'first', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(0))', absolute: 'expr(object.isAbsolute())'))] +#[Hateoas\Relation(name: 'last', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters((object.getTotal() - 1) - (object.getTotal() - 1) % object.getLimit()))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getTotal() === null)'))] +#[Hateoas\Relation(name: 'next', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(object.getOffset() + object.getLimit()))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getTotal() !== null && (object.getOffset() + object.getLimit()) >= object.getTotal())'))] +#[Hateoas\Relation(name: 'previous', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters((object.getOffset() > object.getLimit()) ? object.getOffset() - object.getLimit() : 0))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr(! object.getOffset())'))] class OffsetRepresentation extends AbstractSegmentedRepresentation { /** @@ -62,6 +69,8 @@ class OffsetRepresentation extends AbstractSegmentedRepresentation * * @var int */ + #[Serializer\Expose] + #[Serializer\XmlAttribute] private $offset; /** diff --git a/src/Representation/PaginatedRepresentation.php b/src/Representation/PaginatedRepresentation.php index ef864bc5..05be1890 100644 --- a/src/Representation/PaginatedRepresentation.php +++ b/src/Representation/PaginatedRepresentation.php @@ -54,6 +54,13 @@ * ) * ) */ +#[Serializer\ExclusionPolicy('all')] +#[Serializer\XmlRoot('collection')] +#[Serializer\AccessorOrder(order: 'custom', custom: ['page', 'limit', 'pages', 'total'])] +#[Hateoas\Relation(name: 'first', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(1))', absolute: 'expr(object.isAbsolute())'))] +#[Hateoas\Relation(name: 'last', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(object.getPages()))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getPages() === null)'))] +#[Hateoas\Relation(name: 'next', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(object.getPage() + 1))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getPages() !== null && (object.getPage() + 1) > object.getPages())'))] +#[Hateoas\Relation(name: 'previous', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(object.getPage() - 1))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr((object.getPage() - 1) < 1)'))] class PaginatedRepresentation extends AbstractSegmentedRepresentation { /** @@ -63,6 +70,9 @@ class PaginatedRepresentation extends AbstractSegmentedRepresentation * * @var int */ + #[Serializer\Expose] + #[Serializer\Type('integer')] + #[Serializer\XmlAttribute] private $page; /** @@ -72,6 +82,9 @@ class PaginatedRepresentation extends AbstractSegmentedRepresentation * * @var int */ + #[Serializer\Expose] + #[Serializer\Type('integer')] + #[Serializer\XmlAttribute] private $pages; /** diff --git a/src/Representation/RouteAwareRepresentation.php b/src/Representation/RouteAwareRepresentation.php index 6532c90b..12bbda79 100644 --- a/src/Representation/RouteAwareRepresentation.php +++ b/src/Representation/RouteAwareRepresentation.php @@ -19,6 +19,8 @@ * ) * ) */ +#[Serializer\ExclusionPolicy('all')] +#[Hateoas\Relation(name: 'self', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters())', absolute: 'expr(object.isAbsolute())'))] class RouteAwareRepresentation { /** @@ -27,6 +29,8 @@ class RouteAwareRepresentation * * @var mixed */ + #[Serializer\Inline] + #[Serializer\Expose] private $inline; /** diff --git a/src/Representation/VndErrorRepresentation.php b/src/Representation/VndErrorRepresentation.php index 756beb41..46b50a5c 100644 --- a/src/Representation/VndErrorRepresentation.php +++ b/src/Representation/VndErrorRepresentation.php @@ -33,6 +33,11 @@ * ) * ) */ +#[Serializer\ExclusionPolicy('all')] +#[Serializer\XmlRoot('resource')] +#[Hateoas\Relation(name: 'help', href: 'expr(object.getHelp())', exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getHelp() === null)'))] +#[Hateoas\Relation(name: 'describes', href: 'expr(object.getDescribes())', exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getDescribes() === null)'))] +#[Hateoas\Relation(name: 'about', href: 'expr(object.getAbout())', exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getAbout() === null)'))] class VndErrorRepresentation { /** @@ -41,6 +46,8 @@ class VndErrorRepresentation * * @var string */ + #[Serializer\Expose] + #[Serializer\Type('string')] private $message; /** @@ -50,6 +57,9 @@ class VndErrorRepresentation * * @var int */ + #[Serializer\Expose] + #[Serializer\XmlAttribute] + #[Serializer\Type('integer')] private $logref; /** From 4ad9c2ede2d9beee104f8fdfea407dc51a7f6b0c Mon Sep 17 00:00:00 2001 From: W0rma Date: Tue, 22 Oct 2024 11:13:38 +0200 Subject: [PATCH 2/9] Add AnnotationDriver only if doctrine/annotations is installed --- src/HateoasBuilder.php | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/HateoasBuilder.php b/src/HateoasBuilder.php index 81a5fb9a..0a3b86b5 100644 --- a/src/HateoasBuilder.php +++ b/src/HateoasBuilder.php @@ -8,6 +8,7 @@ use Doctrine\Common\Annotations\FileCacheReader; use Hateoas\Configuration\Metadata\ConfigurationExtensionInterface; use Hateoas\Configuration\Metadata\Driver\AnnotationDriver; +use Hateoas\Configuration\Metadata\Driver\AttributeDriver; use Hateoas\Configuration\Metadata\Driver\ExtensionDriver; use Hateoas\Configuration\Metadata\Driver\XmlDriver; use Hateoas\Configuration\Metadata\Driver\YamlDriver; @@ -385,33 +386,35 @@ public function replaceMetadataDir(string $dir, string $namespacePrefix = ''): H private function buildMetadataFactory(): MetadataFactoryInterface { + $expressionEvaluator = $this->getExpressionEvaluator(); + + $typeParser = new Parser(); + $annotationReader = $this->annotationReader; + $drivers = []; - if (null === $annotationReader) { + if (null === $annotationReader && class_exists(AnnotationReader::class)) { $annotationReader = new AnnotationReader(); if (null !== $this->cacheDir) { $this->createDir($this->cacheDir . '/annotations'); $annotationReader = new FileCacheReader($annotationReader, $this->cacheDir . '/annotations', $this->debug); } - } - $expressionEvaluator = $this->getExpressionEvaluator(); + $drivers[] = new AnnotationDriver($annotationReader, $expressionEvaluator, $this->chainProvider, $typeParser); + } - $typeParser = new Parser(); + if (PHP_VERSION_ID >= 80100) { + $drivers[] = new AttributeDriver($expressionEvaluator, $this->chainProvider, $typeParser); + } if (!empty($this->metadataDirs)) { - $fileLocator = new FileLocator($this->metadataDirs); - $metadataDriver = new DriverChain([ - new YamlDriver($fileLocator, $expressionEvaluator, $this->chainProvider, $typeParser), - new XmlDriver($fileLocator, $expressionEvaluator, $this->chainProvider, $typeParser), - new AnnotationDriver($annotationReader, $expressionEvaluator, $this->chainProvider, $typeParser), - ]); - } else { - $metadataDriver = new AnnotationDriver($annotationReader, $expressionEvaluator, $this->chainProvider, $typeParser); + $fileLocator = new FileLocator($this->metadataDirs); + $drivers[] = new YamlDriver($fileLocator, $expressionEvaluator, $this->chainProvider, $typeParser); + $drivers[] = new XmlDriver($fileLocator, $expressionEvaluator, $this->chainProvider, $typeParser); } - $metadataDriver = new ExtensionDriver($metadataDriver, $this->configurationExtensions); + $metadataDriver = new ExtensionDriver(new DriverChain($drivers), $this->configurationExtensions); $metadataFactory = new MetadataFactory($metadataDriver, null, $this->debug); $metadataFactory->setIncludeInterfaces($this->includeInterfaceMetadata); From 8ee547f79016589dc72166f820f1f0d892074dea Mon Sep 17 00:00:00 2001 From: W0rma Date: Tue, 22 Oct 2024 11:14:45 +0200 Subject: [PATCH 3/9] Add copies of fixtures with native php attributes --- .../Metadata/Driver/AttributeDriverTest.php | 4 +- .../Tests/Fixtures/Attribute/AdrienBrault.php | 76 +++++++++++++++++++ .../Fixtures/Attribute/CircularReference1.php | 28 +++++++ .../Fixtures/Attribute/CircularReference2.php | 28 +++++++ .../Tests/Fixtures/Attribute/Computer.php | 26 +++++++ .../Hateoas/Tests/Fixtures/Attribute/Foo1.php | 19 +++++ .../Hateoas/Tests/Fixtures/Attribute/Foo2.php | 19 +++++ .../Hateoas/Tests/Fixtures/Attribute/Foo3.php | 16 ++++ .../Tests/Fixtures/Attribute/Gh236Bar.php | 17 +++++ .../Tests/Fixtures/Attribute/Gh236Foo.php | 35 +++++++++ .../Hateoas/Tests/Fixtures/Attribute/Post.php | 29 +++++++ .../Tests/Fixtures/Attribute/Smartphone.php | 26 +++++++ .../Hateoas/Tests/Fixtures/Attribute/User.php | 49 ++++++++++++ .../Attribute/UsersRepresentation.php | 22 ++++++ .../Hateoas/Tests/Fixtures/Attribute/Will.php | 41 ++++++++++ .../Attribute/WithAlternativeRouter.php | 19 +++++ .../Tests/Fixtures/UserPhpAttributes.php | 54 ------------- 17 files changed, 452 insertions(+), 56 deletions(-) create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/AdrienBrault.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/CircularReference1.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/CircularReference2.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/Computer.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/Foo1.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/Foo2.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/Foo3.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/Gh236Bar.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/Gh236Foo.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/Post.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/Smartphone.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/User.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/UsersRepresentation.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/Will.php create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/WithAlternativeRouter.php delete mode 100644 tests/Hateoas/Tests/Fixtures/UserPhpAttributes.php diff --git a/tests/Hateoas/Tests/Configuration/Metadata/Driver/AttributeDriverTest.php b/tests/Hateoas/Tests/Configuration/Metadata/Driver/AttributeDriverTest.php index 98a6c105..08e7dfef 100644 --- a/tests/Hateoas/Tests/Configuration/Metadata/Driver/AttributeDriverTest.php +++ b/tests/Hateoas/Tests/Configuration/Metadata/Driver/AttributeDriverTest.php @@ -5,7 +5,7 @@ namespace Hateoas\Tests\Configuration\Metadata\Driver; use Hateoas\Configuration\Metadata\Driver\AttributeDriver; -use Hateoas\Tests\Fixtures\UserPhpAttributes; +use Hateoas\Tests\Fixtures\Attribute\User; class AttributeDriverTest extends AbstractDriverTest { @@ -27,6 +27,6 @@ public function createDriver() protected function getUserClass(): string { - return UserPhpAttributes::class; + return User::class; } } diff --git a/tests/Hateoas/Tests/Fixtures/Attribute/AdrienBrault.php b/tests/Hateoas/Tests/Fixtures/Attribute/AdrienBrault.php new file mode 100644 index 00000000..5a5e49ec --- /dev/null +++ b/tests/Hateoas/Tests/Fixtures/Attribute/AdrienBrault.php @@ -0,0 +1,76 @@ +reference2 = $reference2; + } + + public function getReference2() + { + return $this->reference2; + } +} diff --git a/tests/Hateoas/Tests/Fixtures/Attribute/CircularReference2.php b/tests/Hateoas/Tests/Fixtures/Attribute/CircularReference2.php new file mode 100644 index 00000000..47e0912e --- /dev/null +++ b/tests/Hateoas/Tests/Fixtures/Attribute/CircularReference2.php @@ -0,0 +1,28 @@ +reference1 = $reference1; + } + + public function getReference1() + { + return $this->reference1; + } +} diff --git a/tests/Hateoas/Tests/Fixtures/Attribute/Computer.php b/tests/Hateoas/Tests/Fixtures/Attribute/Computer.php new file mode 100644 index 00000000..722eae26 --- /dev/null +++ b/tests/Hateoas/Tests/Fixtures/Attribute/Computer.php @@ -0,0 +1,26 @@ +name = $name; + } + + /** + * @return mixed + */ + public function getName() + { + return $this->name; + } +} diff --git a/tests/Hateoas/Tests/Fixtures/Attribute/Foo1.php b/tests/Hateoas/Tests/Fixtures/Attribute/Foo1.php new file mode 100644 index 00000000..c50c10eb --- /dev/null +++ b/tests/Hateoas/Tests/Fixtures/Attribute/Foo1.php @@ -0,0 +1,19 @@ +a = new Gh236Bar(); + $this->a->inner = new Gh236Bar(); + + $this->b = new Gh236Bar(); + $this->b->xxx = 'zzz'; + $this->b->inner = new Gh236Bar(); + } +} diff --git a/tests/Hateoas/Tests/Fixtures/Attribute/Post.php b/tests/Hateoas/Tests/Fixtures/Attribute/Post.php new file mode 100644 index 00000000..3b0abc90 --- /dev/null +++ b/tests/Hateoas/Tests/Fixtures/Attribute/Post.php @@ -0,0 +1,29 @@ + 'expr(object.getId())'], + ), +)] +class Post +{ + private $id; + + public function __construct($id) + { + $this->id = $id; + } + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Hateoas/Tests/Fixtures/Attribute/Smartphone.php b/tests/Hateoas/Tests/Fixtures/Attribute/Smartphone.php new file mode 100644 index 00000000..6790d414 --- /dev/null +++ b/tests/Hateoas/Tests/Fixtures/Attribute/Smartphone.php @@ -0,0 +1,26 @@ +name = $name; + } + + /** + * @return mixed + */ + public function getName() + { + return $this->name; + } +} diff --git a/tests/Hateoas/Tests/Fixtures/Attribute/User.php b/tests/Hateoas/Tests/Fixtures/Attribute/User.php new file mode 100644 index 00000000..dc80426a --- /dev/null +++ b/tests/Hateoas/Tests/Fixtures/Attribute/User.php @@ -0,0 +1,49 @@ + 'application/json'])] +#[Hateoas\Relation('foo', href: new Hateoas\Route(name: 'user_get', parameters: ['id' => 'expr(object.getId())']), embedded: 'expr(object.getFoo())')] +#[Hateoas\Relation('bar', href: 'foo', embedded: new Hateoas\Embedded(content: 'data', xmlElementName: 'barTag'))] +#[Hateoas\Relation('baz', href: new Hateoas\Route(name: 'user_get', parameters: ['id' => 'expr(object.getId())'], absolute: true), embedded: 'expr(object.getFoo())')] +#[Hateoas\Relation('boom', href: new Hateoas\Route(name: 'user_get', parameters: ['id' => 'expr(object.getId())'], absolute: false), embedded: 'expr(object.getFoo())')] +#[Hateoas\Relation('badaboom', embedded: 'expr(object.getFoo())')] +#[Hateoas\Relation( + 'hello', + href: '/hello', + exclusion: new Hateoas\Exclusion( + groups: ['group1', 'group2'], + sinceVersion: '1', + untilVersion: '2.2', + maxDepth: 42, + excludeIf: 'foo', + ), + embedded: new Hateoas\Embedded( + 'hello', + xmlElementName: 'barTag', + type: 'string', + exclusion: new Hateoas\Exclusion( + groups: ['group3', 'group4'], + sinceVersion: '1.1', + untilVersion: '2.3', + maxDepth: 43, + excludeIf: 'bar', + ) + ) +)] +#[Hateoas\Relation(name: 'attribute_with_expression', href: 'baz', attributes: ['baz' => 'expr(object.getId())'])] +#[Hateoas\RelationProvider(name: 'Hateoas\Tests\Fixtures\Attribute\User::getRelations')] +class User +{ + /** + * do not use for functional testing + */ + public static function getRelations() + { + return []; + } +} diff --git a/tests/Hateoas/Tests/Fixtures/Attribute/UsersRepresentation.php b/tests/Hateoas/Tests/Fixtures/Attribute/UsersRepresentation.php new file mode 100644 index 00000000..ad71c41b --- /dev/null +++ b/tests/Hateoas/Tests/Fixtures/Attribute/UsersRepresentation.php @@ -0,0 +1,22 @@ +inline = $inline; + } +} diff --git a/tests/Hateoas/Tests/Fixtures/Attribute/Will.php b/tests/Hateoas/Tests/Fixtures/Attribute/Will.php new file mode 100644 index 00000000..4539c275 --- /dev/null +++ b/tests/Hateoas/Tests/Fixtures/Attribute/Will.php @@ -0,0 +1,41 @@ + 'expr(object.getId())'], + ), +)] +#[Hateoas\Relation( + 'post', + href: "expr(link(object.getPost(), 'self', true))", +)] +class Will +{ + private $id; + + private $post; + + public function __construct($id, ?Post $post = null) + { + $this->id = $id; + $this->post = $post; + } + + public function getId() + { + return $this->id; + } + + public function getPost() + { + return $this->post; + } +} diff --git a/tests/Hateoas/Tests/Fixtures/Attribute/WithAlternativeRouter.php b/tests/Hateoas/Tests/Fixtures/Attribute/WithAlternativeRouter.php new file mode 100644 index 00000000..e823c5d8 --- /dev/null +++ b/tests/Hateoas/Tests/Fixtures/Attribute/WithAlternativeRouter.php @@ -0,0 +1,19 @@ + 'hello'], + generator: 'my_generator', + ), +)] +class WithAlternativeRouter +{ +} diff --git a/tests/Hateoas/Tests/Fixtures/UserPhpAttributes.php b/tests/Hateoas/Tests/Fixtures/UserPhpAttributes.php deleted file mode 100644 index ea57bbd8..00000000 --- a/tests/Hateoas/Tests/Fixtures/UserPhpAttributes.php +++ /dev/null @@ -1,54 +0,0 @@ - 'application/json'])] -#[Hateoas\Relation( - name: 'foo', - href: new Hateoas\Route(name: 'user_get', parameters: ['id' => 'expr(object.getId())']), - embedded: 'expr(object.getFoo())' -)] -#[Hateoas\Relation(name: 'bar', href: 'foo', embedded: new Hateoas\Embedded(content: 'data', xmlElementName: 'barTag'))] -#[Hateoas\Relation( - name: 'baz', - href: new Hateoas\Route(name: 'user_get', parameters: ['id' => 'expr(object.getId())'], absolute: true), - embedded: 'expr(object.getFoo())' -)] -#[Hateoas\Relation( - name: 'boom', - href: new Hateoas\Route(name: 'user_get', parameters: ['id' => 'expr(object.getId())'], absolute: false), - embedded: 'expr(object.getFoo())' -)] -#[Hateoas\Relation(name: 'badaboom', embedded: 'expr(object.getFoo())')] -#[Hateoas\Relation( - name: 'hello', - href: '/hello', - embedded: new Hateoas\Embedded( - content: 'hello', - type: 'string', - xmlElementName: 'barTag', - exclusion: new Hateoas\Exclusion( - groups: ['group3', 'group4'], - sinceVersion: '1.1', - untilVersion: '2.3', - maxDepth: 43, - excludeIf: 'bar' - ) - ), - exclusion: new Hateoas\Exclusion( - groups: ['group1', 'group2'], - sinceVersion: '1', - untilVersion: '2.2', - maxDepth: 42, - excludeIf: 'foo' - ) -)] -#[Hateoas\Relation(name: 'attribute_with_expression', href: 'baz', attributes: ['baz' => 'expr(object.getId())'])] -#[Hateoas\RelationProvider(name: 'Hateoas\Tests\Fixtures\User::getRelations')] -class UserPhpAttributes -{ -} From de343f6d63872703814169033146290c8a257503 Mon Sep 17 00:00:00 2001 From: W0rma Date: Tue, 22 Oct 2024 11:16:01 +0200 Subject: [PATCH 4/9] Use fixture depending on whether doctrine/annotations is installed --- .../Metadata/Driver/AnnotationDriverTest.php | 7 ++++ tests/Hateoas/Tests/HateoasBuilderTest.php | 36 +++++++++++++++---- tests/Hateoas/Tests/HateoasTest.php | 32 +++++++++++++---- .../OffsetRepresentationTest.php | 13 +++++-- .../PaginatedRepresentationTest.php | 13 +++++-- .../Serializer/JsonHalSerializerTest.php | 27 +++++++++++--- .../Tests/Serializer/XmlHalSerializerTest.php | 14 ++++++-- .../Tests/Serializer/XmlSerializerTest.php | 14 ++++++-- .../LinkExtensionIntegrationTest.php | 7 +++- .../functions/link_href.test | 20 +++++++++++ 10 files changed, 157 insertions(+), 26 deletions(-) create mode 100644 tests/Hateoas/Tests/Twig/FixturesAttribute/functions/link_href.test diff --git a/tests/Hateoas/Tests/Configuration/Metadata/Driver/AnnotationDriverTest.php b/tests/Hateoas/Tests/Configuration/Metadata/Driver/AnnotationDriverTest.php index 7c7556e0..9aa85059 100644 --- a/tests/Hateoas/Tests/Configuration/Metadata/Driver/AnnotationDriverTest.php +++ b/tests/Hateoas/Tests/Configuration/Metadata/Driver/AnnotationDriverTest.php @@ -9,6 +9,13 @@ class AnnotationDriverTest extends AbstractDriverTest { + public function setUp(): void + { + if (!class_exists(AnnotationReader::class)) { + $this->markTestSkipped('AnnotationReader is not available'); + } + } + public function createDriver() { return new AnnotationDriver( diff --git a/tests/Hateoas/Tests/HateoasBuilderTest.php b/tests/Hateoas/Tests/HateoasBuilderTest.php index 7bb1050a..07313fab 100644 --- a/tests/Hateoas/Tests/HateoasBuilderTest.php +++ b/tests/Hateoas/Tests/HateoasBuilderTest.php @@ -4,8 +4,10 @@ namespace Hateoas\Tests; +use Doctrine\Common\Annotations\AnnotationReader; use Hateoas\HateoasBuilder; use Hateoas\Tests\Fixtures\AdrienBrault; +use Hateoas\Tests\Fixtures\Attribute; use Hateoas\Tests\Fixtures\CircularReference1; use Hateoas\Tests\Fixtures\CircularReference2; use Hateoas\Tests\Fixtures\NoAnnotations; @@ -31,8 +33,14 @@ public function testSerializeAdrienBraultWithExclusion() { $hateoas = HateoasBuilder::buildHateoas(); - $adrienBrault = new AdrienBrault(); - $fakeAdrienBrault = new AdrienBrault(); + if (class_exists(AnnotationReader::class)) { + $adrienBrault = new AdrienBrault(); + $fakeAdrienBrault = new AdrienBrault(); + } else { + $adrienBrault = new Attribute\AdrienBrault(); + $fakeAdrienBrault = new Attribute\AdrienBrault(); + } + $fakeAdrienBrault->firstName = 'John'; $fakeAdrienBrault->lastName = 'Smith'; @@ -78,6 +86,12 @@ public function testAlternativeUrlGenerator() ->setUrlGenerator('my_generator', $brokenUrlGenerator) ->build(); + if (class_exists(AnnotationReader::class)) { + $withAlternativeRouter = new WithAlternativeRouter(); + } else { + $withAlternativeRouter = new Attribute\WithAlternativeRouter(); + } + $this->assertSame( << @@ -87,7 +101,7 @@ public function testAlternativeUrlGenerator() XML , - $hateoas->serialize(new WithAlternativeRouter(), 'xml') + $hateoas->serialize($withAlternativeRouter, 'xml') ); } @@ -95,8 +109,14 @@ public function testCyclicalReferences() { $hateoas = HateoasBuilder::create()->build(); - $reference1 = new CircularReference1(); - $reference2 = new CircularReference2(); + if (class_exists(AnnotationReader::class)) { + $reference1 = new CircularReference1(); + $reference2 = new CircularReference2(); + } else { + $reference1 = new Attribute\CircularReference1(); + $reference2 = new Attribute\CircularReference2(); + } + $reference1->setReference2($reference2); $reference2->setReference1($reference1); @@ -134,7 +154,11 @@ public function testWithNullInEmbedded() { $hateoas = HateoasBuilder::create()->build(); - $reference1 = new CircularReference1(); + if (class_exists(AnnotationReader::class)) { + $reference1 = new CircularReference1(); + } else { + $reference1 = new Attribute\CircularReference1(); + } $this->assertSame( <<expectException(\RuntimeException::class); - $this->expectExceptionMessage('Can not find the relation "unknown-rel" for the "Hateoas\Tests\Fixtures\Will" class'); - $this->assertNull($this->hateoas->getLinkHelper()->getLinkHref(new Will(123), 'unknown-rel')); - $this->assertNull($this->hateoas->getLinkHelper()->getLinkHref(new Will(123), 'unknown-rel', true)); + $this->expectExceptionMessage(sprintf('Can not find the relation "unknown-rel" for the "%s" class', $className)); + $this->assertNull($this->hateoas->getLinkHelper()->getLinkHref(new $className(123), 'unknown-rel')); + $this->assertNull($this->hateoas->getLinkHelper()->getLinkHref(new $className(123), 'unknown-rel', true)); } public function testGetLinkHrefUrl() { - $this->assertEquals('/users/123', $this->hateoas->getLinkHelper()->getLinkHref(new Will(123), 'self')); - $this->assertEquals('/users/123', $this->hateoas->getLinkHelper()->getLinkHref(new Will(123), 'self', false)); + if (class_exists(AnnotationReader::class)) { + $className = Will::class; + } else { + $className = Attribute\Will::class; + } + + $this->assertEquals('/users/123', $this->hateoas->getLinkHelper()->getLinkHref(new $className(123), 'self')); + $this->assertEquals('/users/123', $this->hateoas->getLinkHelper()->getLinkHref(new $className(123), 'self', false)); } public function testGetLinkHrefUrlWithAbsoluteTrue() { - $this->assertEquals('http://example.com/users/123', $this->hateoas->getLinkHelper()->getLinkHref(new Will(123), 'self', true)); + if (class_exists(AnnotationReader::class)) { + $className = Will::class; + } else { + $className = Attribute\Will::class; + } + + $this->assertEquals('http://example.com/users/123', $this->hateoas->getLinkHelper()->getLinkHref(new $className(123), 'self', true)); } } diff --git a/tests/Hateoas/Tests/Representation/OffsetRepresentationTest.php b/tests/Hateoas/Tests/Representation/OffsetRepresentationTest.php index 4d1d7cdb..a2bc486b 100644 --- a/tests/Hateoas/Tests/Representation/OffsetRepresentationTest.php +++ b/tests/Hateoas/Tests/Representation/OffsetRepresentationTest.php @@ -4,8 +4,10 @@ namespace Hateoas\Tests\Representation; +use Doctrine\Common\Annotations\AnnotationReader; use Hateoas\Representation\CollectionRepresentation; use Hateoas\Representation\OffsetRepresentation; +use Hateoas\Tests\Fixtures\Attribute; use Hateoas\Tests\Fixtures\UsersRepresentation; class OffsetRepresentationTest extends RepresentationTestCase @@ -65,6 +67,13 @@ public function testSerialize() , $this->halHateoas->serialize($collection, 'xml') ); + + if (class_exists(AnnotationReader::class)) { + $usersRepresentation = new UsersRepresentation($collection); + } else { + $usersRepresentation = new Attribute\UsersRepresentation($collection); + } + $this->assertSame( << @@ -82,7 +91,7 @@ public function testSerialize() XML , - $this->hateoas->serialize(new UsersRepresentation($collection), 'xml') + $this->hateoas->serialize($usersRepresentation, 'xml') ); $this->assertSame( @@ -99,7 +108,7 @@ public function testSerialize() XML , - $this->halHateoas->serialize(new UsersRepresentation($collection), 'xml') + $this->halHateoas->serialize($usersRepresentation, 'xml') ); $this->assertSame( diff --git a/tests/Hateoas/Tests/Representation/PaginatedRepresentationTest.php b/tests/Hateoas/Tests/Representation/PaginatedRepresentationTest.php index c5b40edf..f7d96c37 100644 --- a/tests/Hateoas/Tests/Representation/PaginatedRepresentationTest.php +++ b/tests/Hateoas/Tests/Representation/PaginatedRepresentationTest.php @@ -4,8 +4,10 @@ namespace Hateoas\Tests\Representation; +use Doctrine\Common\Annotations\AnnotationReader; use Hateoas\Representation\CollectionRepresentation; use Hateoas\Representation\PaginatedRepresentation; +use Hateoas\Tests\Fixtures\Attribute; use Hateoas\Tests\Fixtures\UsersRepresentation; class PaginatedRepresentationTest extends RepresentationTestCase @@ -65,6 +67,13 @@ public function testSerialize() , $this->halHateoas->serialize($collection, 'xml') ); + + if (class_exists(AnnotationReader::class)) { + $usersRepresentation = new UsersRepresentation($collection); + } else { + $usersRepresentation = new Attribute\UsersRepresentation($collection); + } + $this->assertSame( << @@ -82,7 +91,7 @@ public function testSerialize() XML , - $this->hateoas->serialize(new UsersRepresentation($collection), 'xml') + $this->hateoas->serialize($usersRepresentation, 'xml') ); $this->assertSame( <<halHateoas->serialize(new UsersRepresentation($collection), 'xml') + $this->halHateoas->serialize($usersRepresentation, 'xml') ); $this->assertSame( '{' diff --git a/tests/Hateoas/Tests/Serializer/JsonHalSerializerTest.php b/tests/Hateoas/Tests/Serializer/JsonHalSerializerTest.php index 09415679..f8e25784 100644 --- a/tests/Hateoas/Tests/Serializer/JsonHalSerializerTest.php +++ b/tests/Hateoas/Tests/Serializer/JsonHalSerializerTest.php @@ -4,6 +4,7 @@ namespace Hateoas\Tests\Serializer; +use Doctrine\Common\Annotations\AnnotationReader; use Hateoas\HateoasBuilder; use Hateoas\Model\Embedded; use Hateoas\Model\Link; @@ -11,6 +12,7 @@ use Hateoas\Serializer\JsonHalSerializer; use Hateoas\Serializer\Metadata\RelationPropertyMetadata; use Hateoas\Tests\Fixtures\AdrienBrault; +use Hateoas\Tests\Fixtures\Attribute; use Hateoas\Tests\Fixtures\Foo1; use Hateoas\Tests\Fixtures\Foo2; use Hateoas\Tests\Fixtures\Foo3; @@ -202,7 +204,11 @@ public function testSerializeCuriesWithMultipleEntriesShouldBeAnArray() public function testSerializeAdrienBrault() { $hateoas = HateoasBuilder::buildHateoas(); - $adrienBrault = new AdrienBrault(); + if (class_exists(AnnotationReader::class)) { + $adrienBrault = new AdrienBrault(); + } else { + $adrienBrault = new Attribute\AdrienBrault(); + } $this->assertSame( <<inline = $foo2; $foo2->inline = $foo3; @@ -284,7 +297,11 @@ public function testSerializeInlineJson() public function testGh236() { - $data = new CollectionRepresentation([new Gh236Foo()]); + if (class_exists(AnnotationReader::class)) { + $data = new CollectionRepresentation([new Gh236Foo()]); + } else { + $data = new CollectionRepresentation([new Attribute\Gh236Foo()]); + } $hateoas = HateoasBuilder::buildHateoas(); diff --git a/tests/Hateoas/Tests/Serializer/XmlHalSerializerTest.php b/tests/Hateoas/Tests/Serializer/XmlHalSerializerTest.php index 7088133e..f7a4b74c 100644 --- a/tests/Hateoas/Tests/Serializer/XmlHalSerializerTest.php +++ b/tests/Hateoas/Tests/Serializer/XmlHalSerializerTest.php @@ -4,10 +4,12 @@ namespace Hateoas\Tests\Serializer; +use Doctrine\Common\Annotations\AnnotationReader; use Hateoas\HateoasBuilder; use Hateoas\Representation\CollectionRepresentation; use Hateoas\Serializer\XmlHalSerializer; use Hateoas\Tests\Fixtures\AdrienBrault; +use Hateoas\Tests\Fixtures\Attribute; use Hateoas\Tests\Fixtures\Gh236Foo; use Hateoas\Tests\Fixtures\LinkAttributes; use Hateoas\Tests\TestCase; @@ -20,7 +22,11 @@ public function testSerializeAdrienBrault() $hateoas = HateoasBuilder::create() ->setXmlSerializer(new XmlHalSerializer()) ->build(); - $adrienBrault = new AdrienBrault(); + if (class_exists(AnnotationReader::class)) { + $adrienBrault = new AdrienBrault(); + } else { + $adrienBrault = new Attribute\AdrienBrault(); + } $this->assertSame( <<setXmlSerializer(new XmlHalSerializer()) diff --git a/tests/Hateoas/Tests/Serializer/XmlSerializerTest.php b/tests/Hateoas/Tests/Serializer/XmlSerializerTest.php index 7d581df5..0752e886 100644 --- a/tests/Hateoas/Tests/Serializer/XmlSerializerTest.php +++ b/tests/Hateoas/Tests/Serializer/XmlSerializerTest.php @@ -4,6 +4,7 @@ namespace Hateoas\Tests\Serializer; +use Doctrine\Common\Annotations\AnnotationReader; use Hateoas\HateoasBuilder; use Hateoas\Model\Embedded; use Hateoas\Model\Link; @@ -11,6 +12,7 @@ use Hateoas\Serializer\Metadata\RelationPropertyMetadata; use Hateoas\Serializer\XmlSerializer; use Hateoas\Tests\Fixtures\AdrienBrault; +use Hateoas\Tests\Fixtures\Attribute; use Hateoas\Tests\Fixtures\Gh236Foo; use Hateoas\Tests\Fixtures\LinkAttributes; use Hateoas\Tests\TestCase; @@ -98,7 +100,11 @@ public function testSerializeEmbeddeds() public function testSerializeAdrienBrault() { $hateoas = HateoasBuilder::buildHateoas(); - $adrienBrault = new AdrienBrault(); + if (class_exists(AnnotationReader::class)) { + $adrienBrault = new AdrienBrault(); + } else { + $adrienBrault = new Attribute\AdrienBrault(); + } $this->assertSame( << new \Hateoas\Tests\Fixtures\Attribute\Will(123, new \Hateoas\Tests\Fixtures\Attribute\Post(345)), +) +--EXPECT-- +/user_get/123 +/user_get/123 +http://example.com/user_get/123 +http://example.com/post_get/345 +http://example.com/post_get/345 +http://example.com/post_get/345 From 04f8eb1c4103f4a829e3f5f19801955fb7b653ef Mon Sep 17 00:00:00 2001 From: W0rma Date: Tue, 22 Oct 2024 11:27:08 +0200 Subject: [PATCH 5/9] Move doctrine/annotations to "require-dev" --- .github/workflows/ci.yaml | 19 +++++++++++++++++++ README.md | 11 +++++++++++ composer.json | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b912e993..4976849f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,10 +38,19 @@ jobs: symfony-require: "^6.0" - php-version: 8.1 symfony-require: "^6.0" + - php-version: 8.1 + symfony-require: "^6.0" + remove-annotations: yes + - php-version: 8.2 + symfony-require: "^6.0" - php-version: 8.2 symfony-require: "^6.0" + remove-annotations: yes + - php-version: 8.3 + symfony-require: "^6.0" - php-version: 8.3 symfony-require: "^6.0" + remove-annotations: yes - php-version: 8.4 symfony-require: "^6.0" composer-options: "--ignore-platform-req=php+" # TODO remove once phpspec/prophecy supports PHP 8.4 @@ -49,6 +58,9 @@ jobs: symfony-require: "^7.0" - php-version: 8.3 symfony-require: "^7.0" + - php-version: 8.3 + symfony-require: "^7.0" + remove-annotations: yes - php-version: 8.4 symfony-require: "^7.0" composer-options: "--ignore-platform-req=php+" # TODO remove once phpspec/prophecy supports PHP 8.4 @@ -65,6 +77,13 @@ jobs: ini-values: "zend.assertions=1" tools: "flex" + - name: "Remove remove-annotations if required" + if: "${{ matrix.remove-annotations == 'yes' }}" + env: + SYMFONY_REQUIRE: "${{ matrix.symfony-require }}" + run: | + composer remove --no-update --dev doctrine/annotations + - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v2" env: diff --git a/README.md b/README.md index 07a0d40d..8987fbe2 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,17 @@ This will resolve the latest stable version. Otherwise, install the library and setup the autoloader yourself. +If you want to use [**annotations**](#annotations) for configuration you need +to install the `doctrine/annotations` package: + +```sh +composer require doctrine/annotations +``` + +If your app uses PHP 8.1 or higher it is recommended to use native PHP +attributes. +In this case you don't need to install the Doctrine package. + ### Working With Symfony There is a bundle for that! Install the diff --git a/composer.json b/composer.json index 387935dd..14247e32 100644 --- a/composer.json +++ b/composer.json @@ -18,13 +18,13 @@ ], "require": { "php": "^7.2 | ^8.0", - "doctrine/annotations": "^1.13.2 || ^2.0", "jms/metadata": "^2.0", "jms/serializer": "^3.18.2", "symfony/expression-language": "~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0" }, "require-dev": { "phpunit/phpunit": "^7 | ^9.5.10", + "doctrine/annotations": "^1.13.2 || ^2.0", "doctrine/coding-standard": "^12.0", "doctrine/persistence": "^1.3.4 | ^2.0 | ^3.0", "pagerfanta/core": "^2.4 || ^3.0", From 54ddf683b5a7c125f6f301c2088f83dbd831af56 Mon Sep 17 00:00:00 2001 From: W0rma Date: Thu, 24 Oct 2024 05:47:28 +0200 Subject: [PATCH 6/9] Drop support for PHP < 8.1 --- .github/workflows/ci.yaml | 29 ++++-------- composer.json | 10 ++--- src/HateoasBuilder.php | 6 +-- .../CollectionRepresentation.php | 7 ++- src/Representation/OffsetRepresentation.php | 45 +++++++++++++++++-- .../PaginatedRepresentation.php | 45 +++++++++++++++++-- .../RouteAwareRepresentation.php | 9 +++- src/Representation/VndErrorRepresentation.php | 24 ++++++++-- .../Metadata/Driver/AttributeDriverTest.php | 7 --- 9 files changed, 131 insertions(+), 51 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4976849f..a95aa369 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,50 +17,37 @@ jobs: fail-fast: false matrix: php-version: - - "7.3" - - "7.4" - - "8.0" - "8.1" - "8.2" - "8.3" dependencies: - "highest" - "lowest" + remove-annotations: + - "yes" + - "no" symfony-require: - "^3.0" - "^4.0" - "^5.0" + - "^6.0" include: - php-version: 8.4 symfony-require: "^5.0" composer-options: "--ignore-platform-req=php+" # TODO remove once phpspec/prophecy supports PHP 8.4 - - php-version: 8.0 - symfony-require: "^6.0" - - php-version: 8.1 - symfony-require: "^6.0" - - php-version: 8.1 - symfony-require: "^6.0" - remove-annotations: yes - - php-version: 8.2 - symfony-require: "^6.0" - - php-version: 8.2 - symfony-require: "^6.0" - remove-annotations: yes - - php-version: 8.3 - symfony-require: "^6.0" - - php-version: 8.3 - symfony-require: "^6.0" - remove-annotations: yes - php-version: 8.4 symfony-require: "^6.0" composer-options: "--ignore-platform-req=php+" # TODO remove once phpspec/prophecy supports PHP 8.4 - php-version: 8.2 symfony-require: "^7.0" + - php-version: 8.2 + symfony-require: "^7.0" + remove-annotations: "yes" - php-version: 8.3 symfony-require: "^7.0" - php-version: 8.3 symfony-require: "^7.0" - remove-annotations: yes + remove-annotations: "yes" - php-version: 8.4 symfony-require: "^7.0" composer-options: "--ignore-platform-req=php+" # TODO remove once phpspec/prophecy supports PHP 8.4 diff --git a/composer.json b/composer.json index 14247e32..edfc7429 100644 --- a/composer.json +++ b/composer.json @@ -17,13 +17,13 @@ } ], "require": { - "php": "^7.2 | ^8.0", + "php": "^8.1", "jms/metadata": "^2.0", "jms/serializer": "^3.18.2", - "symfony/expression-language": "~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0" + "symfony/expression-language": "^3.4.47 || ~4.0 || ~5.0 || ~6.0 || ~7.0" }, "require-dev": { - "phpunit/phpunit": "^7 | ^9.5.10", + "phpunit/phpunit": "^9.5.10", "doctrine/annotations": "^1.13.2 || ^2.0", "doctrine/coding-standard": "^12.0", "doctrine/persistence": "^1.3.4 | ^2.0 | ^3.0", @@ -31,8 +31,8 @@ "phpdocumentor/type-resolver": "^1.5.1", "phpspec/prophecy-phpunit": "^2.0.1", "phpspec/prophecy": "^1.16", - "symfony/routing": "~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0", - "symfony/yaml": "~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0", + "symfony/routing": "^3.4.47 || ~4.0 || ~5.0 || ~6.0 || ~7.0", + "symfony/yaml": "^3.4.47 || ~4.0 || ~5.0 || ~6.0 || ~7.0", "twig/twig": "^1.43 || ^2.13 || ^3.0" }, "suggest": { diff --git a/src/HateoasBuilder.php b/src/HateoasBuilder.php index 0a3b86b5..3f3be6e2 100644 --- a/src/HateoasBuilder.php +++ b/src/HateoasBuilder.php @@ -391,7 +391,7 @@ private function buildMetadataFactory(): MetadataFactoryInterface $typeParser = new Parser(); $annotationReader = $this->annotationReader; - $drivers = []; + $drivers = [new AttributeDriver($expressionEvaluator, $this->chainProvider, $typeParser)]; if (null === $annotationReader && class_exists(AnnotationReader::class)) { $annotationReader = new AnnotationReader(); @@ -404,10 +404,6 @@ private function buildMetadataFactory(): MetadataFactoryInterface $drivers[] = new AnnotationDriver($annotationReader, $expressionEvaluator, $this->chainProvider, $typeParser); } - if (PHP_VERSION_ID >= 80100) { - $drivers[] = new AttributeDriver($expressionEvaluator, $this->chainProvider, $typeParser); - } - if (!empty($this->metadataDirs)) { $fileLocator = new FileLocator($this->metadataDirs); $drivers[] = new YamlDriver($fileLocator, $expressionEvaluator, $this->chainProvider, $typeParser); diff --git a/src/Representation/CollectionRepresentation.php b/src/Representation/CollectionRepresentation.php index be677eee..4dd0b9cc 100644 --- a/src/Representation/CollectionRepresentation.php +++ b/src/Representation/CollectionRepresentation.php @@ -18,7 +18,12 @@ */ #[Serializer\ExclusionPolicy('all')] #[Serializer\XmlRoot('collection')] -#[Hateoas\Relation(name: 'items', embedded: new Hateoas\Embedded(content: 'expr(object.getResources())'))] +#[Hateoas\Relation( + 'items', + embedded: new Hateoas\Embedded( + content: 'expr(object.getResources())', + ), +)] class CollectionRepresentation { /** diff --git a/src/Representation/OffsetRepresentation.php b/src/Representation/OffsetRepresentation.php index 6fa68e32..61048ede 100644 --- a/src/Representation/OffsetRepresentation.php +++ b/src/Representation/OffsetRepresentation.php @@ -57,10 +57,47 @@ #[Serializer\ExclusionPolicy('all')] #[Serializer\XmlRoot('collection')] #[Serializer\AccessorOrder(order: 'custom', custom: ['offset', 'limit', 'total'])] -#[Hateoas\Relation(name: 'first', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(0))', absolute: 'expr(object.isAbsolute())'))] -#[Hateoas\Relation(name: 'last', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters((object.getTotal() - 1) - (object.getTotal() - 1) % object.getLimit()))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getTotal() === null)'))] -#[Hateoas\Relation(name: 'next', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(object.getOffset() + object.getLimit()))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getTotal() !== null && (object.getOffset() + object.getLimit()) >= object.getTotal())'))] -#[Hateoas\Relation(name: 'previous', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters((object.getOffset() > object.getLimit()) ? object.getOffset() - object.getLimit() : 0))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr(! object.getOffset())'))] +#[Hateoas\Relation( + 'first', + href: new Hateoas\Route( + 'expr(object.getRoute())', + parameters: 'expr(object.getParameters(0))', + absolute: 'expr(object.isAbsolute())', + ), +)] +#[Hateoas\Relation( + 'last', + href: new Hateoas\Route( + 'expr(object.getRoute())', + parameters: 'expr(object.getParameters((object.getTotal() - 1) - (object.getTotal() - 1) % object.getLimit()))', + absolute: 'expr(object.isAbsolute())', + ), + exclusion: new Hateoas\Exclusion( + excludeIf: 'expr(object.getTotal() === null)', + ) +)] +#[Hateoas\Relation( + 'next', + href: new Hateoas\Route( + name: 'expr(object.getRoute())', + parameters: 'expr(object.getParameters(object.getOffset() + object.getLimit()))', + absolute: 'expr(object.isAbsolute())' + ), + exclusion: new Hateoas\Exclusion( + excludeIf: 'expr(object.getTotal() !== null && (object.getOffset() + object.getLimit()) >= object.getTotal())', + ), +)] +#[Hateoas\Relation( + 'previous', + href: new Hateoas\Route( + 'expr(object.getRoute())', + parameters: 'expr(object.getParameters((object.getOffset() > object.getLimit()) ? object.getOffset() - object.getLimit() : 0))', + absolute: 'expr(object.isAbsolute())', + ), + exclusion: new Hateoas\Exclusion( + excludeIf: 'expr(! object.getOffset())', + ), +)] class OffsetRepresentation extends AbstractSegmentedRepresentation { /** diff --git a/src/Representation/PaginatedRepresentation.php b/src/Representation/PaginatedRepresentation.php index 05be1890..73beeb6c 100644 --- a/src/Representation/PaginatedRepresentation.php +++ b/src/Representation/PaginatedRepresentation.php @@ -57,10 +57,47 @@ #[Serializer\ExclusionPolicy('all')] #[Serializer\XmlRoot('collection')] #[Serializer\AccessorOrder(order: 'custom', custom: ['page', 'limit', 'pages', 'total'])] -#[Hateoas\Relation(name: 'first', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(1))', absolute: 'expr(object.isAbsolute())'))] -#[Hateoas\Relation(name: 'last', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(object.getPages()))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getPages() === null)'))] -#[Hateoas\Relation(name: 'next', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(object.getPage() + 1))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getPages() !== null && (object.getPage() + 1) > object.getPages())'))] -#[Hateoas\Relation(name: 'previous', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters(object.getPage() - 1))', absolute: 'expr(object.isAbsolute())'), exclusion: new Hateoas\Exclusion(excludeIf: 'expr((object.getPage() - 1) < 1)'))] +#[Hateoas\Relation( + 'first', + href: new Hateoas\Route( + 'expr(object.getRoute())', + parameters: 'expr(object.getParameters(1))', + absolute: 'expr(object.isAbsolute())', + ), +)] +#[Hateoas\Relation( + 'last', + href: new Hateoas\Route( + 'expr(object.getRoute())', + parameters: 'expr(object.getParameters(object.getPages()))', + absolute: 'expr(object.isAbsolute())' + ), + exclusion: new Hateoas\Exclusion( + excludeIf: 'expr(object.getPages() === null)', + ), +)] +#[Hateoas\Relation( + 'next', + href: new Hateoas\Route( + 'expr(object.getRoute())', + parameters: 'expr(object.getParameters(object.getPage() + 1))', + absolute: 'expr(object.isAbsolute())', + ), + exclusion: new Hateoas\Exclusion( + excludeIf: 'expr(object.getPages() !== null && (object.getPage() + 1) > object.getPages())', + ), +)] +#[Hateoas\Relation( + 'previous', + href: new Hateoas\Route( + 'expr(object.getRoute())', + parameters: 'expr(object.getParameters(object.getPage() - 1))', + absolute: 'expr(object.isAbsolute())', + ), + exclusion: new Hateoas\Exclusion( + excludeIf: 'expr((object.getPage() - 1) < 1)', + ), +)] class PaginatedRepresentation extends AbstractSegmentedRepresentation { /** diff --git a/src/Representation/RouteAwareRepresentation.php b/src/Representation/RouteAwareRepresentation.php index 12bbda79..9dac48d0 100644 --- a/src/Representation/RouteAwareRepresentation.php +++ b/src/Representation/RouteAwareRepresentation.php @@ -20,7 +20,14 @@ * ) */ #[Serializer\ExclusionPolicy('all')] -#[Hateoas\Relation(name: 'self', href: new Hateoas\Route(name: 'expr(object.getRoute())', parameters: 'expr(object.getParameters())', absolute: 'expr(object.isAbsolute())'))] +#[Hateoas\Relation( + 'self', + href: new Hateoas\Route( + 'expr(object.getRoute())', + parameters: 'expr(object.getParameters())', + absolute: 'expr(object.isAbsolute())', + ), +)] class RouteAwareRepresentation { /** diff --git a/src/Representation/VndErrorRepresentation.php b/src/Representation/VndErrorRepresentation.php index 46b50a5c..1072603f 100644 --- a/src/Representation/VndErrorRepresentation.php +++ b/src/Representation/VndErrorRepresentation.php @@ -35,9 +35,27 @@ */ #[Serializer\ExclusionPolicy('all')] #[Serializer\XmlRoot('resource')] -#[Hateoas\Relation(name: 'help', href: 'expr(object.getHelp())', exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getHelp() === null)'))] -#[Hateoas\Relation(name: 'describes', href: 'expr(object.getDescribes())', exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getDescribes() === null)'))] -#[Hateoas\Relation(name: 'about', href: 'expr(object.getAbout())', exclusion: new Hateoas\Exclusion(excludeIf: 'expr(object.getAbout() === null)'))] +#[Hateoas\Relation( + 'help', + href: 'expr(object.getHelp())', + exclusion: new Hateoas\Exclusion( + excludeIf: 'expr(object.getHelp() === null)', + ), +)] +#[Hateoas\Relation( + 'describes', + href: 'expr(object.getDescribes())', + exclusion: new Hateoas\Exclusion( + excludeIf: 'expr(object.getDescribes() === null)', + ), +)] +#[Hateoas\Relation( + 'about', + href: 'expr(object.getAbout())', + exclusion: new Hateoas\Exclusion( + excludeIf: 'expr(object.getAbout() === null)', + ), +)] class VndErrorRepresentation { /** diff --git a/tests/Hateoas/Tests/Configuration/Metadata/Driver/AttributeDriverTest.php b/tests/Hateoas/Tests/Configuration/Metadata/Driver/AttributeDriverTest.php index 08e7dfef..5e7e8e3e 100644 --- a/tests/Hateoas/Tests/Configuration/Metadata/Driver/AttributeDriverTest.php +++ b/tests/Hateoas/Tests/Configuration/Metadata/Driver/AttributeDriverTest.php @@ -9,13 +9,6 @@ class AttributeDriverTest extends AbstractDriverTest { - public function setUp(): void - { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('AttributeDriver is available as of PHP 8.1.0'); - } - } - public function createDriver() { return new AttributeDriver( From f04e0ccf0c2fd010c7fa1ded28a6ad1f40ffdcaf Mon Sep 17 00:00:00 2001 From: W0rma Date: Thu, 24 Oct 2024 06:00:07 +0200 Subject: [PATCH 7/9] Run cs on PHP 8.1 --- .github/workflows/coding-standards.yaml | 2 +- phpcs.xml.dist | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coding-standards.yaml b/.github/workflows/coding-standards.yaml index acb0cc78..581d9299 100644 --- a/.github/workflows/coding-standards.yaml +++ b/.github/workflows/coding-standards.yaml @@ -16,7 +16,7 @@ jobs: strategy: matrix: php-version: - - "7.4" + - "8.1" steps: - name: "Checkout" diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 60a288af..d90ad003 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -48,6 +48,15 @@ + + + + + + + + + From 9e8a232bc00d9d97269712c33b67f088a87211e4 Mon Sep 17 00:00:00 2001 From: W0rma Date: Thu, 31 Oct 2024 09:19:00 +0100 Subject: [PATCH 8/9] Remove native type hint from first argument to make attributes usable like annotations --- src/Configuration/Annotation/Embedded.php | 5 +++-- src/Configuration/Annotation/Exclusion.php | 5 ++++- src/Configuration/Annotation/Relation.php | 3 ++- src/Configuration/Annotation/RelationProvider.php | 5 ++++- src/Configuration/Annotation/Route.php | 3 ++- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Configuration/Annotation/Embedded.php b/src/Configuration/Annotation/Embedded.php index 25dd394d..0cb4bc3f 100644 --- a/src/Configuration/Annotation/Embedded.php +++ b/src/Configuration/Annotation/Embedded.php @@ -39,9 +39,10 @@ class Embedded public $exclusion = null; /** - * @param string|array $content + * @param array|string|null $values + * @param array|string|null $content */ - public function __construct(array $values = [], $content = null, ?string $type = null, ?string $xmlElementName = null, ?Exclusion $exclusion = null) + public function __construct($values = [], $content = null, ?string $type = null, ?string $xmlElementName = null, ?Exclusion $exclusion = null) { $this->loadAnnotationParameters(get_defined_vars()); } diff --git a/src/Configuration/Annotation/Exclusion.php b/src/Configuration/Annotation/Exclusion.php index 27061ab6..2966b7bb 100644 --- a/src/Configuration/Annotation/Exclusion.php +++ b/src/Configuration/Annotation/Exclusion.php @@ -44,7 +44,10 @@ final class Exclusion */ public $excludeIf = null; - public function __construct(array $values = [], ?array $groups = null, ?string $sinceVersion = null, ?string $untilVersion = null, ?int $maxDepth = null, ?string $excludeIf = null) + /** + * @param array|null $values + */ + public function __construct($values = [], ?array $groups = null, ?string $sinceVersion = null, ?string $untilVersion = null, ?int $maxDepth = null, ?string $excludeIf = null) { $this->loadAnnotationParameters(get_defined_vars()); } diff --git a/src/Configuration/Annotation/Relation.php b/src/Configuration/Annotation/Relation.php index 398bce3a..54eb2a95 100644 --- a/src/Configuration/Annotation/Relation.php +++ b/src/Configuration/Annotation/Relation.php @@ -44,10 +44,11 @@ final class Relation public $exclusion = null; /** + * @param array|string|null $values * @param string|Route $href * @param string|Embedded $embedded */ - public function __construct(array $values = [], ?string $name = null, $href = null, $embedded = null, array $attributes = [], ?Exclusion $exclusion = null) + public function __construct($values = [], ?string $name = null, $href = null, $embedded = null, array $attributes = [], ?Exclusion $exclusion = null) { $this->loadAnnotationParameters(get_defined_vars()); } diff --git a/src/Configuration/Annotation/RelationProvider.php b/src/Configuration/Annotation/RelationProvider.php index 4b74dc9f..3c8ae3ab 100644 --- a/src/Configuration/Annotation/RelationProvider.php +++ b/src/Configuration/Annotation/RelationProvider.php @@ -20,7 +20,10 @@ class RelationProvider */ public $name; - public function __construct(array $values = [], ?string $name = null) + /** + * @param array|string|null $values + */ + public function __construct($values = [], ?string $name = null) { $this->loadAnnotationParameters(get_defined_vars()); } diff --git a/src/Configuration/Annotation/Route.php b/src/Configuration/Annotation/Route.php index 629fc758..65d6313f 100644 --- a/src/Configuration/Annotation/Route.php +++ b/src/Configuration/Annotation/Route.php @@ -38,10 +38,11 @@ class Route public $generator = null; /** + * @param array|string|null $values * @param array|string $parameters * @param bool|string $absolute */ - public function __construct(array $values = [], ?string $name = null, $parameters = null, $absolute = false, ?string $generator = null) + public function __construct($values = [], ?string $name = null, $parameters = null, $absolute = false, ?string $generator = null) { $this->loadAnnotationParameters(get_defined_vars()); } From 08619439f03df869f03d0e10442d893aa32bfbd2 Mon Sep 17 00:00:00 2001 From: W0rma Date: Thu, 31 Oct 2024 16:52:33 +0100 Subject: [PATCH 9/9] Add test case covering parallel usage of attributes and annotations --- .../AdrienBraultAttributesAndAnnotations.php | 91 +++++++++++++++++++ tests/Hateoas/Tests/HateoasBuilderTest.php | 33 +++++-- 2 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 tests/Hateoas/Tests/Fixtures/Attribute/AdrienBraultAttributesAndAnnotations.php diff --git a/tests/Hateoas/Tests/Fixtures/Attribute/AdrienBraultAttributesAndAnnotations.php b/tests/Hateoas/Tests/Fixtures/Attribute/AdrienBraultAttributesAndAnnotations.php new file mode 100644 index 00000000..5f0f787e --- /dev/null +++ b/tests/Hateoas/Tests/Fixtures/Attribute/AdrienBraultAttributesAndAnnotations.php @@ -0,0 +1,91 @@ +assertInstanceOf(SerializerInterface::class, $hateoas); } - public function testSerializeAdrienBraultWithExclusion() + /** + * @dataProvider getTestSerializeAdrienBraultWithExclusionData + */ + public function testSerializeAdrienBraultWithExclusion($adrienBrault, $fakeAdrienBrault) { $hateoas = HateoasBuilder::buildHateoas(); - if (class_exists(AnnotationReader::class)) { - $adrienBrault = new AdrienBrault(); - $fakeAdrienBrault = new AdrienBrault(); - } else { - $adrienBrault = new Attribute\AdrienBrault(); - $fakeAdrienBrault = new Attribute\AdrienBrault(); - } - $fakeAdrienBrault->firstName = 'John'; $fakeAdrienBrault->lastName = 'Smith'; @@ -76,6 +71,26 @@ public function testSerializeAdrienBraultWithExclusion() ); } + private static function getTestSerializeAdrienBraultWithExclusionData(): iterable + { + yield [ + new Attribute\AdrienBrault(), + new Attribute\AdrienBrault(), + ]; + + if (class_exists(AnnotationReader::class)) { + yield [ + new AdrienBrault(), + new AdrienBrault(), + ]; + + yield [ + new Attribute\AdrienBraultAttributesAndAnnotations(), + new Attribute\AdrienBraultAttributesAndAnnotations(), + ]; + } + } + public function testAlternativeUrlGenerator() { $brokenUrlGenerator = new CallableUrlGenerator(function ($name, $parameters) {