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;