From db380b716aaabd7ae5459a66b7e6c60024e64b84 Mon Sep 17 00:00:00 2001 From: Dmitri Goosens <1250047+dgoosens@users.noreply.github.com> Date: Sat, 17 Sep 2022 18:27:26 +0200 Subject: [PATCH 01/14] ignore api_platform.state.item_provider when Doctrine is not enabled (#4954) --- src/Symfony/Bundle/Resources/config/state.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/Resources/config/state.xml b/src/Symfony/Bundle/Resources/config/state.xml index f0ecb8e23af..030088b4dd0 100644 --- a/src/Symfony/Bundle/Resources/config/state.xml +++ b/src/Symfony/Bundle/Resources/config/state.xml @@ -42,7 +42,7 @@ - + From d565f954fdea5ac751875d3c6363a9892a397e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 29 Sep 2022 13:49:56 +0200 Subject: [PATCH 02/14] chore: update branch-alias (#5030) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 84f58b654b1..b1fa38ca6ae 100644 --- a/composer.json +++ b/composer.json @@ -143,7 +143,7 @@ }, "extra": { "branch-alias": { - "dev-main": "3.0.x-dev" + "dev-main": "3.1.x-dev" }, "symfony": { "require": "^6.1" From c493676d59c8636b4aa3c71d09ea3a435e6b43d5 Mon Sep 17 00:00:00 2001 From: davy-beauzil <38990335+davy-beauzil@users.noreply.github.com> Date: Fri, 4 Nov 2022 16:37:58 +0100 Subject: [PATCH 03/14] chore: adding of stale bot (#5087) --- .github/stale.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000000..9abd4e0d252 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - bug + - enhancement +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false From ac70771ea802a54759648cd2b9f83bb2ae001c87 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 4 Nov 2022 17:29:49 +0100 Subject: [PATCH 04/14] ci: add automatic release notes (#5119) --- .github/workflows/tagged-release.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/tagged-release.yml diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml new file mode 100644 index 00000000000..06b836b6de5 --- /dev/null +++ b/.github/workflows/tagged-release.yml @@ -0,0 +1,24 @@ +--- +name: "tagged-release" + +on: + push: + tags: + - "v*" + +jobs: + gh_tagged_release: + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout source code" + uses: "actions/checkout@v2.3.4" + with: + lfs: true + fetch-depth: 0 + + - uses: "marvinpinto/action-automatic-releases@latest" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + prerelease: false + id: "automatic_releases" From 6abd0fe0a69d4842eb6d5c31ef2bd6dce0e1d372 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 4 Nov 2022 20:11:53 +0100 Subject: [PATCH 05/14] Merge 3.0 into main (#5121) * fix: update yaml extractor test file coding standard (#5068) * fix(graphql): add clearer error message when TwigBundle is disabled but graphQL clients are enabled (#5064) * fix(metadata): add class key in payload argument resolver (#5067) * fix: add class key in payload argument resolver * add null if everything else goes wrong * fix: upgrade command remove ApiSubresource attribute (#5049) Fixes #5038 * fix(doctrine): use abitrary index instead of value (#5079) * fix: uri template should respect rfc 6570 (#5080) * fix: remove @internal annotation for Operations (#5089) See #5084 * fix(metadata): define a name on a single operation (#5090) fixes #5082 * fix(metadata): deprecate when user decorates in legacy mode (#5091) fixes #5078 * fix(graphql): use right nested operation (#5102) * Revert "fix(graphql): use right nested operation (#5102)" (#5111) This reverts commit 44337ddb3908d7b05ed75b75325b7941581f575b. * fix(graphql): always allow to query nested resources (#5112) * fix(graphql): always allow to query nested resources * review Co-authored-by: Alan Poulain * chore: php-cs-fixer update * fix: only alias if exists for opcache preload Fixes https://github.com/api-platform/api-platform/issues/2284 (#5110) Co-authored-by: Liviu Mirea * chore: php-cs-fixer update (#5118) * chore: php-cs-fixer update * chore: php-cs-fixer update * fix(metadata): upgrade script keep operation name (#5109) origin: https://github.com/api-platform/core/pull/5105 Co-authored-by: WilliamPeralta * chore: v2.7.3 changelog * chore: v3.0.3 changelog Co-authored-by: helyakin Co-authored-by: ArnoudThibaut Co-authored-by: davy-beauzil <38990335+davy-beauzil@users.noreply.github.com> Co-authored-by: Baptiste Leduc Co-authored-by: Xavier Laviron Co-authored-by: Alan Poulain Co-authored-by: Liviu Cristian Mirea-Ghiban Co-authored-by: Liviu Mirea Co-authored-by: WilliamPeralta --- CHANGELOG.md | 32 ++++ features/graphql/collection.feature | 46 +++++ features/main/default_order.feature | 36 +++- generate-changelog.sh | 25 +++ src/Doctrine/Odm/Extension/OrderExtension.php | 3 +- src/Doctrine/Orm/Filter/SearchFilter.php | 6 +- src/GraphQl/Action/EntrypointAction.php | 6 +- ...NestedOperationResourceMetadataFactory.php | 67 ++++++++ src/GraphQl/Type/FieldsBuilder.php | 71 ++++---- src/Metadata/Operations.php | 7 +- ...butesResourceMetadataCollectionFactory.php | 139 +-------------- ...actorResourceMetadataCollectionFactory.php | 25 ++- ...ltersResourceMetadataCollectionFactory.php | 10 +- .../Factory/OperationDefaultsTrait.php | 160 ++++++++++++++++++ ...plateResourceMetadataCollectionFactory.php | 8 +- src/OpenApi/Factory/OpenApiFactory.php | 3 +- .../NormalizeOperationNameTrait.php | 3 +- .../PayloadArgumentResolver.php | 2 +- .../ApiPlatformExtension.php | 17 +- .../Resources/config/doctrine_mongodb_odm.xml | 5 + .../Bundle/Resources/config/doctrine_orm.xml | 6 + .../Bundle/Resources/config/graphql.xml | 13 +- src/Symfony/Routing/ApiLoader.php | 5 + src/Symfony/Routing/SkolemIriConverter.php | 3 +- src/Test/DoctrineMongoDbOdmTestCase.php | 6 +- tests/Action/ExceptionActionTest.php | 41 +++-- tests/Behat/DoctrineContext.php | 17 +- .../Common/Filter/SearchFilterTestTrait.php | 12 ++ .../Odm/Extension/OrderExtensionTest.php | 11 +- .../Doctrine/Odm/Filter/SearchFilterTest.php | 15 ++ .../Doctrine/Orm/Filter/SearchFilterTest.php | 8 + .../TestBundle/Document/AbsoluteUrlDummy.php | 2 +- tests/Fixtures/TestBundle/Document/Answer.php | 4 +- tests/Fixtures/TestBundle/Document/Book.php | 2 +- tests/Fixtures/TestBundle/Document/Dummy.php | 4 +- .../Document/DummyAggregateOffer.php | 4 +- .../TestBundle/Document/DummyOffer.php | 6 +- .../TestBundle/Document/DummyProduct.php | 2 +- .../TestBundle/Document/DummyValidation.php | 2 +- .../Fixtures/TestBundle/Document/FooDummy.php | 13 ++ .../TestBundle/Document/FourthLevel.php | 12 +- .../Fixtures/TestBundle/Document/Greeting.php | 2 +- .../TestBundle/Document/NetworkPathDummy.php | 2 +- .../Fixtures/TestBundle/Document/Question.php | 4 +- .../TestBundle/Document/RelatedDummy.php | 14 +- .../Document/RelatedToDummyFriend.php | 10 +- .../TestBundle/Document/SlugChildDummy.php | 4 +- .../TestBundle/Document/SlugParentDummy.php | 4 +- .../TestBundle/Document/ThirdLevel.php | 10 +- .../TestBundle/Document/VoDummyInspection.php | 12 +- .../TestBundle/Entity/AbsoluteUrlDummy.php | 2 +- tests/Fixtures/TestBundle/Entity/Answer.php | 4 +- .../Entity/AttributeOnlyOperation.php | 21 +++ .../TestBundle/Entity/AttributeResource.php | 2 +- .../TestBundle/Entity/AttributeResources.php | 11 +- tests/Fixtures/TestBundle/Entity/Book.php | 2 +- tests/Fixtures/TestBundle/Entity/Dummy.php | 4 +- .../TestBundle/Entity/DummyAggregateOffer.php | 4 +- .../Fixtures/TestBundle/Entity/DummyOffer.php | 6 +- .../TestBundle/Entity/DummyProduct.php | 2 +- .../Entity/DummyToUpgradeProduct.php | 49 ++++++ .../DummyToUpgradeWithOnlyAnnotation.php | 61 +++++++ .../DummyToUpgradeWithOnlyAttribute.php | 49 ++++++ .../TestBundle/Entity/DummyValidation.php | 2 +- tests/Fixtures/TestBundle/Entity/FooDummy.php | 13 ++ .../TestBundle/Entity/FourthLevel.php | 12 +- tests/Fixtures/TestBundle/Entity/Greeting.php | 2 +- .../TestBundle/Entity/NetworkPathDummy.php | 2 +- tests/Fixtures/TestBundle/Entity/Question.php | 4 +- .../TestBundle/Entity/RelatedDummy.php | 24 ++- .../Entity/RelatedToDummyFriend.php | 10 +- .../TestBundle/Entity/SlugChildDummy.php | 4 +- .../TestBundle/Entity/SlugParentDummy.php | 4 +- tests/Fixtures/TestBundle/Entity/SoMany.php | 3 + .../Fixtures/TestBundle/Entity/ThirdLevel.php | 10 +- .../TestBundle/Entity/VoDummyInspection.php | 12 +- ...edOperationResourceMetadataFactoryTest.php | 44 +++++ tests/GraphQl/Type/FieldsBuilderTest.php | 137 +++++++++------ tests/GraphQl/Type/TypeBuilderTest.php | 4 +- tests/HttpCache/VarnishPurgerTest.php | 9 +- tests/HttpCache/VarnishXKeyPurgerTest.php | 9 +- .../Command/JsonSchemaGenerateCommandTest.php | 6 +- .../PropertyMetadataCompatibilityTest.php | 3 +- .../ResourceMetadataCompatibilityTest.php | 23 ++- tests/Metadata/Extractor/XmlExtractorTest.php | 6 +- .../Metadata/Extractor/YamlExtractorTest.php | 8 +- tests/Metadata/Extractor/xml/valid.xml | 4 +- tests/Metadata/Extractor/yaml/valid.yaml | 4 +- ...sResourceMetadataCollectionFactoryTest.php | 37 ++-- ...eResourceMetadataCollectionFactoryTest.php | 8 +- .../Pagination/TraversablePaginatorTest.php | 3 +- .../PayloadArgumentResolverTest.php | 8 +- .../ApiPlatformExtensionTest.php | 37 ++++ 93 files changed, 1128 insertions(+), 462 deletions(-) create mode 100755 generate-changelog.sh create mode 100644 src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php create mode 100644 src/Metadata/Resource/Factory/OperationDefaultsTrait.php create mode 100644 tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php create mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php create mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php create mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php create mode 100644 tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be83b8dcb3..1921bdef057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## v3.0.3 + +### Bug fixes + +* [176fff2cb](https://github.com/api-platform/core/commit/176fff2cb15efa01b6c898d0442a4f540d4ddeaa) fix(metadata): upgrade script keep operation name (#5109) +* [1b64ebf6a](https://github.com/api-platform/core/commit/1b64ebf6a438222ae091ec3690063d0fb1b61977) fix: upgrade command remove ApiSubresource attribute (#5049) +* [27fcdc6b2](https://github.com/api-platform/core/commit/27fcdc6b270d1699e76c37ccda690b8a5ed8b4c9) fix(metadata): deprecate when user decorates in legacy mode (#5091) +* [310363d56](https://github.com/api-platform/core/commit/310363d56129c94cf4d51977f85486729e582fbc) fix: remove @internal annotation for Operations (#5089) +* [41bbad94e](https://github.com/api-platform/core/commit/41bbad94e93df49eb4ade0fe1307b20d9cd07102) fix: update yaml extractor test file coding standard (#5068) +* [44337ddb3](https://github.com/api-platform/core/commit/44337ddb3908d7b05ed75b75325b7941581f575b) fix(graphql): use right nested operation (#5102) +* [541b738e9](https://github.com/api-platform/core/commit/541b738e942156b711665952b50fbd4f060fcdea) fix(graphql): add clearer error message when TwigBundle is disabled but graphQL clients are enabled (#5064) +* [59826bbe9](https://github.com/api-platform/core/commit/59826bbe9e246cf839bdc0c4d0d470f54e27b453) fix: only alias if exists for opcache preload +* [7044c5a1b](https://github.com/api-platform/core/commit/7044c5a1b2895e72f0579d1e788740606f94dece) fix(doctrine): use abitrary index instead of value (#5079) +* [8250d41a3](https://github.com/api-platform/core/commit/8250d41a38913a17364d617875bb5a90f434ec48) fix(metadata): define a name on a single operation (#5090) +* [9c19fa171](https://github.com/api-platform/core/commit/9c19fa17110aac7dd39bff827091c00b42a80d4f) fix(metadata): add class key in payload argument resolver (#5067) +* [a4cd12b2a](https://github.com/api-platform/core/commit/a4cd12b2a73bc0f726c5724de790f885884e6113) fix: uri template should respect rfc 6570 (#5080) +* [bbeaf7082](https://github.com/api-platform/core/commit/bbeaf7082bba4a019206c3862425cf849d55addd) fix(graphql): always allow to query nested resources (#5112) +* [c1cb3cd2f](https://github.com/api-platform/core/commit/c1cb3cd2ff32c8b1ee694b0989efeb133fbd8438) Revert "fix(graphql): use right nested operation (#5102)" (#5111) + ## 3.0.2 * Metadata: generate skolem IRI by default, use `genId: false` to disable **BC** @@ -54,6 +73,19 @@ Breaking changes: * Serializer: `skip_null_values` now defaults to `true` * Metadata: `Patch` is added to the automatic CRUD +## v2.7.3 + +### Bug fixes + +* [176fff2cb](https://github.com/api-platform/core/commit/176fff2cb15efa01b6c898d0442a4f540d4ddeaa) fix(metadata): upgrade script keep operation name (#5109) +* [1b64ebf6a](https://github.com/api-platform/core/commit/1b64ebf6a438222ae091ec3690063d0fb1b61977) fix: upgrade command remove ApiSubresource attribute (#5049) +* [27fcdc6b2](https://github.com/api-platform/core/commit/27fcdc6b270d1699e76c37ccda690b8a5ed8b4c9) fix(metadata): deprecate when user decorates in legacy mode (#5091) +* [310363d56](https://github.com/api-platform/core/commit/310363d56129c94cf4d51977f85486729e582fbc) fix: remove @internal annotation for Operations (#5089) +* [41bbad94e](https://github.com/api-platform/core/commit/41bbad94e93df49eb4ade0fe1307b20d9cd07102) fix: update yaml extractor test file coding standard (#5068) +* [59826bbe9](https://github.com/api-platform/core/commit/59826bbe9e246cf839bdc0c4d0d470f54e27b453) fix: only alias if exists for opcache preload +* [8250d41a3](https://github.com/api-platform/core/commit/8250d41a38913a17364d617875bb5a90f434ec48) fix(metadata): define a name on a single operation (#5090) +* [9c19fa171](https://github.com/api-platform/core/commit/9c19fa17110aac7dd39bff827091c00b42a80d4f) fix(metadata): add class key in payload argument resolver (#5067) + ## 2.7.2 * Metadata: no skolem IRI by default diff --git a/features/graphql/collection.feature b/features/graphql/collection.feature index 1540549f7d8..9767505171a 100644 --- a/features/graphql/collection.feature +++ b/features/graphql/collection.feature @@ -910,3 +910,49 @@ Feature: GraphQL collection support Then the response status code should be 200 And the response should be in JSON And the JSON node "data.fooDummies.collection" should have 1 element + + @createSchema + Scenario: Retrieve paginated collections using mixed pagination + Given there are 5 fooDummy objects with fake names + When I send the following GraphQL request: + """ + { + fooDummies(page: 1) { + collection { + id + name + soManies(first: 2) { + edges { + node { + content + } + cursor + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + } + } + paginationInfo { + itemsPerPage + lastPage + totalCount + } + } + } + """ + Then the response status code should be 200 + And the response should be in JSON + And the JSON node "data.fooDummies.collection" should have 3 elements + And the JSON node "data.fooDummies.collection[2].id" should exist + And the JSON node "data.fooDummies.collection[2].name" should exist + And the JSON node "data.fooDummies.collection[2].soManies" should exist + And the JSON node "data.fooDummies.collection[2].soManies.edges" should have 2 elements + And the JSON node "data.fooDummies.collection[2].soManies.edges[1].node.content" should be equal to "So many 1" + And the JSON node "data.fooDummies.collection[2].soManies.pageInfo.startCursor" should be equal to "MA==" + And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3 + And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2 + And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5 diff --git a/features/main/default_order.feature b/features/main/default_order.feature index 91211014315..7458c2456e9 100644 --- a/features/main/default_order.feature +++ b/features/main/default_order.feature @@ -79,35 +79,61 @@ Feature: Default order "@type": "FooDummy", "id": 5, "name": "Balbo", - "dummy": "/dummies/5" + "dummy": "/dummies/5", + "soManies": [ + "/so_manies/13", + "/so_manies/14", + "/so_manies/15" + ] + }, { "@id": "/foo_dummies/3", "@type": "FooDummy", "id": 3, "name": "Sthenelus", - "dummy": "/dummies/3" + "dummy": "/dummies/3", + "soManies": [ + "/so_manies/7", + "/so_manies/8", + "/so_manies/9" + ] }, { "@id": "/foo_dummies/2", "@type": "FooDummy", "id": 2, "name": "Ephesian", - "dummy": "/dummies/2" + "dummy": "/dummies/2", + "soManies": [ + "/so_manies/4", + "/so_manies/5", + "/so_manies/6" + ] }, { "@id": "/foo_dummies/1", "@type": "FooDummy", "id": 1, "name": "Hawsepipe", - "dummy": "/dummies/1" + "dummy": "/dummies/1", + "soManies": [ + "/so_manies/1", + "/so_manies/2", + "/so_manies/3" + ] }, { "@id": "/foo_dummies/4", "@type": "FooDummy", "id": 4, "name": "Separativeness", - "dummy": "/dummies/4" + "dummy": "/dummies/4", + "soManies": [ + "/so_manies/10", + "/so_manies/11", + "/so_manies/12" + ] } ], "hydra:totalItems": 5, diff --git a/generate-changelog.sh b/generate-changelog.sh new file mode 100755 index 00000000000..f2903beb1b3 --- /dev/null +++ b/generate-changelog.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# usage: generate-changelog.sh previous_tag next_tag +# example: generate-changelog.sh v2.7.2 v2.7.3 > CHANGELOG.new.md +log=$(git log "$1..HEAD" --pretty='format:* [%h](https://github.com/api-platform/core/commit/%H) %s' --no-merges) + +diff=$( +printf "# Changelog\n\n" +printf "## %s\n\n" "$2" + +if [[ 0 != $(echo "$log" | grep fix | grep -v chore | wc -l) ]]; +then + printf "### Bug fixes\n\n" + printf "$log" | grep fix | grep -v chore | sort + printf "\n\n" +fi + +if [[ 0 != $(echo "$log" | grep feat | grep -v chore | wc -l) ]]; +then + printf "### Features\n\n" + printf "$log" | grep feat | grep -v chore | sort +fi +) + +changelog=$(tail -n+2 CHANGELOG.md) +printf "%s\n%s" "$diff" "$changelog" diff --git a/src/Doctrine/Odm/Extension/OrderExtension.php b/src/Doctrine/Odm/Extension/OrderExtension.php index f3cf5eb3725..77cd255d69b 100644 --- a/src/Doctrine/Odm/Extension/OrderExtension.php +++ b/src/Doctrine/Odm/Extension/OrderExtension.php @@ -19,7 +19,6 @@ use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort; use Doctrine\Persistence\ManagerRegistry; -use OutOfRangeException; /** * Applies selected ordering while querying resource collection. @@ -100,7 +99,7 @@ private function hasSortStage(Builder $aggregationBuilder): bool // If at least one stage is sort, then it has sorting return true; } - } catch (OutOfRangeException) { + } catch (\OutOfRangeException $outOfRangeException) { // There is no more stages on the aggregation builder $shouldStop = true; } diff --git a/src/Doctrine/Orm/Filter/SearchFilter.php b/src/Doctrine/Orm/Filter/SearchFilter.php index 3f35706b3b5..8401ebb842a 100644 --- a/src/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Doctrine/Orm/Filter/SearchFilter.php @@ -179,7 +179,7 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild $parameters = []; foreach ($values as $key => $value) { $keyValueParameter = sprintf('%s_%s', $valueParameter, $key); - $parameters[$caseSensitive ? $value : strtolower($value)] = $keyValueParameter; + $parameters[] = [$caseSensitive ? $value : strtolower($value), $keyValueParameter]; $ors[] = match ($strategy) { self::STRATEGY_PARTIAL => $queryBuilder->expr()->like( @@ -209,7 +209,9 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild } $queryBuilder->andWhere($queryBuilder->expr()->orX(...$ors)); - array_walk($parameters, $queryBuilder->setParameter(...)); + foreach ($parameters as $parameter) { + $queryBuilder->setParameter($parameter[1], $parameter[0]); + } } /** diff --git a/src/GraphQl/Action/EntrypointAction.php b/src/GraphQl/Action/EntrypointAction.php index f4c9af93e68..dcdd4a1dfd8 100644 --- a/src/GraphQl/Action/EntrypointAction.php +++ b/src/GraphQl/Action/EntrypointAction.php @@ -34,7 +34,7 @@ final class EntrypointAction { private int $debug; - public function __construct(private readonly SchemaBuilderInterface $schemaBuilder, private readonly ExecutorInterface $executor, private readonly GraphiQlAction $graphiQlAction, private readonly GraphQlPlaygroundAction $graphQlPlaygroundAction, private readonly NormalizerInterface $normalizer, private readonly ErrorHandlerInterface $errorHandler, bool $debug = false, private readonly bool $graphiqlEnabled = false, private readonly bool $graphQlPlaygroundEnabled = false, private readonly ?string $defaultIde = null) + public function __construct(private readonly SchemaBuilderInterface $schemaBuilder, private readonly ExecutorInterface $executor, private readonly ?GraphiQlAction $graphiQlAction, private readonly ?GraphQlPlaygroundAction $graphQlPlaygroundAction, private readonly NormalizerInterface $normalizer, private readonly ErrorHandlerInterface $errorHandler, bool $debug = false, private readonly bool $graphiqlEnabled = false, private readonly bool $graphQlPlaygroundEnabled = false, private readonly ?string $defaultIde = null) { $this->debug = $debug ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : DebugFlag::NONE; } @@ -43,11 +43,11 @@ public function __invoke(Request $request): Response { try { if ($request->isMethod('GET') && 'html' === $request->getRequestFormat()) { - if ('graphiql' === $this->defaultIde && $this->graphiqlEnabled) { + if ('graphiql' === $this->defaultIde && $this->graphiqlEnabled && $this->graphiQlAction) { return ($this->graphiQlAction)($request); } - if ('graphql-playground' === $this->defaultIde && $this->graphQlPlaygroundEnabled) { + if ('graphql-playground' === $this->defaultIde && $this->graphQlPlaygroundEnabled && $this->graphQlPlaygroundAction) { return ($this->graphQlPlaygroundAction)($request); } } diff --git a/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php b/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php new file mode 100644 index 00000000000..05163b0c2b7 --- /dev/null +++ b/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php @@ -0,0 +1,67 @@ + + * + * 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\GraphQl\Metadata\Factory; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Resource\Factory\OperationDefaultsTrait; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; + +final class GraphQlNestedOperationResourceMetadataFactory implements ResourceMetadataCollectionFactoryInterface +{ + use OperationDefaultsTrait; + + public function __construct(array $defaults, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, ?LoggerInterface $logger = null) + { + $this->defaults = $defaults; + $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); + $this->logger = $logger ?? new NullLogger(); + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); + + if ($this->decorated) { + $resourceMetadataCollection = $this->decorated->create($resourceClass); + } + + if (0 < \count($resourceMetadataCollection)) { + return $resourceMetadataCollection; + } + + $shortName = (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass; + + $apiResource = new ApiResource( + class: $resourceClass, + shortName: $shortName + ); + + if (class_exists($resourceClass)) { + $refl = new \ReflectionClass($resourceClass); + $attribute = $refl->getAttributes(ApiResource::class)[0] ?? null; + $attributeInstance = $attribute?->newInstance(); + if ($filters = $attributeInstance?->getFilters()) { + $apiResource = $apiResource->withFilters($filters); + } + } + + $resourceMetadataCollection[0] = $this->addDefaultGraphQlOperations($apiResource); + + return $resourceMetadataCollection; + } +} diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 8817899c8ef..d64f1cb800b 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -20,9 +20,7 @@ use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; -use ApiPlatform\Metadata\Operation as AbstractOperation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -47,7 +45,7 @@ */ final class FieldsBuilder implements FieldsBuilderInterface { - public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?ResourceMetadataCollectionFactoryInterface $graphQlNestedOperationResourceMetadataFactory = null) { } @@ -256,7 +254,23 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $resourceClass = $type->getClassName(); } - $graphqlType = $this->convertType($type, $input, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); + $resourceOperation = $rootOperation; + if ($resourceClass && $rootOperation->getClass() && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + try { + $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); + } catch (OperationNotFoundException) { + // If there is no query operation for a nested resource we force one to exist + $nestedResourceMetadataCollection = $this->graphQlNestedOperationResourceMetadataFactory->create($resourceClass); + $resourceOperation = $nestedResourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); + } + } + + if (!$resourceOperation instanceof Operation) { + throw new \LogicException('The resource operation should be a GraphQL operation.'); + } + + $graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType(true) : $graphqlType; $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true); @@ -271,43 +285,22 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $args = []; - $resolverOperation = $rootOperation; - - if ($resourceClass && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - $resolverOperation = $resourceMetadataCollection->getOperation(null, $isCollectionType); - - if (!$resolverOperation instanceof Operation) { - $resolverOperation = ($isCollectionType ? new QueryCollection() : new Query())->withOperation($resolverOperation); - } - } - if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) { - if ($this->pagination->isGraphQlEnabled($rootOperation)) { - $args = $this->getGraphQlPaginationArgs($rootOperation); - } - - // Find the collection operation to get filters, there might be a smarter way to do this - $operation = null; - if (!empty($resourceClass)) { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - try { - $operation = $resourceMetadataCollection->getOperation(null, true); - } catch (OperationNotFoundException) { - } + if ($this->pagination->isGraphQlEnabled($resourceOperation)) { + $args = $this->getGraphQlPaginationArgs($resourceOperation); } - $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $rootOperation, $property, $depth, $operation); + $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); } if ($isStandardGraphqlType || $input) { $resolve = null; } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) { - $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resolverOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resolverOperation); + $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation); } elseif ($this->typeBuilder->isCollection($type)) { - $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resolverOperation); + $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation); } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resolverOperation); + $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation); } return [ @@ -368,13 +361,13 @@ private function getGraphQlPaginationArgs(Operation $queryOperation): array return $args; } - private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $rootOperation, ?string $property, int $depth, ?AbstractOperation $operation = null): array + private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array { - if (null === $operation || null === $resourceClass) { + if (null === $resourceClass) { return $args; } - foreach ($operation->getFilters() ?? [] as $filterId) { + foreach ($resourceOperation->getFilters() ?? [] as $filterId) { if (!$this->filterLocator->has($filterId)) { continue; } @@ -382,7 +375,7 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) { $nullable = isset($value['required']) ? !$value['required'] : true; $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']); - $graphqlFilterType = $this->convertType($filterType, false, $rootOperation, $resourceClass, $rootResource, $property, $depth); + $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth); if (str_ends_with($key, '[]')) { $graphqlFilterType = GraphQLType::listOf($graphqlFilterType); @@ -399,14 +392,14 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root array_walk_recursive($parsed, static function (&$value) use ($graphqlFilterType): void { $value = $graphqlFilterType; }); - $args = $this->mergeFilterArgs($args, $parsed, $operation, $key); + $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key); } } return $this->convertFilterArgsToTypes($args); } - private function mergeFilterArgs(array $args, array $parsed, ?AbstractOperation $operation = null, string $original = ''): array + private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array { foreach ($parsed as $key => $value) { // Never override keys that cannot be merged @@ -470,7 +463,7 @@ private function convertFilterArgsToTypes(array $args): array * * @throws InvalidTypeException */ - private function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull + private function convertType(Type $type, bool $input, Operation $resourceOperation, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull { $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth); @@ -487,7 +480,7 @@ private function convertType(Type $type, bool $input, Operation $rootOperation, } if ($this->typeBuilder->isCollection($type)) { - return $this->pagination->isGraphQlEnabled($rootOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $rootOperation) : GraphQLType::listOf($graphqlType); + return $this->pagination->isGraphQlEnabled($resourceOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation) : GraphQLType::listOf($graphqlType); } return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName()) diff --git a/src/Metadata/Operations.php b/src/Metadata/Operations.php index cdbd4fe6bf3..5565bc99bd0 100644 --- a/src/Metadata/Operations.php +++ b/src/Metadata/Operations.php @@ -13,11 +13,6 @@ namespace ApiPlatform\Metadata; -use RuntimeException; - -/** - * @internal - */ final class Operations implements \IteratorAggregate, \Countable { private array $operations = []; @@ -73,7 +68,7 @@ public function remove(string $key): self } } - throw new RuntimeException(sprintf('Could not remove operation "%s".', $key)); + throw new \RuntimeException(sprintf('Could not remove operation "%s".', $key)); } public function has(string $key): bool diff --git a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index 9255c236fa8..9b125aed5c3 100644 --- a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -14,20 +14,12 @@ namespace ApiPlatform\Metadata\Resource\Factory; use ApiPlatform\Exception\ResourceClassNotFoundException; -use ApiPlatform\Exception\RuntimeException; use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\GraphQl\DeleteMutation; -use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -45,12 +37,12 @@ */ final class AttributesResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - private readonly LoggerInterface $logger; - private readonly CamelCaseToSnakeCaseNameConverter $camelCaseToSnakeCaseNameConverter; + use OperationDefaultsTrait; - public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, LoggerInterface $logger = null, private readonly array $defaults = [], private readonly bool $graphQlEnabled = false) + public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, LoggerInterface $logger = null, array $defaults = [], private readonly bool $graphQlEnabled = false) { $this->logger = $logger ?? new NullLogger(); + $this->defaults = $defaults; $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); } @@ -164,115 +156,6 @@ private function buildResourceOperations(array $attributes, string $resourceClas return $resources; } - private function getOperationWithDefaults(ApiResource $resource, Operation $operation, bool $generated = false): array - { - // Inherit from resource defaults - foreach (get_class_methods($resource) as $methodName) { - if (!str_starts_with($methodName, 'get')) { - continue; - } - - if (!method_exists($operation, $methodName) || null !== $operation->{$methodName}()) { - continue; - } - - if (null === ($value = $resource->{$methodName}())) { - continue; - } - - $operation = $operation->{'with'.substr($methodName, 3)}($value); - } - - $operation = $operation->withExtraProperties(array_merge( - $resource->getExtraProperties(), - $operation->getExtraProperties(), - $generated ? ['generated_operation' => true] : [] - )); - - // Add global defaults attributes to the operation - $operation = $this->addGlobalDefaults($operation); - - if ($operation instanceof GraphQlOperation) { - if (!$operation->getName()) { - throw new RuntimeException('No GraphQL operation name.'); - } - - if ($operation instanceof Mutation) { - $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); - } - - return [$operation->getName(), $operation]; - } - - if (!$operation instanceof HttpOperation) { - throw new RuntimeException(sprintf('Operation should be an instance of "%s"', HttpOperation::class)); - } - - if ($operation->getRouteName()) { - /** @var HttpOperation $operation */ - $operation = $operation->withName($operation->getRouteName()); - } - - // Check for name conflict - if ($operation->getName()) { - if (null !== $resource->getOperations() && !$resource->getOperations()->has($operation->getName())) { - return [$operation->getName(), $operation]; - } - - $this->logger->warning(sprintf('The operation "%s" already exists on the resource "%s", pick a different name or leave it empty. In the meantime we will generate a unique name.', $operation->getName(), $resource->getClass())); - /** @var HttpOperation $operation */ - $operation = $operation->withName(''); - } - - return [ - sprintf( - '_api_%s_%s%s', - $operation->getUriTemplate() ?: $operation->getShortName(), - strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), - $operation instanceof CollectionOperationInterface ? '_collection' : '', - ), - $operation, - ]; - } - - private function addGlobalDefaults(ApiResource|HttpOperation|GraphQlOperation $operation): ApiResource|HttpOperation|GraphQlOperation - { - $extraProperties = []; - foreach ($this->defaults as $key => $value) { - $upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key)); - $getter = 'get'.$upperKey; - - if (!method_exists($operation, $getter)) { - if (!isset($extraProperties[$key])) { - $extraProperties[$key] = $value; - } - } else { - $currentValue = $operation->{$getter}(); - - if (\is_array($currentValue) && $currentValue) { - $operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue)); - } - - if (null !== $currentValue) { - continue; - } - - $operation = $operation->{'with'.$upperKey}($value); - } - } - - return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties())); - } - - private function getResourceWithDefaults(string $resourceClass, string $shortName, ApiResource $resource): ApiResource - { - $resource = $resource - ->withShortName($resource->getShortName() ?? $shortName) - ->withClass($resourceClass); - - return $this->addGlobalDefaults($resource); - } - private function hasResourceAttributes(\ReflectionClass $reflectionClass): bool { foreach ($reflectionClass->getAttributes() as $attribute) { @@ -303,22 +186,6 @@ private function hasSameOperation(ApiResource $resource, string $operationClass, return false; } - private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource - { - $graphQlOperations = []; - foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $i => $operation) { - [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); - $graphQlOperations[$key] = $operation; - } - - if ($resource->getMercure()) { - [$key, $operation] = $this->getOperationWithDefaults($resource, (new Subscription())->withDescription("Subscribes to the update event of a {$operation->getShortName()}.")); - $graphQlOperations[$key] = $operation; - } - - return $resource->withGraphQlOperations($graphQlOperations); - } - private function getDefaultHttpOperations($resource): iterable { $post = new Post(); diff --git a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php index c44358897ca..86c154f7bc4 100644 --- a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php @@ -19,7 +19,12 @@ use ApiPlatform\Metadata\Extractor\ResourceExtractorInterface; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -88,9 +93,7 @@ private function buildResources(array $nodes, string $resourceClass): array } } - if (isset($node['graphQlOperations'])) { - $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'], $resource)); - } + $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'] ?? null, $resource)); $resources[] = $resource->withOperations(new Operations($this->buildOperations($node['operations'] ?? null, $resource))); } @@ -148,6 +151,20 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar { $operations = []; + if (null === $data) { + foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $operation) { + $operation = $this->getOperationWithDefaults($resource, $operation); + + if ($operation instanceof Mutation) { + $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); + } + + $operations[$operation->getName()] = $operation; + } + + return $operations; + } + foreach ($data as $attributes) { /** @var HttpOperation $operation */ $operation = (new $attributes['graphql_operation_class']())->withShortName($resource->getShortName()); @@ -175,7 +192,7 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar return $operations; } - private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation + private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation { foreach (($this->defaults['attributes'] ?? []) as $key => $value) { $key = $this->camelCaseToSnakeCaseNameConverter->denormalize($key); diff --git a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php index bb68448c4c2..10a711fe71a 100644 --- a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php @@ -50,17 +50,21 @@ public function create(string $resourceClass): ResourceMetadataCollection $filters = array_keys($this->readFilterAttributes($reflectionClass)); foreach ($resourceMetadataCollection as $i => $resource) { - foreach ($operations = $resource->getOperations() as $operationName => $operation) { + foreach ($operations = $resource->getOperations() ?? [] as $operationName => $operation) { $operations->add($operationName, $operation->withFilters(array_unique(array_merge($resource->getFilters() ?? [], $operation->getFilters() ?? [], $filters)))); } - $resourceMetadataCollection[$i] = $resource->withOperations($operations); + if ($operations) { + $resourceMetadataCollection[$i] = $resource->withOperations($operations); + } foreach ($graphQlOperations = $resource->getGraphQlOperations() ?? [] as $operationName => $operation) { $graphQlOperations[$operationName] = $operation->withFilters(array_unique(array_merge($resource->getFilters() ?? [], $operation->getFilters() ?? [], $filters))); } - $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); + if ($graphQlOperations) { + $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); + } } return $resourceMetadataCollection; diff --git a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php new file mode 100644 index 00000000000..cdb91e879a7 --- /dev/null +++ b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php @@ -0,0 +1,160 @@ + + * + * 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\Metadata\Resource\Factory; + +use ApiPlatform\Exception\RuntimeException; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Metadata\GraphQl\Subscription; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use Psr\Log\LoggerInterface; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; + +trait OperationDefaultsTrait +{ + private CamelCaseToSnakeCaseNameConverter $camelCaseToSnakeCaseNameConverter; + private array $defaults = []; + private LoggerInterface $logger; + + private function addGlobalDefaults(ApiResource|Operation $operation): ApiResource|Operation + { + $extraProperties = []; + foreach ($this->defaults as $key => $value) { + $upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key)); + $getter = 'get'.$upperKey; + + if (!method_exists($operation, $getter)) { + if (!isset($extraProperties[$key])) { + $extraProperties[$key] = $value; + } + } else { + $currentValue = $operation->{$getter}(); + + if (\is_array($currentValue) && $currentValue) { + $operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue)); + } + + if (null !== $currentValue) { + continue; + } + + $operation = $operation->{'with'.$upperKey}($value); + } + } + + return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties())); + } + + private function getResourceWithDefaults(string $resourceClass, string $shortName, ApiResource $resource): ApiResource + { + $resource = $resource + ->withShortName($resource->getShortName() ?? $shortName) + ->withClass($resourceClass); + + return $this->addGlobalDefaults($resource); + } + + private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource + { + $graphQlOperations = []; + foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $i => $operation) { + [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); + $graphQlOperations[$key] = $operation; + } + + if ($resource->getMercure()) { + [$key, $operation] = $this->getOperationWithDefaults($resource, (new Subscription())->withDescription("Subscribes to the update event of a {$operation->getShortName()}.")); + $graphQlOperations[$key] = $operation; + } + + return $resource->withGraphQlOperations($graphQlOperations); + } + + private function getOperationWithDefaults(ApiResource $resource, Operation $operation, bool $generated = false): array + { + // Inherit from resource defaults + foreach (get_class_methods($resource) as $methodName) { + if (!str_starts_with($methodName, 'get')) { + continue; + } + + if (!method_exists($operation, $methodName) || null !== $operation->{$methodName}()) { + continue; + } + + if (null === ($value = $resource->{$methodName}())) { + continue; + } + + $operation = $operation->{'with'.substr($methodName, 3)}($value); + } + + $operation = $operation->withExtraProperties(array_merge( + $resource->getExtraProperties(), + $operation->getExtraProperties(), + $generated ? ['generated_operation' => true] : [] + )); + + // Add global defaults attributes to the operation + $operation = $this->addGlobalDefaults($operation); + + if ($operation instanceof GraphQlOperation) { + if (!$operation->getName()) { + throw new RuntimeException('No GraphQL operation name.'); + } + + if ($operation instanceof Mutation) { + $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); + } + + return [$operation->getName(), $operation]; + } + + if (!$operation instanceof HttpOperation) { + throw new RuntimeException(sprintf('Operation should be an instance of "%s"', HttpOperation::class)); + } + + if ($operation->getRouteName()) { + /** @var HttpOperation $operation */ + $operation = $operation->withName($operation->getRouteName()); + } + + // Check for name conflict + if ($operation->getName() && null !== ($operations = $resource->getOperations())) { + if (!$operations->has($operation->getName())) { + return [$operation->getName(), $operation]; + } + + $this->logger->warning(sprintf('The operation "%s" already exists on the resource "%s", pick a different name or leave it empty. In the meantime we will generate a unique name.', $operation->getName(), $resource->getClass())); + /** @var HttpOperation $operation */ + $operation = $operation->withName(''); + } + + return [ + sprintf( + '_api_%s_%s%s', + $operation->getUriTemplate() ?: $operation->getShortName(), + strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), + $operation instanceof CollectionOperationInterface ? '_collection' : '', + ), + $operation, + ]; + } +} diff --git a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php index 1ceacfc5636..7b4a5ca7c70 100644 --- a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php @@ -105,7 +105,7 @@ private function generateUriTemplate(HttpOperation $operation): string } } - return sprintf('%s.{_format}', $uriTemplate); + return sprintf('%s{._format}', $uriTemplate); } private function configureUriVariables(ApiResource|HttpOperation $operation): ApiResource|HttpOperation @@ -144,8 +144,12 @@ private function configureUriVariables(ApiResource|HttpOperation $operation): Ap } $operation = $operation->withUriVariables($uriVariables); + if (str_ends_with($uriTemplate, '{._format}')) { + $uriTemplate = substr($uriTemplate, 0, -10); + } + $route = (new Route($uriTemplate))->compile(); - $variables = array_filter($route->getPathVariables(), fn ($v): bool => '_format' !== $v); + $variables = $route->getPathVariables(); if (\count($variables) !== \count($uriVariables)) { $newUriVariables = []; diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 34dc68f2cfe..75f1d2ee8bb 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -333,7 +333,8 @@ private function flattenMimeTypes(array $responseFormats): array */ private function getPath(string $path): string { - if (str_ends_with($path, '.{_format}')) { + // Handle either API Platform's URI Template (rfc6570) or Symfony's route + if (str_ends_with($path, '{._format}') || str_ends_with($path, '.{_format}')) { $path = substr($path, 0, -10); } diff --git a/src/OpenApi/Serializer/NormalizeOperationNameTrait.php b/src/OpenApi/Serializer/NormalizeOperationNameTrait.php index 53bb0b22a10..2d37d93b8f1 100644 --- a/src/OpenApi/Serializer/NormalizeOperationNameTrait.php +++ b/src/OpenApi/Serializer/NormalizeOperationNameTrait.php @@ -22,7 +22,6 @@ trait NormalizeOperationNameTrait { private function normalizeOperationName(string $operationName): string { - // .{_format} is related to the symfony router - return preg_replace('/^_/', '', str_replace(['/', '.{_format}', '{', '}'], ['', '', '_', ''], $operationName)); + return preg_replace('/^_/', '', str_replace(['/', '{._format}', '{', '}'], ['', '', '_', ''], $operationName)); } } diff --git a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php index 97cd93678b3..bf11ad4d37b 100644 --- a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php +++ b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php @@ -71,6 +71,6 @@ private function getExpectedInputClass(Request $request): ?string $context = $this->serializationContextBuilder->createFromRequest($request, false, RequestAttributesExtractor::extractAttributes($request)); - return $context['input'] ?? $context['resource_class']; + return $context['input']['class'] ?? $context['resource_class'] ?? null; } } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index fcc023abb68..3b1c8faeb5a 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -53,6 +53,7 @@ use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Yaml\Yaml; +use Twig\Environment; /** * The extension of this bundle. @@ -462,9 +463,12 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array { $enabled = $this->isConfigEnabled($container, $config['graphql']); + $graphiqlEnabled = $enabled && $this->isConfigEnabled($container, $config['graphql']['graphiql']); + $graphqlPlayGroundEnabled = $enabled && $this->isConfigEnabled($container, $config['graphql']['graphql_playground']); + $container->setParameter('api_platform.graphql.enabled', $enabled); - $container->setParameter('api_platform.graphql.graphiql.enabled', $enabled && $this->isConfigEnabled($container, $config['graphql']['graphiql'])); - $container->setParameter('api_platform.graphql.graphql_playground.enabled', $enabled && $this->isConfigEnabled($container, $config['graphql']['graphql_playground'])); + $container->setParameter('api_platform.graphql.graphiql.enabled', $graphiqlEnabled); + $container->setParameter('api_platform.graphql.graphql_playground.enabled', $graphqlPlayGroundEnabled); $container->setParameter('api_platform.graphql.collection.pagination', $config['graphql']['collection']['pagination']); if (!$enabled) { @@ -476,6 +480,15 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array $loader->load('graphql.xml'); + // @phpstan-ignore-next-line because PHPStan uses the container of the test env cache and in test the parameter kernel.bundles always contains the key TwigBundle + if (!class_exists(Environment::class) || !isset($container->getParameter('kernel.bundles')['TwigBundle'])) { + if ($graphiqlEnabled || $graphqlPlayGroundEnabled) { + throw new RuntimeException(sprintf('GraphiQL and GraphQL Playground interfaces depend on Twig. Please activate TwigBundle for the %s environnement or disable GraphiQL and GraphQL Playground.', $container->getParameter('kernel.environment'))); + } + $container->removeDefinition('api_platform.graphql.action.graphiql'); + $container->removeDefinition('api_platform.graphql.action.graphql_playground'); + } + $container->registerForAutoconfiguration(QueryItemResolverInterface::class) ->addTag('api_platform.graphql.query_resolver'); $container->registerForAutoconfiguration(QueryCollectionResolverInterface::class) diff --git a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml index 4cecc9d339a..29f9a7a761f 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml @@ -156,6 +156,11 @@ + + + + + diff --git a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml index a46540ab0a9..0b6949bb9b8 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -166,6 +166,12 @@ + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index debaefb508c..0f713b273b9 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -49,8 +49,8 @@ - - + + %kernel.debug% @@ -133,6 +133,7 @@ %api_platform.graphql.nesting_separator% + @@ -274,6 +275,14 @@ + + + %api_platform.defaults% + + + + + diff --git a/src/Symfony/Routing/ApiLoader.php b/src/Symfony/Routing/ApiLoader.php index 923a9b50110..a9b636e063c 100644 --- a/src/Symfony/Routing/ApiLoader.php +++ b/src/Symfony/Routing/ApiLoader.php @@ -75,6 +75,11 @@ public function load(mixed $data, string $type = null): RouteCollection $path = str_replace(sprintf('{%s}', $parameterName), $expandedValue, $path); } + // Within Symfony .{_format} is a special parameter but the rfc6570 specifies label expansion with a dot operator + if (str_ends_with($path, '{._format}')) { + $path = str_replace('{._format}', '.{_format}', $path); + } + if (($controller = $operation->getController()) && !$this->container->has($controller)) { throw new RuntimeException(sprintf('There is no builtin action for the "%s" operation. You need to define the controller yourself.', $operationName)); } diff --git a/src/Symfony/Routing/SkolemIriConverter.php b/src/Symfony/Routing/SkolemIriConverter.php index 051efeccb4e..c11facaa008 100644 --- a/src/Symfony/Routing/SkolemIriConverter.php +++ b/src/Symfony/Routing/SkolemIriConverter.php @@ -17,7 +17,6 @@ use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\Operation; -use SplObjectStorage; use Symfony\Component\Routing\RouterInterface; /** @@ -36,7 +35,7 @@ final class SkolemIriConverter implements IriConverterInterface public function __construct(RouterInterface $router) { $this->router = $router; - $this->objectHashMap = new SplObjectStorage(); + $this->objectHashMap = new \SplObjectStorage(); } /** diff --git a/src/Test/DoctrineMongoDbOdmTestCase.php b/src/Test/DoctrineMongoDbOdmTestCase.php index 5ee6c03ee15..6d63d70b87f 100644 --- a/src/Test/DoctrineMongoDbOdmTestCase.php +++ b/src/Test/DoctrineMongoDbOdmTestCase.php @@ -20,8 +20,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; -use function sys_get_temp_dir; - /** * Source: https://github.com/doctrine/DoctrineMongoDBBundle/blob/0174003844bc566bb4cb3b7d10c5528d1924d719/Tests/TestCase.php * Test got excluded from vendor in 4.x. @@ -32,8 +30,8 @@ public static function createTestDocumentManager($paths = []): DocumentManager { $config = new Configuration(); $config->setAutoGenerateProxyClasses(Configuration::AUTOGENERATE_FILE_NOT_EXISTS); - $config->setProxyDir(sys_get_temp_dir()); - $config->setHydratorDir(sys_get_temp_dir()); + $config->setProxyDir(\sys_get_temp_dir()); + $config->setHydratorDir(\sys_get_temp_dir()); $config->setProxyNamespace('SymfonyTests\Doctrine'); $config->setHydratorNamespace('SymfonyTests\Doctrine'); $config->setMetadataDriverImpl(new AttributeDriver($paths, new AttributeReader())); // @phpstan-ignore-line diff --git a/tests/Action/ExceptionActionTest.php b/tests/Action/ExceptionActionTest.php index 2088fdb7e50..27914032b87 100644 --- a/tests/Action/ExceptionActionTest.php +++ b/tests/Action/ExceptionActionTest.php @@ -21,7 +21,6 @@ use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use DomainException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; @@ -74,7 +73,7 @@ public function testActionWithOperationExceptionToStatus( ?array $operationExceptionToStatus, int $expectedStatusCode ): void { - $exception = new DomainException(); + $exception = new \DomainException(); $flattenException = FlattenException::create($exception); $serializer = $this->prophesize(SerializerInterface::class); @@ -135,86 +134,86 @@ public function provideOperationExceptionToStatusCases(): \Generator ]; yield 'on global attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], null, null, 100, ]; yield 'on global attributes with empty resource and operation attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], [], [], 100, ]; yield 'on global attributes and resource attributes' => [ - [DomainException::class => 100], - [DomainException::class => 200], + [\DomainException::class => 100], + [\DomainException::class => 200], null, 200, ]; yield 'on global attributes and resource attributes with empty operation attributes' => [ - [DomainException::class => 100], - [DomainException::class => 200], + [\DomainException::class => 100], + [\DomainException::class => 200], [], 200, ]; yield 'on global attributes and operation attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], null, - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; yield 'on global attributes and operation attributes with empty resource attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], [], - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; yield 'on global, resource and operation attributes' => [ - [DomainException::class => 100], - [DomainException::class => 200], - [DomainException::class => 300], + [\DomainException::class => 100], + [\DomainException::class => 200], + [\DomainException::class => 300], 300, ]; yield 'on resource attributes' => [ [], - [DomainException::class => 200], + [\DomainException::class => 200], null, 200, ]; yield 'on resource attributes with empty operation attributes' => [ [], - [DomainException::class => 200], + [\DomainException::class => 200], [], 200, ]; yield 'on resource and operation attributes' => [ [], - [DomainException::class => 200], - [DomainException::class => 300], + [\DomainException::class => 200], + [\DomainException::class => 300], 300, ]; yield 'on operation attributes' => [ [], null, - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; yield 'on operation attributes with empty resource attributes' => [ [], [], - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; } diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 5e02353fcfb..37aaa67205b 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -286,10 +286,10 @@ public function thereArePaginationEntities(int $nb): void public function thereAreOfTheseSoManyObjects(int $nb): void { for ($i = 1; $i <= $nb; ++$i) { - $dummy = $this->isOrm() ? new SoMany() : new SoManyDocument(); - $dummy->content = 'Many #'.$i; + $soMany = $this->buildSoMany(); + $soMany->content = 'Many #'.$i; - $this->manager->persist($dummy); + $this->manager->persist($soMany); } $this->manager->flush(); @@ -340,6 +340,12 @@ public function thereAreFooDummyObjectsWithFakeNames($nb): void $foo = $this->buildFooDummy(); $foo->setName($names[$i]); $foo->setDummy($dummy); + for ($j = 0; $j < 3; ++$j) { + $soMany = $this->buildSoMany(); + $soMany->content = "So many $j"; + $soMany->fooDummy = $foo; + $foo->soManies->add($soMany); + } $this->manager->persist($foo); } @@ -2200,6 +2206,11 @@ private function buildRelatedSecureDummy(): RelatedSecuredDummy|RelatedSecuredDu return $this->isOrm() ? new RelatedSecuredDummy() : new RelatedSecuredDummyDocument(); } + private function buildSoMany(): SoMany|SoManyDocument + { + return $this->isOrm() ? new SoMany() : new SoManyDocument(); + } + private function buildThirdLevel(): ThirdLevel|ThirdLevelDocument { return $this->isOrm() ? new ThirdLevel() : new ThirdLevelDocument(); diff --git a/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php b/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php index a7fc3780999..51b8811918d 100644 --- a/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php +++ b/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php @@ -297,6 +297,18 @@ private function provideApplyTestArguments(): array ], ], ], + 'partial (multiple almost same values; case insensitive)' => [ + [ + 'id' => null, + 'name' => 'ipartial', + ], + [ + 'name' => [ + 'blue car', + 'Blue Car', + ], + ], + ], 'start' => [ [ 'id' => null, diff --git a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php index 5c3f97ba515..ce722cdc0c4 100644 --- a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php +++ b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php @@ -22,7 +22,6 @@ use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; -use OutOfRangeException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -39,7 +38,7 @@ public function testApplyToCollectionWithValidOrder(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -60,7 +59,7 @@ public function testApplyToCollectionWithWrongOrder(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldNotBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -81,7 +80,7 @@ public function testApplyToCollectionWithOrderOverridden(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['foo' => 'DESC'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -102,7 +101,7 @@ public function testApplyToCollectionWithOrderOverriddenWithNoDirection(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['foo' => 'ASC'])->shouldBeCalled(); $aggregationBuilderProphecy->sort(['foo' => 'ASC', 'bar' => 'DESC'])->shouldBeCalled(); @@ -130,7 +129,7 @@ public function testApplyToCollectionWithOrderOverriddenWithAssociation(): void $lookupProphecy->alias('author_lkup')->shouldBeCalled(); $aggregationBuilderProphecy->lookup(Dummy::class)->shouldBeCalled()->willReturn($lookupProphecy->reveal()); $aggregationBuilderProphecy->unwind('$author_lkup')->shouldBeCalled(); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['author_lkup.name' => 'ASC'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); diff --git a/tests/Doctrine/Odm/Filter/SearchFilterTest.php b/tests/Doctrine/Odm/Filter/SearchFilterTest.php index 28b721f3475..5dd394ce965 100644 --- a/tests/Doctrine/Odm/Filter/SearchFilterTest.php +++ b/tests/Doctrine/Odm/Filter/SearchFilterTest.php @@ -426,6 +426,21 @@ public function provideApplyTestData(): array ], $filterFactory, ], + 'partial (multiple almost same values; case insensitive)' => [ + [ + [ + '$match' => [ + 'name' => [ + '$in' => [ + new Regex('blue car', 'i'), + new Regex('Blue Car', 'i'), + ], + ], + ], + ], + ], + $filterFactory, + ], 'start' => [ [ [ diff --git a/tests/Doctrine/Orm/Filter/SearchFilterTest.php b/tests/Doctrine/Orm/Filter/SearchFilterTest.php index 2d2f2a79e1f..7ad972534a7 100644 --- a/tests/Doctrine/Orm/Filter/SearchFilterTest.php +++ b/tests/Doctrine/Orm/Filter/SearchFilterTest.php @@ -352,6 +352,14 @@ public function provideApplyTestData(): array ], $filterFactory, ], + 'partial (multiple almost same values; case insensitive)' => [ + sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1_0, \'%%\')) OR LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1_1, \'%%\'))', $this->alias, Dummy::class), + [ + 'name_p1_0' => 'blue car', + 'name_p1_1' => 'blue car', + ], + $filterFactory, + ], 'start' => [ sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(:name_p1_0, \'%%\')', $this->alias, Dummy::class), ['name_p1_0' => 'partial'], diff --git a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php index 1209f780a9a..23b8bea0b1d 100644 --- a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php +++ b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php @@ -20,7 +20,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)] -#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] #[ODM\Document] class AbsoluteUrlDummy { diff --git a/tests/Fixtures/TestBundle/Document/Answer.php b/tests/Fixtures/TestBundle/Document/Answer.php index 03023a4c892..2b45253e55d 100644 --- a/tests/Fixtures/TestBundle/Document/Answer.php +++ b/tests/Fixtures/TestBundle/Document/Answer.php @@ -29,8 +29,8 @@ * Answer. */ #[ApiResource(operations: [new Get(), new Put(), new Patch(), new Delete(), new GetCollection(normalizationContext: ['groups' => ['foobar']])])] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer{._format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] #[ODM\Document] class Answer { diff --git a/tests/Fixtures/TestBundle/Document/Book.php b/tests/Fixtures/TestBundle/Document/Book.php index e95179ce536..c74a8a30328 100644 --- a/tests/Fixtures/TestBundle/Document/Book.php +++ b/tests/Fixtures/TestBundle/Document/Book.php @@ -22,7 +22,7 @@ * * @author Antoine Bluchet */ -#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] +#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}{._format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] #[ODM\Document] class Book { diff --git a/tests/Fixtures/TestBundle/Document/Dummy.php b/tests/Fixtures/TestBundle/Document/Dummy.php index 4a8b56c991c..cfa585544e0 100644 --- a/tests/Fixtures/TestBundle/Document/Dummy.php +++ b/tests/Fixtures/TestBundle/Document/Dummy.php @@ -29,8 +29,8 @@ * @author Alexandre Delplace */ #[ApiResource(extraProperties: ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]]], filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] #[ODM\Document] class Dummy { diff --git a/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php b/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php index ebec8ed9c59..8ccf3657399 100644 --- a/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php +++ b/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php @@ -28,8 +28,8 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyAggregateOffer { diff --git a/tests/Fixtures/TestBundle/Document/DummyOffer.php b/tests/Fixtures/TestBundle/Document/DummyOffer.php index 80bb1aecaa8..518829e632b 100644 --- a/tests/Fixtures/TestBundle/Document/DummyOffer.php +++ b/tests/Fixtures/TestBundle/Document/DummyOffer.php @@ -26,9 +26,9 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyOffer { diff --git a/tests/Fixtures/TestBundle/Document/DummyProduct.php b/tests/Fixtures/TestBundle/Document/DummyProduct.php index 7ea04a0447a..065956e5ee7 100644 --- a/tests/Fixtures/TestBundle/Document/DummyProduct.php +++ b/tests/Fixtures/TestBundle/Document/DummyProduct.php @@ -28,7 +28,7 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyProduct { diff --git a/tests/Fixtures/TestBundle/Document/DummyValidation.php b/tests/Fixtures/TestBundle/Document/DummyValidation.php index 587fd26b7be..a0fa6b10033 100644 --- a/tests/Fixtures/TestBundle/Document/DummyValidation.php +++ b/tests/Fixtures/TestBundle/Document/DummyValidation.php @@ -21,7 +21,7 @@ #[ApiResource(operations: [ new GetCollection(), - new Post(uriTemplate: 'dummy_validation.{_format}'), + new Post(uriTemplate: 'dummy_validation{._format}'), new Post(routeName: 'post_validation_groups', validationContext: ['groups' => ['a']]), new Post(routeName: 'post_validation_sequence', validationContext: ['groups' => 'app.dummy_validation.group_generator']), ] diff --git a/tests/Fixtures/TestBundle/Document/FooDummy.php b/tests/Fixtures/TestBundle/Document/FooDummy.php index cc9fe959f25..8736509e855 100644 --- a/tests/Fixtures/TestBundle/Document/FooDummy.php +++ b/tests/Fixtures/TestBundle/Document/FooDummy.php @@ -15,6 +15,8 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @@ -42,6 +44,17 @@ class FooDummy #[ODM\ReferenceOne(targetDocument: Dummy::class, cascade: ['persist'], storeAs: 'id')] private ?Dummy $dummy = null; + /** + * @var Collection + */ + #[ODM\ReferenceMany(targetDocument: SoMany::class, cascade: ['persist'], storeAs: 'id')] + public Collection $soManies; + + public function __construct() + { + $this->soManies = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Document/FourthLevel.php b/tests/Fixtures/TestBundle/Document/FourthLevel.php index 46a9cd1d615..bf653bfbc37 100644 --- a/tests/Fixtures/TestBundle/Document/FourthLevel.php +++ b/tests/Fixtures/TestBundle/Document/FourthLevel.php @@ -26,12 +26,12 @@ * @author Alan Poulain */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] #[ODM\Document] class FourthLevel { diff --git a/tests/Fixtures/TestBundle/Document/Greeting.php b/tests/Fixtures/TestBundle/Document/Greeting.php index 086b22e84b1..bb6ec15928a 100644 --- a/tests/Fixtures/TestBundle/Document/Greeting.php +++ b/tests/Fixtures/TestBundle/Document/Greeting.php @@ -19,7 +19,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/people/{id}/sent_greetings.{_format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/people/{id}/sent_greetings{._format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class Greeting { diff --git a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php index 41b63270bf8..bd2794e67a5 100644 --- a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php +++ b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php @@ -20,7 +20,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)] -#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] #[ODM\Document] class NetworkPathDummy { diff --git a/tests/Fixtures/TestBundle/Document/Question.php b/tests/Fixtures/TestBundle/Document/Question.php index 6375515e66c..417f78864b9 100644 --- a/tests/Fixtures/TestBundle/Document/Question.php +++ b/tests/Fixtures/TestBundle/Document/Question.php @@ -19,8 +19,8 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class Question { diff --git a/tests/Fixtures/TestBundle/Document/RelatedDummy.php b/tests/Fixtures/TestBundle/Document/RelatedDummy.php index 0ca2c57dc04..a400aa06450 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Document/RelatedDummy.php @@ -37,13 +37,13 @@ * @author Alexandre Delplace */ #[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.mongodb.friends'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] #[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])] #[ODM\Document] class RelatedDummy extends ParentDummy implements \Stringable diff --git a/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php index 254fd5da549..8955436b628 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php +++ b/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php @@ -25,11 +25,11 @@ * Related To Dummy Friend represent an association table for a manytomany relation. */ #[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.mongodb.name'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ODM\Document] class RelatedToDummyFriend { diff --git a/tests/Fixtures/TestBundle/Document/SlugChildDummy.php b/tests/Fixtures/TestBundle/Document/SlugChildDummy.php index 4d210239e04..890cf391652 100644 --- a/tests/Fixtures/TestBundle/Document/SlugChildDummy.php +++ b/tests/Fixtures/TestBundle/Document/SlugChildDummy.php @@ -20,8 +20,8 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class SlugChildDummy { diff --git a/tests/Fixtures/TestBundle/Document/SlugParentDummy.php b/tests/Fixtures/TestBundle/Document/SlugParentDummy.php index 4ff555b8764..ed34ea94bd6 100644 --- a/tests/Fixtures/TestBundle/Document/SlugParentDummy.php +++ b/tests/Fixtures/TestBundle/Document/SlugParentDummy.php @@ -25,8 +25,8 @@ * Custom Identifier Dummy With Subresource. */ #[ApiResource(uriVariables: 'slug')] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] #[ODM\Document] class SlugParentDummy { diff --git a/tests/Fixtures/TestBundle/Document/ThirdLevel.php b/tests/Fixtures/TestBundle/Document/ThirdLevel.php index e4b6fe19cfa..046b89afb16 100644 --- a/tests/Fixtures/TestBundle/Document/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Document/ThirdLevel.php @@ -26,11 +26,11 @@ * @author Alexandre Delplace */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] #[ODM\Document] class ThirdLevel { diff --git a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php index 4501a935a59..703abcff6a3 100644 --- a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; use ApiPlatform\Metadata\ApiResource; -use DateTime; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Symfony\Component\Serializer\Annotation\Groups; @@ -27,9 +26,12 @@ class VoDummyInspection #[ODM\Field(type: 'date')] private \DateTime $performed; - public function __construct(#[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] #[ODM\Field(type: 'bool')] private bool $accepted, #[Groups(['inspection_read', 'inspection_write'])] #[ODM\ReferenceOne(targetDocument: VoDummyCar::class, inversedBy: 'inspections')] private VoDummyCar $car, DateTime $performed = null, private string $attributeWithoutConstructorEquivalent = '') + private $attributeWithoutConstructorEquivalent; + + public function __construct(#[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] #[ODM\Field(type: 'bool')] private bool $accepted, #[Groups(['inspection_read', 'inspection_write'])] #[ODM\ReferenceOne(targetDocument: VoDummyCar::class, inversedBy: 'inspections')] private VoDummyCar $car, \DateTime $performed = null, string $parameterWhichIsNotClassAttribute = '') { - $this->performed = $performed ?: new DateTime(); + $this->performed = $performed ?: new \DateTime(); + $this->attributeWithoutConstructorEquivalent = $parameterWhichIsNotClassAttribute; } public function isAccepted(): bool @@ -42,12 +44,12 @@ public function getCar(): VoDummyCar return $this->car; } - public function getPerformed(): DateTime + public function getPerformed(): \DateTime { return $this->performed; } - public function setPerformed(DateTime $performed) + public function setPerformed(\DateTime $performed) { $this->performed = $performed; diff --git a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php index ff2fb260aca..dfb1f919c21 100644 --- a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php +++ b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php @@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)] -#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] #[ORM\Entity] class AbsoluteUrlDummy { diff --git a/tests/Fixtures/TestBundle/Entity/Answer.php b/tests/Fixtures/TestBundle/Entity/Answer.php index 7a6fb033669..945fd5e3b6d 100644 --- a/tests/Fixtures/TestBundle/Entity/Answer.php +++ b/tests/Fixtures/TestBundle/Entity/Answer.php @@ -29,8 +29,8 @@ * Answer. */ #[ApiResource(operations: [new Get(), new Put(), new Patch(), new Delete(), new GetCollection(normalizationContext: ['groups' => ['foobar']])])] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer{._format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] #[ORM\Entity] class Answer { diff --git a/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php b/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php new file mode 100644 index 00000000000..e13550bb603 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php @@ -0,0 +1,21 @@ + + * + * 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; + +use ApiPlatform\Metadata\Get; + +#[Get(name: 'my own name')] +final class AttributeOnlyOperation +{ +} diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResource.php b/tests/Fixtures/TestBundle/Entity/AttributeResource.php index 560251f7c69..0c4649e27e7 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResource.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResource.php @@ -31,7 +31,7 @@ #[Put] #[Delete] #[ApiResource( - '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', + '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', inputFormats: ['json' => ['application/merge-patch+json']], status: 301, provider: AttributeResourceProvider::class, diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResources.php b/tests/Fixtures/TestBundle/Entity/AttributeResources.php index 6685647c810..e2397c8d938 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResources.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResources.php @@ -17,18 +17,15 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use ApiPlatform\Tests\Fixtures\TestBundle\State\AttributeResourceProvider; -use ArrayIterator; -use IteratorAggregate; -use Traversable; #[ApiResource( - '/attribute_resources.{_format}', + '/attribute_resources{._format}', normalizationContext: ['skip_null_values' => true], provider: AttributeResourceProvider::class )] #[GetCollection] #[Post] -final class AttributeResources implements IteratorAggregate +final class AttributeResources implements \IteratorAggregate { /** * @var AttributeResource[] @@ -40,8 +37,8 @@ public function __construct(AttributeResource ...$collection) $this->collection = $collection; } - public function getIterator(): Traversable + public function getIterator(): \Traversable { - return new ArrayIterator($this->collection); + return new \ArrayIterator($this->collection); } } diff --git a/tests/Fixtures/TestBundle/Entity/Book.php b/tests/Fixtures/TestBundle/Entity/Book.php index 45a30eb6807..f892b81cb3f 100644 --- a/tests/Fixtures/TestBundle/Entity/Book.php +++ b/tests/Fixtures/TestBundle/Entity/Book.php @@ -22,7 +22,7 @@ * * @author Antoine Bluchet */ -#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] +#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}{._format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] #[ORM\Entity] class Book { diff --git a/tests/Fixtures/TestBundle/Entity/Dummy.php b/tests/Fixtures/TestBundle/Entity/Dummy.php index 33fc651dbd7..69e9384db63 100644 --- a/tests/Fixtures/TestBundle/Entity/Dummy.php +++ b/tests/Fixtures/TestBundle/Entity/Dummy.php @@ -28,8 +28,8 @@ * @author Kévin Dunglas */ #[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] #[ORM\Entity] class Dummy { diff --git a/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php b/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php index 33d5def4a21..7089cdf2a70 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php +++ b/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php @@ -28,8 +28,8 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyAggregateOffer { diff --git a/tests/Fixtures/TestBundle/Entity/DummyOffer.php b/tests/Fixtures/TestBundle/Entity/DummyOffer.php index 8448b8bf21c..5e9d52dad81 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyOffer.php +++ b/tests/Fixtures/TestBundle/Entity/DummyOffer.php @@ -26,9 +26,9 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyOffer { diff --git a/tests/Fixtures/TestBundle/Entity/DummyProduct.php b/tests/Fixtures/TestBundle/Entity/DummyProduct.php index 6901e719705..f2a5475d46a 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyProduct.php +++ b/tests/Fixtures/TestBundle/Entity/DummyProduct.php @@ -28,7 +28,7 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyProduct { diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php new file mode 100644 index 00000000000..cabc7a4352c --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php @@ -0,0 +1,49 @@ + + * + * 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; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + * + * @ApiResource + */ +class DummyToUpgradeProduct +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @var Collection + * + * @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAnnotation::class) + */ + private $dummysToUpgradeWithOnlyAnnotation; + + /** + * @var Collection + * + * @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAttribute::class) + */ + private $dummysToUpgradeWithOnlyAttribute; +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php new file mode 100644 index 00000000000..576ca2a55f0 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php @@ -0,0 +1,61 @@ + + * + * 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; + +use ApiPlatform\Core\Annotation\ApiFilter; +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Annotation\ApiSubresource; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * @ORM\Entity + * + * @ApiResource + * + * @ApiFilter(SearchFilter::class, properties={"id"}) + */ +class DummyToUpgradeWithOnlyAnnotation +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + * @Groups({"chicago", "friends"}) + * @ApiProperty(writable=false) + * @ApiFilter(DateFilter::class) + */ + private $id; + + /** + * @var DummyToUpgradeProduct + * + * @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", cascade={"persist"}, inversedBy="dummysToUpgradeWithOnlyAnnotation") + * @ORM\JoinColumn(nullable=false) + * @Groups({"barcelona", "chicago", "friends"}) + * + * @ApiSubresource + * + * @ApiProperty(iri="DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct") + * @ApiFilter(SearchFilter::class) + * @ApiFilter(ExistsFilter::class) + */ + private $dummyToUpgradeProduct; +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php new file mode 100644 index 00000000000..f9a1fd1f592 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php @@ -0,0 +1,49 @@ + + * + * 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; + +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Annotation\ApiSubresource; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * @ORM\Entity + */ +#[ApiResource()] +class DummyToUpgradeWithOnlyAttribute +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[Groups(['chicago', 'friends'])] + #[ApiProperty(writable: false)] + private $id; + + /** + * @var DummyToUpgradeProduct + * + * @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", inversedBy="dummysToUpgradeWithOnlyAttribute") + * @ORM\JoinColumn(nullable=false) + */ + #[Groups(['barcelona', 'chicago', 'friends'])] + #[ApiSubresource] + #[ApiProperty(iri: 'DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct')] + private $dummyToUpgradeProduct; +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyValidation.php b/tests/Fixtures/TestBundle/Entity/DummyValidation.php index e06a4f02d29..8500aa1556b 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyValidation.php +++ b/tests/Fixtures/TestBundle/Entity/DummyValidation.php @@ -21,7 +21,7 @@ #[ApiResource(operations: [ new GetCollection(), - new Post(uriTemplate: 'dummy_validation.{_format}'), + new Post(uriTemplate: 'dummy_validation{._format}'), new Post(routeName: 'post_validation_groups', validationContext: ['groups' => ['a']]), new Post(routeName: 'post_validation_sequence', validationContext: ['groups' => 'app.dummy_validation.group_generator']), ] diff --git a/tests/Fixtures/TestBundle/Entity/FooDummy.php b/tests/Fixtures/TestBundle/Entity/FooDummy.php index b744c0c2a55..92596dad682 100644 --- a/tests/Fixtures/TestBundle/Entity/FooDummy.php +++ b/tests/Fixtures/TestBundle/Entity/FooDummy.php @@ -15,6 +15,8 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -44,6 +46,17 @@ class FooDummy #[ORM\ManyToOne(targetEntity: Dummy::class, cascade: ['persist'])] private ?Dummy $dummy = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: SoMany::class, mappedBy: 'fooDummy', cascade: ['persist'])] + public Collection $soManies; + + public function __construct() + { + $this->soManies = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Entity/FourthLevel.php b/tests/Fixtures/TestBundle/Entity/FourthLevel.php index 85161c78705..b62eaed1b27 100644 --- a/tests/Fixtures/TestBundle/Entity/FourthLevel.php +++ b/tests/Fixtures/TestBundle/Entity/FourthLevel.php @@ -26,12 +26,12 @@ * @author Alan Poulain */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] #[ORM\Entity] class FourthLevel { diff --git a/tests/Fixtures/TestBundle/Entity/Greeting.php b/tests/Fixtures/TestBundle/Entity/Greeting.php index ee0bcd5602d..d74e8217b55 100644 --- a/tests/Fixtures/TestBundle/Entity/Greeting.php +++ b/tests/Fixtures/TestBundle/Entity/Greeting.php @@ -19,7 +19,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/people/{id}/sent_greetings.{_format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/people/{id}/sent_greetings{._format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class Greeting { diff --git a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php index 30070090fda..46530b96c5e 100644 --- a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php +++ b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php @@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)] -#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] #[ORM\Entity] class NetworkPathDummy { diff --git a/tests/Fixtures/TestBundle/Entity/Question.php b/tests/Fixtures/TestBundle/Entity/Question.php index 0ffc8bfd2cb..78b1afd5026 100644 --- a/tests/Fixtures/TestBundle/Entity/Question.php +++ b/tests/Fixtures/TestBundle/Entity/Question.php @@ -19,8 +19,8 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class Question { diff --git a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php index bbb36c24aca..a6e60f79169 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php @@ -35,14 +35,22 @@ * * @author Kévin Dunglas */ -#[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource( + graphQlOperations: [ + new Query(name: 'item_query'), + new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']]), + ], + types: ['https://schema.org/Product'], + normalizationContext: ['groups' => ['friends']], + filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'] +)] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] #[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])] #[ORM\Entity] class RelatedDummy extends ParentDummy implements \Stringable diff --git a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php index b7823a78d86..c584c30596b 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php @@ -25,11 +25,11 @@ * Related To Dummy Friend represent an association table for a manytomany relation. */ #[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ORM\Entity] class RelatedToDummyFriend { diff --git a/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php b/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php index bab3c01e521..c1d8c5b21e4 100644 --- a/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php +++ b/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php @@ -20,8 +20,8 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class SlugChildDummy { diff --git a/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php b/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php index c6412d78229..104ca2cc97e 100644 --- a/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php +++ b/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php @@ -25,8 +25,8 @@ * Custom Identifier Dummy With Subresource. */ #[ApiResource(uriVariables: 'slug')] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] #[ORM\Entity] class SlugParentDummy { diff --git a/tests/Fixtures/TestBundle/Entity/SoMany.php b/tests/Fixtures/TestBundle/Entity/SoMany.php index 65a2ad93790..e3770b8007d 100644 --- a/tests/Fixtures/TestBundle/Entity/SoMany.php +++ b/tests/Fixtures/TestBundle/Entity/SoMany.php @@ -31,4 +31,7 @@ class SoMany public $id; #[ORM\Column(nullable: true)] public $content; + + #[ORM\ManyToOne] + public ?FooDummy $fooDummy; } diff --git a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php index 1d18d362d73..1ca2e4c6f43 100644 --- a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php @@ -25,11 +25,11 @@ * @author Kévin Dunglas */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] #[ORM\Entity] class ThirdLevel { diff --git a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php index fc3eb0561f8..7eebd395415 100644 --- a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; use ApiPlatform\Metadata\ApiResource; -use DateTime; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; @@ -28,9 +27,12 @@ class VoDummyInspection #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private \DateTime $performed; - public function __construct(#[ORM\Column(type: 'boolean')] #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private bool $accepted, #[ORM\ManyToOne(targetEntity: VoDummyCar::class, inversedBy: 'inspections')] #[Groups(['inspection_read', 'inspection_write'])] private ?VoDummyCar $car, DateTime $performed = null, private string $attributeWithoutConstructorEquivalent = '') + private $attributeWithoutConstructorEquivalent; + + public function __construct(#[ORM\Column(type: 'boolean')] #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private bool $accepted, #[ORM\ManyToOne(targetEntity: VoDummyCar::class, inversedBy: 'inspections')] #[Groups(['inspection_read', 'inspection_write'])] private ?VoDummyCar $car, \DateTime $performed = null, string $parameterWhichIsNotClassAttribute = '') { - $this->performed = $performed ?: new DateTime(); + $this->performed = $performed ?: new \DateTime(); + $this->attributeWithoutConstructorEquivalent = $parameterWhichIsNotClassAttribute; } public function isAccepted(): bool @@ -43,12 +45,12 @@ public function getCar(): ?VoDummyCar return $this->car; } - public function getPerformed(): DateTime + public function getPerformed(): \DateTime { return $this->performed; } - public function setPerformed(DateTime $performed) + public function setPerformed(\DateTime $performed) { $this->performed = $performed; diff --git a/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php b/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php new file mode 100644 index 00000000000..65c4b65c51f --- /dev/null +++ b/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.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\Tests\GraphQl\Metadata\Factory; + +use ApiPlatform\GraphQl\Metadata\Factory\GraphQlNestedOperationResourceMetadataFactory; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; + +final class GraphQlNestedOperationResourceMetadataFactoryTest extends TestCase +{ + use ProphecyTrait; + + public function testCreate(): void + { + $decorated = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $decorated->create('someClass')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('someClass')); + + $metadataFactory = new GraphQlNestedOperationResourceMetadataFactory(['status' => 500], $decorated->reveal()); + $apiResource = $metadataFactory->create('someClass')[0]; + $this->assertCount(5, $apiResource->getGraphQlOperations()); + } + + public function testCreateWithResource(): void + { + $metadataFactory = new GraphQlNestedOperationResourceMetadataFactory(['status' => 500]); + $apiResource = $metadataFactory->create(RelatedDummy::class)[0]; + $this->assertNotEmpty($apiResource->getFilters()); + $this->assertEquals('RelatedDummy', $apiResource->getShortName()); + } +} diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php index c7bb3ba897a..c5b59ad50b7 100644 --- a/tests/GraphQl/Type/FieldsBuilderTest.php +++ b/tests/GraphQl/Type/FieldsBuilderTest.php @@ -56,29 +56,18 @@ class FieldsBuilderTest extends TestCase use ProphecyTrait; private ObjectProphecy $propertyNameCollectionFactoryProphecy; - private ObjectProphecy $propertyMetadataFactoryProphecy; - private ObjectProphecy $resourceMetadataCollectionFactoryProphecy; - + private ObjectProphecy $graphQlNestedOperationResourceMetadataFactoryProphecy; private ObjectProphecy $typesContainerProphecy; - private ObjectProphecy $typeBuilderProphecy; - private ObjectProphecy $typeConverterProphecy; - private ObjectProphecy $itemResolverFactoryProphecy; - private ObjectProphecy $collectionResolverFactoryProphecy; - private ObjectProphecy $itemMutationResolverFactoryProphecy; - private ObjectProphecy $itemSubscriptionResolverFactoryProphecy; - private ObjectProphecy $filterLocatorProphecy; - private ObjectProphecy $resourceClassResolverProphecy; - private FieldsBuilder $fieldsBuilder; /** @@ -98,12 +87,13 @@ protected function setUp(): void $this->itemSubscriptionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); $this->filterLocatorProphecy = $this->prophesize(ContainerInterface::class); $this->resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $this->graphQlNestedOperationResourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $this->fieldsBuilder = $this->buildFieldsBuilder(); } private function buildFieldsBuilder(?AdvancedNameConverterInterface $advancedNameConverter = null): FieldsBuilder { - return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); + return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__', $this->graphQlNestedOperationResourceMetadataFactoryProphecy->reveal()); } public function testGetNodeQueryFields(): void @@ -139,7 +129,6 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $queryFields = $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration); @@ -150,8 +139,8 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati public function itemQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new Query())->withName('action'), [], null, null, []], - 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, + 'no resource field configuration' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action'), [], null, null, []], + 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, [ 'actionShortName' => [ 'type' => GraphQLType::string(), @@ -164,7 +153,7 @@ public function itemQueryFieldsProvider(): array ], ], ], - 'nominal item case' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { + 'nominal item case' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { }, [ 'actionShortName' => [ @@ -179,7 +168,7 @@ public function itemQueryFieldsProvider(): array ], ], 'empty overridden args and add fields' => [ - 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, + 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -192,7 +181,7 @@ public function itemQueryFieldsProvider(): array ], ], 'override args with custom ones' => [ - 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, + 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -220,7 +209,6 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true); $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation)->willReturn($graphqlType); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $this->filterLocatorProphecy->has('my_filter')->willReturn(true); $filterProphecy = $this->prophesize(FilterInterface::class); @@ -244,8 +232,8 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o public function collectionQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withName('action'), [], null, null, []], - 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action'), [], null, null, []], + 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -274,7 +262,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with filters' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with filters' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -308,7 +296,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection empty overridden args and add fields' => [ - 'resourceClass', (new QueryCollection())->withArgs([])->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withArgs([])->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -322,7 +310,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection override args with custom ones' => [ - 'resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -338,7 +326,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -371,8 +359,6 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemMutationResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($mutationResolver); $mutationFields = $this->fieldsBuilder->getMutationFields($resourceClass, $operation); @@ -383,7 +369,7 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio public function mutationFieldsProvider(): array { return [ - 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -403,7 +389,7 @@ public function mutationFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'custom description' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -435,7 +421,6 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemSubscriptionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($subscriptionResolver); @@ -447,9 +432,9 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper public function subscriptionFieldsProvider(): array { return [ - 'mercure not enabled' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], + 'mercure not enabled' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], ], - 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -469,7 +454,7 @@ public function subscriptionFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'custom description' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -498,6 +483,8 @@ public function subscriptionFieldsProvider(): array public function testGetResourceObjectTypeFields(string $resourceClass, Operation $operation, array $properties, bool $input, int $depth, ?array $ioMetadata, array $expectedResourceObjectTypeFields, ?AdvancedNameConverterInterface $advancedNameConverter = null): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); + $this->resourceClassResolverProphecy->isResourceClass('nestedResourceClass')->willReturn(true); + $this->resourceClassResolverProphecy->isResourceClass('nestedResourceNoQueryClass')->willReturn(true); $this->resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(false); $this->propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection(array_keys($properties))); foreach ($properties as $propertyName => $propertyMetadata) { @@ -505,20 +492,32 @@ public function testGetResourceObjectTypeFields(string $resourceClass, Operation $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_NULL), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(null); $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_CALLABLE), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn('NotRegisteredType'); $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::string()); + $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); + if ('propertyObject' === $propertyName) { $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); - $this->resourceMetadataCollectionFactoryProphecy->create('objectClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation)->willReturn(static function (): void { }); } - $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'anotherResourceClass', $propertyName, $depth + 1)->willReturn(GraphQLType::string()); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); + if ('propertyNestedResource' === $propertyName) { + $nestedResourceQueryOperation = new Query(); + $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceClass')->willReturn(new ResourceMetadataCollection('nestedResourceClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->itemResolverFactoryProphecy->__invoke('nestedResourceClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { + }); + } + if ('propertyNestedResourceNoQuery' === $propertyName) { + $nestedResourceQueryOperation = new Query(); + $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceNoQueryClass')->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withDescription('A description.')->withGraphQlOperations([])])); + $this->graphQlNestedOperationResourceMetadataFactoryProphecy->create('nestedResourceNoQueryClass')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceNoQueryClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->itemResolverFactoryProphecy->__invoke('nestedResourceNoQueryClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { + }); + } } $this->typesContainerProphecy->has('NotRegisteredType')->willReturn(false); $this->typesContainerProphecy->all()->willReturn([]); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->resourceMetadataCollectionFactoryProphecy->create('resourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); - $this->resourceMetadataCollectionFactoryProphecy->create('anotherResourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $fieldsBuilder = $this->fieldsBuilder; if ($advancedNameConverter) { @@ -535,7 +534,7 @@ public function resourceObjectTypeFieldsProvider(): array $advancedNameConverter->normalize('field', 'resourceClass')->willReturn('normalizedField'); return [ - 'query' => ['resourceClass', new Query(), + 'query' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(false), @@ -563,7 +562,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with advanced name converter' => ['resourceClass', new Query(), + 'query with advanced name converter' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'field' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(false), ], @@ -582,7 +581,7 @@ public function resourceObjectTypeFieldsProvider(): array ], $advancedNameConverter->reveal(), ], - 'query input' => ['resourceClass', new Query(), + 'query input' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(false), @@ -601,7 +600,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with simple non-null string array property' => ['resourceClass', new Query(), + 'query with simple non-null string array property' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'property' => (new ApiProperty())->withBuiltinTypes([ new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), @@ -621,12 +620,40 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'mutation non input' => ['resourceClass', (new Mutation())->withName('mutation'), + 'query with nested resources' => ['resourceClass', (new Query())->withClass('resourceClass'), + [ + 'propertyNestedResource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceClass')])->withReadable(true)->withWritable(true), + 'propertyNestedResourceNoQuery' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceNoQueryClass')])->withReadable(true)->withWritable(true), + ], + false, 0, null, + [ + 'id' => [ + 'type' => GraphQLType::nonNull(GraphQLType::id()), + ], + 'propertyNestedResource' => [ + 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), + 'description' => null, + 'args' => [], + 'resolve' => static function (): void { + }, + 'deprecationReason' => null, + ], + 'propertyNestedResourceNoQuery' => [ + 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), + 'description' => null, + 'args' => [], + 'resolve' => static function (): void { + }, + 'deprecationReason' => null, + ], + ], + ], + 'mutation non input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), 'propertyReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(true), - 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL, false, 'objectClass')])->withReadable(true)->withWritable(true), + 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'objectClass')])->withReadable(true)->withWritable(true), ], false, 0, null, [ @@ -650,7 +677,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'mutation input' => ['resourceClass', (new Mutation())->withName('mutation'), + 'mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -686,7 +713,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'mutation nested input' => ['resourceClass', (new Mutation())->withName('mutation'), + 'mutation nested input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -705,7 +732,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'delete mutation input' => ['resourceClass', (new Mutation())->withName('delete'), + 'delete mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('delete'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -717,7 +744,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'create mutation input' => ['resourceClass', (new Mutation())->withName('create'), + 'create mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('create'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -733,7 +760,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'update mutation input' => ['resourceClass', (new Mutation())->withName('update'), + 'update mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('update'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -752,7 +779,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'subscription non input' => ['resourceClass', new Subscription(), + 'subscription non input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), @@ -772,7 +799,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'subscription input' => ['resourceClass', new Subscription(), + 'subscription input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -787,13 +814,13 @@ public function resourceObjectTypeFieldsProvider(): array 'clientSubscriptionId' => GraphQLType::string(), ], ], - 'null io metadata non input' => ['resourceClass', new Query(), + 'null io metadata non input' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], false, 0, ['class' => null], [], ], - 'null io metadata input' => ['resourceClass', new Query(), + 'null io metadata input' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -802,7 +829,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'invalid types' => ['resourceClass', new Query(), + 'invalid types' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'propertyInvalidType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_NULL)])->withReadable(true)->withWritable(false), 'propertyNotRegisteredType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_CALLABLE)])->withReadable(true)->withWritable(false), diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index 66b0e7cd855..77ba4f7e842 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -483,7 +483,7 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPageInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); $this->assertSame('StringCursorConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Cursor connection for String.', $resourcePaginatedCollectionType->description); @@ -538,7 +538,7 @@ public function testPageBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPaginationInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); $this->assertSame('StringPageConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Page connection for String.', $resourcePaginatedCollectionType->description); diff --git a/tests/HttpCache/VarnishPurgerTest.php b/tests/HttpCache/VarnishPurgerTest.php index ffc2a5b53f4..faca342dc91 100644 --- a/tests/HttpCache/VarnishPurgerTest.php +++ b/tests/HttpCache/VarnishPurgerTest.php @@ -17,7 +17,6 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; -use LogicException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; @@ -79,12 +78,12 @@ public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLengt public function send(RequestInterface $request, array $options = []): ResponseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function request($method, $uri, array $options = []): ResponseInterface @@ -96,12 +95,12 @@ public function request($method, $uri, array $options = []): ResponseInterface public function requestAsync($method, $uri, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function getConfig($option = null): void { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } }; diff --git a/tests/HttpCache/VarnishXKeyPurgerTest.php b/tests/HttpCache/VarnishXKeyPurgerTest.php index 33c64da8678..96e380752b9 100644 --- a/tests/HttpCache/VarnishXKeyPurgerTest.php +++ b/tests/HttpCache/VarnishXKeyPurgerTest.php @@ -17,7 +17,6 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; -use LogicException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; @@ -109,12 +108,12 @@ public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLengt public function send(RequestInterface $request, array $options = []): ResponseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function request($method, $uri, array $options = []): ResponseInterface @@ -126,12 +125,12 @@ public function request($method, $uri, array $options = []): ResponseInterface public function requestAsync($method, $uri, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function getConfig($option = null): void { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } }; diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index fb6040a5de1..ae69822be21 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -49,21 +49,21 @@ public function testExecuteWithoutOption(): void public function testExecuteWithItemOperationGet(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies/{id}.{_format}_get', '--type' => 'output']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies/{id}{._format}_get', '--type' => 'output']); $this->assertJson($this->tester->getDisplay()); } public function testExecuteWithCollectionOperationGet(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies.{_format}_get_collection', '--type' => 'output']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies{._format}_get_collection', '--type' => 'output']); $this->assertJson($this->tester->getDisplay()); } public function testExecuteWithJsonldFormatOption(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies.{_format}_post', '--format' => 'jsonld']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies{._format}_post', '--format' => 'jsonld']); $result = $this->tester->getDisplay(); $this->assertStringContainsString('@id', $result); diff --git a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php index 04279076970..4c34da9000f 100644 --- a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php @@ -21,7 +21,6 @@ use ApiPlatform\Tests\Metadata\Extractor\Adapter\PropertyAdapterInterface; use ApiPlatform\Tests\Metadata\Extractor\Adapter\XmlPropertyAdapter; use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlPropertyAdapter; -use Exception; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Type; @@ -87,7 +86,7 @@ public function testValidMetadata(string $extractorClass, PropertyAdapterInterfa $extractor = new $extractorClass($adapter(self::RESOURCE_CLASS, self::PROPERTY, $parameters, self::FIXTURES)); $factory = new ExtractorPropertyMetadataFactory($extractor); $property = $factory->create(self::RESOURCE_CLASS, self::PROPERTY); - } catch (Exception $exception) { + } catch (\Exception $exception) { throw new AssertionFailedError('Failed asserting that the schema is valid according to '.ApiProperty::class, 0, $exception); } diff --git a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php index 20342ad36e6..e6ce7a3727d 100644 --- a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php @@ -20,10 +20,13 @@ use ApiPlatform\Metadata\Extractor\YamlResourceExtractor; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -34,7 +37,6 @@ use ApiPlatform\Tests\Metadata\Extractor\Adapter\ResourceAdapterInterface; use ApiPlatform\Tests\Metadata\Extractor\Adapter\XmlResourceAdapter; use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlResourceAdapter; -use Exception; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; @@ -224,7 +226,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase [ 'name' => 'custom_operation_name', 'method' => 'GET', - 'uriTemplate' => '/users/{userId}/comments.{_format}', + 'uriTemplate' => '/users/{userId}/comments{._format}', 'shortName' => self::SHORT_NAME, 'description' => 'A list of Comments', 'types' => ['Comment'], @@ -330,7 +332,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase ], ], [ - 'uriTemplate' => '/users/{userId}/comments/{commentId}.{_format}', + 'uriTemplate' => '/users/{userId}/comments/{commentId}{._format}', 'class' => Get::class, 'uriVariables' => [ 'userId' => [ @@ -420,7 +422,7 @@ public function testValidMetadata(string $extractorClass, ResourceAdapterInterfa $extractor = new $extractorClass($adapter(self::RESOURCE_CLASS, $parameters, self::FIXTURES)); $factory = new ExtractorResourceMetadataCollectionFactory($extractor); $collection = $factory->create(self::RESOURCE_CLASS); - } catch (Exception $exception) { + } catch (\Exception $exception) { throw new AssertionFailedError('Failed asserting that the schema is valid according to '.ApiResource::class, 0, $exception); } @@ -453,7 +455,16 @@ private function buildApiResources(): array $operations[$operationName] = $this->getOperationWithDefaults($resource, $operation)->withName($operationName); } - $resources[] = $resource->withOperations(new Operations($operations)); + $resource = $resource->withOperations(new Operations($operations)); + + // Build default GraphQL operations + $graphQlOperations = []; + foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $graphQlOperation) { + $description = $graphQlOperation instanceof Mutation ? ucfirst("{$graphQlOperation->getName()}s a {$resource->getShortName()}.") : null; + $graphQlOperations[$graphQlOperation->getName()] = $this->getOperationWithDefaults($resource, $graphQlOperation)->withName($graphQlOperation->getName())->withDescription($description); + } + + $resources[] = $resource->withGraphQlOperations($graphQlOperations); continue; } @@ -591,7 +602,7 @@ private function withGraphQlOperations(array $values, ?array $fixtures): array return $operations; } - private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation + private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation { foreach (get_class_methods($resource) as $methodName) { if (!str_starts_with($methodName, 'get')) { diff --git a/tests/Metadata/Extractor/XmlExtractorTest.php b/tests/Metadata/Extractor/XmlExtractorTest.php index dc1f9b45e8f..b229f52f1b6 100644 --- a/tests/Metadata/Extractor/XmlExtractorTest.php +++ b/tests/Metadata/Extractor/XmlExtractorTest.php @@ -99,7 +99,7 @@ public function testValidXML(): void 'write' => null, ], [ - 'uriTemplate' => '/users/{author}/comments.{_format}', + 'uriTemplate' => '/users/{author}/comments{._format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, @@ -177,7 +177,7 @@ public function testValidXML(): void [ 'name' => 'custom_operation_name', 'class' => GetCollection::class, - 'uriTemplate' => '/users/{author}/comments.{_format}', + 'uriTemplate' => '/users/{author}/comments{._format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, @@ -268,7 +268,7 @@ public function testValidXML(): void [ 'name' => null, 'class' => Get::class, - 'uriTemplate' => '/users/{userId}/comments/{id}.{_format}', + 'uriTemplate' => '/users/{userId}/comments/{id}{._format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, diff --git a/tests/Metadata/Extractor/YamlExtractorTest.php b/tests/Metadata/Extractor/YamlExtractorTest.php index 843df41f723..cede4108ebc 100644 --- a/tests/Metadata/Extractor/YamlExtractorTest.php +++ b/tests/Metadata/Extractor/YamlExtractorTest.php @@ -166,7 +166,7 @@ public function testValidYaml(): void 'write' => null, ], [ - 'uriTemplate' => '/users/{author}/programs.{_format}', + 'uriTemplate' => '/users/{author}/programs{._format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -230,7 +230,7 @@ public function testValidYaml(): void [ 'name' => null, 'class' => GetCollection::class, - 'uriTemplate' => '/users/{author}/programs.{_format}', + 'uriTemplate' => '/users/{author}/programs{._format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -305,7 +305,7 @@ public function testValidYaml(): void [ 'name' => null, 'class' => Get::class, - 'uriTemplate' => '/users/{userId}/programs/{id}.{_format}', + 'uriTemplate' => '/users/{userId}/programs/{id}{._format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -471,7 +471,7 @@ public function testInvalidYaml(string $path, string $error): void public function getInvalidPaths(): array { return [ - [__DIR__.'/yaml/invalid/invalid_resources.yaml', '"resources" setting is expected to be null or an array, string given in "'.__DIR__.'/yaml/invalid/invalid_resources.yaml'.'".'], + [__DIR__.'/yaml/invalid/invalid_resources.yaml', '"resources" setting is expected to be null or an array, string given in "'.__DIR__.'/yaml/invalid/invalid_resources.yaml".'], ]; } } diff --git a/tests/Metadata/Extractor/xml/valid.xml b/tests/Metadata/Extractor/xml/valid.xml index 3269a7ba104..cfcccc202d3 100644 --- a/tests/Metadata/Extractor/xml/valid.xml +++ b/tests/Metadata/Extractor/xml/valid.xml @@ -7,7 +7,7 @@ @@ -96,7 +96,7 @@ - + diff --git a/tests/Metadata/Extractor/yaml/valid.yaml b/tests/Metadata/Extractor/yaml/valid.yaml index d001ba20899..c3a80a5acac 100644 --- a/tests/Metadata/Extractor/yaml/valid.yaml +++ b/tests/Metadata/Extractor/yaml/valid.yaml @@ -3,7 +3,7 @@ resources: ApiPlatform\Tests\Fixtures\TestBundle\Entity\Program: - ~ - - uriTemplate: /users/{author}/programs.{_format} + - uriTemplate: /users/{author}/programs{._format} uriVariables: ['author'] types: ['someirischema'] description: User programs @@ -13,7 +13,7 @@ resources: operations: ApiPlatform\Metadata\GetCollection: ~ ApiPlatform\Metadata\Get: - uriTemplate: /users/{userId}/programs/{id}.{_format} + uriTemplate: /users/{userId}/programs/{id}{._format} types: ['anotheririschema'] uriVariables: userId: [ApiPlatform\Tests\Fixtures\TestBundle\Entity\User, 'author'] diff --git a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php index 0420c8d3be8..6510080c88d 100644 --- a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php @@ -28,6 +28,7 @@ use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeDefaultOperations; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeOnlyOperation; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResources; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ExtraPropertiesResource; @@ -81,11 +82,11 @@ class: AttributeResource::class, new ApiResource( shortName: 'AttributeResource', class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', operations: [ - '_api_/dummy/{dummyId}/attribute_resources/{identifier}.{_format}_get' => new Get( + '_api_/dummy/{dummyId}/attribute_resources/{identifier}{._format}_get' => new Get( class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', shortName: 'AttributeResource', inputFormats: ['json' => ['application/merge-patch+json']], priority: 4, @@ -94,9 +95,9 @@ class: AttributeResource::class, // @noRector \Rector\Php81\Rector\Array_\FirstClassCallableRector processor: [AttributeResourceProcessor::class, 'process'] ), - '_api_/dummy/{dummyId}/attribute_resources/{identifier}.{_format}_patch' => new Patch( + '_api_/dummy/{dummyId}/attribute_resources/{identifier}{._format}_patch' => new Patch( class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', shortName: 'AttributeResource', inputFormats: ['json' => ['application/merge-patch+json']], priority: 5, @@ -119,17 +120,17 @@ class: AttributeResource::class, $this->assertEquals( new ResourceMetadataCollection(AttributeResources::class, [ new ApiResource( - uriTemplate: '/attribute_resources.{_format}', + uriTemplate: '/attribute_resources{._format}', shortName: 'AttributeResources', normalizationContext: ['skip_null_values' => true], class: AttributeResources::class, provider: AttributeResourceProvider::class, operations: [ - '_api_/attribute_resources.{_format}_get_collection' => new GetCollection( - shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources.{_format}', normalizationContext: ['skip_null_values' => true], priority: 1, provider: AttributeResourceProvider::class, + '_api_/attribute_resources{._format}_get_collection' => new GetCollection( + shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources{._format}', normalizationContext: ['skip_null_values' => true], priority: 1, provider: AttributeResourceProvider::class, ), - '_api_/attribute_resources.{_format}_post' => new Post( - shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources.{_format}', normalizationContext: ['skip_null_values' => true], priority: 2, provider: AttributeResourceProvider::class, + '_api_/attribute_resources{._format}_post' => new Post( + shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources{._format}', normalizationContext: ['skip_null_values' => true], priority: 2, provider: AttributeResourceProvider::class, ), ], graphQlOperations: $this->getDefaultGraphqlOperations('AttributeResources', AttributeResources::class, AttributeResourceProvider::class) @@ -209,4 +210,20 @@ public function testExtraProperties(): void $this->assertEquals($extraPropertiesResource[0]->getExtraProperties(), ['foo' => 'bar']); $this->assertEquals($extraPropertiesResource->getOperation('_api_ExtraPropertiesResource_get')->getExtraProperties(), ['foo' => 'bar']); } + + public function testOverrideNameWithoutOperations(): void + { + $attributeResourceMetadataCollectionFactory = new AttributesResourceMetadataCollectionFactory(); + + $operation = new HttpOperation(shortName: 'AttributeOnlyOperation', class: AttributeOnlyOperation::class); + $this->assertEquals(new ResourceMetadataCollection(AttributeOnlyOperation::class, [ + new ApiResource( + shortName: 'AttributeOnlyOperation', + class: AttributeOnlyOperation::class, + operations: [ + 'my own name' => (new Get(name: 'my own name', priority: 1))->withOperation($operation), + ] + ), + ]), $attributeResourceMetadataCollectionFactory->create(AttributeOnlyOperation::class)); + } } diff --git a/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php index d60638d3f71..7603509b3a9 100644 --- a/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php @@ -115,10 +115,10 @@ class: AttributeResource::class, shortName: 'AttributeResource', class: AttributeResource::class, operations: [ - '_api_/attribute_resources/{id}.{_format}_get' => new Get(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_get'), - '_api_/attribute_resources/{id}.{_format}_put' => new Put(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_put'), - '_api_/attribute_resources/{id}.{_format}_delete' => new Delete(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_delete'), - '_api_/attribute_resources.{_format}_get_collection' => new GetCollection(uriTemplate: '/attribute_resources.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', name: '_api_/attribute_resources.{_format}_get_collection'), + '_api_/attribute_resources/{id}{._format}_get' => new Get(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_get'), + '_api_/attribute_resources/{id}{._format}_put' => new Put(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_put'), + '_api_/attribute_resources/{id}{._format}_delete' => new Delete(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_delete'), + '_api_/attribute_resources{._format}_get_collection' => new GetCollection(uriTemplate: '/attribute_resources{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', name: '_api_/attribute_resources{._format}_get_collection'), ] ), new ApiResource( diff --git a/tests/State/Pagination/TraversablePaginatorTest.php b/tests/State/Pagination/TraversablePaginatorTest.php index ba51143119d..e9138e7571d 100644 --- a/tests/State/Pagination/TraversablePaginatorTest.php +++ b/tests/State/Pagination/TraversablePaginatorTest.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Tests\State\Pagination; use ApiPlatform\State\Pagination\TraversablePaginator; -use ArrayIterator; use PHPUnit\Framework\TestCase; class TraversablePaginatorTest extends TestCase @@ -30,7 +29,7 @@ public function testInitialize( float $lastPage, int $currentItems ): void { - $traversable = new ArrayIterator($results); + $traversable = new \ArrayIterator($results); $paginator = new TraversablePaginator($traversable, $currentPage, $perPage, $totalItems); diff --git a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php index ad0f265a015..a574d5b9dc2 100644 --- a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php +++ b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php @@ -205,7 +205,7 @@ public function provideIntegrationCases(): iterable yield 'simple' => [ $this->createRequest('PUT', [ '_api_resource_class' => ResourceImplementation::class, - '_api_operation_name' => '_api_/resource_implementations.{_format}_put', + '_api_operation_name' => '_api_/resource_implementations{._format}_put', 'data' => $resource, ]), static function (ResourceImplementation $payload): void {}, @@ -215,7 +215,7 @@ static function (ResourceImplementation $payload): void {}, yield 'with another argument named $data' => [ $this->createRequest('PUT', [ '_api_resource_class' => ResourceImplementation::class, - '_api_operation_name' => '_api_/resource_implementations.{_format}_put', + '_api_operation_name' => '_api_/resource_implementations{._format}_put', 'data' => $resource, ]), static function (ResourceImplementation $payload, $data): void {}, @@ -230,7 +230,7 @@ private function createArgumentResolver(): PayloadArgumentResolver (new ApiResource())->withShortName('ResourceImplementation')->withOperations(new Operations([ 'update' => new Put(), 'update_no_deserialize' => (new Put())->withDeserialize(false), - 'update_with_dto' => (new Put())->withInput(['class' => NotResource::class]), + 'update_with_dto' => (new Put())->withInput(['class' => NotResource::class, 'name' => 'NotResource']), 'create' => new Post(), ])), ])); @@ -251,7 +251,7 @@ private function createArgumentResolver(): PayloadArgumentResolver ]; if ('update_with_dto' === $request->attributes->get('_api_operation_name')) { - $context['input'] = NotResource::class; + $context['input'] = ['class' => NotResource::class, 'name' => 'NotResource']; } else { $context['input'] = null; } diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index cf89456b6af..5be3cc47aed 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -70,7 +70,9 @@ use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Uid\AbstractUid; @@ -163,6 +165,7 @@ protected function setUp(): void 'kernel.bundles' => [ 'DoctrineBundle' => DoctrineBundle::class, 'SecurityBundle' => SecurityBundle::class, + 'TwigBundle' => TwigBundle::class, ], 'kernel.bundles_metadata' => [ 'TestBundle' => [ @@ -173,6 +176,7 @@ protected function setUp(): void ], 'kernel.project_dir' => __DIR__.'/../../../Fixtures/app', 'kernel.debug' => false, + 'kernel.environment' => 'test', ]); $this->container = new ContainerBuilder($containerParameterBag); @@ -667,6 +671,8 @@ public function testGraphQlConfiguration(): void 'api_platform.graphql.normalizer.validation_exception', 'api_platform.graphql.normalizer.http_exception', 'api_platform.graphql.normalizer.runtime_exception', + 'api_platform.graphql_metadata.resource.metadata_collection_factory', + 'api_platform.graphql_metadata.resource.metadata_collection_factory.filters', ]; $aliases = [ @@ -693,6 +699,37 @@ public function testGraphQlConfiguration(): void $this->assertServiceHasTags('api_platform.graphql.normalizer.runtime_exception', ['serializer.normalizer']); } + public function testRuntimeExceptionIsThrownIfTwigIsNotEnabledButGraphqlClientsAre(): void + { + $config = self::DEFAULT_CONFIG; + $config['api_platform']['graphql']['enabled'] = true; + $this->container->getParameterBag()->set('kernel.bundles', [ + 'DoctrineBundle' => DoctrineBundle::class, + 'SecurityBundle' => SecurityBundle::class, + ]); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('GraphiQL and GraphQL Playground interfaces depend on Twig. Please activate TwigBundle for the test environnement or disable GraphiQL and GraphQL Playground.'); + + (new ApiPlatformExtension())->load($config, $this->container); + } + + public function testGraphqlClientsDefinitionsAreRemovedIfDisabled(): void + { + $config = self::DEFAULT_CONFIG; + $config['api_platform']['graphql']['enabled'] = true; + $config['api_platform']['graphql']['graphiql']['enabled'] = false; + $config['api_platform']['graphql']['graphql_playground']['enabled'] = false; + $this->container->getParameterBag()->set('kernel.bundles', [ + 'DoctrineBundle' => DoctrineBundle::class, + 'SecurityBundle' => SecurityBundle::class, + ]); + + (new ApiPlatformExtension())->load($config, $this->container); + + $this->assertNotContainerHasService('api_platform.graphql.action.graphiql'); + $this->assertNotContainerHasService('api_platform.graphql.action.graphql_playground'); + } + public function testDoctrineOrmConfiguration(): void { $config = self::DEFAULT_CONFIG; From d44ddb152780dc9a9f4ac4c157a074331a67a99f Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 4 Nov 2022 20:20:44 +0100 Subject: [PATCH 06/14] Revert "Merge 3.0 into main (#5121)" This reverts commit 6abd0fe0a69d4842eb6d5c31ef2bd6dce0e1d372. --- CHANGELOG.md | 32 ---- features/graphql/collection.feature | 46 ----- features/main/default_order.feature | 36 +--- generate-changelog.sh | 25 --- src/Doctrine/Odm/Extension/OrderExtension.php | 3 +- src/Doctrine/Orm/Filter/SearchFilter.php | 6 +- src/GraphQl/Action/EntrypointAction.php | 6 +- ...NestedOperationResourceMetadataFactory.php | 67 -------- src/GraphQl/Type/FieldsBuilder.php | 71 ++++---- src/Metadata/Operations.php | 7 +- ...butesResourceMetadataCollectionFactory.php | 139 ++++++++++++++- ...actorResourceMetadataCollectionFactory.php | 25 +-- ...ltersResourceMetadataCollectionFactory.php | 10 +- .../Factory/OperationDefaultsTrait.php | 160 ------------------ ...plateResourceMetadataCollectionFactory.php | 8 +- src/OpenApi/Factory/OpenApiFactory.php | 3 +- .../NormalizeOperationNameTrait.php | 3 +- .../PayloadArgumentResolver.php | 2 +- .../ApiPlatformExtension.php | 17 +- .../Resources/config/doctrine_mongodb_odm.xml | 5 - .../Bundle/Resources/config/doctrine_orm.xml | 6 - .../Bundle/Resources/config/graphql.xml | 13 +- src/Symfony/Routing/ApiLoader.php | 5 - src/Symfony/Routing/SkolemIriConverter.php | 3 +- src/Test/DoctrineMongoDbOdmTestCase.php | 6 +- tests/Action/ExceptionActionTest.php | 41 ++--- tests/Behat/DoctrineContext.php | 17 +- .../Common/Filter/SearchFilterTestTrait.php | 12 -- .../Odm/Extension/OrderExtensionTest.php | 11 +- .../Doctrine/Odm/Filter/SearchFilterTest.php | 15 -- .../Doctrine/Orm/Filter/SearchFilterTest.php | 8 - .../TestBundle/Document/AbsoluteUrlDummy.php | 2 +- tests/Fixtures/TestBundle/Document/Answer.php | 4 +- tests/Fixtures/TestBundle/Document/Book.php | 2 +- tests/Fixtures/TestBundle/Document/Dummy.php | 4 +- .../Document/DummyAggregateOffer.php | 4 +- .../TestBundle/Document/DummyOffer.php | 6 +- .../TestBundle/Document/DummyProduct.php | 2 +- .../TestBundle/Document/DummyValidation.php | 2 +- .../Fixtures/TestBundle/Document/FooDummy.php | 13 -- .../TestBundle/Document/FourthLevel.php | 12 +- .../Fixtures/TestBundle/Document/Greeting.php | 2 +- .../TestBundle/Document/NetworkPathDummy.php | 2 +- .../Fixtures/TestBundle/Document/Question.php | 4 +- .../TestBundle/Document/RelatedDummy.php | 14 +- .../Document/RelatedToDummyFriend.php | 10 +- .../TestBundle/Document/SlugChildDummy.php | 4 +- .../TestBundle/Document/SlugParentDummy.php | 4 +- .../TestBundle/Document/ThirdLevel.php | 10 +- .../TestBundle/Document/VoDummyInspection.php | 12 +- .../TestBundle/Entity/AbsoluteUrlDummy.php | 2 +- tests/Fixtures/TestBundle/Entity/Answer.php | 4 +- .../Entity/AttributeOnlyOperation.php | 21 --- .../TestBundle/Entity/AttributeResource.php | 2 +- .../TestBundle/Entity/AttributeResources.php | 11 +- tests/Fixtures/TestBundle/Entity/Book.php | 2 +- tests/Fixtures/TestBundle/Entity/Dummy.php | 4 +- .../TestBundle/Entity/DummyAggregateOffer.php | 4 +- .../Fixtures/TestBundle/Entity/DummyOffer.php | 6 +- .../TestBundle/Entity/DummyProduct.php | 2 +- .../Entity/DummyToUpgradeProduct.php | 49 ------ .../DummyToUpgradeWithOnlyAnnotation.php | 61 ------- .../DummyToUpgradeWithOnlyAttribute.php | 49 ------ .../TestBundle/Entity/DummyValidation.php | 2 +- tests/Fixtures/TestBundle/Entity/FooDummy.php | 13 -- .../TestBundle/Entity/FourthLevel.php | 12 +- tests/Fixtures/TestBundle/Entity/Greeting.php | 2 +- .../TestBundle/Entity/NetworkPathDummy.php | 2 +- tests/Fixtures/TestBundle/Entity/Question.php | 4 +- .../TestBundle/Entity/RelatedDummy.php | 24 +-- .../Entity/RelatedToDummyFriend.php | 10 +- .../TestBundle/Entity/SlugChildDummy.php | 4 +- .../TestBundle/Entity/SlugParentDummy.php | 4 +- tests/Fixtures/TestBundle/Entity/SoMany.php | 3 - .../Fixtures/TestBundle/Entity/ThirdLevel.php | 10 +- .../TestBundle/Entity/VoDummyInspection.php | 12 +- ...edOperationResourceMetadataFactoryTest.php | 44 ----- tests/GraphQl/Type/FieldsBuilderTest.php | 137 ++++++--------- tests/GraphQl/Type/TypeBuilderTest.php | 4 +- tests/HttpCache/VarnishPurgerTest.php | 9 +- tests/HttpCache/VarnishXKeyPurgerTest.php | 9 +- .../Command/JsonSchemaGenerateCommandTest.php | 6 +- .../PropertyMetadataCompatibilityTest.php | 3 +- .../ResourceMetadataCompatibilityTest.php | 23 +-- tests/Metadata/Extractor/XmlExtractorTest.php | 6 +- .../Metadata/Extractor/YamlExtractorTest.php | 8 +- tests/Metadata/Extractor/xml/valid.xml | 4 +- tests/Metadata/Extractor/yaml/valid.yaml | 4 +- ...sResourceMetadataCollectionFactoryTest.php | 37 ++-- ...eResourceMetadataCollectionFactoryTest.php | 8 +- .../Pagination/TraversablePaginatorTest.php | 3 +- .../PayloadArgumentResolverTest.php | 8 +- .../ApiPlatformExtensionTest.php | 37 ---- 93 files changed, 462 insertions(+), 1128 deletions(-) delete mode 100755 generate-changelog.sh delete mode 100644 src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php delete mode 100644 src/Metadata/Resource/Factory/OperationDefaultsTrait.php delete mode 100644 tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php delete mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php delete mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php delete mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php delete mode 100644 tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1921bdef057..2be83b8dcb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,5 @@ # Changelog -## v3.0.3 - -### Bug fixes - -* [176fff2cb](https://github.com/api-platform/core/commit/176fff2cb15efa01b6c898d0442a4f540d4ddeaa) fix(metadata): upgrade script keep operation name (#5109) -* [1b64ebf6a](https://github.com/api-platform/core/commit/1b64ebf6a438222ae091ec3690063d0fb1b61977) fix: upgrade command remove ApiSubresource attribute (#5049) -* [27fcdc6b2](https://github.com/api-platform/core/commit/27fcdc6b270d1699e76c37ccda690b8a5ed8b4c9) fix(metadata): deprecate when user decorates in legacy mode (#5091) -* [310363d56](https://github.com/api-platform/core/commit/310363d56129c94cf4d51977f85486729e582fbc) fix: remove @internal annotation for Operations (#5089) -* [41bbad94e](https://github.com/api-platform/core/commit/41bbad94e93df49eb4ade0fe1307b20d9cd07102) fix: update yaml extractor test file coding standard (#5068) -* [44337ddb3](https://github.com/api-platform/core/commit/44337ddb3908d7b05ed75b75325b7941581f575b) fix(graphql): use right nested operation (#5102) -* [541b738e9](https://github.com/api-platform/core/commit/541b738e942156b711665952b50fbd4f060fcdea) fix(graphql): add clearer error message when TwigBundle is disabled but graphQL clients are enabled (#5064) -* [59826bbe9](https://github.com/api-platform/core/commit/59826bbe9e246cf839bdc0c4d0d470f54e27b453) fix: only alias if exists for opcache preload -* [7044c5a1b](https://github.com/api-platform/core/commit/7044c5a1b2895e72f0579d1e788740606f94dece) fix(doctrine): use abitrary index instead of value (#5079) -* [8250d41a3](https://github.com/api-platform/core/commit/8250d41a38913a17364d617875bb5a90f434ec48) fix(metadata): define a name on a single operation (#5090) -* [9c19fa171](https://github.com/api-platform/core/commit/9c19fa17110aac7dd39bff827091c00b42a80d4f) fix(metadata): add class key in payload argument resolver (#5067) -* [a4cd12b2a](https://github.com/api-platform/core/commit/a4cd12b2a73bc0f726c5724de790f885884e6113) fix: uri template should respect rfc 6570 (#5080) -* [bbeaf7082](https://github.com/api-platform/core/commit/bbeaf7082bba4a019206c3862425cf849d55addd) fix(graphql): always allow to query nested resources (#5112) -* [c1cb3cd2f](https://github.com/api-platform/core/commit/c1cb3cd2ff32c8b1ee694b0989efeb133fbd8438) Revert "fix(graphql): use right nested operation (#5102)" (#5111) - ## 3.0.2 * Metadata: generate skolem IRI by default, use `genId: false` to disable **BC** @@ -73,19 +54,6 @@ Breaking changes: * Serializer: `skip_null_values` now defaults to `true` * Metadata: `Patch` is added to the automatic CRUD -## v2.7.3 - -### Bug fixes - -* [176fff2cb](https://github.com/api-platform/core/commit/176fff2cb15efa01b6c898d0442a4f540d4ddeaa) fix(metadata): upgrade script keep operation name (#5109) -* [1b64ebf6a](https://github.com/api-platform/core/commit/1b64ebf6a438222ae091ec3690063d0fb1b61977) fix: upgrade command remove ApiSubresource attribute (#5049) -* [27fcdc6b2](https://github.com/api-platform/core/commit/27fcdc6b270d1699e76c37ccda690b8a5ed8b4c9) fix(metadata): deprecate when user decorates in legacy mode (#5091) -* [310363d56](https://github.com/api-platform/core/commit/310363d56129c94cf4d51977f85486729e582fbc) fix: remove @internal annotation for Operations (#5089) -* [41bbad94e](https://github.com/api-platform/core/commit/41bbad94e93df49eb4ade0fe1307b20d9cd07102) fix: update yaml extractor test file coding standard (#5068) -* [59826bbe9](https://github.com/api-platform/core/commit/59826bbe9e246cf839bdc0c4d0d470f54e27b453) fix: only alias if exists for opcache preload -* [8250d41a3](https://github.com/api-platform/core/commit/8250d41a38913a17364d617875bb5a90f434ec48) fix(metadata): define a name on a single operation (#5090) -* [9c19fa171](https://github.com/api-platform/core/commit/9c19fa17110aac7dd39bff827091c00b42a80d4f) fix(metadata): add class key in payload argument resolver (#5067) - ## 2.7.2 * Metadata: no skolem IRI by default diff --git a/features/graphql/collection.feature b/features/graphql/collection.feature index 9767505171a..1540549f7d8 100644 --- a/features/graphql/collection.feature +++ b/features/graphql/collection.feature @@ -910,49 +910,3 @@ Feature: GraphQL collection support Then the response status code should be 200 And the response should be in JSON And the JSON node "data.fooDummies.collection" should have 1 element - - @createSchema - Scenario: Retrieve paginated collections using mixed pagination - Given there are 5 fooDummy objects with fake names - When I send the following GraphQL request: - """ - { - fooDummies(page: 1) { - collection { - id - name - soManies(first: 2) { - edges { - node { - content - } - cursor - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - } - } - paginationInfo { - itemsPerPage - lastPage - totalCount - } - } - } - """ - Then the response status code should be 200 - And the response should be in JSON - And the JSON node "data.fooDummies.collection" should have 3 elements - And the JSON node "data.fooDummies.collection[2].id" should exist - And the JSON node "data.fooDummies.collection[2].name" should exist - And the JSON node "data.fooDummies.collection[2].soManies" should exist - And the JSON node "data.fooDummies.collection[2].soManies.edges" should have 2 elements - And the JSON node "data.fooDummies.collection[2].soManies.edges[1].node.content" should be equal to "So many 1" - And the JSON node "data.fooDummies.collection[2].soManies.pageInfo.startCursor" should be equal to "MA==" - And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3 - And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2 - And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5 diff --git a/features/main/default_order.feature b/features/main/default_order.feature index 7458c2456e9..91211014315 100644 --- a/features/main/default_order.feature +++ b/features/main/default_order.feature @@ -79,61 +79,35 @@ Feature: Default order "@type": "FooDummy", "id": 5, "name": "Balbo", - "dummy": "/dummies/5", - "soManies": [ - "/so_manies/13", - "/so_manies/14", - "/so_manies/15" - ] - + "dummy": "/dummies/5" }, { "@id": "/foo_dummies/3", "@type": "FooDummy", "id": 3, "name": "Sthenelus", - "dummy": "/dummies/3", - "soManies": [ - "/so_manies/7", - "/so_manies/8", - "/so_manies/9" - ] + "dummy": "/dummies/3" }, { "@id": "/foo_dummies/2", "@type": "FooDummy", "id": 2, "name": "Ephesian", - "dummy": "/dummies/2", - "soManies": [ - "/so_manies/4", - "/so_manies/5", - "/so_manies/6" - ] + "dummy": "/dummies/2" }, { "@id": "/foo_dummies/1", "@type": "FooDummy", "id": 1, "name": "Hawsepipe", - "dummy": "/dummies/1", - "soManies": [ - "/so_manies/1", - "/so_manies/2", - "/so_manies/3" - ] + "dummy": "/dummies/1" }, { "@id": "/foo_dummies/4", "@type": "FooDummy", "id": 4, "name": "Separativeness", - "dummy": "/dummies/4", - "soManies": [ - "/so_manies/10", - "/so_manies/11", - "/so_manies/12" - ] + "dummy": "/dummies/4" } ], "hydra:totalItems": 5, diff --git a/generate-changelog.sh b/generate-changelog.sh deleted file mode 100755 index f2903beb1b3..00000000000 --- a/generate-changelog.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# usage: generate-changelog.sh previous_tag next_tag -# example: generate-changelog.sh v2.7.2 v2.7.3 > CHANGELOG.new.md -log=$(git log "$1..HEAD" --pretty='format:* [%h](https://github.com/api-platform/core/commit/%H) %s' --no-merges) - -diff=$( -printf "# Changelog\n\n" -printf "## %s\n\n" "$2" - -if [[ 0 != $(echo "$log" | grep fix | grep -v chore | wc -l) ]]; -then - printf "### Bug fixes\n\n" - printf "$log" | grep fix | grep -v chore | sort - printf "\n\n" -fi - -if [[ 0 != $(echo "$log" | grep feat | grep -v chore | wc -l) ]]; -then - printf "### Features\n\n" - printf "$log" | grep feat | grep -v chore | sort -fi -) - -changelog=$(tail -n+2 CHANGELOG.md) -printf "%s\n%s" "$diff" "$changelog" diff --git a/src/Doctrine/Odm/Extension/OrderExtension.php b/src/Doctrine/Odm/Extension/OrderExtension.php index 77cd255d69b..f3cf5eb3725 100644 --- a/src/Doctrine/Odm/Extension/OrderExtension.php +++ b/src/Doctrine/Odm/Extension/OrderExtension.php @@ -19,6 +19,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort; use Doctrine\Persistence\ManagerRegistry; +use OutOfRangeException; /** * Applies selected ordering while querying resource collection. @@ -99,7 +100,7 @@ private function hasSortStage(Builder $aggregationBuilder): bool // If at least one stage is sort, then it has sorting return true; } - } catch (\OutOfRangeException $outOfRangeException) { + } catch (OutOfRangeException) { // There is no more stages on the aggregation builder $shouldStop = true; } diff --git a/src/Doctrine/Orm/Filter/SearchFilter.php b/src/Doctrine/Orm/Filter/SearchFilter.php index 8401ebb842a..3f35706b3b5 100644 --- a/src/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Doctrine/Orm/Filter/SearchFilter.php @@ -179,7 +179,7 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild $parameters = []; foreach ($values as $key => $value) { $keyValueParameter = sprintf('%s_%s', $valueParameter, $key); - $parameters[] = [$caseSensitive ? $value : strtolower($value), $keyValueParameter]; + $parameters[$caseSensitive ? $value : strtolower($value)] = $keyValueParameter; $ors[] = match ($strategy) { self::STRATEGY_PARTIAL => $queryBuilder->expr()->like( @@ -209,9 +209,7 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild } $queryBuilder->andWhere($queryBuilder->expr()->orX(...$ors)); - foreach ($parameters as $parameter) { - $queryBuilder->setParameter($parameter[1], $parameter[0]); - } + array_walk($parameters, $queryBuilder->setParameter(...)); } /** diff --git a/src/GraphQl/Action/EntrypointAction.php b/src/GraphQl/Action/EntrypointAction.php index dcdd4a1dfd8..f4c9af93e68 100644 --- a/src/GraphQl/Action/EntrypointAction.php +++ b/src/GraphQl/Action/EntrypointAction.php @@ -34,7 +34,7 @@ final class EntrypointAction { private int $debug; - public function __construct(private readonly SchemaBuilderInterface $schemaBuilder, private readonly ExecutorInterface $executor, private readonly ?GraphiQlAction $graphiQlAction, private readonly ?GraphQlPlaygroundAction $graphQlPlaygroundAction, private readonly NormalizerInterface $normalizer, private readonly ErrorHandlerInterface $errorHandler, bool $debug = false, private readonly bool $graphiqlEnabled = false, private readonly bool $graphQlPlaygroundEnabled = false, private readonly ?string $defaultIde = null) + public function __construct(private readonly SchemaBuilderInterface $schemaBuilder, private readonly ExecutorInterface $executor, private readonly GraphiQlAction $graphiQlAction, private readonly GraphQlPlaygroundAction $graphQlPlaygroundAction, private readonly NormalizerInterface $normalizer, private readonly ErrorHandlerInterface $errorHandler, bool $debug = false, private readonly bool $graphiqlEnabled = false, private readonly bool $graphQlPlaygroundEnabled = false, private readonly ?string $defaultIde = null) { $this->debug = $debug ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : DebugFlag::NONE; } @@ -43,11 +43,11 @@ public function __invoke(Request $request): Response { try { if ($request->isMethod('GET') && 'html' === $request->getRequestFormat()) { - if ('graphiql' === $this->defaultIde && $this->graphiqlEnabled && $this->graphiQlAction) { + if ('graphiql' === $this->defaultIde && $this->graphiqlEnabled) { return ($this->graphiQlAction)($request); } - if ('graphql-playground' === $this->defaultIde && $this->graphQlPlaygroundEnabled && $this->graphQlPlaygroundAction) { + if ('graphql-playground' === $this->defaultIde && $this->graphQlPlaygroundEnabled) { return ($this->graphQlPlaygroundAction)($request); } } diff --git a/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php b/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php deleted file mode 100644 index 05163b0c2b7..00000000000 --- a/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * 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\GraphQl\Metadata\Factory; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Resource\Factory\OperationDefaultsTrait; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; -use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; - -final class GraphQlNestedOperationResourceMetadataFactory implements ResourceMetadataCollectionFactoryInterface -{ - use OperationDefaultsTrait; - - public function __construct(array $defaults, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, ?LoggerInterface $logger = null) - { - $this->defaults = $defaults; - $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); - $this->logger = $logger ?? new NullLogger(); - } - - public function create(string $resourceClass): ResourceMetadataCollection - { - $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); - - if ($this->decorated) { - $resourceMetadataCollection = $this->decorated->create($resourceClass); - } - - if (0 < \count($resourceMetadataCollection)) { - return $resourceMetadataCollection; - } - - $shortName = (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass; - - $apiResource = new ApiResource( - class: $resourceClass, - shortName: $shortName - ); - - if (class_exists($resourceClass)) { - $refl = new \ReflectionClass($resourceClass); - $attribute = $refl->getAttributes(ApiResource::class)[0] ?? null; - $attributeInstance = $attribute?->newInstance(); - if ($filters = $attributeInstance?->getFilters()) { - $apiResource = $apiResource->withFilters($filters); - } - } - - $resourceMetadataCollection[0] = $this->addDefaultGraphQlOperations($apiResource); - - return $resourceMetadataCollection; - } -} diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index d64f1cb800b..8817899c8ef 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -20,7 +20,9 @@ use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; +use ApiPlatform\Metadata\Operation as AbstractOperation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -45,7 +47,7 @@ */ final class FieldsBuilder implements FieldsBuilderInterface { - public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?ResourceMetadataCollectionFactoryInterface $graphQlNestedOperationResourceMetadataFactory = null) + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) { } @@ -254,23 +256,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $resourceClass = $type->getClassName(); } - $resourceOperation = $rootOperation; - if ($resourceClass && $rootOperation->getClass() && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - try { - $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); - } catch (OperationNotFoundException) { - // If there is no query operation for a nested resource we force one to exist - $nestedResourceMetadataCollection = $this->graphQlNestedOperationResourceMetadataFactory->create($resourceClass); - $resourceOperation = $nestedResourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); - } - } - - if (!$resourceOperation instanceof Operation) { - throw new \LogicException('The resource operation should be a GraphQL operation.'); - } - - $graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); + $graphqlType = $this->convertType($type, $input, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType(true) : $graphqlType; $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true); @@ -285,22 +271,43 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $args = []; + $resolverOperation = $rootOperation; + + if ($resourceClass && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + $resolverOperation = $resourceMetadataCollection->getOperation(null, $isCollectionType); + + if (!$resolverOperation instanceof Operation) { + $resolverOperation = ($isCollectionType ? new QueryCollection() : new Query())->withOperation($resolverOperation); + } + } + if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) { - if ($this->pagination->isGraphQlEnabled($resourceOperation)) { - $args = $this->getGraphQlPaginationArgs($resourceOperation); + if ($this->pagination->isGraphQlEnabled($rootOperation)) { + $args = $this->getGraphQlPaginationArgs($rootOperation); + } + + // Find the collection operation to get filters, there might be a smarter way to do this + $operation = null; + if (!empty($resourceClass)) { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + try { + $operation = $resourceMetadataCollection->getOperation(null, true); + } catch (OperationNotFoundException) { + } } - $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); + $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $rootOperation, $property, $depth, $operation); } if ($isStandardGraphqlType || $input) { $resolve = null; } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) { - $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation); + $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resolverOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resolverOperation); } elseif ($this->typeBuilder->isCollection($type)) { - $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation); + $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resolverOperation); } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation); + $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resolverOperation); } return [ @@ -361,13 +368,13 @@ private function getGraphQlPaginationArgs(Operation $queryOperation): array return $args; } - private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array + private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $rootOperation, ?string $property, int $depth, ?AbstractOperation $operation = null): array { - if (null === $resourceClass) { + if (null === $operation || null === $resourceClass) { return $args; } - foreach ($resourceOperation->getFilters() ?? [] as $filterId) { + foreach ($operation->getFilters() ?? [] as $filterId) { if (!$this->filterLocator->has($filterId)) { continue; } @@ -375,7 +382,7 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) { $nullable = isset($value['required']) ? !$value['required'] : true; $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']); - $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth); + $graphqlFilterType = $this->convertType($filterType, false, $rootOperation, $resourceClass, $rootResource, $property, $depth); if (str_ends_with($key, '[]')) { $graphqlFilterType = GraphQLType::listOf($graphqlFilterType); @@ -392,14 +399,14 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root array_walk_recursive($parsed, static function (&$value) use ($graphqlFilterType): void { $value = $graphqlFilterType; }); - $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key); + $args = $this->mergeFilterArgs($args, $parsed, $operation, $key); } } return $this->convertFilterArgsToTypes($args); } - private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array + private function mergeFilterArgs(array $args, array $parsed, ?AbstractOperation $operation = null, string $original = ''): array { foreach ($parsed as $key => $value) { // Never override keys that cannot be merged @@ -463,7 +470,7 @@ private function convertFilterArgsToTypes(array $args): array * * @throws InvalidTypeException */ - private function convertType(Type $type, bool $input, Operation $resourceOperation, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull + private function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull { $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth); @@ -480,7 +487,7 @@ private function convertType(Type $type, bool $input, Operation $resourceOperati } if ($this->typeBuilder->isCollection($type)) { - return $this->pagination->isGraphQlEnabled($resourceOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation) : GraphQLType::listOf($graphqlType); + return $this->pagination->isGraphQlEnabled($rootOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $rootOperation) : GraphQLType::listOf($graphqlType); } return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName()) diff --git a/src/Metadata/Operations.php b/src/Metadata/Operations.php index 5565bc99bd0..cdbd4fe6bf3 100644 --- a/src/Metadata/Operations.php +++ b/src/Metadata/Operations.php @@ -13,6 +13,11 @@ namespace ApiPlatform\Metadata; +use RuntimeException; + +/** + * @internal + */ final class Operations implements \IteratorAggregate, \Countable { private array $operations = []; @@ -68,7 +73,7 @@ public function remove(string $key): self } } - throw new \RuntimeException(sprintf('Could not remove operation "%s".', $key)); + throw new RuntimeException(sprintf('Could not remove operation "%s".', $key)); } public function has(string $key): bool diff --git a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index 9b125aed5c3..9255c236fa8 100644 --- a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -14,12 +14,20 @@ namespace ApiPlatform\Metadata\Resource\Factory; use ApiPlatform\Exception\ResourceClassNotFoundException; +use ApiPlatform\Exception\RuntimeException; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; +use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -37,12 +45,12 @@ */ final class AttributesResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - use OperationDefaultsTrait; + private readonly LoggerInterface $logger; + private readonly CamelCaseToSnakeCaseNameConverter $camelCaseToSnakeCaseNameConverter; - public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, LoggerInterface $logger = null, array $defaults = [], private readonly bool $graphQlEnabled = false) + public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, LoggerInterface $logger = null, private readonly array $defaults = [], private readonly bool $graphQlEnabled = false) { $this->logger = $logger ?? new NullLogger(); - $this->defaults = $defaults; $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); } @@ -156,6 +164,115 @@ private function buildResourceOperations(array $attributes, string $resourceClas return $resources; } + private function getOperationWithDefaults(ApiResource $resource, Operation $operation, bool $generated = false): array + { + // Inherit from resource defaults + foreach (get_class_methods($resource) as $methodName) { + if (!str_starts_with($methodName, 'get')) { + continue; + } + + if (!method_exists($operation, $methodName) || null !== $operation->{$methodName}()) { + continue; + } + + if (null === ($value = $resource->{$methodName}())) { + continue; + } + + $operation = $operation->{'with'.substr($methodName, 3)}($value); + } + + $operation = $operation->withExtraProperties(array_merge( + $resource->getExtraProperties(), + $operation->getExtraProperties(), + $generated ? ['generated_operation' => true] : [] + )); + + // Add global defaults attributes to the operation + $operation = $this->addGlobalDefaults($operation); + + if ($operation instanceof GraphQlOperation) { + if (!$operation->getName()) { + throw new RuntimeException('No GraphQL operation name.'); + } + + if ($operation instanceof Mutation) { + $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); + } + + return [$operation->getName(), $operation]; + } + + if (!$operation instanceof HttpOperation) { + throw new RuntimeException(sprintf('Operation should be an instance of "%s"', HttpOperation::class)); + } + + if ($operation->getRouteName()) { + /** @var HttpOperation $operation */ + $operation = $operation->withName($operation->getRouteName()); + } + + // Check for name conflict + if ($operation->getName()) { + if (null !== $resource->getOperations() && !$resource->getOperations()->has($operation->getName())) { + return [$operation->getName(), $operation]; + } + + $this->logger->warning(sprintf('The operation "%s" already exists on the resource "%s", pick a different name or leave it empty. In the meantime we will generate a unique name.', $operation->getName(), $resource->getClass())); + /** @var HttpOperation $operation */ + $operation = $operation->withName(''); + } + + return [ + sprintf( + '_api_%s_%s%s', + $operation->getUriTemplate() ?: $operation->getShortName(), + strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), + $operation instanceof CollectionOperationInterface ? '_collection' : '', + ), + $operation, + ]; + } + + private function addGlobalDefaults(ApiResource|HttpOperation|GraphQlOperation $operation): ApiResource|HttpOperation|GraphQlOperation + { + $extraProperties = []; + foreach ($this->defaults as $key => $value) { + $upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key)); + $getter = 'get'.$upperKey; + + if (!method_exists($operation, $getter)) { + if (!isset($extraProperties[$key])) { + $extraProperties[$key] = $value; + } + } else { + $currentValue = $operation->{$getter}(); + + if (\is_array($currentValue) && $currentValue) { + $operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue)); + } + + if (null !== $currentValue) { + continue; + } + + $operation = $operation->{'with'.$upperKey}($value); + } + } + + return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties())); + } + + private function getResourceWithDefaults(string $resourceClass, string $shortName, ApiResource $resource): ApiResource + { + $resource = $resource + ->withShortName($resource->getShortName() ?? $shortName) + ->withClass($resourceClass); + + return $this->addGlobalDefaults($resource); + } + private function hasResourceAttributes(\ReflectionClass $reflectionClass): bool { foreach ($reflectionClass->getAttributes() as $attribute) { @@ -186,6 +303,22 @@ private function hasSameOperation(ApiResource $resource, string $operationClass, return false; } + private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource + { + $graphQlOperations = []; + foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $i => $operation) { + [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); + $graphQlOperations[$key] = $operation; + } + + if ($resource->getMercure()) { + [$key, $operation] = $this->getOperationWithDefaults($resource, (new Subscription())->withDescription("Subscribes to the update event of a {$operation->getShortName()}.")); + $graphQlOperations[$key] = $operation; + } + + return $resource->withGraphQlOperations($graphQlOperations); + } + private function getDefaultHttpOperations($resource): iterable { $post = new Post(); diff --git a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php index 86c154f7bc4..c44358897ca 100644 --- a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php @@ -19,12 +19,7 @@ use ApiPlatform\Metadata\Extractor\ResourceExtractorInterface; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\GraphQl\DeleteMutation; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -93,7 +88,9 @@ private function buildResources(array $nodes, string $resourceClass): array } } - $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'] ?? null, $resource)); + if (isset($node['graphQlOperations'])) { + $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'], $resource)); + } $resources[] = $resource->withOperations(new Operations($this->buildOperations($node['operations'] ?? null, $resource))); } @@ -151,20 +148,6 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar { $operations = []; - if (null === $data) { - foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $operation) { - $operation = $this->getOperationWithDefaults($resource, $operation); - - if ($operation instanceof Mutation) { - $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); - } - - $operations[$operation->getName()] = $operation; - } - - return $operations; - } - foreach ($data as $attributes) { /** @var HttpOperation $operation */ $operation = (new $attributes['graphql_operation_class']())->withShortName($resource->getShortName()); @@ -192,7 +175,7 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar return $operations; } - private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation + private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation { foreach (($this->defaults['attributes'] ?? []) as $key => $value) { $key = $this->camelCaseToSnakeCaseNameConverter->denormalize($key); diff --git a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php index 10a711fe71a..bb68448c4c2 100644 --- a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php @@ -50,21 +50,17 @@ public function create(string $resourceClass): ResourceMetadataCollection $filters = array_keys($this->readFilterAttributes($reflectionClass)); foreach ($resourceMetadataCollection as $i => $resource) { - foreach ($operations = $resource->getOperations() ?? [] as $operationName => $operation) { + foreach ($operations = $resource->getOperations() as $operationName => $operation) { $operations->add($operationName, $operation->withFilters(array_unique(array_merge($resource->getFilters() ?? [], $operation->getFilters() ?? [], $filters)))); } - if ($operations) { - $resourceMetadataCollection[$i] = $resource->withOperations($operations); - } + $resourceMetadataCollection[$i] = $resource->withOperations($operations); foreach ($graphQlOperations = $resource->getGraphQlOperations() ?? [] as $operationName => $operation) { $graphQlOperations[$operationName] = $operation->withFilters(array_unique(array_merge($resource->getFilters() ?? [], $operation->getFilters() ?? [], $filters))); } - if ($graphQlOperations) { - $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); - } + $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); } return $resourceMetadataCollection; diff --git a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php deleted file mode 100644 index cdb91e879a7..00000000000 --- a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php +++ /dev/null @@ -1,160 +0,0 @@ - - * - * 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\Metadata\Resource\Factory; - -use ApiPlatform\Exception\RuntimeException; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\GraphQl\DeleteMutation; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\GraphQl\Subscription; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; -use Psr\Log\LoggerInterface; -use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; - -trait OperationDefaultsTrait -{ - private CamelCaseToSnakeCaseNameConverter $camelCaseToSnakeCaseNameConverter; - private array $defaults = []; - private LoggerInterface $logger; - - private function addGlobalDefaults(ApiResource|Operation $operation): ApiResource|Operation - { - $extraProperties = []; - foreach ($this->defaults as $key => $value) { - $upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key)); - $getter = 'get'.$upperKey; - - if (!method_exists($operation, $getter)) { - if (!isset($extraProperties[$key])) { - $extraProperties[$key] = $value; - } - } else { - $currentValue = $operation->{$getter}(); - - if (\is_array($currentValue) && $currentValue) { - $operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue)); - } - - if (null !== $currentValue) { - continue; - } - - $operation = $operation->{'with'.$upperKey}($value); - } - } - - return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties())); - } - - private function getResourceWithDefaults(string $resourceClass, string $shortName, ApiResource $resource): ApiResource - { - $resource = $resource - ->withShortName($resource->getShortName() ?? $shortName) - ->withClass($resourceClass); - - return $this->addGlobalDefaults($resource); - } - - private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource - { - $graphQlOperations = []; - foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $i => $operation) { - [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); - $graphQlOperations[$key] = $operation; - } - - if ($resource->getMercure()) { - [$key, $operation] = $this->getOperationWithDefaults($resource, (new Subscription())->withDescription("Subscribes to the update event of a {$operation->getShortName()}.")); - $graphQlOperations[$key] = $operation; - } - - return $resource->withGraphQlOperations($graphQlOperations); - } - - private function getOperationWithDefaults(ApiResource $resource, Operation $operation, bool $generated = false): array - { - // Inherit from resource defaults - foreach (get_class_methods($resource) as $methodName) { - if (!str_starts_with($methodName, 'get')) { - continue; - } - - if (!method_exists($operation, $methodName) || null !== $operation->{$methodName}()) { - continue; - } - - if (null === ($value = $resource->{$methodName}())) { - continue; - } - - $operation = $operation->{'with'.substr($methodName, 3)}($value); - } - - $operation = $operation->withExtraProperties(array_merge( - $resource->getExtraProperties(), - $operation->getExtraProperties(), - $generated ? ['generated_operation' => true] : [] - )); - - // Add global defaults attributes to the operation - $operation = $this->addGlobalDefaults($operation); - - if ($operation instanceof GraphQlOperation) { - if (!$operation->getName()) { - throw new RuntimeException('No GraphQL operation name.'); - } - - if ($operation instanceof Mutation) { - $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); - } - - return [$operation->getName(), $operation]; - } - - if (!$operation instanceof HttpOperation) { - throw new RuntimeException(sprintf('Operation should be an instance of "%s"', HttpOperation::class)); - } - - if ($operation->getRouteName()) { - /** @var HttpOperation $operation */ - $operation = $operation->withName($operation->getRouteName()); - } - - // Check for name conflict - if ($operation->getName() && null !== ($operations = $resource->getOperations())) { - if (!$operations->has($operation->getName())) { - return [$operation->getName(), $operation]; - } - - $this->logger->warning(sprintf('The operation "%s" already exists on the resource "%s", pick a different name or leave it empty. In the meantime we will generate a unique name.', $operation->getName(), $resource->getClass())); - /** @var HttpOperation $operation */ - $operation = $operation->withName(''); - } - - return [ - sprintf( - '_api_%s_%s%s', - $operation->getUriTemplate() ?: $operation->getShortName(), - strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), - $operation instanceof CollectionOperationInterface ? '_collection' : '', - ), - $operation, - ]; - } -} diff --git a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php index 7b4a5ca7c70..1ceacfc5636 100644 --- a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php @@ -105,7 +105,7 @@ private function generateUriTemplate(HttpOperation $operation): string } } - return sprintf('%s{._format}', $uriTemplate); + return sprintf('%s.{_format}', $uriTemplate); } private function configureUriVariables(ApiResource|HttpOperation $operation): ApiResource|HttpOperation @@ -144,12 +144,8 @@ private function configureUriVariables(ApiResource|HttpOperation $operation): Ap } $operation = $operation->withUriVariables($uriVariables); - if (str_ends_with($uriTemplate, '{._format}')) { - $uriTemplate = substr($uriTemplate, 0, -10); - } - $route = (new Route($uriTemplate))->compile(); - $variables = $route->getPathVariables(); + $variables = array_filter($route->getPathVariables(), fn ($v): bool => '_format' !== $v); if (\count($variables) !== \count($uriVariables)) { $newUriVariables = []; diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 75f1d2ee8bb..34dc68f2cfe 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -333,8 +333,7 @@ private function flattenMimeTypes(array $responseFormats): array */ private function getPath(string $path): string { - // Handle either API Platform's URI Template (rfc6570) or Symfony's route - if (str_ends_with($path, '{._format}') || str_ends_with($path, '.{_format}')) { + if (str_ends_with($path, '.{_format}')) { $path = substr($path, 0, -10); } diff --git a/src/OpenApi/Serializer/NormalizeOperationNameTrait.php b/src/OpenApi/Serializer/NormalizeOperationNameTrait.php index 2d37d93b8f1..53bb0b22a10 100644 --- a/src/OpenApi/Serializer/NormalizeOperationNameTrait.php +++ b/src/OpenApi/Serializer/NormalizeOperationNameTrait.php @@ -22,6 +22,7 @@ trait NormalizeOperationNameTrait { private function normalizeOperationName(string $operationName): string { - return preg_replace('/^_/', '', str_replace(['/', '{._format}', '{', '}'], ['', '', '_', ''], $operationName)); + // .{_format} is related to the symfony router + return preg_replace('/^_/', '', str_replace(['/', '.{_format}', '{', '}'], ['', '', '_', ''], $operationName)); } } diff --git a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php index bf11ad4d37b..97cd93678b3 100644 --- a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php +++ b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php @@ -71,6 +71,6 @@ private function getExpectedInputClass(Request $request): ?string $context = $this->serializationContextBuilder->createFromRequest($request, false, RequestAttributesExtractor::extractAttributes($request)); - return $context['input']['class'] ?? $context['resource_class'] ?? null; + return $context['input'] ?? $context['resource_class']; } } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 3b1c8faeb5a..fcc023abb68 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -53,7 +53,6 @@ use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Yaml\Yaml; -use Twig\Environment; /** * The extension of this bundle. @@ -463,12 +462,9 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array { $enabled = $this->isConfigEnabled($container, $config['graphql']); - $graphiqlEnabled = $enabled && $this->isConfigEnabled($container, $config['graphql']['graphiql']); - $graphqlPlayGroundEnabled = $enabled && $this->isConfigEnabled($container, $config['graphql']['graphql_playground']); - $container->setParameter('api_platform.graphql.enabled', $enabled); - $container->setParameter('api_platform.graphql.graphiql.enabled', $graphiqlEnabled); - $container->setParameter('api_platform.graphql.graphql_playground.enabled', $graphqlPlayGroundEnabled); + $container->setParameter('api_platform.graphql.graphiql.enabled', $enabled && $this->isConfigEnabled($container, $config['graphql']['graphiql'])); + $container->setParameter('api_platform.graphql.graphql_playground.enabled', $enabled && $this->isConfigEnabled($container, $config['graphql']['graphql_playground'])); $container->setParameter('api_platform.graphql.collection.pagination', $config['graphql']['collection']['pagination']); if (!$enabled) { @@ -480,15 +476,6 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array $loader->load('graphql.xml'); - // @phpstan-ignore-next-line because PHPStan uses the container of the test env cache and in test the parameter kernel.bundles always contains the key TwigBundle - if (!class_exists(Environment::class) || !isset($container->getParameter('kernel.bundles')['TwigBundle'])) { - if ($graphiqlEnabled || $graphqlPlayGroundEnabled) { - throw new RuntimeException(sprintf('GraphiQL and GraphQL Playground interfaces depend on Twig. Please activate TwigBundle for the %s environnement or disable GraphiQL and GraphQL Playground.', $container->getParameter('kernel.environment'))); - } - $container->removeDefinition('api_platform.graphql.action.graphiql'); - $container->removeDefinition('api_platform.graphql.action.graphql_playground'); - } - $container->registerForAutoconfiguration(QueryItemResolverInterface::class) ->addTag('api_platform.graphql.query_resolver'); $container->registerForAutoconfiguration(QueryCollectionResolverInterface::class) diff --git a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml index 29f9a7a761f..4cecc9d339a 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml @@ -156,11 +156,6 @@ - - - - - diff --git a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml index 0b6949bb9b8..a46540ab0a9 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -166,12 +166,6 @@ - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index 0f713b273b9..debaefb508c 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -49,8 +49,8 @@ - - + + %kernel.debug% @@ -133,7 +133,6 @@ %api_platform.graphql.nesting_separator% - @@ -275,14 +274,6 @@ - - - %api_platform.defaults% - - - - - diff --git a/src/Symfony/Routing/ApiLoader.php b/src/Symfony/Routing/ApiLoader.php index a9b636e063c..923a9b50110 100644 --- a/src/Symfony/Routing/ApiLoader.php +++ b/src/Symfony/Routing/ApiLoader.php @@ -75,11 +75,6 @@ public function load(mixed $data, string $type = null): RouteCollection $path = str_replace(sprintf('{%s}', $parameterName), $expandedValue, $path); } - // Within Symfony .{_format} is a special parameter but the rfc6570 specifies label expansion with a dot operator - if (str_ends_with($path, '{._format}')) { - $path = str_replace('{._format}', '.{_format}', $path); - } - if (($controller = $operation->getController()) && !$this->container->has($controller)) { throw new RuntimeException(sprintf('There is no builtin action for the "%s" operation. You need to define the controller yourself.', $operationName)); } diff --git a/src/Symfony/Routing/SkolemIriConverter.php b/src/Symfony/Routing/SkolemIriConverter.php index c11facaa008..051efeccb4e 100644 --- a/src/Symfony/Routing/SkolemIriConverter.php +++ b/src/Symfony/Routing/SkolemIriConverter.php @@ -17,6 +17,7 @@ use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\Operation; +use SplObjectStorage; use Symfony\Component\Routing\RouterInterface; /** @@ -35,7 +36,7 @@ final class SkolemIriConverter implements IriConverterInterface public function __construct(RouterInterface $router) { $this->router = $router; - $this->objectHashMap = new \SplObjectStorage(); + $this->objectHashMap = new SplObjectStorage(); } /** diff --git a/src/Test/DoctrineMongoDbOdmTestCase.php b/src/Test/DoctrineMongoDbOdmTestCase.php index 6d63d70b87f..5ee6c03ee15 100644 --- a/src/Test/DoctrineMongoDbOdmTestCase.php +++ b/src/Test/DoctrineMongoDbOdmTestCase.php @@ -20,6 +20,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use function sys_get_temp_dir; + /** * Source: https://github.com/doctrine/DoctrineMongoDBBundle/blob/0174003844bc566bb4cb3b7d10c5528d1924d719/Tests/TestCase.php * Test got excluded from vendor in 4.x. @@ -30,8 +32,8 @@ public static function createTestDocumentManager($paths = []): DocumentManager { $config = new Configuration(); $config->setAutoGenerateProxyClasses(Configuration::AUTOGENERATE_FILE_NOT_EXISTS); - $config->setProxyDir(\sys_get_temp_dir()); - $config->setHydratorDir(\sys_get_temp_dir()); + $config->setProxyDir(sys_get_temp_dir()); + $config->setHydratorDir(sys_get_temp_dir()); $config->setProxyNamespace('SymfonyTests\Doctrine'); $config->setHydratorNamespace('SymfonyTests\Doctrine'); $config->setMetadataDriverImpl(new AttributeDriver($paths, new AttributeReader())); // @phpstan-ignore-line diff --git a/tests/Action/ExceptionActionTest.php b/tests/Action/ExceptionActionTest.php index 27914032b87..2088fdb7e50 100644 --- a/tests/Action/ExceptionActionTest.php +++ b/tests/Action/ExceptionActionTest.php @@ -21,6 +21,7 @@ use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use DomainException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; @@ -73,7 +74,7 @@ public function testActionWithOperationExceptionToStatus( ?array $operationExceptionToStatus, int $expectedStatusCode ): void { - $exception = new \DomainException(); + $exception = new DomainException(); $flattenException = FlattenException::create($exception); $serializer = $this->prophesize(SerializerInterface::class); @@ -134,86 +135,86 @@ public function provideOperationExceptionToStatusCases(): \Generator ]; yield 'on global attributes' => [ - [\DomainException::class => 100], + [DomainException::class => 100], null, null, 100, ]; yield 'on global attributes with empty resource and operation attributes' => [ - [\DomainException::class => 100], + [DomainException::class => 100], [], [], 100, ]; yield 'on global attributes and resource attributes' => [ - [\DomainException::class => 100], - [\DomainException::class => 200], + [DomainException::class => 100], + [DomainException::class => 200], null, 200, ]; yield 'on global attributes and resource attributes with empty operation attributes' => [ - [\DomainException::class => 100], - [\DomainException::class => 200], + [DomainException::class => 100], + [DomainException::class => 200], [], 200, ]; yield 'on global attributes and operation attributes' => [ - [\DomainException::class => 100], + [DomainException::class => 100], null, - [\DomainException::class => 300], + [DomainException::class => 300], 300, ]; yield 'on global attributes and operation attributes with empty resource attributes' => [ - [\DomainException::class => 100], + [DomainException::class => 100], [], - [\DomainException::class => 300], + [DomainException::class => 300], 300, ]; yield 'on global, resource and operation attributes' => [ - [\DomainException::class => 100], - [\DomainException::class => 200], - [\DomainException::class => 300], + [DomainException::class => 100], + [DomainException::class => 200], + [DomainException::class => 300], 300, ]; yield 'on resource attributes' => [ [], - [\DomainException::class => 200], + [DomainException::class => 200], null, 200, ]; yield 'on resource attributes with empty operation attributes' => [ [], - [\DomainException::class => 200], + [DomainException::class => 200], [], 200, ]; yield 'on resource and operation attributes' => [ [], - [\DomainException::class => 200], - [\DomainException::class => 300], + [DomainException::class => 200], + [DomainException::class => 300], 300, ]; yield 'on operation attributes' => [ [], null, - [\DomainException::class => 300], + [DomainException::class => 300], 300, ]; yield 'on operation attributes with empty resource attributes' => [ [], [], - [\DomainException::class => 300], + [DomainException::class => 300], 300, ]; } diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 37aaa67205b..5e02353fcfb 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -286,10 +286,10 @@ public function thereArePaginationEntities(int $nb): void public function thereAreOfTheseSoManyObjects(int $nb): void { for ($i = 1; $i <= $nb; ++$i) { - $soMany = $this->buildSoMany(); - $soMany->content = 'Many #'.$i; + $dummy = $this->isOrm() ? new SoMany() : new SoManyDocument(); + $dummy->content = 'Many #'.$i; - $this->manager->persist($soMany); + $this->manager->persist($dummy); } $this->manager->flush(); @@ -340,12 +340,6 @@ public function thereAreFooDummyObjectsWithFakeNames($nb): void $foo = $this->buildFooDummy(); $foo->setName($names[$i]); $foo->setDummy($dummy); - for ($j = 0; $j < 3; ++$j) { - $soMany = $this->buildSoMany(); - $soMany->content = "So many $j"; - $soMany->fooDummy = $foo; - $foo->soManies->add($soMany); - } $this->manager->persist($foo); } @@ -2206,11 +2200,6 @@ private function buildRelatedSecureDummy(): RelatedSecuredDummy|RelatedSecuredDu return $this->isOrm() ? new RelatedSecuredDummy() : new RelatedSecuredDummyDocument(); } - private function buildSoMany(): SoMany|SoManyDocument - { - return $this->isOrm() ? new SoMany() : new SoManyDocument(); - } - private function buildThirdLevel(): ThirdLevel|ThirdLevelDocument { return $this->isOrm() ? new ThirdLevel() : new ThirdLevelDocument(); diff --git a/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php b/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php index 51b8811918d..a7fc3780999 100644 --- a/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php +++ b/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php @@ -297,18 +297,6 @@ private function provideApplyTestArguments(): array ], ], ], - 'partial (multiple almost same values; case insensitive)' => [ - [ - 'id' => null, - 'name' => 'ipartial', - ], - [ - 'name' => [ - 'blue car', - 'Blue Car', - ], - ], - ], 'start' => [ [ 'id' => null, diff --git a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php index ce722cdc0c4..5c3f97ba515 100644 --- a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php +++ b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php @@ -22,6 +22,7 @@ use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; +use OutOfRangeException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -38,7 +39,7 @@ public function testApplyToCollectionWithValidOrder(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -59,7 +60,7 @@ public function testApplyToCollectionWithWrongOrder(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldNotBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -80,7 +81,7 @@ public function testApplyToCollectionWithOrderOverridden(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['foo' => 'DESC'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -101,7 +102,7 @@ public function testApplyToCollectionWithOrderOverriddenWithNoDirection(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['foo' => 'ASC'])->shouldBeCalled(); $aggregationBuilderProphecy->sort(['foo' => 'ASC', 'bar' => 'DESC'])->shouldBeCalled(); @@ -129,7 +130,7 @@ public function testApplyToCollectionWithOrderOverriddenWithAssociation(): void $lookupProphecy->alias('author_lkup')->shouldBeCalled(); $aggregationBuilderProphecy->lookup(Dummy::class)->shouldBeCalled()->willReturn($lookupProphecy->reveal()); $aggregationBuilderProphecy->unwind('$author_lkup')->shouldBeCalled(); - $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['author_lkup.name' => 'ASC'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); diff --git a/tests/Doctrine/Odm/Filter/SearchFilterTest.php b/tests/Doctrine/Odm/Filter/SearchFilterTest.php index 5dd394ce965..28b721f3475 100644 --- a/tests/Doctrine/Odm/Filter/SearchFilterTest.php +++ b/tests/Doctrine/Odm/Filter/SearchFilterTest.php @@ -426,21 +426,6 @@ public function provideApplyTestData(): array ], $filterFactory, ], - 'partial (multiple almost same values; case insensitive)' => [ - [ - [ - '$match' => [ - 'name' => [ - '$in' => [ - new Regex('blue car', 'i'), - new Regex('Blue Car', 'i'), - ], - ], - ], - ], - ], - $filterFactory, - ], 'start' => [ [ [ diff --git a/tests/Doctrine/Orm/Filter/SearchFilterTest.php b/tests/Doctrine/Orm/Filter/SearchFilterTest.php index 7ad972534a7..2d2f2a79e1f 100644 --- a/tests/Doctrine/Orm/Filter/SearchFilterTest.php +++ b/tests/Doctrine/Orm/Filter/SearchFilterTest.php @@ -352,14 +352,6 @@ public function provideApplyTestData(): array ], $filterFactory, ], - 'partial (multiple almost same values; case insensitive)' => [ - sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1_0, \'%%\')) OR LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1_1, \'%%\'))', $this->alias, Dummy::class), - [ - 'name_p1_0' => 'blue car', - 'name_p1_1' => 'blue car', - ], - $filterFactory, - ], 'start' => [ sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(:name_p1_0, \'%%\')', $this->alias, Dummy::class), ['name_p1_0' => 'partial'], diff --git a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php index 23b8bea0b1d..1209f780a9a 100644 --- a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php +++ b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php @@ -20,7 +20,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)] -#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] #[ODM\Document] class AbsoluteUrlDummy { diff --git a/tests/Fixtures/TestBundle/Document/Answer.php b/tests/Fixtures/TestBundle/Document/Answer.php index 2b45253e55d..03023a4c892 100644 --- a/tests/Fixtures/TestBundle/Document/Answer.php +++ b/tests/Fixtures/TestBundle/Document/Answer.php @@ -29,8 +29,8 @@ * Answer. */ #[ApiResource(operations: [new Get(), new Put(), new Patch(), new Delete(), new GetCollection(normalizationContext: ['groups' => ['foobar']])])] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer{._format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] #[ODM\Document] class Answer { diff --git a/tests/Fixtures/TestBundle/Document/Book.php b/tests/Fixtures/TestBundle/Document/Book.php index c74a8a30328..e95179ce536 100644 --- a/tests/Fixtures/TestBundle/Document/Book.php +++ b/tests/Fixtures/TestBundle/Document/Book.php @@ -22,7 +22,7 @@ * * @author Antoine Bluchet */ -#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}{._format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] +#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] #[ODM\Document] class Book { diff --git a/tests/Fixtures/TestBundle/Document/Dummy.php b/tests/Fixtures/TestBundle/Document/Dummy.php index cfa585544e0..4a8b56c991c 100644 --- a/tests/Fixtures/TestBundle/Document/Dummy.php +++ b/tests/Fixtures/TestBundle/Document/Dummy.php @@ -29,8 +29,8 @@ * @author Alexandre Delplace */ #[ApiResource(extraProperties: ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]]], filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] #[ODM\Document] class Dummy { diff --git a/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php b/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php index 8ccf3657399..ebec8ed9c59 100644 --- a/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php +++ b/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php @@ -28,8 +28,8 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyAggregateOffer { diff --git a/tests/Fixtures/TestBundle/Document/DummyOffer.php b/tests/Fixtures/TestBundle/Document/DummyOffer.php index 518829e632b..80bb1aecaa8 100644 --- a/tests/Fixtures/TestBundle/Document/DummyOffer.php +++ b/tests/Fixtures/TestBundle/Document/DummyOffer.php @@ -26,9 +26,9 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyOffer { diff --git a/tests/Fixtures/TestBundle/Document/DummyProduct.php b/tests/Fixtures/TestBundle/Document/DummyProduct.php index 065956e5ee7..7ea04a0447a 100644 --- a/tests/Fixtures/TestBundle/Document/DummyProduct.php +++ b/tests/Fixtures/TestBundle/Document/DummyProduct.php @@ -28,7 +28,7 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyProduct { diff --git a/tests/Fixtures/TestBundle/Document/DummyValidation.php b/tests/Fixtures/TestBundle/Document/DummyValidation.php index a0fa6b10033..587fd26b7be 100644 --- a/tests/Fixtures/TestBundle/Document/DummyValidation.php +++ b/tests/Fixtures/TestBundle/Document/DummyValidation.php @@ -21,7 +21,7 @@ #[ApiResource(operations: [ new GetCollection(), - new Post(uriTemplate: 'dummy_validation{._format}'), + new Post(uriTemplate: 'dummy_validation.{_format}'), new Post(routeName: 'post_validation_groups', validationContext: ['groups' => ['a']]), new Post(routeName: 'post_validation_sequence', validationContext: ['groups' => 'app.dummy_validation.group_generator']), ] diff --git a/tests/Fixtures/TestBundle/Document/FooDummy.php b/tests/Fixtures/TestBundle/Document/FooDummy.php index 8736509e855..cc9fe959f25 100644 --- a/tests/Fixtures/TestBundle/Document/FooDummy.php +++ b/tests/Fixtures/TestBundle/Document/FooDummy.php @@ -15,8 +15,6 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @@ -44,17 +42,6 @@ class FooDummy #[ODM\ReferenceOne(targetDocument: Dummy::class, cascade: ['persist'], storeAs: 'id')] private ?Dummy $dummy = null; - /** - * @var Collection - */ - #[ODM\ReferenceMany(targetDocument: SoMany::class, cascade: ['persist'], storeAs: 'id')] - public Collection $soManies; - - public function __construct() - { - $this->soManies = new ArrayCollection(); - } - public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Document/FourthLevel.php b/tests/Fixtures/TestBundle/Document/FourthLevel.php index bf653bfbc37..46a9cd1d615 100644 --- a/tests/Fixtures/TestBundle/Document/FourthLevel.php +++ b/tests/Fixtures/TestBundle/Document/FourthLevel.php @@ -26,12 +26,12 @@ * @author Alan Poulain */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] #[ODM\Document] class FourthLevel { diff --git a/tests/Fixtures/TestBundle/Document/Greeting.php b/tests/Fixtures/TestBundle/Document/Greeting.php index bb6ec15928a..086b22e84b1 100644 --- a/tests/Fixtures/TestBundle/Document/Greeting.php +++ b/tests/Fixtures/TestBundle/Document/Greeting.php @@ -19,7 +19,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/people/{id}/sent_greetings{._format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/people/{id}/sent_greetings.{_format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class Greeting { diff --git a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php index bd2794e67a5..41b63270bf8 100644 --- a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php +++ b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php @@ -20,7 +20,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)] -#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] #[ODM\Document] class NetworkPathDummy { diff --git a/tests/Fixtures/TestBundle/Document/Question.php b/tests/Fixtures/TestBundle/Document/Question.php index 417f78864b9..6375515e66c 100644 --- a/tests/Fixtures/TestBundle/Document/Question.php +++ b/tests/Fixtures/TestBundle/Document/Question.php @@ -19,8 +19,8 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class Question { diff --git a/tests/Fixtures/TestBundle/Document/RelatedDummy.php b/tests/Fixtures/TestBundle/Document/RelatedDummy.php index a400aa06450..0ca2c57dc04 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Document/RelatedDummy.php @@ -37,13 +37,13 @@ * @author Alexandre Delplace */ #[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.mongodb.friends'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] #[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])] #[ODM\Document] class RelatedDummy extends ParentDummy implements \Stringable diff --git a/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php index 8955436b628..254fd5da549 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php +++ b/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php @@ -25,11 +25,11 @@ * Related To Dummy Friend represent an association table for a manytomany relation. */ #[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.mongodb.name'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ODM\Document] class RelatedToDummyFriend { diff --git a/tests/Fixtures/TestBundle/Document/SlugChildDummy.php b/tests/Fixtures/TestBundle/Document/SlugChildDummy.php index 890cf391652..4d210239e04 100644 --- a/tests/Fixtures/TestBundle/Document/SlugChildDummy.php +++ b/tests/Fixtures/TestBundle/Document/SlugChildDummy.php @@ -20,8 +20,8 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class SlugChildDummy { diff --git a/tests/Fixtures/TestBundle/Document/SlugParentDummy.php b/tests/Fixtures/TestBundle/Document/SlugParentDummy.php index ed34ea94bd6..4ff555b8764 100644 --- a/tests/Fixtures/TestBundle/Document/SlugParentDummy.php +++ b/tests/Fixtures/TestBundle/Document/SlugParentDummy.php @@ -25,8 +25,8 @@ * Custom Identifier Dummy With Subresource. */ #[ApiResource(uriVariables: 'slug')] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] #[ODM\Document] class SlugParentDummy { diff --git a/tests/Fixtures/TestBundle/Document/ThirdLevel.php b/tests/Fixtures/TestBundle/Document/ThirdLevel.php index 046b89afb16..e4b6fe19cfa 100644 --- a/tests/Fixtures/TestBundle/Document/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Document/ThirdLevel.php @@ -26,11 +26,11 @@ * @author Alexandre Delplace */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] #[ODM\Document] class ThirdLevel { diff --git a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php index 703abcff6a3..4501a935a59 100644 --- a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; use ApiPlatform\Metadata\ApiResource; +use DateTime; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Symfony\Component\Serializer\Annotation\Groups; @@ -26,12 +27,9 @@ class VoDummyInspection #[ODM\Field(type: 'date')] private \DateTime $performed; - private $attributeWithoutConstructorEquivalent; - - public function __construct(#[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] #[ODM\Field(type: 'bool')] private bool $accepted, #[Groups(['inspection_read', 'inspection_write'])] #[ODM\ReferenceOne(targetDocument: VoDummyCar::class, inversedBy: 'inspections')] private VoDummyCar $car, \DateTime $performed = null, string $parameterWhichIsNotClassAttribute = '') + public function __construct(#[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] #[ODM\Field(type: 'bool')] private bool $accepted, #[Groups(['inspection_read', 'inspection_write'])] #[ODM\ReferenceOne(targetDocument: VoDummyCar::class, inversedBy: 'inspections')] private VoDummyCar $car, DateTime $performed = null, private string $attributeWithoutConstructorEquivalent = '') { - $this->performed = $performed ?: new \DateTime(); - $this->attributeWithoutConstructorEquivalent = $parameterWhichIsNotClassAttribute; + $this->performed = $performed ?: new DateTime(); } public function isAccepted(): bool @@ -44,12 +42,12 @@ public function getCar(): VoDummyCar return $this->car; } - public function getPerformed(): \DateTime + public function getPerformed(): DateTime { return $this->performed; } - public function setPerformed(\DateTime $performed) + public function setPerformed(DateTime $performed) { $this->performed = $performed; diff --git a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php index dfb1f919c21..ff2fb260aca 100644 --- a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php +++ b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php @@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)] -#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] #[ORM\Entity] class AbsoluteUrlDummy { diff --git a/tests/Fixtures/TestBundle/Entity/Answer.php b/tests/Fixtures/TestBundle/Entity/Answer.php index 945fd5e3b6d..7a6fb033669 100644 --- a/tests/Fixtures/TestBundle/Entity/Answer.php +++ b/tests/Fixtures/TestBundle/Entity/Answer.php @@ -29,8 +29,8 @@ * Answer. */ #[ApiResource(operations: [new Get(), new Put(), new Patch(), new Delete(), new GetCollection(normalizationContext: ['groups' => ['foobar']])])] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer{._format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] #[ORM\Entity] class Answer { diff --git a/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php b/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php deleted file mode 100644 index e13550bb603..00000000000 --- a/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * 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; - -use ApiPlatform\Metadata\Get; - -#[Get(name: 'my own name')] -final class AttributeOnlyOperation -{ -} diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResource.php b/tests/Fixtures/TestBundle/Entity/AttributeResource.php index 0c4649e27e7..560251f7c69 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResource.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResource.php @@ -31,7 +31,7 @@ #[Put] #[Delete] #[ApiResource( - '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', + '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', inputFormats: ['json' => ['application/merge-patch+json']], status: 301, provider: AttributeResourceProvider::class, diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResources.php b/tests/Fixtures/TestBundle/Entity/AttributeResources.php index e2397c8d938..6685647c810 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResources.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResources.php @@ -17,15 +17,18 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use ApiPlatform\Tests\Fixtures\TestBundle\State\AttributeResourceProvider; +use ArrayIterator; +use IteratorAggregate; +use Traversable; #[ApiResource( - '/attribute_resources{._format}', + '/attribute_resources.{_format}', normalizationContext: ['skip_null_values' => true], provider: AttributeResourceProvider::class )] #[GetCollection] #[Post] -final class AttributeResources implements \IteratorAggregate +final class AttributeResources implements IteratorAggregate { /** * @var AttributeResource[] @@ -37,8 +40,8 @@ public function __construct(AttributeResource ...$collection) $this->collection = $collection; } - public function getIterator(): \Traversable + public function getIterator(): Traversable { - return new \ArrayIterator($this->collection); + return new ArrayIterator($this->collection); } } diff --git a/tests/Fixtures/TestBundle/Entity/Book.php b/tests/Fixtures/TestBundle/Entity/Book.php index f892b81cb3f..45a30eb6807 100644 --- a/tests/Fixtures/TestBundle/Entity/Book.php +++ b/tests/Fixtures/TestBundle/Entity/Book.php @@ -22,7 +22,7 @@ * * @author Antoine Bluchet */ -#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}{._format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] +#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] #[ORM\Entity] class Book { diff --git a/tests/Fixtures/TestBundle/Entity/Dummy.php b/tests/Fixtures/TestBundle/Entity/Dummy.php index 69e9384db63..33fc651dbd7 100644 --- a/tests/Fixtures/TestBundle/Entity/Dummy.php +++ b/tests/Fixtures/TestBundle/Entity/Dummy.php @@ -28,8 +28,8 @@ * @author Kévin Dunglas */ #[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] #[ORM\Entity] class Dummy { diff --git a/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php b/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php index 7089cdf2a70..33d5def4a21 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php +++ b/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php @@ -28,8 +28,8 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyAggregateOffer { diff --git a/tests/Fixtures/TestBundle/Entity/DummyOffer.php b/tests/Fixtures/TestBundle/Entity/DummyOffer.php index 5e9d52dad81..8448b8bf21c 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyOffer.php +++ b/tests/Fixtures/TestBundle/Entity/DummyOffer.php @@ -26,9 +26,9 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyOffer { diff --git a/tests/Fixtures/TestBundle/Entity/DummyProduct.php b/tests/Fixtures/TestBundle/Entity/DummyProduct.php index f2a5475d46a..6901e719705 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyProduct.php +++ b/tests/Fixtures/TestBundle/Entity/DummyProduct.php @@ -28,7 +28,7 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyProduct { diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php deleted file mode 100644 index cabc7a4352c..00000000000 --- a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * 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; - -use ApiPlatform\Core\Annotation\ApiResource; -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; - -/** - * @ORM\Entity - * - * @ApiResource - */ -class DummyToUpgradeProduct -{ - /** - * @var int - * - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ - private $id; - - /** - * @var Collection - * - * @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAnnotation::class) - */ - private $dummysToUpgradeWithOnlyAnnotation; - - /** - * @var Collection - * - * @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAttribute::class) - */ - private $dummysToUpgradeWithOnlyAttribute; -} diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php deleted file mode 100644 index 576ca2a55f0..00000000000 --- a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * 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; - -use ApiPlatform\Core\Annotation\ApiFilter; -use ApiPlatform\Core\Annotation\ApiProperty; -use ApiPlatform\Core\Annotation\ApiResource; -use ApiPlatform\Core\Annotation\ApiSubresource; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; -use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; - -/** - * @ORM\Entity - * - * @ApiResource - * - * @ApiFilter(SearchFilter::class, properties={"id"}) - */ -class DummyToUpgradeWithOnlyAnnotation -{ - /** - * @var int - * - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - * @Groups({"chicago", "friends"}) - * @ApiProperty(writable=false) - * @ApiFilter(DateFilter::class) - */ - private $id; - - /** - * @var DummyToUpgradeProduct - * - * @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", cascade={"persist"}, inversedBy="dummysToUpgradeWithOnlyAnnotation") - * @ORM\JoinColumn(nullable=false) - * @Groups({"barcelona", "chicago", "friends"}) - * - * @ApiSubresource - * - * @ApiProperty(iri="DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct") - * @ApiFilter(SearchFilter::class) - * @ApiFilter(ExistsFilter::class) - */ - private $dummyToUpgradeProduct; -} diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php deleted file mode 100644 index f9a1fd1f592..00000000000 --- a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * 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; - -use ApiPlatform\Core\Annotation\ApiProperty; -use ApiPlatform\Core\Annotation\ApiResource; -use ApiPlatform\Core\Annotation\ApiSubresource; -use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; - -/** - * @ORM\Entity - */ -#[ApiResource()] -class DummyToUpgradeWithOnlyAttribute -{ - /** - * @var int - * - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ - #[Groups(['chicago', 'friends'])] - #[ApiProperty(writable: false)] - private $id; - - /** - * @var DummyToUpgradeProduct - * - * @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", inversedBy="dummysToUpgradeWithOnlyAttribute") - * @ORM\JoinColumn(nullable=false) - */ - #[Groups(['barcelona', 'chicago', 'friends'])] - #[ApiSubresource] - #[ApiProperty(iri: 'DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct')] - private $dummyToUpgradeProduct; -} diff --git a/tests/Fixtures/TestBundle/Entity/DummyValidation.php b/tests/Fixtures/TestBundle/Entity/DummyValidation.php index 8500aa1556b..e06a4f02d29 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyValidation.php +++ b/tests/Fixtures/TestBundle/Entity/DummyValidation.php @@ -21,7 +21,7 @@ #[ApiResource(operations: [ new GetCollection(), - new Post(uriTemplate: 'dummy_validation{._format}'), + new Post(uriTemplate: 'dummy_validation.{_format}'), new Post(routeName: 'post_validation_groups', validationContext: ['groups' => ['a']]), new Post(routeName: 'post_validation_sequence', validationContext: ['groups' => 'app.dummy_validation.group_generator']), ] diff --git a/tests/Fixtures/TestBundle/Entity/FooDummy.php b/tests/Fixtures/TestBundle/Entity/FooDummy.php index 92596dad682..b744c0c2a55 100644 --- a/tests/Fixtures/TestBundle/Entity/FooDummy.php +++ b/tests/Fixtures/TestBundle/Entity/FooDummy.php @@ -15,8 +15,6 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -46,17 +44,6 @@ class FooDummy #[ORM\ManyToOne(targetEntity: Dummy::class, cascade: ['persist'])] private ?Dummy $dummy = null; - /** - * @var Collection - */ - #[ORM\OneToMany(targetEntity: SoMany::class, mappedBy: 'fooDummy', cascade: ['persist'])] - public Collection $soManies; - - public function __construct() - { - $this->soManies = new ArrayCollection(); - } - public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Entity/FourthLevel.php b/tests/Fixtures/TestBundle/Entity/FourthLevel.php index b62eaed1b27..85161c78705 100644 --- a/tests/Fixtures/TestBundle/Entity/FourthLevel.php +++ b/tests/Fixtures/TestBundle/Entity/FourthLevel.php @@ -26,12 +26,12 @@ * @author Alan Poulain */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] #[ORM\Entity] class FourthLevel { diff --git a/tests/Fixtures/TestBundle/Entity/Greeting.php b/tests/Fixtures/TestBundle/Entity/Greeting.php index d74e8217b55..ee0bcd5602d 100644 --- a/tests/Fixtures/TestBundle/Entity/Greeting.php +++ b/tests/Fixtures/TestBundle/Entity/Greeting.php @@ -19,7 +19,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/people/{id}/sent_greetings{._format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/people/{id}/sent_greetings.{_format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class Greeting { diff --git a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php index 46530b96c5e..30070090fda 100644 --- a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php +++ b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php @@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)] -#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] #[ORM\Entity] class NetworkPathDummy { diff --git a/tests/Fixtures/TestBundle/Entity/Question.php b/tests/Fixtures/TestBundle/Entity/Question.php index 78b1afd5026..0ffc8bfd2cb 100644 --- a/tests/Fixtures/TestBundle/Entity/Question.php +++ b/tests/Fixtures/TestBundle/Entity/Question.php @@ -19,8 +19,8 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class Question { diff --git a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php index a6e60f79169..bbb36c24aca 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php @@ -35,22 +35,14 @@ * * @author Kévin Dunglas */ -#[ApiResource( - graphQlOperations: [ - new Query(name: 'item_query'), - new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']]), - ], - types: ['https://schema.org/Product'], - normalizationContext: ['groups' => ['friends']], - filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'] -)] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] #[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])] #[ORM\Entity] class RelatedDummy extends ParentDummy implements \Stringable diff --git a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php index c584c30596b..b7823a78d86 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php @@ -25,11 +25,11 @@ * Related To Dummy Friend represent an association table for a manytomany relation. */ #[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ORM\Entity] class RelatedToDummyFriend { diff --git a/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php b/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php index c1d8c5b21e4..bab3c01e521 100644 --- a/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php +++ b/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php @@ -20,8 +20,8 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class SlugChildDummy { diff --git a/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php b/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php index 104ca2cc97e..c6412d78229 100644 --- a/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php +++ b/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php @@ -25,8 +25,8 @@ * Custom Identifier Dummy With Subresource. */ #[ApiResource(uriVariables: 'slug')] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] #[ORM\Entity] class SlugParentDummy { diff --git a/tests/Fixtures/TestBundle/Entity/SoMany.php b/tests/Fixtures/TestBundle/Entity/SoMany.php index e3770b8007d..65a2ad93790 100644 --- a/tests/Fixtures/TestBundle/Entity/SoMany.php +++ b/tests/Fixtures/TestBundle/Entity/SoMany.php @@ -31,7 +31,4 @@ class SoMany public $id; #[ORM\Column(nullable: true)] public $content; - - #[ORM\ManyToOne] - public ?FooDummy $fooDummy; } diff --git a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php index 1ca2e4c6f43..1d18d362d73 100644 --- a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php @@ -25,11 +25,11 @@ * @author Kévin Dunglas */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] #[ORM\Entity] class ThirdLevel { diff --git a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php index 7eebd395415..fc3eb0561f8 100644 --- a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; use ApiPlatform\Metadata\ApiResource; +use DateTime; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; @@ -27,12 +28,9 @@ class VoDummyInspection #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private \DateTime $performed; - private $attributeWithoutConstructorEquivalent; - - public function __construct(#[ORM\Column(type: 'boolean')] #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private bool $accepted, #[ORM\ManyToOne(targetEntity: VoDummyCar::class, inversedBy: 'inspections')] #[Groups(['inspection_read', 'inspection_write'])] private ?VoDummyCar $car, \DateTime $performed = null, string $parameterWhichIsNotClassAttribute = '') + public function __construct(#[ORM\Column(type: 'boolean')] #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private bool $accepted, #[ORM\ManyToOne(targetEntity: VoDummyCar::class, inversedBy: 'inspections')] #[Groups(['inspection_read', 'inspection_write'])] private ?VoDummyCar $car, DateTime $performed = null, private string $attributeWithoutConstructorEquivalent = '') { - $this->performed = $performed ?: new \DateTime(); - $this->attributeWithoutConstructorEquivalent = $parameterWhichIsNotClassAttribute; + $this->performed = $performed ?: new DateTime(); } public function isAccepted(): bool @@ -45,12 +43,12 @@ public function getCar(): ?VoDummyCar return $this->car; } - public function getPerformed(): \DateTime + public function getPerformed(): DateTime { return $this->performed; } - public function setPerformed(\DateTime $performed) + public function setPerformed(DateTime $performed) { $this->performed = $performed; diff --git a/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php b/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php deleted file mode 100644 index 65c4b65c51f..00000000000 --- a/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * 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\GraphQl\Metadata\Factory; - -use ApiPlatform\GraphQl\Metadata\Factory\GraphQlNestedOperationResourceMetadataFactory; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; - -final class GraphQlNestedOperationResourceMetadataFactoryTest extends TestCase -{ - use ProphecyTrait; - - public function testCreate(): void - { - $decorated = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $decorated->create('someClass')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('someClass')); - - $metadataFactory = new GraphQlNestedOperationResourceMetadataFactory(['status' => 500], $decorated->reveal()); - $apiResource = $metadataFactory->create('someClass')[0]; - $this->assertCount(5, $apiResource->getGraphQlOperations()); - } - - public function testCreateWithResource(): void - { - $metadataFactory = new GraphQlNestedOperationResourceMetadataFactory(['status' => 500]); - $apiResource = $metadataFactory->create(RelatedDummy::class)[0]; - $this->assertNotEmpty($apiResource->getFilters()); - $this->assertEquals('RelatedDummy', $apiResource->getShortName()); - } -} diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php index c5b59ad50b7..c7bb3ba897a 100644 --- a/tests/GraphQl/Type/FieldsBuilderTest.php +++ b/tests/GraphQl/Type/FieldsBuilderTest.php @@ -56,18 +56,29 @@ class FieldsBuilderTest extends TestCase use ProphecyTrait; private ObjectProphecy $propertyNameCollectionFactoryProphecy; + private ObjectProphecy $propertyMetadataFactoryProphecy; + private ObjectProphecy $resourceMetadataCollectionFactoryProphecy; - private ObjectProphecy $graphQlNestedOperationResourceMetadataFactoryProphecy; + private ObjectProphecy $typesContainerProphecy; + private ObjectProphecy $typeBuilderProphecy; + private ObjectProphecy $typeConverterProphecy; + private ObjectProphecy $itemResolverFactoryProphecy; + private ObjectProphecy $collectionResolverFactoryProphecy; + private ObjectProphecy $itemMutationResolverFactoryProphecy; + private ObjectProphecy $itemSubscriptionResolverFactoryProphecy; + private ObjectProphecy $filterLocatorProphecy; + private ObjectProphecy $resourceClassResolverProphecy; + private FieldsBuilder $fieldsBuilder; /** @@ -87,13 +98,12 @@ protected function setUp(): void $this->itemSubscriptionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); $this->filterLocatorProphecy = $this->prophesize(ContainerInterface::class); $this->resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $this->graphQlNestedOperationResourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $this->fieldsBuilder = $this->buildFieldsBuilder(); } private function buildFieldsBuilder(?AdvancedNameConverterInterface $advancedNameConverter = null): FieldsBuilder { - return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__', $this->graphQlNestedOperationResourceMetadataFactoryProphecy->reveal()); + return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); } public function testGetNodeQueryFields(): void @@ -129,6 +139,7 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $queryFields = $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration); @@ -139,8 +150,8 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati public function itemQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action'), [], null, null, []], - 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, + 'no resource field configuration' => ['resourceClass', (new Query())->withName('action'), [], null, null, []], + 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, [ 'actionShortName' => [ 'type' => GraphQLType::string(), @@ -153,7 +164,7 @@ public function itemQueryFieldsProvider(): array ], ], ], - 'nominal item case' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { + 'nominal item case' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { }, [ 'actionShortName' => [ @@ -168,7 +179,7 @@ public function itemQueryFieldsProvider(): array ], ], 'empty overridden args and add fields' => [ - 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, + 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -181,7 +192,7 @@ public function itemQueryFieldsProvider(): array ], ], 'override args with custom ones' => [ - 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, + 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -209,6 +220,7 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true); $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation)->willReturn($graphqlType); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $this->filterLocatorProphecy->has('my_filter')->willReturn(true); $filterProphecy = $this->prophesize(FilterInterface::class); @@ -232,8 +244,8 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o public function collectionQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action'), [], null, null, []], - 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withName('action'), [], null, null, []], + 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -262,7 +274,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with filters' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with filters' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -296,7 +308,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection empty overridden args and add fields' => [ - 'resourceClass', (new QueryCollection())->withArgs([])->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withArgs([])->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -310,7 +322,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection override args with custom ones' => [ - 'resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -326,7 +338,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -359,6 +371,8 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemMutationResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($mutationResolver); $mutationFields = $this->fieldsBuilder->getMutationFields($resourceClass, $operation); @@ -369,7 +383,7 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio public function mutationFieldsProvider(): array { return [ - 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -389,7 +403,7 @@ public function mutationFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'custom description' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -421,6 +435,7 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemSubscriptionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($subscriptionResolver); @@ -432,9 +447,9 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper public function subscriptionFieldsProvider(): array { return [ - 'mercure not enabled' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], + 'mercure not enabled' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], ], - 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -454,7 +469,7 @@ public function subscriptionFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'custom description' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -483,8 +498,6 @@ public function subscriptionFieldsProvider(): array public function testGetResourceObjectTypeFields(string $resourceClass, Operation $operation, array $properties, bool $input, int $depth, ?array $ioMetadata, array $expectedResourceObjectTypeFields, ?AdvancedNameConverterInterface $advancedNameConverter = null): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); - $this->resourceClassResolverProphecy->isResourceClass('nestedResourceClass')->willReturn(true); - $this->resourceClassResolverProphecy->isResourceClass('nestedResourceNoQueryClass')->willReturn(true); $this->resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(false); $this->propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection(array_keys($properties))); foreach ($properties as $propertyName => $propertyMetadata) { @@ -492,32 +505,20 @@ public function testGetResourceObjectTypeFields(string $resourceClass, Operation $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_NULL), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(null); $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_CALLABLE), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn('NotRegisteredType'); $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::string()); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); - if ('propertyObject' === $propertyName) { $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->resourceMetadataCollectionFactoryProphecy->create('objectClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation)->willReturn(static function (): void { }); } - if ('propertyNestedResource' === $propertyName) { - $nestedResourceQueryOperation = new Query(); - $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceClass')->willReturn(new ResourceMetadataCollection('nestedResourceClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); - $this->itemResolverFactoryProphecy->__invoke('nestedResourceClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { - }); - } - if ('propertyNestedResourceNoQuery' === $propertyName) { - $nestedResourceQueryOperation = new Query(); - $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceNoQueryClass')->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withDescription('A description.')->withGraphQlOperations([])])); - $this->graphQlNestedOperationResourceMetadataFactoryProphecy->create('nestedResourceNoQueryClass')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceNoQueryClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); - $this->itemResolverFactoryProphecy->__invoke('nestedResourceNoQueryClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { - }); - } + $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'anotherResourceClass', $propertyName, $depth + 1)->willReturn(GraphQLType::string()); + $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); } $this->typesContainerProphecy->has('NotRegisteredType')->willReturn(false); $this->typesContainerProphecy->all()->willReturn([]); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->resourceMetadataCollectionFactoryProphecy->create('resourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); + $this->resourceMetadataCollectionFactoryProphecy->create('anotherResourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $fieldsBuilder = $this->fieldsBuilder; if ($advancedNameConverter) { @@ -534,7 +535,7 @@ public function resourceObjectTypeFieldsProvider(): array $advancedNameConverter->normalize('field', 'resourceClass')->willReturn('normalizedField'); return [ - 'query' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query' => ['resourceClass', new Query(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(false), @@ -562,7 +563,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with advanced name converter' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query with advanced name converter' => ['resourceClass', new Query(), [ 'field' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(false), ], @@ -581,7 +582,7 @@ public function resourceObjectTypeFieldsProvider(): array ], $advancedNameConverter->reveal(), ], - 'query input' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query input' => ['resourceClass', new Query(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(false), @@ -600,7 +601,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with simple non-null string array property' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query with simple non-null string array property' => ['resourceClass', new Query(), [ 'property' => (new ApiProperty())->withBuiltinTypes([ new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), @@ -620,40 +621,12 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with nested resources' => ['resourceClass', (new Query())->withClass('resourceClass'), - [ - 'propertyNestedResource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceClass')])->withReadable(true)->withWritable(true), - 'propertyNestedResourceNoQuery' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceNoQueryClass')])->withReadable(true)->withWritable(true), - ], - false, 0, null, - [ - 'id' => [ - 'type' => GraphQLType::nonNull(GraphQLType::id()), - ], - 'propertyNestedResource' => [ - 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), - 'description' => null, - 'args' => [], - 'resolve' => static function (): void { - }, - 'deprecationReason' => null, - ], - 'propertyNestedResourceNoQuery' => [ - 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), - 'description' => null, - 'args' => [], - 'resolve' => static function (): void { - }, - 'deprecationReason' => null, - ], - ], - ], - 'mutation non input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + 'mutation non input' => ['resourceClass', (new Mutation())->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), 'propertyReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(true), - 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'objectClass')])->withReadable(true)->withWritable(true), + 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL, false, 'objectClass')])->withReadable(true)->withWritable(true), ], false, 0, null, [ @@ -677,7 +650,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + 'mutation input' => ['resourceClass', (new Mutation())->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -713,7 +686,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'mutation nested input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + 'mutation nested input' => ['resourceClass', (new Mutation())->withName('mutation'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -732,7 +705,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'delete mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('delete'), + 'delete mutation input' => ['resourceClass', (new Mutation())->withName('delete'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -744,7 +717,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'create mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('create'), + 'create mutation input' => ['resourceClass', (new Mutation())->withName('create'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -760,7 +733,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'update mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('update'), + 'update mutation input' => ['resourceClass', (new Mutation())->withName('update'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -779,7 +752,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'subscription non input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), + 'subscription non input' => ['resourceClass', new Subscription(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), @@ -799,7 +772,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'subscription input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), + 'subscription input' => ['resourceClass', new Subscription(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -814,13 +787,13 @@ public function resourceObjectTypeFieldsProvider(): array 'clientSubscriptionId' => GraphQLType::string(), ], ], - 'null io metadata non input' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'null io metadata non input' => ['resourceClass', new Query(), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], false, 0, ['class' => null], [], ], - 'null io metadata input' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'null io metadata input' => ['resourceClass', new Query(), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -829,7 +802,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'invalid types' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'invalid types' => ['resourceClass', new Query(), [ 'propertyInvalidType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_NULL)])->withReadable(true)->withWritable(false), 'propertyNotRegisteredType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_CALLABLE)])->withReadable(true)->withWritable(false), diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index 77ba4f7e842..66b0e7cd855 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -483,7 +483,7 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPageInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); $this->assertSame('StringCursorConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Cursor connection for String.', $resourcePaginatedCollectionType->description); @@ -538,7 +538,7 @@ public function testPageBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPaginationInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); $this->assertSame('StringPageConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Page connection for String.', $resourcePaginatedCollectionType->description); diff --git a/tests/HttpCache/VarnishPurgerTest.php b/tests/HttpCache/VarnishPurgerTest.php index faca342dc91..ffc2a5b53f4 100644 --- a/tests/HttpCache/VarnishPurgerTest.php +++ b/tests/HttpCache/VarnishPurgerTest.php @@ -17,6 +17,7 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; +use LogicException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; @@ -78,12 +79,12 @@ public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLengt public function send(RequestInterface $request, array $options = []): ResponseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function request($method, $uri, array $options = []): ResponseInterface @@ -95,12 +96,12 @@ public function request($method, $uri, array $options = []): ResponseInterface public function requestAsync($method, $uri, array $options = []): PromiseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function getConfig($option = null): void { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } }; diff --git a/tests/HttpCache/VarnishXKeyPurgerTest.php b/tests/HttpCache/VarnishXKeyPurgerTest.php index 96e380752b9..33c64da8678 100644 --- a/tests/HttpCache/VarnishXKeyPurgerTest.php +++ b/tests/HttpCache/VarnishXKeyPurgerTest.php @@ -17,6 +17,7 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; +use LogicException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; @@ -108,12 +109,12 @@ public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLengt public function send(RequestInterface $request, array $options = []): ResponseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function request($method, $uri, array $options = []): ResponseInterface @@ -125,12 +126,12 @@ public function request($method, $uri, array $options = []): ResponseInterface public function requestAsync($method, $uri, array $options = []): PromiseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function getConfig($option = null): void { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } }; diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index ae69822be21..fb6040a5de1 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -49,21 +49,21 @@ public function testExecuteWithoutOption(): void public function testExecuteWithItemOperationGet(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies/{id}{._format}_get', '--type' => 'output']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies/{id}.{_format}_get', '--type' => 'output']); $this->assertJson($this->tester->getDisplay()); } public function testExecuteWithCollectionOperationGet(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies{._format}_get_collection', '--type' => 'output']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies.{_format}_get_collection', '--type' => 'output']); $this->assertJson($this->tester->getDisplay()); } public function testExecuteWithJsonldFormatOption(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies{._format}_post', '--format' => 'jsonld']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies.{_format}_post', '--format' => 'jsonld']); $result = $this->tester->getDisplay(); $this->assertStringContainsString('@id', $result); diff --git a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php index 4c34da9000f..04279076970 100644 --- a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php @@ -21,6 +21,7 @@ use ApiPlatform\Tests\Metadata\Extractor\Adapter\PropertyAdapterInterface; use ApiPlatform\Tests\Metadata\Extractor\Adapter\XmlPropertyAdapter; use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlPropertyAdapter; +use Exception; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Type; @@ -86,7 +87,7 @@ public function testValidMetadata(string $extractorClass, PropertyAdapterInterfa $extractor = new $extractorClass($adapter(self::RESOURCE_CLASS, self::PROPERTY, $parameters, self::FIXTURES)); $factory = new ExtractorPropertyMetadataFactory($extractor); $property = $factory->create(self::RESOURCE_CLASS, self::PROPERTY); - } catch (\Exception $exception) { + } catch (Exception $exception) { throw new AssertionFailedError('Failed asserting that the schema is valid according to '.ApiProperty::class, 0, $exception); } diff --git a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php index e6ce7a3727d..20342ad36e6 100644 --- a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php @@ -20,13 +20,10 @@ use ApiPlatform\Metadata\Extractor\YamlResourceExtractor; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\GraphQl\DeleteMutation; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -37,6 +34,7 @@ use ApiPlatform\Tests\Metadata\Extractor\Adapter\ResourceAdapterInterface; use ApiPlatform\Tests\Metadata\Extractor\Adapter\XmlResourceAdapter; use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlResourceAdapter; +use Exception; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; @@ -226,7 +224,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase [ 'name' => 'custom_operation_name', 'method' => 'GET', - 'uriTemplate' => '/users/{userId}/comments{._format}', + 'uriTemplate' => '/users/{userId}/comments.{_format}', 'shortName' => self::SHORT_NAME, 'description' => 'A list of Comments', 'types' => ['Comment'], @@ -332,7 +330,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase ], ], [ - 'uriTemplate' => '/users/{userId}/comments/{commentId}{._format}', + 'uriTemplate' => '/users/{userId}/comments/{commentId}.{_format}', 'class' => Get::class, 'uriVariables' => [ 'userId' => [ @@ -422,7 +420,7 @@ public function testValidMetadata(string $extractorClass, ResourceAdapterInterfa $extractor = new $extractorClass($adapter(self::RESOURCE_CLASS, $parameters, self::FIXTURES)); $factory = new ExtractorResourceMetadataCollectionFactory($extractor); $collection = $factory->create(self::RESOURCE_CLASS); - } catch (\Exception $exception) { + } catch (Exception $exception) { throw new AssertionFailedError('Failed asserting that the schema is valid according to '.ApiResource::class, 0, $exception); } @@ -455,16 +453,7 @@ private function buildApiResources(): array $operations[$operationName] = $this->getOperationWithDefaults($resource, $operation)->withName($operationName); } - $resource = $resource->withOperations(new Operations($operations)); - - // Build default GraphQL operations - $graphQlOperations = []; - foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $graphQlOperation) { - $description = $graphQlOperation instanceof Mutation ? ucfirst("{$graphQlOperation->getName()}s a {$resource->getShortName()}.") : null; - $graphQlOperations[$graphQlOperation->getName()] = $this->getOperationWithDefaults($resource, $graphQlOperation)->withName($graphQlOperation->getName())->withDescription($description); - } - - $resources[] = $resource->withGraphQlOperations($graphQlOperations); + $resources[] = $resource->withOperations(new Operations($operations)); continue; } @@ -602,7 +591,7 @@ private function withGraphQlOperations(array $values, ?array $fixtures): array return $operations; } - private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation + private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation { foreach (get_class_methods($resource) as $methodName) { if (!str_starts_with($methodName, 'get')) { diff --git a/tests/Metadata/Extractor/XmlExtractorTest.php b/tests/Metadata/Extractor/XmlExtractorTest.php index b229f52f1b6..dc1f9b45e8f 100644 --- a/tests/Metadata/Extractor/XmlExtractorTest.php +++ b/tests/Metadata/Extractor/XmlExtractorTest.php @@ -99,7 +99,7 @@ public function testValidXML(): void 'write' => null, ], [ - 'uriTemplate' => '/users/{author}/comments{._format}', + 'uriTemplate' => '/users/{author}/comments.{_format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, @@ -177,7 +177,7 @@ public function testValidXML(): void [ 'name' => 'custom_operation_name', 'class' => GetCollection::class, - 'uriTemplate' => '/users/{author}/comments{._format}', + 'uriTemplate' => '/users/{author}/comments.{_format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, @@ -268,7 +268,7 @@ public function testValidXML(): void [ 'name' => null, 'class' => Get::class, - 'uriTemplate' => '/users/{userId}/comments/{id}{._format}', + 'uriTemplate' => '/users/{userId}/comments/{id}.{_format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, diff --git a/tests/Metadata/Extractor/YamlExtractorTest.php b/tests/Metadata/Extractor/YamlExtractorTest.php index cede4108ebc..843df41f723 100644 --- a/tests/Metadata/Extractor/YamlExtractorTest.php +++ b/tests/Metadata/Extractor/YamlExtractorTest.php @@ -166,7 +166,7 @@ public function testValidYaml(): void 'write' => null, ], [ - 'uriTemplate' => '/users/{author}/programs{._format}', + 'uriTemplate' => '/users/{author}/programs.{_format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -230,7 +230,7 @@ public function testValidYaml(): void [ 'name' => null, 'class' => GetCollection::class, - 'uriTemplate' => '/users/{author}/programs{._format}', + 'uriTemplate' => '/users/{author}/programs.{_format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -305,7 +305,7 @@ public function testValidYaml(): void [ 'name' => null, 'class' => Get::class, - 'uriTemplate' => '/users/{userId}/programs/{id}{._format}', + 'uriTemplate' => '/users/{userId}/programs/{id}.{_format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -471,7 +471,7 @@ public function testInvalidYaml(string $path, string $error): void public function getInvalidPaths(): array { return [ - [__DIR__.'/yaml/invalid/invalid_resources.yaml', '"resources" setting is expected to be null or an array, string given in "'.__DIR__.'/yaml/invalid/invalid_resources.yaml".'], + [__DIR__.'/yaml/invalid/invalid_resources.yaml', '"resources" setting is expected to be null or an array, string given in "'.__DIR__.'/yaml/invalid/invalid_resources.yaml'.'".'], ]; } } diff --git a/tests/Metadata/Extractor/xml/valid.xml b/tests/Metadata/Extractor/xml/valid.xml index cfcccc202d3..3269a7ba104 100644 --- a/tests/Metadata/Extractor/xml/valid.xml +++ b/tests/Metadata/Extractor/xml/valid.xml @@ -7,7 +7,7 @@ @@ -96,7 +96,7 @@ - + diff --git a/tests/Metadata/Extractor/yaml/valid.yaml b/tests/Metadata/Extractor/yaml/valid.yaml index c3a80a5acac..d001ba20899 100644 --- a/tests/Metadata/Extractor/yaml/valid.yaml +++ b/tests/Metadata/Extractor/yaml/valid.yaml @@ -3,7 +3,7 @@ resources: ApiPlatform\Tests\Fixtures\TestBundle\Entity\Program: - ~ - - uriTemplate: /users/{author}/programs{._format} + - uriTemplate: /users/{author}/programs.{_format} uriVariables: ['author'] types: ['someirischema'] description: User programs @@ -13,7 +13,7 @@ resources: operations: ApiPlatform\Metadata\GetCollection: ~ ApiPlatform\Metadata\Get: - uriTemplate: /users/{userId}/programs/{id}{._format} + uriTemplate: /users/{userId}/programs/{id}.{_format} types: ['anotheririschema'] uriVariables: userId: [ApiPlatform\Tests\Fixtures\TestBundle\Entity\User, 'author'] diff --git a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php index 6510080c88d..0420c8d3be8 100644 --- a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php @@ -28,7 +28,6 @@ use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeDefaultOperations; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeOnlyOperation; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResources; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ExtraPropertiesResource; @@ -82,11 +81,11 @@ class: AttributeResource::class, new ApiResource( shortName: 'AttributeResource', class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', operations: [ - '_api_/dummy/{dummyId}/attribute_resources/{identifier}{._format}_get' => new Get( + '_api_/dummy/{dummyId}/attribute_resources/{identifier}.{_format}_get' => new Get( class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', shortName: 'AttributeResource', inputFormats: ['json' => ['application/merge-patch+json']], priority: 4, @@ -95,9 +94,9 @@ class: AttributeResource::class, // @noRector \Rector\Php81\Rector\Array_\FirstClassCallableRector processor: [AttributeResourceProcessor::class, 'process'] ), - '_api_/dummy/{dummyId}/attribute_resources/{identifier}{._format}_patch' => new Patch( + '_api_/dummy/{dummyId}/attribute_resources/{identifier}.{_format}_patch' => new Patch( class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', shortName: 'AttributeResource', inputFormats: ['json' => ['application/merge-patch+json']], priority: 5, @@ -120,17 +119,17 @@ class: AttributeResource::class, $this->assertEquals( new ResourceMetadataCollection(AttributeResources::class, [ new ApiResource( - uriTemplate: '/attribute_resources{._format}', + uriTemplate: '/attribute_resources.{_format}', shortName: 'AttributeResources', normalizationContext: ['skip_null_values' => true], class: AttributeResources::class, provider: AttributeResourceProvider::class, operations: [ - '_api_/attribute_resources{._format}_get_collection' => new GetCollection( - shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources{._format}', normalizationContext: ['skip_null_values' => true], priority: 1, provider: AttributeResourceProvider::class, + '_api_/attribute_resources.{_format}_get_collection' => new GetCollection( + shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources.{_format}', normalizationContext: ['skip_null_values' => true], priority: 1, provider: AttributeResourceProvider::class, ), - '_api_/attribute_resources{._format}_post' => new Post( - shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources{._format}', normalizationContext: ['skip_null_values' => true], priority: 2, provider: AttributeResourceProvider::class, + '_api_/attribute_resources.{_format}_post' => new Post( + shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources.{_format}', normalizationContext: ['skip_null_values' => true], priority: 2, provider: AttributeResourceProvider::class, ), ], graphQlOperations: $this->getDefaultGraphqlOperations('AttributeResources', AttributeResources::class, AttributeResourceProvider::class) @@ -210,20 +209,4 @@ public function testExtraProperties(): void $this->assertEquals($extraPropertiesResource[0]->getExtraProperties(), ['foo' => 'bar']); $this->assertEquals($extraPropertiesResource->getOperation('_api_ExtraPropertiesResource_get')->getExtraProperties(), ['foo' => 'bar']); } - - public function testOverrideNameWithoutOperations(): void - { - $attributeResourceMetadataCollectionFactory = new AttributesResourceMetadataCollectionFactory(); - - $operation = new HttpOperation(shortName: 'AttributeOnlyOperation', class: AttributeOnlyOperation::class); - $this->assertEquals(new ResourceMetadataCollection(AttributeOnlyOperation::class, [ - new ApiResource( - shortName: 'AttributeOnlyOperation', - class: AttributeOnlyOperation::class, - operations: [ - 'my own name' => (new Get(name: 'my own name', priority: 1))->withOperation($operation), - ] - ), - ]), $attributeResourceMetadataCollectionFactory->create(AttributeOnlyOperation::class)); - } } diff --git a/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php index 7603509b3a9..d60638d3f71 100644 --- a/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php @@ -115,10 +115,10 @@ class: AttributeResource::class, shortName: 'AttributeResource', class: AttributeResource::class, operations: [ - '_api_/attribute_resources/{id}{._format}_get' => new Get(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_get'), - '_api_/attribute_resources/{id}{._format}_put' => new Put(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_put'), - '_api_/attribute_resources/{id}{._format}_delete' => new Delete(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_delete'), - '_api_/attribute_resources{._format}_get_collection' => new GetCollection(uriTemplate: '/attribute_resources{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', name: '_api_/attribute_resources{._format}_get_collection'), + '_api_/attribute_resources/{id}.{_format}_get' => new Get(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_get'), + '_api_/attribute_resources/{id}.{_format}_put' => new Put(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_put'), + '_api_/attribute_resources/{id}.{_format}_delete' => new Delete(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_delete'), + '_api_/attribute_resources.{_format}_get_collection' => new GetCollection(uriTemplate: '/attribute_resources.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', name: '_api_/attribute_resources.{_format}_get_collection'), ] ), new ApiResource( diff --git a/tests/State/Pagination/TraversablePaginatorTest.php b/tests/State/Pagination/TraversablePaginatorTest.php index e9138e7571d..ba51143119d 100644 --- a/tests/State/Pagination/TraversablePaginatorTest.php +++ b/tests/State/Pagination/TraversablePaginatorTest.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\State\Pagination; use ApiPlatform\State\Pagination\TraversablePaginator; +use ArrayIterator; use PHPUnit\Framework\TestCase; class TraversablePaginatorTest extends TestCase @@ -29,7 +30,7 @@ public function testInitialize( float $lastPage, int $currentItems ): void { - $traversable = new \ArrayIterator($results); + $traversable = new ArrayIterator($results); $paginator = new TraversablePaginator($traversable, $currentPage, $perPage, $totalItems); diff --git a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php index a574d5b9dc2..ad0f265a015 100644 --- a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php +++ b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php @@ -205,7 +205,7 @@ public function provideIntegrationCases(): iterable yield 'simple' => [ $this->createRequest('PUT', [ '_api_resource_class' => ResourceImplementation::class, - '_api_operation_name' => '_api_/resource_implementations{._format}_put', + '_api_operation_name' => '_api_/resource_implementations.{_format}_put', 'data' => $resource, ]), static function (ResourceImplementation $payload): void {}, @@ -215,7 +215,7 @@ static function (ResourceImplementation $payload): void {}, yield 'with another argument named $data' => [ $this->createRequest('PUT', [ '_api_resource_class' => ResourceImplementation::class, - '_api_operation_name' => '_api_/resource_implementations{._format}_put', + '_api_operation_name' => '_api_/resource_implementations.{_format}_put', 'data' => $resource, ]), static function (ResourceImplementation $payload, $data): void {}, @@ -230,7 +230,7 @@ private function createArgumentResolver(): PayloadArgumentResolver (new ApiResource())->withShortName('ResourceImplementation')->withOperations(new Operations([ 'update' => new Put(), 'update_no_deserialize' => (new Put())->withDeserialize(false), - 'update_with_dto' => (new Put())->withInput(['class' => NotResource::class, 'name' => 'NotResource']), + 'update_with_dto' => (new Put())->withInput(['class' => NotResource::class]), 'create' => new Post(), ])), ])); @@ -251,7 +251,7 @@ private function createArgumentResolver(): PayloadArgumentResolver ]; if ('update_with_dto' === $request->attributes->get('_api_operation_name')) { - $context['input'] = ['class' => NotResource::class, 'name' => 'NotResource']; + $context['input'] = NotResource::class; } else { $context['input'] = null; } diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 5be3cc47aed..cf89456b6af 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -70,9 +70,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\SecurityBundle\SecurityBundle; -use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Uid\AbstractUid; @@ -165,7 +163,6 @@ protected function setUp(): void 'kernel.bundles' => [ 'DoctrineBundle' => DoctrineBundle::class, 'SecurityBundle' => SecurityBundle::class, - 'TwigBundle' => TwigBundle::class, ], 'kernel.bundles_metadata' => [ 'TestBundle' => [ @@ -176,7 +173,6 @@ protected function setUp(): void ], 'kernel.project_dir' => __DIR__.'/../../../Fixtures/app', 'kernel.debug' => false, - 'kernel.environment' => 'test', ]); $this->container = new ContainerBuilder($containerParameterBag); @@ -671,8 +667,6 @@ public function testGraphQlConfiguration(): void 'api_platform.graphql.normalizer.validation_exception', 'api_platform.graphql.normalizer.http_exception', 'api_platform.graphql.normalizer.runtime_exception', - 'api_platform.graphql_metadata.resource.metadata_collection_factory', - 'api_platform.graphql_metadata.resource.metadata_collection_factory.filters', ]; $aliases = [ @@ -699,37 +693,6 @@ public function testGraphQlConfiguration(): void $this->assertServiceHasTags('api_platform.graphql.normalizer.runtime_exception', ['serializer.normalizer']); } - public function testRuntimeExceptionIsThrownIfTwigIsNotEnabledButGraphqlClientsAre(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['graphql']['enabled'] = true; - $this->container->getParameterBag()->set('kernel.bundles', [ - 'DoctrineBundle' => DoctrineBundle::class, - 'SecurityBundle' => SecurityBundle::class, - ]); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('GraphiQL and GraphQL Playground interfaces depend on Twig. Please activate TwigBundle for the test environnement or disable GraphiQL and GraphQL Playground.'); - - (new ApiPlatformExtension())->load($config, $this->container); - } - - public function testGraphqlClientsDefinitionsAreRemovedIfDisabled(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['graphql']['enabled'] = true; - $config['api_platform']['graphql']['graphiql']['enabled'] = false; - $config['api_platform']['graphql']['graphql_playground']['enabled'] = false; - $this->container->getParameterBag()->set('kernel.bundles', [ - 'DoctrineBundle' => DoctrineBundle::class, - 'SecurityBundle' => SecurityBundle::class, - ]); - - (new ApiPlatformExtension())->load($config, $this->container); - - $this->assertNotContainerHasService('api_platform.graphql.action.graphiql'); - $this->assertNotContainerHasService('api_platform.graphql.action.graphql_playground'); - } - public function testDoctrineOrmConfiguration(): void { $config = self::DEFAULT_CONFIG; From 114b31e88dc243295b09635a0ba38af566eaf4a6 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Sun, 6 Nov 2022 17:16:26 +0100 Subject: [PATCH 07/14] ci: remove autorelease, improve stalebot (#5166) * ci: use stable version of auto-release * ci: remove tagged-release * chore: use a stale label instead of wontfix --- .github/stale.yml | 5 ++++- .github/workflows/tagged-release.yml | 24 ------------------------ 2 files changed, 4 insertions(+), 25 deletions(-) delete mode 100644 .github/workflows/tagged-release.yml diff --git a/.github/stale.yml b/.github/stale.yml index 9abd4e0d252..6ad93d1570f 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -4,10 +4,13 @@ daysUntilStale: 60 daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: + - Hacktoberfest - bug - enhancement + - RFC + - ⭐ EU-FOSSA Hackathon # Label to use when marking an issue as stale -staleLabel: wontfix +staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml deleted file mode 100644 index 06b836b6de5..00000000000 --- a/.github/workflows/tagged-release.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: "tagged-release" - -on: - push: - tags: - - "v*" - -jobs: - gh_tagged_release: - runs-on: "ubuntu-latest" - - steps: - - name: "Checkout source code" - uses: "actions/checkout@v2.3.4" - with: - lfs: true - fetch-depth: 0 - - - uses: "marvinpinto/action-automatic-releases@latest" - with: - repo_token: "${{ secrets.GITHUB_TOKEN }}" - prerelease: false - id: "automatic_releases" From f1ecc30a38e50536a2a65ae85ef23eb6dc095af3 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Mon, 7 Nov 2022 17:40:25 +0100 Subject: [PATCH 08/14] feat(openapi): add backed enum support (#5120) --- features/openapi/docs.feature | 16 ++++ phpstan.neon.dist | 2 + .../Odm/PropertyInfo/DoctrineExtractor.php | 17 +++- src/JsonSchema/SchemaFactory.php | 3 + src/JsonSchema/TypeFactory.php | 16 +++- tests/Behat/OpenApiContext.php | 91 +++++-------------- .../PropertyInfo/DoctrineExtractorTest.php | 10 ++ .../PropertyInfo/Fixtures/DoctrineEnum.php | 37 ++++++++ .../Odm/PropertyInfo/Fixtures/EnumInt.php | 20 ++++ .../Odm/PropertyInfo/Fixtures/EnumString.php | 20 ++++ tests/Fixtures/TestBundle/Document/Person.php | 13 ++- tests/Fixtures/TestBundle/Entity/Person.php | 13 ++- .../TestBundle/Enum/GenderTypeEnum.php | 20 ++++ tests/JsonSchema/SchemaFactoryTest.php | 18 +++- tests/JsonSchema/TypeFactoryTest.php | 7 ++ 15 files changed, 221 insertions(+), 82 deletions(-) create mode 100644 tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineEnum.php create mode 100644 tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumInt.php create mode 100644 tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumString.php create mode 100644 tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature index 2878240e3b5..2cbf3b33c45 100644 --- a/features/openapi/docs.feature +++ b/features/openapi/docs.feature @@ -36,6 +36,7 @@ Feature: Documentation support And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_put" exists And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_read" exists And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_write" exists + And the OpenAPI class "Person" exists And the OpenAPI class "RelatedDummy" exists And the OpenAPI class "NoCollectionDummy" exists And the OpenAPI class "RelatedToDummyFriend" exists @@ -57,6 +58,21 @@ Feature: Documentation support # Properties And the "id" property exists for the OpenAPI class "Dummy" And the "name" property is required for the OpenAPI class "Dummy" + And the "genderType" property exists for the OpenAPI class "Person" + And the "genderType" property for the OpenAPI class "Person" should be equal to: + """ + { + "default": "male", + "example": "male", + "type": "string", + "enum": [ + "male", + "female", + null + ], + "nullable": true + } + """ # Enable these tests when SF 4.4 / PHP 7.1 support is dropped #And the "isDummyBoolean" property exists for the OpenAPI class "DummyBoolean" #And the "isDummyBoolean" property is not read only for the OpenAPI class "DummyBoolean" diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 12a2bdc85c9..b671cb89893 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -83,3 +83,5 @@ parameters: - message: '#^Property .+ is unused.$#' path: tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineDummy.php + # Waiting for https://github.com/laminas/laminas-code/pull/150 + - '#Call to an undefined method ReflectionEnum::.+#' diff --git a/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php b/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php index 9969c8606fd..93ce4a4d5d0 100644 --- a/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php +++ b/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php @@ -92,6 +92,10 @@ public function getTypes($class, $property, array $context = []): ?array if ($metadata->hasField($property)) { $typeOfField = $metadata->getTypeOfField($property); $nullable = $metadata instanceof MongoDbClassMetadata && $metadata->isNullable($property); + $enumType = null; + if (null !== $enumClass = $metadata instanceof MongoDbClassMetadata ? $metadata->getFieldMapping($property)['enumType'] ?? null : null) { + $enumType = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $enumClass); + } switch ($typeOfField) { case MongoDbType::DATE: @@ -102,11 +106,16 @@ public function getTypes($class, $property, array $context = []): ?array return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; case MongoDbType::COLLECTION: return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT))]; - default: - $builtinType = $this->getPhpType($typeOfField); - - return $builtinType ? [new Type($builtinType, $nullable)] : null; + case MongoDbType::INT: + case MongoDbType::STRING: + if ($enumType) { + return [$enumType]; + } } + + $builtinType = $this->getPhpType($typeOfField); + + return $builtinType ? [new Type($builtinType, $nullable)] : null; } return null; diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 1cdd167e739..aee436fb591 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -175,6 +175,9 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str } if (!isset($propertySchema['default']) && !empty($default = $propertyMetadata->getDefault())) { + if ($default instanceof \BackedEnum) { + $default = $default->value; + } $propertySchema['default'] = $default; } diff --git a/src/JsonSchema/TypeFactory.php b/src/JsonSchema/TypeFactory.php index a96c5549d8e..e67d510b3ce 100644 --- a/src/JsonSchema/TypeFactory.php +++ b/src/JsonSchema/TypeFactory.php @@ -72,7 +72,7 @@ private function makeBasicType(Type $type, string $format = 'json', ?bool $reada Type::BUILTIN_TYPE_INT => ['type' => 'integer'], Type::BUILTIN_TYPE_FLOAT => ['type' => 'number'], Type::BUILTIN_TYPE_BOOL => ['type' => 'boolean'], - Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $format, $readableLink, $serializerContext, $schema), + Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $type->isNullable(), $format, $readableLink, $serializerContext, $schema), default => ['type' => 'string'], }; } @@ -80,7 +80,7 @@ private function makeBasicType(Type $type, string $format = 'json', ?bool $reada /** * Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided. */ - private function getClassType(?string $className, string $format, ?bool $readableLink, ?array $serializerContext, ?Schema $schema): array + private function getClassType(?string $className, bool $nullable, string $format, ?bool $readableLink, ?array $serializerContext, ?Schema $schema): array { if (null === $className) { return ['type' => 'string']; @@ -116,6 +116,18 @@ private function getClassType(?string $className, string $format, ?bool $readabl 'format' => 'binary', ]; } + if (is_a($className, \BackedEnum::class, true)) { + $rEnum = new \ReflectionEnum($className); + $enumCases = array_map(static fn (\ReflectionEnumBackedCase $rCase) => $rCase->getBackingValue(), $rEnum->getCases()); + if ($nullable) { + $enumCases[] = null; + } + + return [ + 'type' => (string) $rEnum->getBackingType(), + 'enum' => $enumCases, + ]; + } // Skip if $schema is null (filters only support basic types) if (null === $schema) { diff --git a/tests/Behat/OpenApiContext.php b/tests/Behat/OpenApiContext.php index 6865763e9be..c651149e3ff 100644 --- a/tests/Behat/OpenApiContext.php +++ b/tests/Behat/OpenApiContext.php @@ -16,7 +16,9 @@ use Behat\Behat\Context\Context; use Behat\Behat\Context\Environment\InitializedContextEnvironment; use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\PyStringNode; use Behatch\Context\RestContext; +use Behatch\Json\Json; use PHPUnit\Framework\Assert; use PHPUnit\Framework\ExpectationFailedException; @@ -42,51 +44,25 @@ public function gatherContexts(BeforeScenarioScope $scope): void $this->restContext = $restContext; } - /** - * @Then the Swagger class :class exists - */ - public function assertTheSwaggerClassExist(string $className): void - { - try { - $this->getClassInfo($className); - } catch (\InvalidArgumentException $e) { - throw new ExpectationFailedException(sprintf('The class "%s" doesn\'t exist.', $className), null, $e); - } - } - /** * @Then the OpenAPI class :class exists */ public function assertTheOpenApiClassExist(string $className): void { try { - $this->getClassInfo($className, 3); + $this->getClassInfo($className); } catch (\InvalidArgumentException $e) { throw new ExpectationFailedException(sprintf('The class "%s" doesn\'t exist.', $className), null, $e); } } - /** - * @Then the Swagger class :class doesn't exist - */ - public function assertTheSwaggerClassNotExist(string $className): void - { - try { - $this->getClassInfo($className); - } catch (\InvalidArgumentException) { - return; - } - - throw new ExpectationFailedException(sprintf('The class "%s" exists.', $className)); - } - /** * @Then the OpenAPI class :class doesn't exist */ public function assertTheOpenAPIClassNotExist(string $className): void { try { - $this->getClassInfo($className, 3); + $this->getClassInfo($className); } catch (\InvalidArgumentException) { return; } @@ -95,7 +71,6 @@ public function assertTheOpenAPIClassNotExist(string $className): void } /** - * @Then the Swagger path :arg1 exists * @Then the OpenAPI path :arg1 exists */ public function assertThePathExist(string $path): void @@ -105,54 +80,32 @@ public function assertThePathExist(string $path): void Assert::assertTrue(isset($json->paths) && isset($json->paths->{$path})); } - /** - * @Then the :prop property exists for the Swagger class :class - */ - public function assertThePropertyExistForTheSwaggerClass(string $propertyName, string $className): void - { - try { - $this->getPropertyInfo($propertyName, $className); - } catch (\InvalidArgumentException $e) { - throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" doesn\'t exist.', $propertyName, $className), null, $e); - } - } - /** * @Then the :prop property exists for the OpenAPI class :class */ public function assertThePropertyExistForTheOpenApiClass(string $propertyName, string $className): void { try { - $this->getPropertyInfo($propertyName, $className, 3); + $this->getPropertyInfo($propertyName, $className); } catch (\InvalidArgumentException $e) { throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" doesn\'t exist.', $propertyName, $className), null, $e); } } - /** - * @Then the :prop property is required for the Swagger class :class - */ - public function assertThePropertyIsRequiredForTheSwaggerClass(string $propertyName, string $className): void - { - if (!\in_array($propertyName, $this->getClassInfo($className)->required, true)) { - throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should be required', $propertyName, $className)); - } - } - /** * @Then the :prop property is required for the OpenAPI class :class */ public function assertThePropertyIsRequiredForTheOpenAPIClass(string $propertyName, string $className): void { - if (!\in_array($propertyName, $this->getClassInfo($className, 3)->required, true)) { + if (!\in_array($propertyName, $this->getClassInfo($className)->required, true)) { throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should be required', $propertyName, $className)); } } /** - * @Then the :prop property is not read only for the Swagger class :class + * @Then the :prop property is not read only for the OpenAPI class :class */ - public function assertThePropertyIsNotReadOnlyForTheSwaggerClass(string $propertyName, string $className): void + public function assertThePropertyIsNotReadOnlyForTheOpenAPIClass(string $propertyName, string $className): void { $propertyInfo = $this->getPropertyInfo($propertyName, $className); if (property_exists($propertyInfo, 'readOnly') && $propertyInfo->readOnly) { @@ -161,13 +114,15 @@ public function assertThePropertyIsNotReadOnlyForTheSwaggerClass(string $propert } /** - * @Then the :prop property is not read only for the OpenAPI class :class + * @Then the :prop property for the OpenAPI class :class should be equal to: */ - public function assertThePropertyIsNotReadOnlyForTheOpenAPIClass(string $propertyName, string $className): void + public function assertThePropertyForTheOpenAPIClassShouldBeEqualTo(string $propertyName, string $className, PyStringNode $propertyContent): void { - $propertyInfo = $this->getPropertyInfo($propertyName, $className, 3); - if (property_exists($propertyInfo, 'readOnly') && $propertyInfo->readOnly) { - throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should not be read only', $propertyName, $className)); + $propertyInfo = $this->getPropertyInfo($propertyName, $className); + $propertyInfoJson = new Json(json_encode($propertyInfo)); + + if (new Json($propertyContent) != $propertyInfoJson) { + throw new ExpectationFailedException(sprintf("Property \"%s\" of class \"%s\" is '%s'", $propertyName, $className, $propertyInfoJson)); } } @@ -176,12 +131,10 @@ public function assertThePropertyIsNotReadOnlyForTheOpenAPIClass(string $propert * * @throws \InvalidArgumentException */ - private function getPropertyInfo(string $propertyName, string $className, int $specVersion = 2): \stdClass + private function getPropertyInfo(string $propertyName, string $className): \stdClass { - /** - * @var iterable $properties - */ - $properties = $this->getProperties($className, $specVersion); + /** @var iterable $properties */ + $properties = $this->getProperties($className); foreach ($properties as $classPropertyName => $property) { if ($classPropertyName === $propertyName) { return $property; @@ -194,9 +147,9 @@ private function getPropertyInfo(string $propertyName, string $className, int $s /** * Gets all operations of a given class. */ - private function getProperties(string $className, int $specVersion = 2): \stdClass + private function getProperties(string $className): \stdClass { - return $this->getClassInfo($className, $specVersion)->{'properties'} ?? new \stdClass(); + return $this->getClassInfo($className)->{'properties'} ?? new \stdClass(); } /** @@ -204,9 +157,9 @@ private function getProperties(string $className, int $specVersion = 2): \stdCla * * @throws \InvalidArgumentException */ - private function getClassInfo(string $className, int $specVersion = 2): \stdClass + private function getClassInfo(string $className): \stdClass { - $nodes = 2 === $specVersion ? $this->getLastJsonResponse()->{'definitions'} : $this->getLastJsonResponse()->{'components'}->{'schemas'}; + $nodes = $this->getLastJsonResponse()->{'components'}->{'schemas'}; foreach ($nodes as $classTitle => $classData) { if ($classTitle === $className) { return $classData; diff --git a/tests/Doctrine/Odm/PropertyInfo/DoctrineExtractorTest.php b/tests/Doctrine/Odm/PropertyInfo/DoctrineExtractorTest.php index 50c697632e2..84b10c33b8e 100644 --- a/tests/Doctrine/Odm/PropertyInfo/DoctrineExtractorTest.php +++ b/tests/Doctrine/Odm/PropertyInfo/DoctrineExtractorTest.php @@ -17,10 +17,13 @@ use ApiPlatform\Test\DoctrineMongoDbOdmSetup; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineDummy; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineEmbeddable; +use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineEnum; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineFooType; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineGeneratedValue; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineRelation; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineWithEmbedded; +use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\EnumInt; +use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\EnumString; use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Types\Type as MongoDbType; @@ -128,6 +131,13 @@ public function testExtractWithEmbedMany(): void $this->assertEquals($expectedTypes, $actualTypes); } + public function testExtractEnum(): void + { + $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString')); + $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumInt::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumInt')); + $this->assertNull($this->createExtractor()->getTypes(DoctrineEnum::class, 'enumCustom')); + } + public function typesProvider(): array { return [ diff --git a/tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineEnum.php b/tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineEnum.php new file mode 100644 index 00000000000..57efa0ec47e --- /dev/null +++ b/tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineEnum.php @@ -0,0 +1,37 @@ + + * + * 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\Doctrine\Odm\PropertyInfo\Fixtures; + +use Doctrine\ODM\MongoDB\Mapping\Annotations\Document; +use Doctrine\ODM\MongoDB\Mapping\Annotations\Field; +use Doctrine\ODM\MongoDB\Mapping\Annotations\Id; + +/** + * @author Alan Poulain + */ +#[Document] +class DoctrineEnum +{ + #[Id] + public int $id; + + #[Field(enumType: EnumString::class)] + protected EnumString $enumString; + + #[Field(type: 'int', enumType: EnumInt::class)] + protected EnumInt $enumInt; + + #[Field(type: 'custom_foo', enumType: EnumInt::class)] + protected EnumInt $enumCustom; +} diff --git a/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumInt.php b/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumInt.php new file mode 100644 index 00000000000..0fc31cff2a5 --- /dev/null +++ b/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumInt.php @@ -0,0 +1,20 @@ + + * + * 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\Doctrine\Odm\PropertyInfo\Fixtures; + +enum EnumInt: int +{ + case Foo = 0; + case Bar = 1; +} diff --git a/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumString.php b/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumString.php new file mode 100644 index 00000000000..f96c6e29bd3 --- /dev/null +++ b/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumString.php @@ -0,0 +1,20 @@ + + * + * 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\Doctrine\Odm\PropertyInfo\Fixtures; + +enum EnumString: string +{ + case Foo = 'f'; + case Bar = 'b'; +} diff --git a/tests/Fixtures/TestBundle/Document/Person.php b/tests/Fixtures/TestBundle/Document/Person.php index 0e4f098366a..f7d11a309aa 100644 --- a/tests/Fixtures/TestBundle/Document/Person.php +++ b/tests/Fixtures/TestBundle/Document/Person.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; @@ -29,13 +30,19 @@ class Person { #[ODM\Id(strategy: 'INCREMENT', type: 'int')] - private $id; + private ?int $id = null; + #[Groups(['people.pets'])] #[ODM\Field(type: 'string')] - public $name; + public string $name; + + #[ODM\Field(type: 'string', enumType: GenderTypeEnum::class, nullable: true)] + public ?GenderTypeEnum $genderType = GenderTypeEnum::MALE; + #[Groups(['people.pets'])] #[ODM\ReferenceMany(targetDocument: PersonToPet::class, mappedBy: 'person')] public Collection|iterable $pets; + #[ODM\ReferenceMany(targetDocument: Greeting::class, mappedBy: 'sender')] public Collection|iterable|null $sentGreetings = null; @@ -44,7 +51,7 @@ public function __construct() $this->pets = new ArrayCollection(); } - public function getId() + public function getId(): ?int { return $this->id; } diff --git a/tests/Fixtures/TestBundle/Entity/Person.php b/tests/Fixtures/TestBundle/Entity/Person.php index 59b452b8103..775969b2786 100644 --- a/tests/Fixtures/TestBundle/Entity/Person.php +++ b/tests/Fixtures/TestBundle/Entity/Person.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -31,13 +32,19 @@ class Person #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue(strategy: 'AUTO')] - private $id; + private ?int $id = null; + + #[ORM\Column(type: 'string', enumType: GenderTypeEnum::class, nullable: true)] + public ?GenderTypeEnum $genderType = GenderTypeEnum::MALE; + #[ORM\Column(type: 'string')] #[Groups(['people.pets'])] - public $name; + public string $name; + #[ORM\OneToMany(targetEntity: PersonToPet::class, mappedBy: 'person')] #[Groups(['people.pets'])] public Collection|iterable $pets; + #[ORM\OneToMany(targetEntity: Greeting::class, mappedBy: 'sender')] public Collection|iterable|null $sentGreetings = null; @@ -46,7 +53,7 @@ public function __construct() $this->pets = new ArrayCollection(); } - public function getId() + public function getId(): ?int { return $this->id; } diff --git a/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php b/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php new file mode 100644 index 00000000000..12327a2e6df --- /dev/null +++ b/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php @@ -0,0 +1,20 @@ + + * + * 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\Enum; + +enum GenderTypeEnum: string +{ + case MALE = 'male'; + case FEMALE = 'female'; +} diff --git a/tests/JsonSchema/SchemaFactoryTest.php b/tests/JsonSchema/SchemaFactoryTest.php index a7f75ff6ece..062a16a8566 100644 --- a/tests/JsonSchema/SchemaFactoryTest.php +++ b/tests/JsonSchema/SchemaFactoryTest.php @@ -28,6 +28,7 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Tests\Fixtures\NotAResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\OverriddenOperationDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -53,15 +54,22 @@ public function testBuildSchemaForNonResourceClass(): void ), Argument::cetera())->willReturn([ 'type' => 'integer', ]); + $typeFactoryProphecy->getType(Argument::allOf( + Argument::type(Type::class), + Argument::which('getBuiltinType', Type::BUILTIN_TYPE_OBJECT) + ), Argument::cetera())->willReturn([ + 'type' => 'object', + ]); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(NotAResource::class, Argument::cetera())->willReturn(new PropertyNameCollection(['foo', 'bar'])); + $propertyNameCollectionFactoryProphecy->create(NotAResource::class, Argument::cetera())->willReturn(new PropertyNameCollection(['foo', 'bar', 'genderType'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(NotAResource::class, 'foo', Argument::cetera())->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)); $propertyMetadataFactoryProphecy->create(NotAResource::class, 'bar', Argument::cetera())->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withReadable(true)->withDefault('default_bar')->withExample('example_bar')); + $propertyMetadataFactoryProphecy->create(NotAResource::class, 'genderType', Argument::cetera())->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT)])->withReadable(true)->withDefault(GenderTypeEnum::MALE)); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false); @@ -91,6 +99,14 @@ public function testBuildSchemaForNonResourceClass(): void $this->assertSame('integer', $definitions[$rootDefinitionKey]['properties']['bar']['type']); $this->assertSame('default_bar', $definitions[$rootDefinitionKey]['properties']['bar']['default']); $this->assertSame('example_bar', $definitions[$rootDefinitionKey]['properties']['bar']['example']); + + $this->assertArrayHasKey('genderType', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayHasKey('default', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayHasKey('example', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertSame('object', $definitions[$rootDefinitionKey]['properties']['genderType']['type']); + $this->assertSame('male', $definitions[$rootDefinitionKey]['properties']['genderType']['default']); + $this->assertSame('male', $definitions[$rootDefinitionKey]['properties']['genderType']['example']); } public function testBuildSchemaWithSerializerGroups(): void diff --git a/tests/JsonSchema/TypeFactoryTest.php b/tests/JsonSchema/TypeFactoryTest.php index 0b29bce5e10..418139459ef 100644 --- a/tests/JsonSchema/TypeFactoryTest.php +++ b/tests/JsonSchema/TypeFactoryTest.php @@ -17,6 +17,7 @@ use ApiPlatform\JsonSchema\SchemaFactoryInterface; use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -53,6 +54,8 @@ public function typeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['nullable' => true, 'type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; + yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield [['type' => 'string', 'enum' => ['male', 'female', null], 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable' => [ ['nullable' => true, 'type' => 'array', 'items' => ['type' => 'string']], @@ -174,6 +177,8 @@ public function jsonSchemaTypeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['type' => ['string', 'null'], 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; + yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield [['type' => ['string', 'null'], 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable' => [ ['type' => ['array', 'null'], 'items' => ['type' => 'string']], @@ -288,6 +293,8 @@ public function openAPIV2TypeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; + yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield [['type' => 'string', 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable, but ignored in OpenAPI V2' => [ ['type' => 'array', 'items' => ['type' => 'string']], From 36d930edad8cd733a977d0327b850fd41df6fee6 Mon Sep 17 00:00:00 2001 From: ArnoudThibaut Date: Thu, 10 Nov 2022 19:26:06 +0100 Subject: [PATCH 09/14] feat(graphql): enable profiler panel when using graphql (#5072) --- .../Factory/CollectionResolverFactory.php | 10 +- .../Bundle/DataCollector/DataCollected.php | 43 ++++++ .../DataCollector/RequestDataCollector.php | 101 +++++++------ .../ApiPlatformExtension.php | 30 ++++ .../Bundle/Resources/config/graphql.xml | 1 - .../views/DataCollector/request.html.twig | 143 ++++++++++-------- .../Factory/DataCollectorResolverFactory.php | 40 +++++ .../Factory/CollectionResolverFactoryTest.php | 20 --- .../RequestDataCollectorTest.php | 23 ++- .../ApiPlatformExtensionTest.php | 5 + .../DataCollectorResolverFactoryTest.php | 52 +++++++ 11 files changed, 313 insertions(+), 155 deletions(-) create mode 100644 src/Symfony/Bundle/DataCollector/DataCollected.php create mode 100644 src/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactory.php create mode 100644 tests/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactoryTest.php diff --git a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php index ce34306b619..931814fe33c 100644 --- a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php @@ -22,7 +22,6 @@ use ApiPlatform\Util\CloneTrait; use GraphQL\Type\Definition\ResolveInfo; use Psr\Container\ContainerInterface; -use Symfony\Component\HttpFoundation\RequestStack; /** * Creates a function retrieving a collection to resolve a GraphQL query or a field returned by a mutation. @@ -35,7 +34,7 @@ final class CollectionResolverFactory implements ResolverFactoryInterface { use CloneTrait; - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, private readonly SerializeStageInterface $serializeStage, private readonly ContainerInterface $queryResolverLocator, private readonly ?RequestStack $requestStack = null) + public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, private readonly SerializeStageInterface $serializeStage, private readonly ContainerInterface $queryResolverLocator) { } @@ -47,13 +46,6 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul return null; } - if ($this->requestStack && null !== $request = $this->requestStack->getCurrentRequest()) { - $request->attributes->set( - '_graphql_collections_args', - [$resourceClass => $args] + $request->attributes->get('_graphql_collections_args', []) - ); - } - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; $collection = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); diff --git a/src/Symfony/Bundle/DataCollector/DataCollected.php b/src/Symfony/Bundle/DataCollector/DataCollected.php new file mode 100644 index 00000000000..33732226bc9 --- /dev/null +++ b/src/Symfony/Bundle/DataCollector/DataCollected.php @@ -0,0 +1,43 @@ + + * + * 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\Bundle\DataCollector; + +use Symfony\Component\VarDumper\Cloner\Data; + +final class DataCollected +{ + public function __construct(private readonly string $resourceClass, private readonly Data $resourceMetadataCollection, private readonly array $filters, private readonly array $counters) + { + } + + public function getResourceClass(): string + { + return $this->resourceClass; + } + + public function getResourceMetadataCollection(): Data + { + return $this->resourceMetadataCollection; + } + + public function getFilters(): array + { + return $this->filters; + } + + public function getCounters(): array + { + return $this->counters; + } +} diff --git a/src/Symfony/Bundle/DataCollector/RequestDataCollector.php b/src/Symfony/Bundle/DataCollector/RequestDataCollector.php index ba48baec1d2..38386ddb2a1 100644 --- a/src/Symfony/Bundle/DataCollector/RequestDataCollector.php +++ b/src/Symfony/Bundle/DataCollector/RequestDataCollector.php @@ -21,6 +21,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\VarDumper\Cloner\Data; /** * @author Julien DENIAU @@ -37,20 +38,10 @@ public function __construct(private readonly ResourceMetadataCollectionFactoryIn */ public function collect(Request $request, Response $response, \Throwable $exception = null): void { - $resourceClass = $request->attributes->get('_api_resource_class'); - $resourceMetadataCollection = $resourceClass ? $this->metadataFactory->create($resourceClass) : []; - - $filters = []; - $counters = ['ignored_filters' => 0]; - $resourceMetadataCollectionData = []; - - /** @var ApiResource $resourceMetadata */ - foreach ($resourceMetadataCollection as $index => $resourceMetadata) { - $this->setFilters($resourceMetadata, $index, $filters, $counters); - $resourceMetadataCollectionData[] = [ - 'resource' => $resourceMetadata, - 'operations' => null !== $resourceMetadata->getOperations() ? iterator_to_array($resourceMetadata->getOperations()) : [], - ]; + if ($request->attributes->get('_graphql', false)) { + $resourceClasses = array_keys($request->attributes->get('_graphql_args', [])); + } else { + $resourceClasses = array_filter([$request->attributes->get('_api_resource_class')]); } $requestAttributes = RequestAttributesExtractor::extractAttributes($request); @@ -58,14 +49,9 @@ public function collect(Request $request, Response $response, \Throwable $except $requestAttributes['previous_data'] = $this->cloneVar($requestAttributes['previous_data']); } - $this->data = [ - 'resource_class' => $resourceClass, - 'resource_metadata_collection' => $this->cloneVar($resourceMetadataCollectionData), - 'acceptable_content_types' => $request->getAcceptableContentTypes(), - 'filters' => $filters, - 'counters' => $counters, - 'request_attributes' => $requestAttributes, - ]; + $this->data['request_attributes'] = $requestAttributes; + $this->data['acceptable_content_types'] = $request->getAcceptableContentTypes(); + $this->data['resources'] = array_map(fn (string $resourceClass): DataCollected => $this->collectDataByResource($resourceClass, $request), $resourceClasses); } private function setFilters(ApiResource $resourceMetadata, int $index, array &$filters, array &$counters): void @@ -81,58 +67,75 @@ private function setFilters(ApiResource $resourceMetadata, int $index, array &$f } } - public function getAcceptableContentTypes(): array + public function getVersion(): ?string { - return $this->data['acceptable_content_types'] ?? []; - } + if (!class_exists(Versions::class)) { + return null; + } - public function getResourceClass() - { - return $this->data['resource_class'] ?? null; + $version = Versions::getVersion('api-platform/core'); + preg_match('/^v(.*?)@/', (string) $version, $output); + + return $output[1] ?? strtok($version, '@'); } - public function getResourceMetadataCollection() + /** + * {@inheritdoc} + */ + public function getName(): string { - return $this->data['resource_metadata_collection'] ?? null; + return 'api_platform.data_collector.request'; } - public function getRequestAttributes(): array + public function getData(): array|Data { - return $this->data['request_attributes'] ?? []; + return $this->data; } - public function getFilters(): array + public function getAcceptableContentTypes(): array { - return $this->data['filters'] ?? []; + return $this->data['acceptable_content_types'] ?? []; } - public function getCounters(): array + public function getRequestAttributes(): array { - return $this->data['counters'] ?? []; + return $this->data['request_attributes'] ?? []; } - public function getVersion(): ?string + public function getResources(): array { - if (!class_exists(Versions::class)) { - return null; - } - - $version = Versions::getVersion('api-platform/core'); - preg_match('/^v(.*?)@/', (string) $version, $output); - - return $output[1] ?? strtok($version, '@'); + return $this->data['resources'] ?? []; } /** * {@inheritdoc} */ - public function getName(): string + public function reset(): void { - return 'api_platform.data_collector.request'; + $this->data = []; } - public function reset(): void + private function collectDataByResource(string $resourceClass, Request $request): DataCollected { - $this->data = []; + $resourceMetadataCollection = $resourceClass ? $this->metadataFactory->create($resourceClass) : []; + $filters = []; + $counters = ['ignored_filters' => 0]; + $resourceMetadataCollectionData = []; + + /** @var ApiResource $resourceMetadata */ + foreach ($resourceMetadataCollection as $index => $resourceMetadata) { + $this->setFilters($resourceMetadata, $index, $filters, $counters); + $resourceMetadataCollectionData[] = [ + 'resource' => $resourceMetadata, + 'operations' => null !== $resourceMetadata->getOperations() ? iterator_to_array($resourceMetadata->getOperations()) : [], + ]; + } + + return new DataCollected( + $resourceClass, + $this->cloneVar($resourceMetadataCollectionData), + $filters, + $counters + ); } } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 3b1c8faeb5a..75bcb846867 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -32,6 +32,7 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Symfony\GraphQl\Resolver\Factory\DataCollectorResolverFactory; use ApiPlatform\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRestrictionMetadataInterface; use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface; use Doctrine\Persistence\ManagerRegistry; @@ -41,6 +42,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -499,6 +501,34 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array ->addTag('api_platform.graphql.type'); $container->registerForAutoconfiguration(ErrorHandlerInterface::class) ->addTag('api_platform.graphql.error_handler'); + + if (!$container->getParameter('kernel.debug')) { + return; + } + + $requestStack = new Reference('request_stack', ContainerInterface::NULL_ON_INVALID_REFERENCE); + $collectionDataCollectorResolverFactory = (new Definition(DataCollectorResolverFactory::class)) + ->setDecoratedService('api_platform.graphql.resolver.factory.collection') + ->setArguments([new Reference('api_platform.graphql.data_collector.resolver.factory.collection.inner'), $requestStack]); + + $itemDataCollectorResolverFactory = (new Definition(DataCollectorResolverFactory::class)) + ->setDecoratedService('api_platform.graphql.resolver.factory.item') + ->setArguments([new Reference('api_platform.graphql.data_collector.resolver.factory.item.inner'), $requestStack]); + + $itemMutationDataCollectorResolverFactory = (new Definition(DataCollectorResolverFactory::class)) + ->setDecoratedService('api_platform.graphql.resolver.factory.item_mutation') + ->setArguments([new Reference('api_platform.graphql.data_collector.resolver.factory.item_mutation.inner'), $requestStack]); + + $itemSubscriptionDataCollectorResolverFactory = (new Definition(DataCollectorResolverFactory::class)) + ->setDecoratedService('api_platform.graphql.resolver.factory.item_subscription') + ->setArguments([new Reference('api_platform.graphql.data_collector.resolver.factory.item_subscription.inner'), $requestStack]); + + $container->addDefinitions([ + 'api_platform.graphql.data_collector.resolver.factory.collection' => $collectionDataCollectorResolverFactory, + 'api_platform.graphql.data_collector.resolver.factory.item' => $itemDataCollectorResolverFactory, + 'api_platform.graphql.data_collector.resolver.factory.item_mutation' => $itemMutationDataCollectorResolverFactory, + 'api_platform.graphql.data_collector.resolver.factory.item_subscription' => $itemSubscriptionDataCollectorResolverFactory, + ]); } private function registerCacheConfiguration(ContainerBuilder $container): void diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index 0f713b273b9..b22c836e00e 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -163,7 +163,6 @@ - diff --git a/src/Symfony/Bundle/Resources/views/DataCollector/request.html.twig b/src/Symfony/Bundle/Resources/views/DataCollector/request.html.twig index 2aecfd097d1..dedfca3e5fc 100644 --- a/src/Symfony/Bundle/Resources/views/DataCollector/request.html.twig +++ b/src/Symfony/Bundle/Resources/views/DataCollector/request.html.twig @@ -89,16 +89,24 @@ {{ collector.version }} {% endif %} -
- Resource Class - {{ collector.resourceClass|default('Not an API Platform resource') }} -
- {% if collector.counters.ignored_filters|default(false) %} + {% if collector.resources|length == 0 %}
- Ignored Filters - {{ collector.counters.ignored_filters }} + Resource Class + Not an API Platform resource
{% endif %} + {% for resource in collector.resources %} +
+ Resource Class + {{ resource.resourceClass }} +
+ {% if resource.counters.ignored_filters|default(false) %} +
+ Ignored Filters + {{ collector.counters.ignored_filters }} +
+ {% endif %} + {% endfor %} {% endset %} {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': true, status: status_color }) }} @@ -106,7 +114,7 @@ {% block menu %} {# This left-hand menu appears when using the full-screen profiler. #} - + {{ include('@ApiPlatform/DataCollector/api-platform.svg') }} @@ -115,77 +123,86 @@ {% endblock %} {% block panel %} -
-
- {{ collector.resourceClass|default('Not an API Platform resource') }} - Resource class + {% if collector.resources|length == 0 %} +
+
+ Not an API Platform resource + Resource Class +
-
- - {% if collector.resourceMetadataCollection is not empty %} -
-
- -

Resources

-
- - {% endif %} + {% endif %} + {% endfor %} {% endblock %} diff --git a/src/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactory.php b/src/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactory.php new file mode 100644 index 00000000000..9091ab3dc9e --- /dev/null +++ b/src/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactory.php @@ -0,0 +1,40 @@ + + * + * 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\GraphQl\Resolver\Factory; + +use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; +use ApiPlatform\Metadata\GraphQl\Operation; +use GraphQL\Type\Definition\ResolveInfo; +use Symfony\Component\HttpFoundation\RequestStack; + +final class DataCollectorResolverFactory implements ResolverFactoryInterface +{ + public function __construct(private readonly ResolverFactoryInterface $resolverFactory, private readonly ?RequestStack $requestStack) + { + } + + public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable + { + return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) { + if ($this->requestStack && null !== $request = $this->requestStack->getCurrentRequest()) { + $request->attributes->set( + '_graphql_args', + [$resourceClass => $args] + $request->attributes->get('_graphql_args', []) + ); + } + + return ($this->resolverFactory)($resourceClass, $rootClass, $operation)($source, $args, $context, $info); + }; + } +} diff --git a/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php b/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php index f6854c74167..33f5cbb1c4e 100644 --- a/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php +++ b/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php @@ -24,9 +24,6 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Container\ContainerInterface; -use Symfony\Component\HttpFoundation\ParameterBag; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; /** * @author Alan Poulain @@ -42,7 +39,6 @@ class CollectionResolverFactoryTest extends TestCase private ObjectProphecy $securityPostDenormalizeStageProphecy; private ObjectProphecy $serializeStageProphecy; private ObjectProphecy $queryResolverLocatorProphecy; - private ObjectProphecy $requestStackProphecy; /** * {@inheritdoc} @@ -54,7 +50,6 @@ protected function setUp(): void $this->securityPostDenormalizeStageProphecy = $this->prophesize(SecurityPostDenormalizeStageInterface::class); $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); $this->queryResolverLocatorProphecy = $this->prophesize(ContainerInterface::class); - $this->requestStackProphecy = $this->prophesize(RequestStack::class); $this->collectionResolverFactory = new CollectionResolverFactory( $this->readStageProphecy->reveal(), @@ -62,7 +57,6 @@ protected function setUp(): void $this->securityPostDenormalizeStageProphecy->reveal(), $this->serializeStageProphecy->reveal(), $this->queryResolverLocatorProphecy->reveal(), - $this->requestStackProphecy->reveal() ); } @@ -78,13 +72,6 @@ public function testResolve(): void $info->fieldName = 'testField'; $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - $request = new Request(); - $attributesParameterBagProphecy = $this->prophesize(ParameterBag::class); - $attributesParameterBagProphecy->get('_graphql_collections_args', [])->willReturn(['collection_args']); - $attributesParameterBagProphecy->set('_graphql_collections_args', [$resourceClass => $args, 'collection_args'])->shouldBeCalled(); - $request->attributes = $attributesParameterBagProphecy->reveal(); - $this->requestStackProphecy->getCurrentRequest()->willReturn($request); - $readStageCollection = [new \stdClass()]; $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); @@ -148,13 +135,6 @@ public function testResolveNullSource(): void $info = $this->prophesize(ResolveInfo::class)->reveal(); $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - $request = new Request(); - $attributesParameterBagProphecy = $this->prophesize(ParameterBag::class); - $attributesParameterBagProphecy->get('_graphql_collections_args', [])->willReturn(['collection_args']); - $attributesParameterBagProphecy->set('_graphql_collections_args', [$resourceClass => $args, 'collection_args'])->shouldBeCalled(); - $request->attributes = $attributesParameterBagProphecy->reveal(); - $this->requestStackProphecy->getCurrentRequest()->willReturn($request); - $readStageCollection = [new \stdClass()]; $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); diff --git a/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php b/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php index 9a0fb828f8f..c9af7f056eb 100644 --- a/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php +++ b/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php @@ -73,11 +73,8 @@ public function testNoResourceClass(): void ); $this->assertEquals([], $dataCollector->getRequestAttributes()); - $this->assertEquals([], $dataCollector->getFilters()); - $this->assertEquals(['ignored_filters' => 0], $dataCollector->getCounters()); $this->assertEquals(['foo', 'bar'], $dataCollector->getAcceptableContentTypes()); - $this->assertNull($dataCollector->getResourceClass()); - $this->assertEmpty($dataCollector->getResourceMetadataCollection()->getValue()); + $this->assertEquals([], $dataCollector->getResources()); } public function testNotCallingCollect(): void @@ -92,10 +89,7 @@ public function testNotCallingCollect(): void $this->assertEquals([], $dataCollector->getRequestAttributes()); $this->assertEquals([], $dataCollector->getAcceptableContentTypes()); - $this->assertEquals([], $dataCollector->getFilters()); - $this->assertEquals([], $dataCollector->getCounters()); - $this->assertNull($dataCollector->getResourceClass()); - $this->assertNull($dataCollector->getResourceMetadataCollection()); + $this->assertEquals([], $dataCollector->getResources()); } public function testWithResource(): void @@ -126,10 +120,12 @@ public function testWithResource(): void 'persist' => true, ], $dataCollector->getRequestAttributes()); $this->assertEquals(['foo', 'bar'], $dataCollector->getAcceptableContentTypes()); - $this->assertSame(DummyEntity::class, $dataCollector->getResourceClass()); - $this->assertEquals([['foo' => null, 'a_filter' => \stdClass::class]], $dataCollector->getFilters()); - $this->assertEquals(['ignored_filters' => 1], $dataCollector->getCounters()); - $this->assertInstanceOf(Data::class, $dataCollector->getResourceMetadataCollection()); + + $resource = $dataCollector->getResources()[0]; + $this->assertSame(DummyEntity::class, $resource->getResourceClass()); + $this->assertEquals([['foo' => null, 'a_filter' => \stdClass::class]], $resource->getFilters()); + $this->assertEquals(['ignored_filters' => 1], $resource->getCounters()); + $this->assertInstanceOf(Data::class, $resource->getResourceMetadataCollection()); } public function testWithResourceWithTraceables(): void @@ -199,7 +195,8 @@ public function testWithPreviousData(): void private function apiResourceClassWillReturn(?string $data, array $context = []): void { $this->attributes->get('_api_resource_class')->shouldBeCalled()->willReturn($data); - $this->attributes->all()->shouldBeCalled()->willReturn([ + $this->attributes->get('_graphql', false)->shouldBeCalled()->willReturn(false); + $this->attributes->all()->willReturn([ '_api_resource_class' => $data, ] + $context); $this->request->attributes = $this->attributes->reveal(); diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 5be3cc47aed..089c10fdd29 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -626,6 +626,7 @@ public function testGraphQlConfiguration(): void { $config = self::DEFAULT_CONFIG; $config['api_platform']['graphql']['enabled'] = true; + $this->container->setParameter('kernel.debug', true); (new ApiPlatformExtension())->load($config, $this->container); $services = [ @@ -673,6 +674,10 @@ public function testGraphQlConfiguration(): void 'api_platform.graphql.normalizer.runtime_exception', 'api_platform.graphql_metadata.resource.metadata_collection_factory', 'api_platform.graphql_metadata.resource.metadata_collection_factory.filters', + 'api_platform.graphql.data_collector.resolver.factory.collection', + 'api_platform.graphql.data_collector.resolver.factory.item', + 'api_platform.graphql.data_collector.resolver.factory.item_mutation', + 'api_platform.graphql.data_collector.resolver.factory.item_subscription', ]; $aliases = [ diff --git a/tests/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactoryTest.php b/tests/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactoryTest.php new file mode 100644 index 00000000000..433ab3fd9a1 --- /dev/null +++ b/tests/Symfony/GraphQl/Resolver/Factory/DataCollectorResolverFactoryTest.php @@ -0,0 +1,52 @@ + + * + * 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\Symfony\GraphQl\Resolver\Factory; + +use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; +use ApiPlatform\Symfony\GraphQl\Resolver\Factory\DataCollectorResolverFactory; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use GraphQL\Type\Definition\ResolveInfo; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; + +class DataCollectorResolverFactoryTest extends TestCase +{ + use ProphecyTrait; + + private ObjectProphecy $requestStack; + private ObjectProphecy $resolverFactory; + private DataCollectorResolverFactory $dataCollectorResolverFactory; + + protected function setUp(): void + { + $this->requestStack = $this->prophesize(RequestStack::class); + $this->resolverFactory = $this->prophesize(ResolverFactoryInterface::class); + $this->dataCollectorResolverFactory = new DataCollectorResolverFactory($this->resolverFactory->reveal(), $this->requestStack->reveal()); + } + + public function testDataCollectorAddDataInsideRequestAttribute(): void + { + $request = new Request(); + $this->requestStack->getCurrentRequest()->willReturn($request); + $this->resolverFactory->__invoke(Dummy::class, null, null)->willReturn(static fn (?array $source, array $args, $context, ResolveInfo $info): array => $args); + + $result = $this->dataCollectorResolverFactory->__invoke(Dummy::class)(null, ['bar'], [], $this->prophesize(ResolveInfo::class)->reveal()); + + $this->assertEquals(['bar'], $result); + $this->assertEquals([Dummy::class => ['bar']], $request->attributes->get('_graphql_args')); + } +} From a828af0e8eb9281d2ebf59692e5722bf08727861 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Tue, 29 Nov 2022 17:17:40 +0100 Subject: [PATCH 10/14] feat: use phpdoc-parser instead of phpdocumentor (#5214) PHPStan PHPDoc-Parser is used instead of phpDocumentor because the latter is not actively maintained anymore. --- composer.json | 5 +- ...hpDocResourceMetadataCollectionFactory.php | 135 ++++++++++++++---- .../ApiPlatformExtension.php | 3 +- .../ApiPlatformExtensionTest.php | 5 +- 4 files changed, 116 insertions(+), 32 deletions(-) diff --git a/composer.json b/composer.json index 0273a293007..b07e362e7de 100644 --- a/composer.json +++ b/composer.json @@ -45,10 +45,9 @@ "guzzlehttp/guzzle": "^6.0 || ^7.0", "jangregor/phpstan-prophecy": "^1.0", "justinrainbow/json-schema": "^5.2.1", - "phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.1", - "phpdocumentor/type-resolver": "^0.3 || ^0.4 || ^1.4", "phpspec/prophecy-phpunit": "^2.0", "phpstan/extension-installer": "^1.1", + "phpstan/phpdoc-parser": "^1.13", "phpstan/phpstan": "^1.1", "phpstan/phpstan-doctrine": "^1.0", "phpstan/phpstan-phpunit": "^1.0", @@ -101,7 +100,7 @@ "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.", "elasticsearch/elasticsearch": "To support Elasticsearch.", "ocramius/package-versions": "To display the API Platform's version in the debug bar.", - "phpdocumentor/reflection-docblock": "To support extracting metadata from PHPDoc.", + "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.", "psr/cache-implementation": "To use metadata caching.", "ramsey/uuid": "To support Ramsey's UUID identifiers.", "symfony/cache": "To have metadata caching when using Symfony integration.", diff --git a/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php index b94e6bffdf1..819e089e4e6 100644 --- a/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php @@ -18,6 +18,13 @@ use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactoryInterface; use phpDocumentor\Reflection\Types\ContextFactory; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; +use PHPStan\PhpDocParser\Lexer\Lexer; +use PHPStan\PhpDocParser\Parser\ConstExprParser; +use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\PhpDocParser\Parser\TokenIterator; +use PHPStan\PhpDocParser\Parser\TypeParser; /** * Extracts descriptions from PHPDoc. @@ -26,13 +33,33 @@ */ final class PhpDocResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - private readonly DocBlockFactoryInterface $docBlockFactory; - private readonly ContextFactory $contextFactory; + private readonly ?DocBlockFactoryInterface $docBlockFactory; + private readonly ?ContextFactory $contextFactory; + private readonly ?PhpDocParser $phpDocParser; + private readonly ?Lexer $lexer; - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated, DocBlockFactoryInterface $docBlockFactory = null) + /** @var array */ + private array $docBlocks = []; + + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated, ?DocBlockFactoryInterface $docBlockFactory = null) { - $this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance(); - $this->contextFactory = new ContextFactory(); + $contextFactory = null; + if ($docBlockFactory instanceof DocBlockFactoryInterface) { + trigger_deprecation('api-platform/core', '3.1', 'Using a 2nd argument to PhpDocResourceMetadataCollectionFactory is deprecated.'); + } + if (class_exists(DocBlockFactory::class) && class_exists(ContextFactory::class)) { + $docBlockFactory = $docBlockFactory ?? DocBlockFactory::createInstance(); + $contextFactory = new ContextFactory(); + } + $this->docBlockFactory = $docBlockFactory; + $this->contextFactory = $contextFactory; + if (class_exists(DocBlockFactory::class) && !class_exists(PhpDocParser::class)) { + trigger_deprecation('api-platform/core', '3.1', 'Using phpdocumentor/reflection-docblock is deprecated. Require phpstan/phpdoc-parser instead.'); + } + if (class_exists(PhpDocParser::class)) { + $this->phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); + $this->lexer = new Lexer(); + } } /** @@ -47,41 +74,97 @@ public function create(string $resourceClass): ResourceMetadataCollection continue; } - $reflectionClass = new \ReflectionClass($resourceClass); + $description = null; - try { - $docBlock = $this->docBlockFactory->create($reflectionClass, $this->contextFactory->createFromReflector($reflectionClass)); - $resourceMetadataCollection[$key] = $resourceMetadata->withDescription($docBlock->getSummary()); + // Deprecated path. To remove in API Platform 4. + if (!$this->phpDocParser instanceof PhpDocParser && $this->docBlockFactory instanceof DocBlockFactoryInterface && $this->contextFactory) { + $reflectionClass = new \ReflectionClass($resourceClass); - $operations = $resourceMetadata->getOperations() ?? new Operations(); - foreach ($operations as $operationName => $operation) { - if (null !== $operation->getDescription()) { - continue; - } - - $operations->add($operationName, $operation->withDescription($docBlock->getSummary())); + try { + $docBlock = $this->docBlockFactory->create($reflectionClass, $this->contextFactory->createFromReflector($reflectionClass)); + $description = $docBlock->getSummary(); + } catch (\InvalidArgumentException) { + // Ignore empty DocBlocks } + } else { + $description = $this->getShortDescription($resourceClass); + } - $resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withOperations($operations); + if (!$description) { + return $resourceMetadataCollection; + } - if (!$resourceMetadata->getGraphQlOperations()) { + $resourceMetadataCollection[$key] = $resourceMetadata->withDescription($description); + + $operations = $resourceMetadata->getOperations() ?? new Operations(); + foreach ($operations as $operationName => $operation) { + if (null !== $operation->getDescription()) { continue; } - foreach ($graphQlOperations = $resourceMetadata->getGraphQlOperations() as $operationName => $operation) { - if (null !== $operation->getDescription()) { - continue; - } + $operations->add($operationName, $operation->withDescription($description)); + } - $graphQlOperations[$operationName] = $operation->withDescription($docBlock->getSummary()); + $resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withOperations($operations); + + if (!$resourceMetadata->getGraphQlOperations()) { + continue; + } + + foreach ($graphQlOperations = $resourceMetadata->getGraphQlOperations() as $operationName => $operation) { + if (null !== $operation->getDescription()) { + continue; } - $resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withGraphQlOperations($graphQlOperations); - } catch (\InvalidArgumentException) { - // Ignore empty DocBlocks + $graphQlOperations[$operationName] = $operation->withDescription($description); } + + $resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withGraphQlOperations($graphQlOperations); } return $resourceMetadataCollection; } + + /** + * Gets the short description of the class. + */ + private function getShortDescription(string $class): ?string + { + if (!$docBlock = $this->getDocBlock($class)) { + return null; + } + + foreach ($docBlock->children as $docChild) { + if ($docChild instanceof PhpDocTextNode && !empty($docChild->text)) { + return $docChild->text; + } + } + + return null; + } + + private function getDocBlock(string $class): ?PhpDocNode + { + if (isset($this->docBlocks[$class])) { + return $this->docBlocks[$class]; + } + + try { + $reflectionClass = new \ReflectionClass($class); + } catch (\ReflectionException) { + return null; + } + + $rawDocNode = $reflectionClass->getDocComment(); + + if (!$rawDocNode) { + return null; + } + + $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode)); + $phpDocNode = $this->phpDocParser->parse($tokens); + $tokens->consumeTokenType(Lexer::TOKEN_END); + + return $this->docBlocks[$class] = $phpDocNode; + } } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index f66f3293a4e..40d550e7d7f 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -37,6 +37,7 @@ use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface; use Doctrine\Persistence\ManagerRegistry; use phpDocumentor\Reflection\DocBlockFactoryInterface; +use PHPStan\PhpDocParser\Parser\PhpDocParser; use Ramsey\Uuid\Uuid; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Config\FileLocator; @@ -266,7 +267,7 @@ private function registerMetadataConfiguration(ContainerBuilder $container, arra $container->getDefinition('api_platform.metadata.resource_extractor.xml')->replaceArgument(0, $xmlResources); $container->getDefinition('api_platform.metadata.property_extractor.xml')->replaceArgument(0, $xmlResources); - if (interface_exists(DocBlockFactoryInterface::class)) { + if (class_exists(PhpDocParser::class) || interface_exists(DocBlockFactoryInterface::class)) { $loader->load('metadata/php_doc.xml'); } diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 668f1833f82..05e02befe07 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -66,6 +66,7 @@ use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\ORM\OptimisticLockException; use phpDocumentor\Reflection\DocBlockFactoryInterface; +use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; @@ -427,8 +428,8 @@ public function testMetadataConfiguration(): void public function testMetadataConfigurationDocBlockFactoryInterface(): void { - if (!interface_exists(DocBlockFactoryInterface::class)) { - $this->markTestSkipped('class phpDocumentor\Reflection\DocBlockFactoryInterface does not exist'); + if (!class_exists(PhpDocParser::class) || !interface_exists(DocBlockFactoryInterface::class)) { + $this->markTestSkipped('class PHPStan\PhpDocParser\Parser\PhpDocParser or phpDocumentor\Reflection\DocBlockFactoryInterface does not exist'); } $config = self::DEFAULT_CONFIG; From 8744857a3a9c7cba99440405ad057b1a55952eff Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Tue, 29 Nov 2022 19:00:14 +0100 Subject: [PATCH 11/14] feat: add GraphQL enum support (#5199) * fix(openapi): resource classes are no longer considered as enum * feat: make property metadata factories work with enum cases * feat: add GraphQL enum support --- .php-cs-fixer.dist.php | 3 + features/graphql/introspection.feature | 55 ++++++++++++++++ features/graphql/mutation.feature | 63 ++++++++++++++++++ features/openapi/docs.feature | 8 +++ src/GraphQl/Type/FieldsBuilder.php | 41 +++++++++++- .../Type/FieldsBuilderEnumInterface.php | 64 +++++++++++++++++++ src/GraphQl/Type/FieldsBuilderInterface.php | 2 + src/GraphQl/Type/SchemaBuilder.php | 5 +- src/GraphQl/Type/TypeBuilder.php | 49 +++++++++++++- src/GraphQl/Type/TypeBuilderEnumInterface.php | 57 +++++++++++++++++ src/GraphQl/Type/TypeBuilderInterface.php | 4 ++ src/GraphQl/Type/TypeConverter.php | 28 +++++++- src/JsonSchema/SchemaFactory.php | 7 +- src/JsonSchema/TypeFactory.php | 2 +- src/Metadata/ApiProperty.php | 2 +- .../AttributePropertyMetadataFactory.php | 21 ++++++ tests/Fixtures/TestBundle/Document/Person.php | 7 +- .../TestBundle/Document/VideoGame.php | 37 +++++++++++ tests/Fixtures/TestBundle/Entity/Person.php | 1 + .../Fixtures/TestBundle/Entity/VideoGame.php | 39 +++++++++++ .../TestBundle/Enum/EnumWithDescriptions.php | 29 +++++++++ .../Fixtures/TestBundle/Enum/GamePlayMode.php | 53 +++++++++++++++ .../TestBundle/Enum/GenderTypeEnum.php | 8 +++ tests/Fixtures/app/config/config_common.yml | 1 + tests/GraphQl/Type/FieldsBuilderTest.php | 26 +++++++- tests/GraphQl/Type/SchemaBuilderTest.php | 9 +-- tests/GraphQl/Type/TypeBuilderTest.php | 62 +++++++++++++----- tests/GraphQl/Type/TypeConverterTest.php | 13 ++-- tests/JsonSchema/SchemaFactoryTest.php | 15 ++++- tests/JsonSchema/TypeFactoryTest.php | 35 +++++++--- .../AttributePropertyMetadataFactoryTest.php | 4 ++ 31 files changed, 692 insertions(+), 58 deletions(-) create mode 100644 src/GraphQl/Type/FieldsBuilderEnumInterface.php create mode 100644 src/GraphQl/Type/TypeBuilderEnumInterface.php create mode 100644 tests/Fixtures/TestBundle/Document/VideoGame.php create mode 100644 tests/Fixtures/TestBundle/Entity/VideoGame.php create mode 100644 tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php create mode 100644 tests/Fixtures/TestBundle/Enum/GamePlayMode.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 9e5241ceca6..846e5a32281 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -24,6 +24,9 @@ ->notPath('src/Annotation/ApiResource.php') // temporary ->notPath('src/Annotation/ApiSubresource.php') // temporary ->notPath('tests/Fixtures/TestBundle/Entity/DummyPhp8.php') // temporary + ->notPath('tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php') // PHPDoc on enum cases + ->notPath('tests/Fixtures/TestBundle/Enum/GamePlayMode.php') // PHPDoc on enum cases + ->notPath('tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php') // PHPDoc on enum cases ->append([ 'tests/Fixtures/app/console', ]); diff --git a/features/graphql/introspection.feature b/features/graphql/introspection.feature index 22934bbbb31..03be840718e 100644 --- a/features/graphql/introspection.feature +++ b/features/graphql/introspection.feature @@ -566,3 +566,58 @@ Feature: GraphQL introspection support And the JSON node "errors[0].debugMessage" should be equal to 'Type with id "VoDummyInspectionCursorConnection" is not present in the types container' And the JSON node "data.typeNotAvailable" should be null And the JSON node "data.typeOwner.fields[1].type.name" should be equal to "VoDummyInspectionCursorConnection" + + Scenario: Introspect an enum + When I send the following GraphQL request: + """ + { + person: __type(name: "Person") { + name + fields { + name + type { + name + description + enumValues { + name + description + } + } + } + } + } + """ + 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" + And the JSON node "data.person.fields[1].type.name" should be equal to "GenderTypeEnum" + #And the JSON node "data.person.fields[1].type.description" should be equal to "An enumeration of genders." + And the JSON node "data.person.fields[1].type.enumValues[0].name" should be equal to "MALE" + #And the JSON node "data.person.fields[1].type.enumValues[0].description" should be equal to "The male gender." + And the JSON node "data.person.fields[1].type.enumValues[1].name" should be equal to "FEMALE" + And the JSON node "data.person.fields[1].type.enumValues[1].description" should be equal to "The female gender." + + Scenario: Introspect an enum resource + When I send the following GraphQL request: + """ + { + videoGame: __type(name: "VideoGame") { + name + fields { + name + type { + name + kind + ofType { + name + kind + } + } + } + } + } + """ + 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" + And the JSON node "data.videoGame.fields[3].type.ofType.name" should be equal to "GamePlayMode" diff --git a/features/graphql/mutation.feature b/features/graphql/mutation.feature index e67e55554b4..b0532bf4d80 100644 --- a/features/graphql/mutation.feature +++ b/features/graphql/mutation.feature @@ -485,6 +485,69 @@ Feature: GraphQL mutation support And the JSON node "data.createDummy.dummy.arrayData[1]" should be equal to baz And the JSON node "data.createDummy.clientMutationId" should be equal to "myId" + Scenario: Create an item with an enum + When I send the following GraphQL request: + """ + mutation { + createPerson(input: {name: "Mob", genderType: FEMALE}) { + person { + id + name + genderType + } + } + } + """ + 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" + And the JSON node "data.createPerson.person.id" should be equal to "/people/1" + And the JSON node "data.createPerson.person.name" should be equal to "Mob" + And the JSON node "data.createPerson.person.genderType" should be equal to "FEMALE" + + Scenario: Create an item with an enum as a resource + When I send the following GraphQL request: + """ + { + gamePlayModes { + id + name + } + gamePlayMode(id: "/game_play_modes/SINGLE_PLAYER") { + name + } + } + """ + 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" + And the JSON node "data.gamePlayModes" should have 3 elements + And the JSON node "data.gamePlayModes[2].id" should be equal to "/game_play_modes/SINGLE_PLAYER" + And the JSON node "data.gamePlayModes[2].name" should be equal to "SINGLE_PLAYER" + And the JSON node "data.gamePlayMode.name" should be equal to "SINGLE_PLAYER" + When I send the following GraphQL request: + """ + mutation { + createVideoGame(input: {name: "Baten Kaitos", playMode: "/game_play_modes/SINGLE_PLAYER"}) { + videoGame { + id + name + playMode { + id + name + } + } + } + } + """ + 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" + And the JSON node "data.createVideoGame.videoGame.id" should be equal to "/video_games/1" + And the JSON node "data.createVideoGame.videoGame.name" should be equal to "Baten Kaitos" + And the JSON node "data.createVideoGame.videoGame.playMode.id" should be equal to "/game_play_modes/SINGLE_PLAYER" + And the JSON node "data.createVideoGame.videoGame.playMode.name" should be equal to "SINGLE_PLAYER" + Scenario: Delete an item through a mutation When I send the following GraphQL request: """ diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature index 2cbf3b33c45..3a413fa916f 100644 --- a/features/openapi/docs.feature +++ b/features/openapi/docs.feature @@ -73,6 +73,14 @@ Feature: Documentation support "nullable": true } """ + And the "playMode" property exists for the OpenAPI class "VideoGame" + And the "playMode" property for the OpenAPI class "VideoGame" should be equal to: + """ + { + "type": "string", + "format": "iri-reference" + } + """ # Enable these tests when SF 4.4 / PHP 7.1 support is dropped #And the "isDummyBoolean" property exists for the OpenAPI class "DummyBoolean" #And the "isDummyBoolean" property is not read only for the OpenAPI class "DummyBoolean" diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 26a9c7d42c3..77bb27c9dbc 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -42,10 +42,16 @@ * * @author Alan Poulain */ -final class FieldsBuilder implements FieldsBuilderInterface +final class FieldsBuilder implements FieldsBuilderInterface, FieldsBuilderEnumInterface { - public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) + private readonly TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder; + + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) { + if ($typeBuilder instanceof TypeBuilderInterface) { + @trigger_error(sprintf('$typeBuilder argument of FieldsBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED); + } + $this->typeBuilder = $typeBuilder; } /** @@ -226,6 +232,26 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o return $fields; } + /** + * {@inheritdoc} + */ + public function getEnumFields(string $enumClass): array + { + $rEnum = new \ReflectionEnum($enumClass); + + $enumCases = []; + foreach ($rEnum->getCases() as $rCase) { + $enumCase = ['value' => $rCase->getBackingValue()]; + $propertyMetadata = $this->propertyMetadataFactory->create($enumClass, $rCase->getName()); + if ($enumCaseDescription = $propertyMetadata->getDescription()) { + $enumCase['description'] = $enumCaseDescription; + } + $enumCases[$rCase->getName()] = $enumCase; + } + + return $enumCases; + } + /** * {@inheritdoc} */ @@ -481,7 +507,16 @@ private function convertType(Type $type, bool $input, Operation $resourceOperati } if ($this->typeBuilder->isCollection($type)) { - return $this->pagination->isGraphQlEnabled($resourceOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation) : GraphQLType::listOf($graphqlType); + if (!$input && $this->pagination->isGraphQlEnabled($resourceOperation)) { + // Deprecated path, to remove in API Platform 4. + if ($this->typeBuilder instanceof TypeBuilderInterface) { + return $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation); + } + + return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation); + } + + return GraphQLType::listOf($graphqlType); } return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName()) diff --git a/src/GraphQl/Type/FieldsBuilderEnumInterface.php b/src/GraphQl/Type/FieldsBuilderEnumInterface.php new file mode 100644 index 00000000000..0517796e71c --- /dev/null +++ b/src/GraphQl/Type/FieldsBuilderEnumInterface.php @@ -0,0 +1,64 @@ + + * + * 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\GraphQl\Type; + +use ApiPlatform\Metadata\GraphQl\Operation; + +/** + * Interface implemented to build GraphQL fields. + * + * @author Alan Poulain + */ +interface FieldsBuilderEnumInterface +{ + /** + * Gets the fields of a node for a query. + */ + public function getNodeQueryFields(): array; + + /** + * Gets the item query fields of the schema. + */ + public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array; + + /** + * Gets the collection query fields of the schema. + */ + public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array; + + /** + * Gets the mutation fields of the schema. + */ + public function getMutationFields(string $resourceClass, Operation $operation): array; + + /** + * Gets the subscription fields of the schema. + */ + public function getSubscriptionFields(string $resourceClass, Operation $operation): array; + + /** + * Gets the fields of the type of the given resource. + */ + public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, int $depth = 0, ?array $ioMetadata = null): array; + + /** + * Gets the fields (cases) of the enum. + */ + public function getEnumFields(string $enumClass): array; + + /** + * Resolve the args of a resource by resolving its types. + */ + public function resolveResourceArgs(array $args, Operation $operation): array; +} diff --git a/src/GraphQl/Type/FieldsBuilderInterface.php b/src/GraphQl/Type/FieldsBuilderInterface.php index afab39d8aaa..dc4bd57f003 100644 --- a/src/GraphQl/Type/FieldsBuilderInterface.php +++ b/src/GraphQl/Type/FieldsBuilderInterface.php @@ -19,6 +19,8 @@ * Interface implemented to build GraphQL fields. * * @author Alan Poulain + * + * @deprecated Since API Platform 3.1. Use @see FieldsBuilderEnumInterface instead. */ interface FieldsBuilderInterface { diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php index 8be0411010d..c1e2d1ce942 100644 --- a/src/GraphQl/Type/SchemaBuilder.php +++ b/src/GraphQl/Type/SchemaBuilder.php @@ -32,8 +32,11 @@ */ final class SchemaBuilder implements SchemaBuilderInterface { - public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly TypesFactoryInterface $typesFactory, private readonly TypesContainerInterface $typesContainer, private readonly FieldsBuilderInterface $fieldsBuilder) + public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly TypesFactoryInterface $typesFactory, private readonly TypesContainerInterface $typesContainer, private readonly FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder) { + if ($this->fieldsBuilder instanceof FieldsBuilderInterface) { + @trigger_error(sprintf('$fieldsBuilder argument of SchemaBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED); + } } public function getSchema(): Schema diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index 480b102bb2c..4c9fec5e649 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -22,6 +22,7 @@ use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\State\Pagination\Pagination; +use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\NonNull; @@ -35,7 +36,7 @@ * * @author Alan Poulain */ -final class TypeBuilder implements TypeBuilderInterface +final class TypeBuilder implements TypeBuilderInterface, TypeBuilderEnumInterface { private $defaultFieldResolver; @@ -201,6 +202,16 @@ public function getNodeInterface(): InterfaceType * {@inheritdoc} */ public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType + { + @trigger_error('Using getResourcePaginatedCollectionType method of TypeBuilder is deprecated since API Platform 3.1. Use getPaginatedCollectionType method instead.', \E_USER_DEPRECATED); + + return $this->getPaginatedCollectionType($resourceType, $operation); + } + + /** + * {@inheritdoc} + */ + public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType { $shortName = $resourceType->name; $paginationType = $this->pagination->getGraphQlPaginationType($operation); @@ -226,6 +237,42 @@ public function getResourcePaginatedCollectionType(GraphQLType $resourceType, st return $resourcePaginatedCollectionType; } + public function getEnumType(Operation $operation): GraphQLType + { + $enumName = $operation->getShortName(); + $enumKey = $enumName; + if (!str_ends_with($enumName, 'Enum')) { + $enumKey = sprintf('%sEnum', $enumName); + } + + if ($this->typesContainer->has($enumKey)) { + return $this->typesContainer->get($enumKey); + } + + /** @var FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder */ + $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder'); + $enumCases = []; + // Remove the condition in API Platform 4. + if ($fieldsBuilder instanceof FieldsBuilderEnumInterface) { + $enumCases = $fieldsBuilder->getEnumFields($operation->getClass()); + } else { + @trigger_error(sprintf('api_platform.graphql.fields_builder service implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED); + } + + $enumConfig = [ + 'name' => $enumName, + 'values' => $enumCases, + ]; + if ($enumDescription = $operation->getDescription()) { + $enumConfig['description'] = $enumDescription; + } + + $enumType = new EnumType($enumConfig); + $this->typesContainer->set($enumKey, $enumType); + + return $enumType; + } + /** * {@inheritdoc} */ diff --git a/src/GraphQl/Type/TypeBuilderEnumInterface.php b/src/GraphQl/Type/TypeBuilderEnumInterface.php new file mode 100644 index 00000000000..9bbaa5215b0 --- /dev/null +++ b/src/GraphQl/Type/TypeBuilderEnumInterface.php @@ -0,0 +1,57 @@ + + * + * 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\GraphQl\Type; + +use ApiPlatform\Metadata\GraphQl\Operation; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use GraphQL\Type\Definition\InterfaceType; +use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\Type as GraphQLType; +use Symfony\Component\PropertyInfo\Type; + +/** + * Interface implemented to build a GraphQL type. + * + * @author Alan Poulain + */ +interface TypeBuilderEnumInterface +{ + /** + * Gets the object type of the given resource. + * + * @return ObjectType|NonNull the object type, possibly wrapped by NonNull + */ + public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType; + + /** + * Get the interface type of a node. + */ + public function getNodeInterface(): InterfaceType; + + /** + * Gets the type of a paginated collection of the given resource type. + */ + public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType; + + /** + * Gets the type corresponding to an enum. + */ + public function getEnumType(Operation $operation): GraphQLType; + + /** + * Returns true if a type is a collection. + */ + public function isCollection(Type $type): bool; +} diff --git a/src/GraphQl/Type/TypeBuilderInterface.php b/src/GraphQl/Type/TypeBuilderInterface.php index 50bb0077893..8b782e32461 100644 --- a/src/GraphQl/Type/TypeBuilderInterface.php +++ b/src/GraphQl/Type/TypeBuilderInterface.php @@ -25,6 +25,8 @@ * Interface implemented to build a GraphQL type. * * @author Alan Poulain + * + * @deprecated Since API Platform 3.1. Use @see TypeBuilderEnumInterface instead. */ interface TypeBuilderInterface { @@ -42,6 +44,8 @@ public function getNodeInterface(): InterfaceType; /** * Gets the type of a paginated collection of the given resource type. + * + * @deprecated Since API Platform 3.1. Use @see TypeBuilderEnumInterface::getPaginatedCollectionType() method instead. */ public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType; diff --git a/src/GraphQl/Type/TypeConverter.php b/src/GraphQl/Type/TypeConverter.php index 6ba6df9192e..f868962c6ae 100644 --- a/src/GraphQl/Type/TypeConverter.php +++ b/src/GraphQl/Type/TypeConverter.php @@ -37,8 +37,11 @@ */ final class TypeConverter implements TypeConverterInterface { - public function __construct(private readonly TypeBuilderInterface $typeBuilder, private readonly TypesContainerInterface $typesContainer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory) + public function __construct(private readonly TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypesContainerInterface $typesContainer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory) { + if ($typeBuilder instanceof TypeBuilderInterface) { + @trigger_error(sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED); + } } /** @@ -67,7 +70,28 @@ public function convertType(Type $type, bool $input, Operation $rootOperation, s return GraphQLType::string(); } - return $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth); + $resourceType = $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth); + + if (!$resourceType && is_a($type->getClassName(), \BackedEnum::class, true)) { + // Remove the condition in API Platform 4. + if ($this->typeBuilder instanceof TypeBuilderEnumInterface) { + $operation = null; + try { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($type->getClassName()); + $operation = $resourceMetadataCollection->getOperation(); + } catch (ResourceClassNotFoundException|OperationNotFoundException) { + } + /** @var Query $enumOperation */ + $enumOperation = (new Query()) + ->withClass($type->getClassName()) + ->withShortName($operation?->getShortName() ?? (new \ReflectionClass($type->getClassName()))->getShortName()) + ->withDescription($operation?->getDescription()); + + return $this->typeBuilder->getEnumType($enumOperation); + } + } + + return $resourceType; default: return null; } diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index aee436fb591..41a39e5e5e5 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -174,7 +174,10 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $propertySchema['externalDocs'] = ['url' => $iri]; } - if (!isset($propertySchema['default']) && !empty($default = $propertyMetadata->getDefault())) { + // TODO: 3.0 support multiple types + $type = $propertyMetadata->getBuiltinTypes()[0] ?? null; + + if (!isset($propertySchema['default']) && !empty($default = $propertyMetadata->getDefault()) && (null === $type?->getClassName() || !$this->isResourceClass($type->getClassName()))) { if ($default instanceof \BackedEnum) { $default = $default->value; } @@ -190,8 +193,6 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str } $valueSchema = []; - // TODO: 3.0 support multiple types - $type = $propertyMetadata->getBuiltinTypes()[0] ?? null; if (null !== $type) { if ($isCollection = $type->isCollection()) { $keyType = $type->getCollectionKeyTypes()[0] ?? null; diff --git a/src/JsonSchema/TypeFactory.php b/src/JsonSchema/TypeFactory.php index e67d510b3ce..62c06e4cc83 100644 --- a/src/JsonSchema/TypeFactory.php +++ b/src/JsonSchema/TypeFactory.php @@ -116,7 +116,7 @@ private function getClassType(?string $className, bool $nullable, string $format 'format' => 'binary', ]; } - if (is_a($className, \BackedEnum::class, true)) { + if (!$this->isResourceClass($className) && is_a($className, \BackedEnum::class, true)) { $rEnum = new \ReflectionEnum($className); $enumCases = array_map(static fn (\ReflectionEnumBackedCase $rCase) => $rCase->getBackingValue(), $rEnum->getCases()); if ($nullable) { diff --git a/src/Metadata/ApiProperty.php b/src/Metadata/ApiProperty.php index b01bad439f2..8541dc210af 100644 --- a/src/Metadata/ApiProperty.php +++ b/src/Metadata/ApiProperty.php @@ -20,7 +20,7 @@ * * @author Kévin Dunglas */ -#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)] +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT)] final class ApiProperty { /** diff --git a/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php b/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php index 94fa423dad4..6100f784393 100644 --- a/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php +++ b/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php @@ -42,9 +42,30 @@ public function create(string $resourceClass, string $property, array $options = } } + $reflectionClass = null; + $reflectionEnum = null; + try { $reflectionClass = new \ReflectionClass($resourceClass); } catch (\ReflectionException) { + } + try { + $reflectionEnum = new \ReflectionEnum($resourceClass); + } catch (\ReflectionException) { + } + + if (!$reflectionClass && !$reflectionEnum) { + return $this->handleNotFound($parentPropertyMetadata, $resourceClass, $property); + } + + if ($reflectionEnum) { + if ($reflectionEnum->hasCase($property)) { + $reflectionCase = $reflectionEnum->getCase($property); + if ($attributes = $reflectionCase->getAttributes(ApiProperty::class)) { + return $this->createMetadata($attributes[0]->newInstance(), $parentPropertyMetadata); + } + } + return $this->handleNotFound($parentPropertyMetadata, $resourceClass, $property); } diff --git a/tests/Fixtures/TestBundle/Document/Person.php b/tests/Fixtures/TestBundle/Document/Person.php index f7d11a309aa..21d287dc6ad 100644 --- a/tests/Fixtures/TestBundle/Document/Person.php +++ b/tests/Fixtures/TestBundle/Document/Person.php @@ -32,13 +32,14 @@ class Person #[ODM\Id(strategy: 'INCREMENT', type: 'int')] private ?int $id = null; + #[ODM\Field(type: 'string', enumType: GenderTypeEnum::class, nullable: true)] + #[Groups(['people.pets'])] + public ?GenderTypeEnum $genderType = GenderTypeEnum::MALE; + #[Groups(['people.pets'])] #[ODM\Field(type: 'string')] public string $name; - #[ODM\Field(type: 'string', enumType: GenderTypeEnum::class, nullable: true)] - public ?GenderTypeEnum $genderType = GenderTypeEnum::MALE; - #[Groups(['people.pets'])] #[ODM\ReferenceMany(targetDocument: PersonToPet::class, mappedBy: 'person')] public Collection|iterable $pets; diff --git a/tests/Fixtures/TestBundle/Document/VideoGame.php b/tests/Fixtures/TestBundle/Document/VideoGame.php new file mode 100644 index 00000000000..60dc2a5abf4 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/VideoGame.php @@ -0,0 +1,37 @@ + + * + * 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\Document; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GamePlayMode; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +#[ApiResource] +#[ODM\Document] +class VideoGame +{ + #[ODM\Id(strategy: 'INCREMENT', type: 'int')] + private ?int $id = null; + + #[ODM\Field(type: 'string')] + public string $name; + + #[ODM\Field(type: 'string', enumType: GamePlayMode::class)] + public GamePlayMode $playMode = GamePlayMode::SINGLE_PLAYER; + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Person.php b/tests/Fixtures/TestBundle/Entity/Person.php index 775969b2786..809da04a5b6 100644 --- a/tests/Fixtures/TestBundle/Entity/Person.php +++ b/tests/Fixtures/TestBundle/Entity/Person.php @@ -35,6 +35,7 @@ class Person private ?int $id = null; #[ORM\Column(type: 'string', enumType: GenderTypeEnum::class, nullable: true)] + #[Groups(['people.pets'])] public ?GenderTypeEnum $genderType = GenderTypeEnum::MALE; #[ORM\Column(type: 'string')] diff --git a/tests/Fixtures/TestBundle/Entity/VideoGame.php b/tests/Fixtures/TestBundle/Entity/VideoGame.php new file mode 100644 index 00000000000..34dd41b2f29 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/VideoGame.php @@ -0,0 +1,39 @@ + + * + * 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; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GamePlayMode; +use Doctrine\ORM\Mapping as ORM; + +#[ApiResource] +#[ORM\Entity] +class VideoGame +{ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + + #[ORM\Column(type: 'string')] + public string $name; + + #[ORM\Column(type: 'string', enumType: GamePlayMode::class)] + public GamePlayMode $playMode = GamePlayMode::SINGLE_PLAYER; + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php b/tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php new file mode 100644 index 00000000000..301d159815a --- /dev/null +++ b/tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.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\Enum; + +enum EnumWithDescriptions +{ + /** + * A short description for case one. + */ + case ONE; + + /** + * A short description for case two. + * + * A long description for case two. + */ + case TWO; +} diff --git a/tests/Fixtures/TestBundle/Enum/GamePlayMode.php b/tests/Fixtures/TestBundle/Enum/GamePlayMode.php new file mode 100644 index 00000000000..ec6c38a2fe7 --- /dev/null +++ b/tests/Fixtures/TestBundle/Enum/GamePlayMode.php @@ -0,0 +1,53 @@ + + * + * 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\Enum; + +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Tests\Fixtures\TestBundle\Metadata\Get; + +#[Get(description: 'Indicates whether this game is multi-player, co-op or single-player.', provider: self::class.'::getCase')] +#[GetCollection(provider: self::class.'::getCases')] +#[Query(provider: self::class.'::getCase')] +#[QueryCollection(provider: self::class.'::getCases', paginationEnabled: false)] +enum GamePlayMode: string +{ + /** Co-operative games, where you play on the same team with friends. */ + case CO_OP = 'CoOp'; + + /** Requiring or allowing multiple human players to play simultaneously. */ + case MULTI_PLAYER = 'MultiPlayer'; + + /** Which is played by a lone player. */ + case SINGLE_PLAYER = 'SinglePlayer'; + + public function getId(): string + { + return $this->name; + } + + public static function getCase(Operation $operation, array $uriVariables): GamePlayMode + { + $name = $uriVariables['id'] ?? null; + + return \constant(self::class."::$name"); + } + + public static function getCases(): array + { + return self::cases(); + } +} diff --git a/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php b/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php index 12327a2e6df..7eb7acffc53 100644 --- a/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php +++ b/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php @@ -13,8 +13,16 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Enum; +use ApiPlatform\Metadata\ApiProperty; + +/** + * An enumeration of genders. + */ enum GenderTypeEnum: string { + /** The male gender. */ case MALE = 'male'; + + #[ApiProperty(description: 'The female gender.')] case FEMALE = 'female'; } diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 98ff470df5e..f8055e315fd 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -78,6 +78,7 @@ api_platform: doctrine_mongodb_odm: false mapping: paths: + - '%kernel.project_dir%/../TestBundle/Enum' - '%kernel.project_dir%/../TestBundle/Model' parameters: diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php index 5313db3537d..853b962223f 100644 --- a/tests/GraphQl/Type/FieldsBuilderTest.php +++ b/tests/GraphQl/Type/FieldsBuilderTest.php @@ -17,7 +17,7 @@ use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; use ApiPlatform\GraphQl\Type\FieldsBuilder; -use ApiPlatform\GraphQl\Type\TypeBuilderInterface; +use ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface; use ApiPlatform\GraphQl\Type\TypeConverterInterface; use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\Metadata\ApiProperty; @@ -33,6 +33,7 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\State\Pagination\Pagination; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use ApiPlatform\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; @@ -78,7 +79,7 @@ protected function setUp(): void $this->propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class); - $this->typeBuilderProphecy = $this->prophesize(TypeBuilderInterface::class); + $this->typeBuilderProphecy = $this->prophesize(TypeBuilderEnumInterface::class); $this->typeConverterProphecy = $this->prophesize(TypeConverterInterface::class); $this->itemResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); $this->collectionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); @@ -207,7 +208,7 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation)->willReturn($graphqlType); + $this->typeBuilderProphecy->getPaginatedCollectionType($graphqlType, $operation)->willReturn($graphqlType); $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $this->filterLocatorProphecy->has('my_filter')->willReturn(true); $filterProphecy = $this->prophesize(FilterInterface::class); @@ -827,6 +828,25 @@ public function resourceObjectTypeFieldsProvider(): array ]; } + public function testGetEnumFields(): void + { + $enumClass = GenderTypeEnum::class; + + $this->propertyMetadataFactoryProphecy->create($enumClass, GenderTypeEnum::MALE->name)->willReturn(new ApiProperty( + description: 'Description of MALE case', + )); + $this->propertyMetadataFactoryProphecy->create($enumClass, GenderTypeEnum::FEMALE->name)->willReturn(new ApiProperty( + description: 'Description of FEMALE case', + )); + + $enumFields = $this->fieldsBuilder->getEnumFields($enumClass); + + $this->assertSame([ + GenderTypeEnum::MALE->name => ['value' => GenderTypeEnum::MALE->value, 'description' => 'Description of MALE case'], + GenderTypeEnum::FEMALE->name => ['value' => GenderTypeEnum::FEMALE->value, 'description' => 'Description of FEMALE case'], + ], $enumFields); + } + /** * @dataProvider resolveResourceArgsProvider */ diff --git a/tests/GraphQl/Type/SchemaBuilderTest.php b/tests/GraphQl/Type/SchemaBuilderTest.php index 294d5312c3d..1850951d402 100644 --- a/tests/GraphQl/Type/SchemaBuilderTest.php +++ b/tests/GraphQl/Type/SchemaBuilderTest.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Tests\GraphQl\Type; -use ApiPlatform\GraphQl\Type\FieldsBuilderInterface; +use ApiPlatform\GraphQl\Type\FieldsBuilderEnumInterface; use ApiPlatform\GraphQl\Type\SchemaBuilder; use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\GraphQl\Type\TypesFactoryInterface; @@ -42,15 +42,10 @@ class SchemaBuilderTest extends TestCase use ProphecyTrait; private ObjectProphecy $resourceNameCollectionFactoryProphecy; - private ObjectProphecy $resourceMetadataCollectionFactoryProphecy; - private ObjectProphecy $typesFactoryProphecy; - private ObjectProphecy $typesContainerProphecy; - private ObjectProphecy $fieldsBuilderProphecy; - private SchemaBuilder $schemaBuilder; /** @@ -62,7 +57,7 @@ protected function setUp(): void $this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $this->typesFactoryProphecy = $this->prophesize(TypesFactoryInterface::class); $this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class); - $this->fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $this->fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $this->schemaBuilder = new SchemaBuilder($this->resourceNameCollectionFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->typesFactoryProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->fieldsBuilderProphecy->reveal()); } diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index ee6dc591d4e..0eed16b666f 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -14,7 +14,7 @@ namespace ApiPlatform\Tests\GraphQl\Type; use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Type\FieldsBuilderInterface; +use ApiPlatform\GraphQl\Type\FieldsBuilderEnumInterface; use ApiPlatform\GraphQl\Type\TypeBuilder; use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\Metadata\ApiResource; @@ -26,6 +26,8 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\State\Pagination\Pagination; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GamePlayMode; +use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; @@ -48,12 +50,9 @@ class TypeBuilderTest extends TestCase use ProphecyTrait; private ObjectProphecy $typesContainerProphecy; - /** @var callable */ private $defaultFieldResolver; - private ObjectProphecy $fieldsBuilderLocatorProphecy; - private TypeBuilder $typeBuilder; /** @@ -93,7 +92,7 @@ public function testGetResourceObjectType(): void $this->assertArrayHasKey('interfaces', $resourceObjectType->config); $this->assertArrayHasKey('fields', $resourceObjectType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); @@ -119,7 +118,7 @@ public function testGetResourceObjectTypeOutputClass(): void $this->assertArrayHasKey('interfaces', $resourceObjectType->config); $this->assertArrayHasKey('fields', $resourceObjectType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('outputClass', $operation, false, 0, ['class' => 'outputClass'])->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); @@ -188,7 +187,7 @@ public function testGetResourceObjectTypeInput(): void $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); @@ -214,7 +213,7 @@ public function testGetResourceObjectTypeNestedInput(): void $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); @@ -240,7 +239,7 @@ public function testGetResourceObjectTypeCustomMutationInputArgs(): void $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 0, null) ->shouldBeCalled()->willReturn(['clientMutationId' => GraphQLType::string()]); $fieldsBuilderProphecy->resolveResourceArgs([], $operation)->shouldBeCalled(); @@ -320,7 +319,7 @@ public function testGetResourceObjectTypeMutationWrappedType(): void $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); @@ -344,7 +343,7 @@ public function testGetResourceObjectTypeMutationNested(): void $this->assertArrayHasKey('interfaces', $resourceObjectType->config); $this->assertArrayHasKey('fields', $resourceObjectType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); @@ -425,7 +424,7 @@ public function testGetResourceObjectTypeSubscriptionWrappedType(): void $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); @@ -449,7 +448,7 @@ public function testGetResourceObjectTypeSubscriptionNested(): void $this->assertArrayHasKey('interfaces', $resourceObjectType->config); $this->assertArrayHasKey('fields', $resourceObjectType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); @@ -477,7 +476,7 @@ public function testGetNodeInterface(): void $this->assertSame(GraphQLType::string(), $resolvedType); } - public function testCursorBasedGetResourcePaginatedCollectionType(): void + public function testCursorBasedGetPaginatedCollectionType(): void { /** @var Operation $operation */ $operation = (new Query())->withPaginationType('cursor'); @@ -487,7 +486,7 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPageInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getPaginatedCollectionType(GraphQLType::string(), $operation); $this->assertSame('StringCursorConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Cursor connection for String.', $resourcePaginatedCollectionType->description); @@ -533,7 +532,7 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void $this->assertSame(GraphQLType::int(), $totalCountType->getWrappedType()); } - public function testPageBasedGetResourcePaginatedCollectionType(): void + public function testPageBasedGetPaginatedCollectionType(): void { /** @var Operation $operation */ $operation = (new Query())->withPaginationType('page'); @@ -542,7 +541,7 @@ public function testPageBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPaginationInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getPaginatedCollectionType(GraphQLType::string(), $operation); $this->assertSame('StringPageConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Page connection for String.', $resourcePaginatedCollectionType->description); @@ -568,6 +567,35 @@ public function testPageBasedGetResourcePaginatedCollectionType(): void $this->assertSame(GraphQLType::int(), $paginationInfoObjectTypeFields['totalCount']->getType()->getWrappedType()); } + public function testGetEnumType(): void + { + $enumClass = GamePlayMode::class; + $enumName = 'GamePlayMode'; + $enumDescription = 'GamePlayModeEnum description'; + /** @var Operation $operation */ + $operation = (new Operation()) + ->withClass($enumClass) + ->withShortName($enumName) + ->withDescription('GamePlayModeEnum description'); + + $this->typesContainerProphecy->has('GamePlayModeEnum')->shouldBeCalled()->willReturn(false); + $this->typesContainerProphecy->set('GamePlayModeEnum', Argument::type(EnumType::class))->shouldBeCalled(); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); + $enumValues = [ + GamePlayMode::CO_OP->name => ['value' => GamePlayMode::CO_OP->value], + GamePlayMode::MULTI_PLAYER->name => ['value' => GamePlayMode::MULTI_PLAYER->value], + GamePlayMode::SINGLE_PLAYER->name => ['value' => GamePlayMode::SINGLE_PLAYER->value, 'description' => 'Which is played by a lone player.'], + ]; + $fieldsBuilderProphecy->getEnumFields($enumClass)->willReturn($enumValues); + $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->willReturn($fieldsBuilderProphecy->reveal()); + + self::assertEquals(new EnumType([ + 'name' => $enumName, + 'description' => $enumDescription, + 'values' => $enumValues, + ]), $this->typeBuilder->getEnumType($operation)); + } + /** * @dataProvider typesProvider */ diff --git a/tests/GraphQl/Type/TypeConverterTest.php b/tests/GraphQl/Type/TypeConverterTest.php index 6e7634b7900..500fdd03921 100644 --- a/tests/GraphQl/Type/TypeConverterTest.php +++ b/tests/GraphQl/Type/TypeConverterTest.php @@ -14,7 +14,7 @@ namespace ApiPlatform\Tests\GraphQl\Type; use ApiPlatform\Exception\ResourceClassNotFoundException; -use ApiPlatform\GraphQl\Type\TypeBuilderInterface; +use ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface; use ApiPlatform\GraphQl\Type\TypeConverter; use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\Metadata\ApiProperty; @@ -25,7 +25,9 @@ use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Type\Definition\DateTimeType; +use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type as GraphQLType; use PHPUnit\Framework\TestCase; @@ -42,13 +44,9 @@ class TypeConverterTest extends TestCase use ProphecyTrait; private ObjectProphecy $typeBuilderProphecy; - private ObjectProphecy $typesContainerProphecy; - private ObjectProphecy $resourceMetadataCollectionFactoryProphecy; - private ObjectProphecy $propertyMetadataFactoryProphecy; - private TypeConverter $typeConverter; /** @@ -56,7 +54,7 @@ class TypeConverterTest extends TestCase */ protected function setUp(): void { - $this->typeBuilderProphecy = $this->prophesize(TypeBuilderInterface::class); + $this->typeBuilderProphecy = $this->prophesize(TypeBuilderEnumInterface::class); $this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class); $this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $this->propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); @@ -69,6 +67,8 @@ protected function setUp(): void public function testConvertType(Type $type, bool $input, int $depth, GraphQLType|string|null $expectedGraphqlType): void { $this->typeBuilderProphecy->isCollection($type)->willReturn(false); + $this->resourceMetadataCollectionFactoryProphecy->create(Argument::type('string'))->willThrow(new ResourceClassNotFoundException()); + $this->typeBuilderProphecy->getEnumType(Argument::type(Operation::class))->willReturn($expectedGraphqlType); /** @var Operation $operation */ $operation = (new Query())->withName('test'); @@ -86,6 +86,7 @@ public function convertTypeProvider(): array [new Type(Type::BUILTIN_TYPE_ARRAY), false, 0, 'Iterable'], [new Type(Type::BUILTIN_TYPE_ITERABLE), false, 0, 'Iterable'], [new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeInterface::class), false, 0, GraphQLType::string()], + [new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class), false, 0, new EnumType(['name' => 'GenderTypeEnum'])], [new Type(Type::BUILTIN_TYPE_OBJECT), false, 0, null], [new Type(Type::BUILTIN_TYPE_CALLABLE), false, 0, null], [new Type(Type::BUILTIN_TYPE_NULL), false, 0, null], diff --git a/tests/JsonSchema/SchemaFactoryTest.php b/tests/JsonSchema/SchemaFactoryTest.php index 062a16a8566..415cdbb6d6e 100644 --- a/tests/JsonSchema/SchemaFactoryTest.php +++ b/tests/JsonSchema/SchemaFactoryTest.php @@ -118,6 +118,12 @@ public function testBuildSchemaWithSerializerGroups(): void ), Argument::cetera())->willReturn([ 'type' => 'string', ]); + $typeFactoryProphecy->getType(Argument::allOf( + Argument::type(Type::class), + Argument::which('getBuiltinType', Type::BUILTIN_TYPE_OBJECT) + ), Argument::cetera())->willReturn([ + 'type' => 'object', + ]); $shortName = (new \ReflectionClass(OverriddenOperationDummy::class))->getShortName(); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); @@ -135,14 +141,16 @@ public function testBuildSchemaWithSerializerGroups(): void $serializerGroup = 'custom_operation_dummy'; $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(OverriddenOperationDummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['alias', 'description'])); + $propertyNameCollectionFactoryProphecy->create(OverriddenOperationDummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['alias', 'description', 'genderType'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'alias', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)); $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'description', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)); + $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'genderType', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)])->withReadable(true)->withDefault(GenderTypeEnum::MALE)); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(OverriddenOperationDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(GenderTypeEnum::class)->willReturn(true); $schemaFactory = new SchemaFactory($typeFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), null, $resourceClassResolverProphecy->reveal()); $resultSchema = $schemaFactory->buildSchema(OverriddenOperationDummy::class, 'json', Schema::TYPE_OUTPUT, null, null, ['groups' => $serializerGroup, AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false]); @@ -163,6 +171,11 @@ public function testBuildSchemaWithSerializerGroups(): void $this->assertArrayHasKey('description', $definitions[$rootDefinitionKey]['properties']); $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['description']); $this->assertSame('string', $definitions[$rootDefinitionKey]['properties']['description']['type']); + $this->assertArrayHasKey('genderType', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayNotHasKey('default', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayNotHasKey('example', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertSame('object', $definitions[$rootDefinitionKey]['properties']['genderType']['type']); } public function testBuildSchemaForAssociativeArray(): void diff --git a/tests/JsonSchema/TypeFactoryTest.php b/tests/JsonSchema/TypeFactoryTest.php index 418139459ef..996027a62f1 100644 --- a/tests/JsonSchema/TypeFactoryTest.php +++ b/tests/JsonSchema/TypeFactoryTest.php @@ -13,10 +13,12 @@ namespace ApiPlatform\Tests\JsonSchema; +use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryInterface; use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GamePlayMode; use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -32,7 +34,10 @@ class TypeFactoryTest extends TestCase */ public function testGetType(array $schema, Type $type): void { - $typeFactory = new TypeFactory(); + $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); + $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); + $typeFactory = new TypeFactory($resourceClassResolver->reveal()); $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_OPENAPI))); } @@ -54,8 +59,10 @@ public function typeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['nullable' => true, 'type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield [['type' => 'string', 'enum' => ['male', 'female', null], 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield ['nullable enum' => ['type' => 'string', 'enum' => ['male', 'female', null], 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; + yield ['nullable enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable' => [ ['nullable' => true, 'type' => 'array', 'items' => ['type' => 'string']], @@ -155,7 +162,10 @@ public function typeProvider(): iterable */ public function testGetTypeWithJsonSchemaSyntax(array $schema, Type $type): void { - $typeFactory = new TypeFactory(); + $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); + $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); + $typeFactory = new TypeFactory($resourceClassResolver->reveal()); $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_JSON_SCHEMA))); } @@ -177,8 +187,10 @@ public function jsonSchemaTypeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['type' => ['string', 'null'], 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield [['type' => ['string', 'null'], 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield ['nullable enum' => ['type' => ['string', 'null'], 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; + yield ['nullable enum resource' => ['type' => ['string', 'null'], 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable' => [ ['type' => ['array', 'null'], 'items' => ['type' => 'string']], @@ -271,7 +283,10 @@ public function jsonSchemaTypeProvider(): iterable /** @dataProvider openAPIV2TypeProvider */ public function testGetTypeWithOpenAPIV2Syntax(array $schema, Type $type): void { - $typeFactory = new TypeFactory(); + $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); + $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); + $typeFactory = new TypeFactory($resourceClassResolver->reveal()); $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_SWAGGER))); } @@ -293,8 +308,10 @@ public function openAPIV2TypeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield [['type' => 'string', 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield ['nullable enum' => ['type' => 'string', 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; + yield ['nullable enum resource' => ['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable, but ignored in OpenAPI V2' => [ ['type' => 'array', 'items' => ['type' => 'string']], diff --git a/tests/Metadata/Property/Factory/AttributePropertyMetadataFactoryTest.php b/tests/Metadata/Property/Factory/AttributePropertyMetadataFactoryTest.php index 35ad053b952..21c38321e36 100644 --- a/tests/Metadata/Property/Factory/AttributePropertyMetadataFactoryTest.php +++ b/tests/Metadata/Property/Factory/AttributePropertyMetadataFactoryTest.php @@ -18,6 +18,7 @@ use ApiPlatform\Metadata\Property\Factory\AttributePropertyMetadataFactory; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyPhp8ApiPropertyAttribute; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -38,6 +39,9 @@ public function testCreateAttribute(): void $metadata = $factory->create(DummyPhp8ApiPropertyAttribute::class, 'foo'); $this->assertSame('a foo', $metadata->getDescription()); + + $metadata = $factory->create(GenderTypeEnum::class, 'FEMALE'); + $this->assertSame('The female gender.', $metadata->getDescription()); } public function testClassNotFound(): void From 87e64e14b9c9aaf30ba3bdc75ad68836ba0cdb27 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Fri, 2 Dec 2022 12:00:09 +0100 Subject: [PATCH 12/14] fix: not initialized params in PhpDocResourceMetadataCollectionFactory (#5247) --- .../Factory/PhpDocResourceMetadataCollectionFactory.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php index 819e089e4e6..2a5163f424c 100644 --- a/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php @@ -56,10 +56,14 @@ public function __construct(private readonly ResourceMetadataCollectionFactoryIn if (class_exists(DocBlockFactory::class) && !class_exists(PhpDocParser::class)) { trigger_deprecation('api-platform/core', '3.1', 'Using phpdocumentor/reflection-docblock is deprecated. Require phpstan/phpdoc-parser instead.'); } + $phpDocParser = null; + $lexer = null; if (class_exists(PhpDocParser::class)) { - $this->phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); - $this->lexer = new Lexer(); + $phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); + $lexer = new Lexer(); } + $this->phpDocParser = $phpDocParser; + $this->lexer = $lexer; } /** From 06185b7c96eec95328fcd06375a74abc91564477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20B=C3=AEnzari?= Date: Wed, 7 Dec 2022 10:21:44 +0200 Subject: [PATCH 13/14] feat: add groups filter whitelist info to swagger (#5244) --- src/Api/FilterInterface.php | 5 +++++ src/Serializer/Filter/GroupFilter.php | 24 +++++++++++++++------ tests/Serializer/Filter/GroupFilterTest.php | 22 +++++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/Api/FilterInterface.php b/src/Api/FilterInterface.php index d4467041fda..967147ce336 100644 --- a/src/Api/FilterInterface.php +++ b/src/Api/FilterInterface.php @@ -43,6 +43,11 @@ interface FilterInterface * 'type' => 'integer', * ] * ] + * - schema (optional): schema definition, + * e.g. 'schema' => [ + * 'type' => 'string', + * 'enum' => ['value_1', 'value_2'], + * ] * The description can contain additional data specific to a filter. * * @see \ApiPlatform\OpenApi\Factory\OpenApiFactory::getFiltersParameters diff --git a/src/Serializer/Filter/GroupFilter.php b/src/Serializer/Filter/GroupFilter.php index c8d09c9aee3..0e96fe49bcb 100644 --- a/src/Serializer/Filter/GroupFilter.php +++ b/src/Serializer/Filter/GroupFilter.php @@ -58,13 +58,23 @@ public function apply(Request $request, bool $normalization, array $attributes, */ public function getDescription(string $resourceClass): array { - return [ - "$this->parameterName[]" => [ - 'property' => null, - 'type' => 'string', - 'is_collection' => true, - 'required' => false, - ], + $description = [ + 'property' => null, + 'type' => 'string', + 'is_collection' => true, + 'required' => false, ]; + + if ($this->whitelist) { + $description['schema'] = [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'enum' => $this->whitelist, + ], + ]; + } + + return ["$this->parameterName[]" => $description]; } } diff --git a/tests/Serializer/Filter/GroupFilterTest.php b/tests/Serializer/Filter/GroupFilterTest.php index 12f7238a987..168ffec3319 100644 --- a/tests/Serializer/Filter/GroupFilterTest.php +++ b/tests/Serializer/Filter/GroupFilterTest.php @@ -125,4 +125,26 @@ public function testGetDescription(): void $this->assertSame($expectedDescription, $groupFilter->getDescription(DummyGroup::class)); } + + public function testGetDescriptionWithWhitelist(): void + { + $groupFilter = new GroupFilter('custom_groups', false, ['default_group', 'another_default_group']); + $expectedDescription = [ + 'custom_groups[]' => [ + 'property' => null, + 'type' => 'string', + 'is_collection' => true, + 'required' => false, + 'schema' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'enum' => ['default_group', 'another_default_group'], + ], + ], + ], + ]; + + $this->assertSame($expectedDescription, $groupFilter->getDescription(DummyGroup::class)); + } } From 990148f73c70ce860eb49554952decd1c63050fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Barray?= Date: Tue, 6 Dec 2022 11:35:47 +0100 Subject: [PATCH 14/14] fix: get back return phpdoc on ProviderInterface During migration from 2.7 to 3.0 I noticed we lost this very useful return --- src/State/ProviderInterface.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/State/ProviderInterface.php b/src/State/ProviderInterface.php index ea72049c7f7..8323433b074 100644 --- a/src/State/ProviderInterface.php +++ b/src/State/ProviderInterface.php @@ -26,6 +26,8 @@ interface ProviderInterface { /** * Provides data. + * + * @return T|Pagination\PartialPaginatorInterface|iterable|null */ public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null; }