diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b912e993..a95aa369 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,38 +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.2 - symfony-require: "^6.0" - - php-version: 8.3 - symfony-require: "^6.0" - 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" - 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 +64,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/.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/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..edfc7429 100644 --- a/composer.json +++ b/composer.json @@ -17,22 +17,22 @@ } ], "require": { - "php": "^7.2 | ^8.0", - "doctrine/annotations": "^1.13.2 || ^2.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", "pagerfanta/core": "^2.4 || ^3.0", "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/phpcs.xml.dist b/phpcs.xml.dist index 60a288af..d90ad003 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -48,6 +48,15 @@ + + + + + + + + + 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()); } diff --git a/src/HateoasBuilder.php b/src/HateoasBuilder.php index 81a5fb9a..3f3be6e2 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,31 @@ public function replaceMetadataDir(string $dir, string $namespacePrefix = ''): H private function buildMetadataFactory(): MetadataFactoryInterface { + $expressionEvaluator = $this->getExpressionEvaluator(); + + $typeParser = new Parser(); + $annotationReader = $this->annotationReader; + $drivers = [new AttributeDriver($expressionEvaluator, $this->chainProvider, $typeParser)]; - 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(); - - $typeParser = new Parser(); + $drivers[] = new AnnotationDriver($annotationReader, $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); 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..4dd0b9cc 100644 --- a/src/Representation/CollectionRepresentation.php +++ b/src/Representation/CollectionRepresentation.php @@ -16,6 +16,14 @@ * embedded = @Hateoas\Embedded("expr(object.getResources())") * ) */ +#[Serializer\ExclusionPolicy('all')] +#[Serializer\XmlRoot('collection')] +#[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 bf2847b7..61048ede 100644 --- a/src/Representation/OffsetRepresentation.php +++ b/src/Representation/OffsetRepresentation.php @@ -54,6 +54,50 @@ * ) * ) */ +#[Serializer\ExclusionPolicy('all')] +#[Serializer\XmlRoot('collection')] +#[Serializer\AccessorOrder(order: 'custom', custom: ['offset', 'limit', 'total'])] +#[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 { /** @@ -62,6 +106,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..73beeb6c 100644 --- a/src/Representation/PaginatedRepresentation.php +++ b/src/Representation/PaginatedRepresentation.php @@ -54,6 +54,50 @@ * ) * ) */ +#[Serializer\ExclusionPolicy('all')] +#[Serializer\XmlRoot('collection')] +#[Serializer\AccessorOrder(order: 'custom', custom: ['page', 'limit', 'pages', 'total'])] +#[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 { /** @@ -63,6 +107,9 @@ class PaginatedRepresentation extends AbstractSegmentedRepresentation * * @var int */ + #[Serializer\Expose] + #[Serializer\Type('integer')] + #[Serializer\XmlAttribute] private $page; /** @@ -72,6 +119,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..9dac48d0 100644 --- a/src/Representation/RouteAwareRepresentation.php +++ b/src/Representation/RouteAwareRepresentation.php @@ -19,6 +19,15 @@ * ) * ) */ +#[Serializer\ExclusionPolicy('all')] +#[Hateoas\Relation( + 'self', + href: new Hateoas\Route( + 'expr(object.getRoute())', + parameters: 'expr(object.getParameters())', + absolute: 'expr(object.isAbsolute())', + ), +)] class RouteAwareRepresentation { /** @@ -27,6 +36,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..1072603f 100644 --- a/src/Representation/VndErrorRepresentation.php +++ b/src/Representation/VndErrorRepresentation.php @@ -33,6 +33,29 @@ * ) * ) */ +#[Serializer\ExclusionPolicy('all')] +#[Serializer\XmlRoot('resource')] +#[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 { /** @@ -41,6 +64,8 @@ class VndErrorRepresentation * * @var string */ + #[Serializer\Expose] + #[Serializer\Type('string')] private $message; /** @@ -50,6 +75,9 @@ class VndErrorRepresentation * * @var int */ + #[Serializer\Expose] + #[Serializer\XmlAttribute] + #[Serializer\Type('integer')] private $logref; /** 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/Configuration/Metadata/Driver/AttributeDriverTest.php b/tests/Hateoas/Tests/Configuration/Metadata/Driver/AttributeDriverTest.php index 98a6c105..5e7e8e3e 100644 --- a/tests/Hateoas/Tests/Configuration/Metadata/Driver/AttributeDriverTest.php +++ b/tests/Hateoas/Tests/Configuration/Metadata/Driver/AttributeDriverTest.php @@ -5,17 +5,10 @@ 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 { - 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( @@ -27,6 +20,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 -{ -} diff --git a/tests/Hateoas/Tests/HateoasBuilderTest.php b/tests/Hateoas/Tests/HateoasBuilderTest.php index 7bb1050a..70e7d926 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; @@ -27,12 +29,13 @@ public function testBuild() $this->assertInstanceOf(SerializerInterface::class, $hateoas); } - public function testSerializeAdrienBraultWithExclusion() + /** + * @dataProvider getTestSerializeAdrienBraultWithExclusionData + */ + public function testSerializeAdrienBraultWithExclusion($adrienBrault, $fakeAdrienBrault) { $hateoas = HateoasBuilder::buildHateoas(); - $adrienBrault = new AdrienBrault(); - $fakeAdrienBrault = new AdrienBrault(); $fakeAdrienBrault->firstName = 'John'; $fakeAdrienBrault->lastName = 'Smith'; @@ -68,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) { @@ -78,6 +101,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 +116,7 @@ public function testAlternativeUrlGenerator() XML , - $hateoas->serialize(new WithAlternativeRouter(), 'xml') + $hateoas->serialize($withAlternativeRouter, 'xml') ); } @@ -95,8 +124,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 +169,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