diff --git a/CHANGELOG.md b/CHANGELOG.md index d895178dbb5..bf14149313d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v3.2.4 + +### Bug fixes + +* [436921f3b](https://github.com/api-platform/core/commit/436921f3bfb15f77f3d6f9c7462df7882120cd37) fix(serializer): json violation list normalizer (#5941) + +## v3.2.3 + +### Bug fixes + +* [0f015214c](https://github.com/api-platform/core/commit/0f015214c311c31f4065d3c4b3c171c3152a4e64) fix(symfony): 404 wrongly normalized (#5936) +* [495f75f81](https://github.com/api-platform/core/commit/495f75f811aedee491e3e419ca9d7040aea7355c) fix(serializer): json non-resource intermitent class (#5937) + ## v3.2.2 ### Bug fixes @@ -1892,4 +1905,4 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link ## 1.0.0 beta 2 * Preserve indexes when normalizing and denormalizing associative arrays -* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance +* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance \ No newline at end of file diff --git a/features/issues/5926.feature b/features/issues/5926.feature new file mode 100644 index 00000000000..668d205a095 --- /dev/null +++ b/features/issues/5926.feature @@ -0,0 +1,12 @@ +Feature: Issue 5926 + In order to reproduce the issue at https://github.com/api-platform/core/issues/5926 + As a client software developer + I need to be able to use every operation on a resource with non-resources embed objects + + @!mongodb + Scenario: Create and retrieve a WriteResource + When I add "Accept" header equal to "application/json" + And I send a "GET" request to "/test_issue5926s/1" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/json; charset=utf-8" diff --git a/features/main/validation.feature b/features/main/validation.feature index e0e29dfe181..170c4c51a27 100644 --- a/features/main/validation.feature +++ b/features/main/validation.feature @@ -157,3 +157,34 @@ Feature: Using validations groups ] } """ + + @!mongodb + Scenario: Get violations constraints + When I add "Accept" header equal to "application/json" + And I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/issue5912s" with body: + """ + { + "title": "" + } + """ + Then the response status code should be 422 + And the response should be in JSON + And the JSON should be equal to: + """ + { + "status": 422, + "violations": [ + { + "propertyPath": "title", + "message": "This value should not be blank.", + "code": "c1051bb4-d103-4f74-8988-acbcafc7fdc3" + } + ], + "detail": "title: This value should not be blank.", + "type": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3", + "title": "An error occurred" + } + """ + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" + diff --git a/src/Doctrine/Odm/Filter/BooleanFilter.php b/src/Doctrine/Odm/Filter/BooleanFilter.php index ed3a16ccceb..15f91deaf92 100644 --- a/src/Doctrine/Odm/Filter/BooleanFilter.php +++ b/src/Doctrine/Odm/Filter/BooleanFilter.php @@ -23,7 +23,8 @@ * * Syntax: `?property=`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books with the following query: `/books?published=true`. * diff --git a/src/Doctrine/Odm/Filter/DateFilter.php b/src/Doctrine/Odm/Filter/DateFilter.php index 0aa81df608f..0b848b2a49b 100644 --- a/src/Doctrine/Odm/Filter/DateFilter.php +++ b/src/Doctrine/Odm/Filter/DateFilter.php @@ -36,7 +36,8 @@ * - Consider items as youngest: use `ApiPlatform\Doctrine\Odm\Filter\DateFilter::INCLUDE_NULL_AFTER` (`include_null_after`) strategy * - Always include items: use `ApiPlatform\Doctrine\Odm\Filter\DateFilter::INCLUDE_NULL_BEFORE_AND_AFTER` (`include_null_before_and_after`) strategy * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books by date with the following query: `/books?createdAt[after]=2018-03-19`. * diff --git a/src/Doctrine/Odm/Filter/ExistsFilter.php b/src/Doctrine/Odm/Filter/ExistsFilter.php index 81ed2bced35..7235b25d3f7 100644 --- a/src/Doctrine/Odm/Filter/ExistsFilter.php +++ b/src/Doctrine/Odm/Filter/ExistsFilter.php @@ -27,7 +27,8 @@ * * Syntax: `?exists[property]=`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books with the following query: `/books?exists[comment]=true`. * diff --git a/src/Doctrine/Odm/Filter/NumericFilter.php b/src/Doctrine/Odm/Filter/NumericFilter.php index 47f72b79676..8d9174c42ac 100644 --- a/src/Doctrine/Odm/Filter/NumericFilter.php +++ b/src/Doctrine/Odm/Filter/NumericFilter.php @@ -23,7 +23,8 @@ * * Syntax: `?property=`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books with the following query: `/books?price=10`. * diff --git a/src/Doctrine/Odm/Filter/OrderFilter.php b/src/Doctrine/Odm/Filter/OrderFilter.php index 3058fbcccd4..2f4e570921e 100644 --- a/src/Doctrine/Odm/Filter/OrderFilter.php +++ b/src/Doctrine/Odm/Filter/OrderFilter.php @@ -26,7 +26,8 @@ * * Syntax: `?order[property]=`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books by title in ascending order and then by ID in descending order with the following query: `/books?order[title]=desc&order[id]=asc`. * * By default, whenever the query does not specify the direction explicitly (e.g.: `/books?order[title]&order[id]`), filters will not be applied unless you configure a default order direction to use: * - * + *
+ * * ```php * * * ``` - * + * + *
* * When the property used for ordering can contain `null` values, you may want to specify how `null` values are treated in the comparison: * - Use the default behavior of the DBMS: use `null` strategy diff --git a/src/Doctrine/Odm/Filter/RangeFilter.php b/src/Doctrine/Odm/Filter/RangeFilter.php index df89d572170..15743b3d080 100644 --- a/src/Doctrine/Odm/Filter/RangeFilter.php +++ b/src/Doctrine/Odm/Filter/RangeFilter.php @@ -23,7 +23,8 @@ * * Syntax: `?property[]=value`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books with the following query: `/books?price[between]=12.99..15.99`. * diff --git a/src/Doctrine/Odm/Filter/SearchFilter.php b/src/Doctrine/Odm/Filter/SearchFilter.php index 7b0a680307f..230fff70fe1 100644 --- a/src/Doctrine/Odm/Filter/SearchFilter.php +++ b/src/Doctrine/Odm/Filter/SearchFilter.php @@ -56,7 +56,8 @@ * * Syntax: `?property[]=foo&property[]=bar`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * @author Kévin Dunglas * @author Alan Poulain diff --git a/src/Doctrine/Orm/Filter/BooleanFilter.php b/src/Doctrine/Orm/Filter/BooleanFilter.php index e0edd0098d0..cb040c49a42 100644 --- a/src/Doctrine/Orm/Filter/BooleanFilter.php +++ b/src/Doctrine/Orm/Filter/BooleanFilter.php @@ -25,7 +25,8 @@ * * Syntax: `?property=`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books with the following query: `/books?published=true`. * diff --git a/src/Doctrine/Orm/Filter/DateFilter.php b/src/Doctrine/Orm/Filter/DateFilter.php index 0317b854354..d895cc68cbc 100644 --- a/src/Doctrine/Orm/Filter/DateFilter.php +++ b/src/Doctrine/Orm/Filter/DateFilter.php @@ -39,7 +39,8 @@ * - Consider items as youngest: use `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_AFTER` (`include_null_after`) strategy * - Always include items: use `ApiPlatform\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_BEFORE_AND_AFTER` (`include_null_before_and_after`) strategy * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books by date with the following query: `/books?createdAt[after]=2018-03-19`. * diff --git a/src/Doctrine/Orm/Filter/ExistsFilter.php b/src/Doctrine/Orm/Filter/ExistsFilter.php index a222e166f7b..f7f2cbab842 100644 --- a/src/Doctrine/Orm/Filter/ExistsFilter.php +++ b/src/Doctrine/Orm/Filter/ExistsFilter.php @@ -30,7 +30,8 @@ * * Syntax: `?exists[property]=`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books with the following query: `/books?exists[comment]=true`. * diff --git a/src/Doctrine/Orm/Filter/NumericFilter.php b/src/Doctrine/Orm/Filter/NumericFilter.php index a2033d4c0cf..bbcd1d67a0f 100644 --- a/src/Doctrine/Orm/Filter/NumericFilter.php +++ b/src/Doctrine/Orm/Filter/NumericFilter.php @@ -25,7 +25,8 @@ * * Syntax: `?property=`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books with the following query: `/books?price=10`. * diff --git a/src/Doctrine/Orm/Filter/OrderFilter.php b/src/Doctrine/Orm/Filter/OrderFilter.php index 5a24e6c7a76..abece578049 100644 --- a/src/Doctrine/Orm/Filter/OrderFilter.php +++ b/src/Doctrine/Orm/Filter/OrderFilter.php @@ -28,7 +28,8 @@ * * Syntax: `?order[property]=`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books by title in ascending order and then by ID in descending order with the following query: `/books?order[title]=desc&order[id]=asc`. * diff --git a/src/Doctrine/Orm/Filter/RangeFilter.php b/src/Doctrine/Orm/Filter/RangeFilter.php index 14233509b52..c2e0aaf4051 100644 --- a/src/Doctrine/Orm/Filter/RangeFilter.php +++ b/src/Doctrine/Orm/Filter/RangeFilter.php @@ -25,7 +25,8 @@ * * Syntax: `?property[]=value`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books with the following query: `/books?price[between]=12.99..15.99`. * diff --git a/src/Doctrine/Orm/Filter/SearchFilter.php b/src/Doctrine/Orm/Filter/SearchFilter.php index 3f7480e9fb2..0ea085811d7 100644 --- a/src/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Doctrine/Orm/Filter/SearchFilter.php @@ -56,7 +56,8 @@ * * Syntax: `?property[]=foo&property[]=bar`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * @author Kévin Dunglas */ diff --git a/src/Elasticsearch/Filter/MatchFilter.php b/src/Elasticsearch/Filter/MatchFilter.php index 24b5bc7c6b7..d9e21a2ee2d 100644 --- a/src/Elasticsearch/Filter/MatchFilter.php +++ b/src/Elasticsearch/Filter/MatchFilter.php @@ -18,7 +18,8 @@ * * Syntax: `?property[]=value`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books by title content with the following query: `/books?title=Foundation`. * diff --git a/src/Elasticsearch/Filter/OrderFilter.php b/src/Elasticsearch/Filter/OrderFilter.php index cd2504a5e8e..b05599bfca9 100644 --- a/src/Elasticsearch/Filter/OrderFilter.php +++ b/src/Elasticsearch/Filter/OrderFilter.php @@ -24,7 +24,8 @@ * * Syntax: `?order[property]=`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books by ID and date in ascending or descending order: `/books?order[id]=asc&order[date]=desc`. * diff --git a/src/Elasticsearch/Filter/TermFilter.php b/src/Elasticsearch/Filter/TermFilter.php index 2dd4fc29469..fba2c549c64 100644 --- a/src/Elasticsearch/Filter/TermFilter.php +++ b/src/Elasticsearch/Filter/TermFilter.php @@ -18,7 +18,8 @@ * * Syntax: `?property[]=value`. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books by title with the following query: `/books?title=Foundation`. * diff --git a/src/Hydra/Serializer/ErrorNormalizer.php b/src/Hydra/Serializer/ErrorNormalizer.php index febf2908d99..448cf6c5060 100644 --- a/src/Hydra/Serializer/ErrorNormalizer.php +++ b/src/Hydra/Serializer/ErrorNormalizer.php @@ -76,7 +76,9 @@ public function supportsNormalization(mixed $data, string $format = null, array return false; } - return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException); + $decoration = $this->itemNormalizer ? $this->itemNormalizer->supportsNormalization($data, $format, $context) : true; + + return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException) && $decoration; } public function getSupportedTypes($format): array diff --git a/src/JsonApi/Serializer/ErrorNormalizer.php b/src/JsonApi/Serializer/ErrorNormalizer.php index 835a9ec0da4..110667b6df4 100644 --- a/src/JsonApi/Serializer/ErrorNormalizer.php +++ b/src/JsonApi/Serializer/ErrorNormalizer.php @@ -77,7 +77,9 @@ public function supportsNormalization(mixed $data, string $format = null, array return false; } - return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException); + $decoration = $this->itemNormalizer ? $this->itemNormalizer->supportsNormalization($data, $format, $context) : true; + + return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException) && $decoration; } public function getSupportedTypes($format): array diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 819c20fc603..5eafbc7a31e 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -81,8 +81,8 @@ public function normalize(mixed $object, string $format = null, array $context = // TODO: we should not remove the resource_class in the normalizeRawCollection as we would find out anyway that it's not the same as the requested one $previousResourceClass = $context['resource_class'] ?? null; $metadata = []; - if ($isResourceClass = $this->resourceClassResolver->isResourceClass($resourceClass)) { - $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null); + if ($isResourceClass = $this->resourceClassResolver->isResourceClass($resourceClass) && (null === $previousResourceClass || $this->resourceClassResolver->isResourceClass($previousResourceClass))) { + $resourceClass = $this->resourceClassResolver->getResourceClass($object, $previousResourceClass); $context = $this->initContext($resourceClass, $context); $metadata = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context); } elseif ($this->contextBuilder instanceof AnonymousContextBuilderInterface) { diff --git a/src/Metadata/ApiProperty.php b/src/Metadata/ApiProperty.php index 48f7c31751e..d95e003ca4c 100644 --- a/src/Metadata/ApiProperty.php +++ b/src/Metadata/ApiProperty.php @@ -55,7 +55,8 @@ public function __construct( /** * The `deprecationReason` option deprecates the current operation with a deprecation message. * - * + *
+ * * ```php * * * ``` - * + * + *
* * - With JSON-lD / Hydra, [an `owl:deprecated` annotation property](https://www.w3.org/TR/owl2-syntax/#Annotation_Properties) will be added to the appropriate data structure * - With Swagger / OpenAPI, [a `deprecated` property](https://swagger.io/docs/specification/2-0/paths-and-operations/) will be added diff --git a/src/Metadata/ApiResource.php b/src/Metadata/ApiResource.php index df1bef15c19..1f8a1836fc6 100644 --- a/src/Metadata/ApiResource.php +++ b/src/Metadata/ApiResource.php @@ -183,7 +183,7 @@ public function __construct( /** * The `sunset` option indicates when a deprecated operation will be removed. * - * + *
* * ```php * * ``` * - * + *
*/ protected ?string $sunset = null, protected ?string $acceptPatch = null, @@ -233,7 +233,8 @@ public function __construct( * * See: [UrlGeneratorInterface::class](/reference/Api/UrlGeneratorInterface) * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?int $urlGenerationStrategy = null, /** * The `deprecationReason` option deprecates the current resource with a deprecation message. * - * + *
+ * * ```php * * * ``` - * + * + *
* * - With JSON-lD / Hydra, [an `owl:deprecated` annotation property](https://www.w3.org/TR/owl2-syntax/#Annotation_Properties) will be added to the appropriate data structure * - With Swagger / OpenAPI, [a `deprecated` property](https://swagger.io/docs/specification/2-0/paths-and-operations/) will be added @@ -331,7 +335,8 @@ public function __construct( /** * The `filters` option configures the filters (declared as services) available on the collection routes for the current resource. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?array $filters = null, protected ?bool $elasticsearch = null, @@ -374,7 +380,8 @@ public function __construct( /** * The `messenger` option dispatches the current resource through the Message Bus. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Note: when using `messenger=true` on a Doctrine entity, the Doctrine Processor is not called. If you want it * to be called, you should [decorate a built-in state processor](/docs/guide/hook-a-persistence-layer-with-a-processor) @@ -426,7 +434,7 @@ public function __construct( * By default, items in the collection are ordered in ascending (ASC) order by their resource identifier(s). If you want to * customize this order, you must add an `order` attribute on your ApiResource annotation: * - * + *
* * ```php * + *
* * This `order` attribute is used as an array: the key defines the order field, the values defines the direction. * If you only specify the key, `ASC` direction will be used as default. @@ -459,7 +467,8 @@ public function __construct( /** * The `paginationClientEnabled` option allows (or disallows) the client to enable (or disable) the pagination for the current resource. * - * + *
+ * * ```php * * * ``` - * + * + *
* * The pagination can now be enabled (or disabled) by adding a query parameter named `pagination`: * - `GET /books?pagination=false`: disabled @@ -501,7 +511,8 @@ public function __construct( /** * The `paginationClientItemsPerPage` option allows (or disallows) the client to set the number of items per page for the current resource. * - * + *
+ * * ```php * * * ``` - * + * + *
* * The number of items can now be set by adding a query parameter named `itemsPerPage`: * - `GET /books?itemsPerPage=50` @@ -542,7 +554,7 @@ public function __construct( /** * The `paginationClientPartial` option allows (or disallows) the client to enable (or disable) the partial pagination for the current resource. * - * + *
* * ```php * * * ``` - * + * + *
* * The partial pagination can now be enabled (or disabled) by adding a query parameter named `partial`: * - `GET /books?partial=false`: disabled @@ -587,7 +600,8 @@ public function __construct( * Select your unique sorted field as well as the direction you'll like the pagination to go via filters. * Note that for now you have to declare a `RangeFilter` and an `OrderFilter` on the property used for the cursor-based pagination:. * - * + *
+ * * ```php * * * ``` - * + * + *
* * To know more about cursor-based pagination take a look at [this blog post on medium (draft)](https://medium.com/@sroze/74fd1d324723). */ @@ -643,7 +658,8 @@ public function __construct( /** * The `paginationEnabled` option enables (or disables) the pagination for the current resource. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?bool $paginationEnabled = null, /** @@ -686,7 +703,8 @@ public function __construct( * When set to `true`, the Doctrine ORM Paginator will perform an additional query, in order to get the * correct number of results. You can configure this using the `paginationFetchJoinCollection` option: * - * + *
+ * * ```php * * * ``` - * + * + *
* * For more information, please see the [Pagination](https://www.doctrine-project.org/projects/doctrine-orm/en/current/tutorials/pagination.html) entry in the Doctrine ORM documentation. */ @@ -731,7 +750,8 @@ public function __construct( * When set to `true`, the Doctrine ORM Paginator will use output walkers, which are compulsory for some types * of queries. You can configure this using the `paginationUseOutputWalkers` option: * - * + *
+ * * ```php * * * ``` - * + * + *
* * For more information, please see the [Pagination](https://www.doctrine-project.org/projects/doctrine-orm/en/current/tutorials/pagination.html) entry in the Doctrine ORM documentation. */ @@ -770,7 +791,8 @@ public function __construct( /** * The `paginationItemsPerPage` option defines the number of items per page for the current resource. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?int $paginationItemsPerPage = null, /** * The `paginationMaximumItemsPerPage` option defines the maximum number of items per page for the current resource. * - * + *
+ * * ```php * * ``` * - * + *
*/ protected ?int $paginationMaximumItemsPerPage = null, /** * The `paginationPartial` option enables (or disables) the partial pagination for the current resource. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?bool $paginationPartial = null, /** * The `paginationType` option defines the type of pagination (`page` or `cursor`) to use for the current resource. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?string $paginationType = null, protected ?string $security = null, diff --git a/src/Metadata/HttpOperation.php b/src/Metadata/HttpOperation.php index ccd8ca94dc0..ed77bb58604 100644 --- a/src/Metadata/HttpOperation.php +++ b/src/Metadata/HttpOperation.php @@ -94,7 +94,8 @@ public function __construct( /** * The `sunset` option indicates when a deprecated operation will be removed. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?string $sunset = null, protected ?string $acceptPatch = null, diff --git a/src/Metadata/Operation.php b/src/Metadata/Operation.php index c07151e4f29..d1f56e98da1 100644 --- a/src/Metadata/Operation.php +++ b/src/Metadata/Operation.php @@ -66,7 +66,8 @@ public function __construct( /** * The `paginationEnabled` option enables (or disables) the pagination for the current collection operation. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?bool $paginationEnabled = null, /** * The `paginationType` option defines the type of pagination (`page` or `cursor`) to use for the current collection operation. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?string $paginationType = null, /** * The `paginationItemsPerPage` option defines the number of items per page for the current collection operation. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?int $paginationItemsPerPage = null, /** * The `paginationMaximumItemsPerPage` option defines the maximum number of items per page for the current resource. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?int $paginationMaximumItemsPerPage = null, /** * The `paginationPartial` option enables (or disables) the partial pagination for the current collection operation. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?bool $paginationPartial = null, /** * The `paginationClientEnabled` option allows (or disallows) the client to enable (or disable) the pagination for the current collection operation. * - * + *
+ * * ```php * * * ``` - * + * + *
* * The pagination can now be enabled (or disabled) by adding a query parameter named `pagination`: * - `GET /books?pagination=false`: disabled @@ -330,7 +342,8 @@ public function __construct( /** * The `paginationClientItemsPerPage` option allows (or disallows) the client to set the number of items per page for the current collection operation. * - * + *
+ * * ```php * * * ``` - * + * + *
* * The number of items can now be set by adding a query parameter named `itemsPerPage`: * - `GET /books?itemsPerPage=50` @@ -376,7 +390,8 @@ public function __construct( /** * The `paginationClientPartial` option allows (or disallows) the client to enable (or disable) the partial pagination for the current collection operation. * - * + *
+ * * ```php * * * ``` - * + * + *
* * The partial pagination can now be enabled (or disabled) by adding a query parameter named `partial`: * - `GET /books?partial=false`: disabled @@ -428,7 +444,8 @@ public function __construct( * When set to `true`, the Doctrine ORM Paginator will perform an additional query, in order to get the * correct number of results. You can configure this using the `paginationFetchJoinCollection` option: * - * + *
+ * * ```php * * * ``` - * + * + *
* * For more information, please see the [Pagination](https://www.doctrine-project.org/projects/doctrine-orm/en/current/tutorials/pagination.html) entry in the Doctrine ORM documentation. */ @@ -478,7 +496,8 @@ public function __construct( * When set to `true`, the Doctrine ORM Paginator will use output walkers, which are compulsory for some types * of queries. You can configure this using the `paginationUseOutputWalkers` option: * - * + *
+ * * ```php * * * ``` - * + * + *
* * For more information, please see the [Pagination](https://www.doctrine-project.org/projects/doctrine-orm/en/current/tutorials/pagination.html) entry in the Doctrine ORM documentation. */ @@ -525,7 +545,8 @@ public function __construct( * Select your unique sorted field as well as the direction you'll like the pagination to go via filters. * Note that for now you have to declare a `RangeFilter` and an `OrderFilter` on the property used for the cursor-based pagination:. * - * + *
+ * * ```php * * * ``` - * + * + *
* * To know more about cursor-based pagination take a look at [this blog post on medium (draft)](https://medium.com/@sroze/74fd1d324723). */ @@ -598,7 +620,8 @@ public function __construct( /** * The `deprecationReason` option deprecates the current operation with a deprecation message. * - * + *
+ * * ```php * * * ``` - * + * + *
* * - With JSON-lD / Hydra, [an `owl:deprecated` annotation property](https://www.w3.org/TR/owl2-syntax/#Annotation_Properties) will be added to the appropriate data structure * - With Swagger / OpenAPI, [a `deprecated` property](https://swagger.io/docs/specification/2-0/paths-and-operations/) will be added @@ -646,7 +670,8 @@ public function __construct( /** * The `filters` option configures the filters (declared as services) available on the collection routes for the current resource. * - * + *
+ * * ```php * * * ``` - * + * + *
*/ protected ?array $filters = null, /** @@ -708,7 +734,8 @@ public function __construct( /** * The `messenger` option dispatches the current resource through the Message Bus. * - * + *
+ * * ```php * * * ``` - * + * + *
* * Note: when using `messenger=true` on a Doctrine entity, the Doctrine Processor is not called. If you want it * to be called, you should [decorate a built-in state processor](/docs/guide/hook-a-persistence-layer-with-a-processor) diff --git a/src/Problem/Serializer/ErrorNormalizer.php b/src/Problem/Serializer/ErrorNormalizer.php index c1da53e12c1..13d764fa11c 100644 --- a/src/Problem/Serializer/ErrorNormalizer.php +++ b/src/Problem/Serializer/ErrorNormalizer.php @@ -75,7 +75,9 @@ public function supportsNormalization(mixed $data, string $format = null, array return false; } - return (self::FORMAT === $format || 'json' === $format) && ($data instanceof \Exception || $data instanceof FlattenException); + $decoration = $this->itemNormalizer ? $this->itemNormalizer->supportsNormalization($data, $format, $context) : true; + + return (self::FORMAT === $format || 'json' === $format) && ($data instanceof \Exception || $data instanceof FlattenException) && $decoration; } public function getSupportedTypes($format): array diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index b3f694a3b04..d0ab05c528c 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -710,8 +710,8 @@ protected function getAttributeValue(object $object, string $attribute, string $ ); // Anonymous resources - if ($type->getClassName()) { - $childContext = $this->createChildContext($this->createOperationContext($context, null), $attribute, $format); + if ($className) { + $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true; $attributeValue = $this->propertyAccessor->getValue($object, $attribute); diff --git a/src/Serializer/Filter/GroupFilter.php b/src/Serializer/Filter/GroupFilter.php index 6d7f16d1bb7..ff4101f2054 100644 --- a/src/Serializer/Filter/GroupFilter.php +++ b/src/Serializer/Filter/GroupFilter.php @@ -28,7 +28,8 @@ * - `overrideDefaultGroups` allows to override the default serialization groups (default: `false`) * - `whitelist` groups whitelist to avoid uncontrolled data exposure (default: `null` to allow all groups) * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter books by serialization groups with the following query: `/books?groups[]=read&groups[]=write`. * diff --git a/src/Serializer/Filter/PropertyFilter.php b/src/Serializer/Filter/PropertyFilter.php index 53576291ed4..d9291757c75 100644 --- a/src/Serializer/Filter/PropertyFilter.php +++ b/src/Serializer/Filter/PropertyFilter.php @@ -31,7 +31,8 @@ * - `overrideDefaultProperties` allows to override the default serialization properties (default: `false`) * - `whitelist` properties whitelist to avoid uncontrolled data exposure (default: `null` to allow all properties) * - * + *
+ * * ```php * * * ``` - * + * + *
* * Given that the collection endpoint is `/books`, you can filter the serialization properties with the following query: `/books?properties[]=title&properties[]=author`. If you want to include some properties of the nested "author" document, use: `/books?properties[]=title&properties[author][]=name`. * diff --git a/src/Symfony/Bundle/Resources/config/symfony/validator.xml b/src/Symfony/Bundle/Resources/config/symfony/validator.xml index 0f8eb782224..cf5c9f92a9c 100644 --- a/src/Symfony/Bundle/Resources/config/symfony/validator.xml +++ b/src/Symfony/Bundle/Resources/config/symfony/validator.xml @@ -31,6 +31,14 @@ + + + %api_platform.validator.serialize_payload_fields% + + + + + diff --git a/src/Symfony/Validator/Exception/ValidationException.php b/src/Symfony/Validator/Exception/ValidationException.php index 9d46283ebb7..ef80badd00d 100644 --- a/src/Symfony/Validator/Exception/ValidationException.php +++ b/src/Symfony/Validator/Exception/ValidationException.php @@ -19,6 +19,7 @@ use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; use ApiPlatform\Metadata\Util\CompositeIdentifierParser; use ApiPlatform\Validator\Exception\ValidationException as BaseValidationException; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\SerializedName; use Symfony\Component\Validator\ConstraintViolationListInterface; @@ -50,7 +51,7 @@ ], graphQlOperations: [] )] -final class ValidationException extends BaseValidationException implements ConstraintViolationListAwareExceptionInterface, \Stringable, ProblemExceptionInterface +final class ValidationException extends BaseValidationException implements ConstraintViolationListAwareExceptionInterface, \Stringable, ProblemExceptionInterface, HttpExceptionInterface { private int $status = 422; @@ -154,4 +155,14 @@ public function __toString(): string return $message; } + + public function getStatusCode(): int + { + return $this->status; + } + + public function getHeaders(): array + { + return []; + } } diff --git a/src/Symfony/Validator/Serializer/ConstraintViolationListNormalizer.php b/src/Symfony/Validator/Serializer/ConstraintViolationListNormalizer.php new file mode 100644 index 00000000000..46fb21cf55c --- /dev/null +++ b/src/Symfony/Validator/Serializer/ConstraintViolationListNormalizer.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Validator\Serializer; + +use ApiPlatform\Serializer\AbstractConstraintViolationListNormalizer; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +/** + * Converts {@see \Symfony\Component\Validator\ConstraintViolationListInterface} the API Problem spec (RFC 7807). + * + * @author Kévin Dunglas + */ +final class ConstraintViolationListNormalizer extends AbstractConstraintViolationListNormalizer +{ + public const FORMAT = 'json'; + + private array $defaultContext = []; + + public function __construct(array $serializePayloadFields = null, NameConverterInterface $nameConverter = null, array $defaultContext = []) + { + parent::__construct($serializePayloadFields, $nameConverter); + + $this->defaultContext = array_merge($this->defaultContext, $defaultContext); + } + + /** + * {@inheritdoc} + */ + public function normalize(mixed $object, string $format = null, array $context = []): array + { + return $this->getViolations($object); + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue5912/Dummy.php b/tests/Fixtures/TestBundle/Entity/Issue5912/Dummy.php new file mode 100644 index 00000000000..46c50d630aa --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue5912/Dummy.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5912; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\NotExposed; +use ApiPlatform\Metadata\Post; +use Symfony\Component\Validator\Constraints\NotBlank; + +#[ApiResource( + shortName: 'Issue5912', + operations: [ + new NotExposed(), + new Post(), + ] +)] +class Dummy +{ + #[NotBlank] + public string $title; +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue5926/ContentItemCollection.php b/tests/Fixtures/TestBundle/Entity/Issue5926/ContentItemCollection.php new file mode 100644 index 00000000000..d9f3c6ba540 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue5926/ContentItemCollection.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5926; + +class ContentItemCollection implements \IteratorAggregate +{ + private array $items; + + public function __construct(ContentItemInterface ...$items) + { + $this->items = $items; + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->items); + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue5926/ContentItemInterface.php b/tests/Fixtures/TestBundle/Entity/Issue5926/ContentItemInterface.php new file mode 100644 index 00000000000..83638913642 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue5926/ContentItemInterface.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5926; + +interface ContentItemInterface +{ +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue5926/Media.php b/tests/Fixtures/TestBundle/Entity/Issue5926/Media.php new file mode 100644 index 00000000000..edcf962e0df --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue5926/Media.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5926; + +use ApiPlatform\Metadata\ApiResource; + +#[ApiResource] +class Media +{ + public function __construct( + private readonly string $id, + private readonly string $title, + ) { + } + + public function getId(): string + { + return $this->id; + } + + public function getTitle(): string + { + return $this->title; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue5926/MediaContentItem.php b/tests/Fixtures/TestBundle/Entity/Issue5926/MediaContentItem.php new file mode 100644 index 00000000000..1cbc6c5e22a --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue5926/MediaContentItem.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5926; + +class MediaContentItem implements ContentItemInterface +{ + public function __construct( + private readonly Media $media, + ) { + } + + public function getMedia(): Media + { + return $this->media; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue5926/TestIssue5926.php b/tests/Fixtures/TestBundle/Entity/Issue5926/TestIssue5926.php new file mode 100644 index 00000000000..e62661bfbd5 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue5926/TestIssue5926.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5926; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; + +#[ApiResource( + operations: [ + new Get( + provider: [TestIssue5926::class, 'provide'] + ), + ] +)] +class TestIssue5926 +{ + public function __construct( + private readonly string $id, + private readonly ?ContentItemCollection $content, + ) { + } + + public function getId(): string + { + return $this->id; + } + + public function getContent(): ?ContentItemCollection + { + return $this->content; + } + + public static function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $media = new Media('1', 'My media'); + $contentItem1 = new MediaContentItem($media); + $media = new Media('2', 'My media 2'); + $contentItem2 = new MediaContentItem($media); + + $collection = new ContentItemCollection($contentItem1, $contentItem2); + + return new self('1', $collection); + } +} diff --git a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php index 4143e7385cf..1ca9df247a8 100644 --- a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php +++ b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php @@ -271,6 +271,20 @@ public function testGetMercureMessages(): void ); } + private function recreateSchema(array $options = []): void + { + self::bootKernel($options); + + /** @var EntityManagerInterface $manager */ + $manager = static::getContainer()->get('doctrine')->getManager(); + /** @var ClassMetadata[] $classes */ + $classes = $manager->getMetadataFactory()->getAllMetadata(); + $schemaTool = new SchemaTool($manager); + + @$schemaTool->dropSchema($classes); + @$schemaTool->createSchema($classes); + } + /** * @group legacy */ @@ -289,17 +303,9 @@ public function testExceptionNormalizer(): void $this->assertEquals($data['hello'], 'world'); } - private function recreateSchema(array $options = []): void + public function testMissingMethod(): void { - self::bootKernel($options); - - /** @var EntityManagerInterface $manager */ - $manager = static::getContainer()->get('doctrine')->getManager(); - /** @var ClassMetadata[] $classes */ - $classes = $manager->getMetadataFactory()->getAllMetadata(); - $schemaTool = new SchemaTool($manager); - - @$schemaTool->dropSchema($classes); - @$schemaTool->createSchema($classes); + $response = self::createClient([], ['headers' => ['accept' => 'application/json']])->request('DELETE', '/something/that/does/not/exist/ever'); + $this->assertResponseStatusCodeSame(404); } }