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 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 f70d0435d9a9b61c04f6e536ebfef338c59feec2 Mon Sep 17 00:00:00 2001 From: Thomas Helmrich Date: Mon, 7 Nov 2022 14:45:18 +0100 Subject: [PATCH 8/8] applied PR #4931 from webda2l --- src/Doctrine/Common/State/LinksHandlerTrait.php | 17 ++++++++++------- src/GraphQl/Resolver/Stage/ReadStage.php | 1 + .../LinkResourceMetadataCollectionFactory.php | 14 +++++++++----- tests/GraphQl/Resolver/Stage/ReadStageTest.php | 3 ++- ...inkResourceMetadataCollectionFactoryTest.php | 1 + 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/Doctrine/Common/State/LinksHandlerTrait.php b/src/Doctrine/Common/State/LinksHandlerTrait.php index badd9d3df50..65aaf3ab447 100644 --- a/src/Doctrine/Common/State/LinksHandlerTrait.php +++ b/src/Doctrine/Common/State/LinksHandlerTrait.php @@ -34,11 +34,13 @@ private function getLinks(string $resourceClass, Operation $operation, array $co return $links; } - $newLinks = []; + $newLink = null; + $linkProperty = $context['linkProperty']; foreach ($links as $link) { - if ($linkClass === $link->getFromClass()) { - $newLinks[] = $link; + if (($linkClass === $link->getFromClass()) && ($linkProperty === $link->getFromProperty())) { + $newLink = $link; + break; } } @@ -62,16 +64,17 @@ private function getLinks(string $resourceClass, Operation $operation, array $co } foreach ($this->getOperationLinks($linkedOperation ?? null) as $link) { - if ($resourceClass === $link->getToClass()) { - $newLinks[] = $link; + if (($resourceClass === $link->getToClass()) && ($linkProperty === $link->getFromProperty())) { + $newLink = $link; + break; } } - if (!$newLinks) { + if (null === $newLink) { throw new RuntimeException(sprintf('The class "%s" cannot be retrieved from "%s".', $resourceClass, $linkClass)); } - return $newLinks; + return [$newLink]; } private function getIdentifierValue(array &$identifiers, string $name = null): mixed diff --git a/src/GraphQl/Resolver/Stage/ReadStage.php b/src/GraphQl/Resolver/Stage/ReadStage.php index b5c7d119030..527b7aea493 100644 --- a/src/GraphQl/Resolver/Stage/ReadStage.php +++ b/src/GraphQl/Resolver/Stage/ReadStage.php @@ -83,6 +83,7 @@ public function __invoke(?string $resourceClass, ?string $rootClass, Operation $ if (isset($source[$info->fieldName], $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY], $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) { $uriVariables = $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY]; $normalizationContext['linkClass'] = $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY]; + $normalizationContext['linkProperty'] = $info->fieldName; } return $this->provider->provide($operation, $uriVariables, $normalizationContext); diff --git a/src/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactory.php index e8e7b9990f0..1c4e5829620 100644 --- a/src/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactory.php @@ -68,18 +68,22 @@ private function mergeLinks(array $links, array $toMergeLinks): array { $classLinks = []; foreach ($links as $link) { - $classLinks[$link->getToClass()] = $link; + $classLinks[$link->getToClass()][$link->getFromProperty()] = $link; } foreach ($toMergeLinks as $link) { - if (isset($classLinks[$link->getToClass()])) { - $classLinks[$link->getToClass()] = $classLinks[$link->getToClass()]->withLink($link); + if (null !== $prevLink = $classLinks[$link->getToClass()][$link->getFromProperty()] ?? null) { + $classLinks[$link->getToClass()][$link->getFromProperty()] = $prevLink->withLink($link); continue; } - $classLinks[$link->getToClass()] = $link; + $classLinks[$link->getToClass()][$link->getFromProperty()] = $link; } - return array_values($classLinks); + // return array_values(array_merge(...array_values($classLinks))); (branch 3.0) + + return array_reduce($classLinks, static function (array $carry, array $item) { + return array_merge($carry, array_values($item)); + }, []); } } diff --git a/tests/GraphQl/Resolver/Stage/ReadStageTest.php b/tests/GraphQl/Resolver/Stage/ReadStageTest.php index 95c53841691..40d81388f37 100644 --- a/tests/GraphQl/Resolver/Stage/ReadStageTest.php +++ b/tests/GraphQl/Resolver/Stage/ReadStageTest.php @@ -199,7 +199,8 @@ public function testApplyCollection(array $args, ?string $rootClass, ?array $sou $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); $this->providerProphecy->provide($operation, [], $normalizationContext + ['filters' => $expectedFilters])->willReturn([]); - $this->providerProphecy->provide($operation, ['id' => 3], $normalizationContext + ['filters' => $expectedFilters, 'linkClass' => 'myResource'])->willReturn(['resource']); + // $this->providerProphecy->provide($operation, ['id' => 3], $normalizationContext + ['filters' => $expectedFilters, 'linkClass' => 'myResource'])->willReturn(['resource']); + $this->providerProphecy->provide($operation, ['id' => 3], $normalizationContext + ['filters' => $expectedFilters, 'linkClass' => 'myResource', 'linkProperty' => 'subresource'])->willReturn(['subresource']); $result = ($this->readStage)($resourceClass, $rootClass, $operation, $context); diff --git a/tests/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php index 174dcb1442b..790bc52fe4d 100644 --- a/tests/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php @@ -84,6 +84,7 @@ class: AttributeResource::class, class: AttributeResource::class, graphQlOperations: [ 'item_query' => (new Query(shortName: 'AttributeResource', class: AttributeResource::class))->withLinks([ + (new Link())->withFromProperty('foo')->withFromClass(AttributeResource::class)->withToClass(Dummy::class)->withIdentifiers(['id']), (new Link())->withFromProperty('foo2')->withFromClass(AttributeResource::class)->withToClass(Dummy::class)->withIdentifiers(['id']), (new Link())->withFromProperty('bar')->withFromClass(AttributeResource::class)->withToClass(RelatedDummy::class)->withIdentifiers(['id']), ]),