diff --git a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php deleted file mode 100644 index 5f7e31f2af0..00000000000 --- a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php +++ /dev/null @@ -1,92 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\QueryCollectionResolverInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\CloneTrait; -use ApiPlatform\State\Pagination\ArrayPaginator; -use GraphQL\Type\Definition\ResolveInfo; -use Psr\Container\ContainerInterface; - -/** - * Creates a function retrieving a collection to resolve a GraphQL query or a field returned by a mutation. - * - * @author Alan Poulain - * @author Kévin Dunglas - * @author Vincent Chalamon - */ -final class CollectionResolverFactory implements ResolverFactoryInterface -{ - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, private readonly SerializeStageInterface $serializeStage, private readonly ContainerInterface $queryResolverLocator) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { - // If authorization has failed for a relation field (e.g. via ApiProperty security), the field is not present in the source: null can be returned directly to ensure the collection isn't in the response. - if (null === $resourceClass || null === $rootClass || (null !== $source && !\array_key_exists($info->fieldName, $source))) { - return null; - } - - if (is_a($resourceClass, \BackedEnum::class, true) && $source && \array_key_exists($info->fieldName, $source)) { - return $source[$info->fieldName]; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - if ($operation instanceof Query && $operation->getNested() && !$operation->getResolver() && !$operation->getProvider() && $source && \array_key_exists($info->fieldName, $source)) { - return ($this->serializeStage)(new ArrayPaginator($source[$info->fieldName], 0, \count($source[$info->fieldName])), $resourceClass, $operation, $resolverContext); - } - - $collection = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (!is_iterable($collection)) { - throw new \LogicException('Collection from read stage should be iterable.'); - } - - $queryResolverId = $operation->getResolver(); - if (null !== $queryResolverId) { - /** @var QueryCollectionResolverInterface $queryResolver */ - $queryResolver = $this->queryResolverLocator->get($queryResolverId); - $collection = $queryResolver($collection, $resolverContext); - } - - // Only perform security stage on the top-level query - if (null === $source) { - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $collection, - ], - ]); - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $collection, - 'previous_object' => $this->clone($collection), - ], - ]); - } - - return ($this->serializeStage)($collection, $resourceClass, $operation, $resolverContext); - }; - } -} diff --git a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php deleted file mode 100644 index ffe1e3d04c2..00000000000 --- a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php +++ /dev/null @@ -1,115 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\MutationResolverInterface; -use ApiPlatform\GraphQl\Resolver\Stage\DeserializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ValidateStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface; -use ApiPlatform\Metadata\DeleteOperationInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Metadata\Util\CloneTrait; -use GraphQL\Type\Definition\ResolveInfo; -use Psr\Container\ContainerInterface; - -/** - * Creates a function resolving a GraphQL mutation of an item. - * - * @author Alan Poulain - * @author Vincent Chalamon - */ -final class ItemMutationResolverFactory implements ResolverFactoryInterface -{ - use ClassInfoTrait; - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, private readonly SerializeStageInterface $serializeStage, private readonly DeserializeStageInterface $deserializeStage, private readonly WriteStageInterface $writeStage, private readonly ValidateStageInterface $validateStage, private readonly ContainerInterface $mutationResolverLocator, private readonly SecurityPostValidationStageInterface $securityPostValidationStage) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { - if (null === $resourceClass || null === $operation) { - return null; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $item = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (null !== $item && !\is_object($item)) { - throw new \LogicException('Item from read stage should be a nullable object.'); - } - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - ], - ]); - $previousItem = $this->clone($item); - - if ('delete' === $operation->getName() || $operation instanceof DeleteOperationInterface) { - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $previousItem, - ], - ]); - $item = ($this->writeStage)($item, $resourceClass, $operation, $resolverContext); - - return ($this->serializeStage)($item, $resourceClass, $operation, $resolverContext); - } - - $item = ($this->deserializeStage)($item, $resourceClass, $operation, $resolverContext); - - $mutationResolverId = $operation->getResolver(); - if (null !== $mutationResolverId) { - /** @var MutationResolverInterface $mutationResolver */ - $mutationResolver = $this->mutationResolverLocator->get($mutationResolverId); - $item = $mutationResolver($item, $resolverContext); - if (null !== $item && $resourceClass !== $itemClass = $this->getObjectClass($item)) { - throw new \LogicException(sprintf('Custom mutation resolver "%s" has to return an item of class %s but returned an item of class %s.', $mutationResolverId, $operation->getShortName(), (new \ReflectionClass($itemClass))->getShortName())); - } - } - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $previousItem, - ], - ]); - - if (null !== $item) { - ($this->validateStage)($item, $resourceClass, $operation, $resolverContext); - - ($this->securityPostValidationStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $previousItem, - ], - ]); - - $persistResult = ($this->writeStage)($item, $resourceClass, $operation, $resolverContext); - } - - return ($this->serializeStage)($persistResult ?? $item, $resourceClass, $operation, $resolverContext); - }; - } -} diff --git a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemResolverFactory.php deleted file mode 100644 index 45820aaeed4..00000000000 --- a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php +++ /dev/null @@ -1,116 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Metadata\Util\CloneTrait; -use GraphQL\Type\Definition\ResolveInfo; -use Psr\Container\ContainerInterface; - -/** - * Creates a function retrieving an item to resolve a GraphQL query. - * - * @author Alan Poulain - * @author Kévin Dunglas - * @author Vincent Chalamon - */ -final class ItemResolverFactory implements ResolverFactoryInterface -{ - use ClassInfoTrait; - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, private readonly SerializeStageInterface $serializeStage, private readonly ContainerInterface $queryResolverLocator) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) { - // Data already fetched and normalized (field or nested resource) - if ($source && \array_key_exists($info->fieldName, $source)) { - return $source[$info->fieldName]; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - if (!$operation) { - $operation = new Query(); - } - - $item = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (null !== $item && !\is_object($item)) { - throw new \LogicException('Item from read stage should be a nullable object.'); - } - - $resourceClass = $operation->getOutput()['class'] ?? $resourceClass; - // The item retrieved can be of another type when using an identifier (see Relay Nodes at query.feature:23) - $resourceClass = $this->getResourceClass($item, $resourceClass); - $queryResolverId = $operation->getResolver(); - if (null !== $queryResolverId) { - /** @var QueryItemResolverInterface $queryResolver */ - $queryResolver = $this->queryResolverLocator->get($queryResolverId); - $item = $queryResolver($item, $resolverContext); - $resourceClass = $this->getResourceClass($item, $resourceClass, sprintf('Custom query resolver "%s"', $queryResolverId).' has to return an item of class %s but returned an item of class %s.'); - } - - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - ], - ]); - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $this->clone($item), - ], - ]); - - return ($this->serializeStage)($item, $resourceClass, $operation, $resolverContext); - }; - } - - /** - * @throws \UnexpectedValueException - */ - private function getResourceClass(?object $item, ?string $resourceClass, string $errorMessage = 'Resolver only handles items of class %s but retrieved item is of class %s.'): string - { - if (null === $item) { - if (null === $resourceClass) { - throw new \UnexpectedValueException('Resource class cannot be determined.'); - } - - return $resourceClass; - } - - $itemClass = $this->getObjectClass($item); - - if (null === $resourceClass) { - return $itemClass; - } - - if ($resourceClass !== $itemClass && !$item instanceof $resourceClass) { - throw new \UnexpectedValueException(sprintf($errorMessage, (new \ReflectionClass($resourceClass))->getShortName(), (new \ReflectionClass($itemClass))->getShortName())); - } - - return $resourceClass; - } -} diff --git a/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php deleted file mode 100644 index b182ba528ff..00000000000 --- a/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface; -use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Metadata\Util\CloneTrait; -use GraphQL\Type\Definition\ResolveInfo; - -/** - * Creates a function resolving a GraphQL subscription of an item. - * - * @author Alan Poulain - */ -final class ItemSubscriptionResolverFactory implements ResolverFactoryInterface -{ - use ClassInfoTrait; - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SerializeStageInterface $serializeStage, private readonly SubscriptionManagerInterface $subscriptionManager, private readonly ?MercureSubscriptionIriGeneratorInterface $mercureSubscriptionIriGenerator) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { - if (null === $resourceClass || null === $operation) { - return null; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $item = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (null !== $item && !\is_object($item)) { - throw new \LogicException('Item from read stage should be a nullable object.'); - } - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - ], - ]); - - $result = ($this->serializeStage)($item, $resourceClass, $operation, $resolverContext); - - $subscriptionId = $this->subscriptionManager->retrieveSubscriptionId($resolverContext, $result); - - if ($subscriptionId && ($mercure = $operation->getMercure())) { - if (!$this->mercureSubscriptionIriGenerator) { - throw new \LogicException('Cannot use Mercure for subscriptions when MercureBundle is not installed. Try running "composer require mercure".'); - } - - $hub = \is_array($mercure) ? ($mercure['hub'] ?? null) : null; - $result['mercureUrl'] = $this->mercureSubscriptionIriGenerator->generateMercureUrl($subscriptionId, $hub); - } - - return $result; - }; - } -} diff --git a/src/GraphQl/Resolver/Stage/DeserializeStage.php b/src/GraphQl/Resolver/Stage/DeserializeStage.php deleted file mode 100644 index 0e7f7daec1e..00000000000 --- a/src/GraphQl/Resolver/Stage/DeserializeStage.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; - -/** - * Deserialize stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class DeserializeStage implements DeserializeStageInterface -{ - public function __construct(private readonly DenormalizerInterface $denormalizer, private readonly SerializerContextBuilderInterface $serializerContextBuilder) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(?object $objectToPopulate, string $resourceClass, Operation $operation, array $context): ?object - { - if (!($operation->canDeserialize() ?? true)) { - return $objectToPopulate; - } - - $denormalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, false); - if (null !== $objectToPopulate) { - $denormalizationContext[AbstractNormalizer::OBJECT_TO_POPULATE] = $objectToPopulate; - } - - $item = $this->denormalizer->denormalize($context['args']['input'], $resourceClass, ItemNormalizer::FORMAT, $denormalizationContext); - - if (!\is_object($item)) { - throw new \UnexpectedValueException('Expected item to be an object.'); - } - - return $item; - } -} diff --git a/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php b/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php deleted file mode 100644 index 586104961c4..00000000000 --- a/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Deserialize stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -interface DeserializeStageInterface -{ - public function __invoke(?object $objectToPopulate, string $resourceClass, Operation $operation, array $context): ?object; -} diff --git a/src/GraphQl/Resolver/Stage/ReadStage.php b/src/GraphQl/Resolver/Stage/ReadStage.php deleted file mode 100644 index 9ab1007eab0..00000000000 --- a/src/GraphQl/Resolver/Stage/ReadStage.php +++ /dev/null @@ -1,190 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\Exception\ItemNotFoundException; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\State\ProviderInterface; -use GraphQL\Type\Definition\ResolveInfo; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - -/** - * Read stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class ReadStage implements ReadStageInterface -{ - use IdentifierTrait; - - public function __construct(private readonly IriConverterInterface $iriConverter, private readonly ProviderInterface $provider, private readonly SerializerContextBuilderInterface $serializerContextBuilder, private readonly string $nestingSeparator) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(?string $resourceClass, ?string $rootClass, Operation $operation, array $context): object|array|null - { - if (!($operation->canRead() ?? true)) { - return $context['is_collection'] ? [] : null; - } - - $args = $context['args']; - $normalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, true); - - if (!$context['is_collection']) { - $identifier = $this->getIdentifierFromContext($context); - $item = $this->getItem($identifier, $normalizationContext); - - if ($identifier && ($context['is_mutation'] || $context['is_subscription'])) { - if (null === $item) { - throw new NotFoundHttpException(sprintf('Item "%s" not found.', $args['input']['id'])); - } - - if ($resourceClass !== $this->getObjectClass($item)) { - throw new \UnexpectedValueException(sprintf('Item "%s" did not match expected type "%s".', $args['input']['id'], $operation->getShortName())); - } - } - - return $item; - } - - if (null === $rootClass) { - return []; - } - - $uriVariables = []; - $normalizationContext['filters'] = $this->getNormalizedFilters($args); - $normalizationContext['operation'] = $operation; - - $source = $context['source']; - /** @var ResolveInfo $info */ - $info = $context['info']; - if (isset($source[$info->fieldName], $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY], $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) { - $uriVariables = $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY]; - $normalizationContext['linkClass'] = $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY]; - $normalizationContext['linkProperty'] = $info->fieldName; - } - - return $this->provider->provide($operation, $uriVariables, $normalizationContext); - } - - private function getItem(?string $identifier, array $normalizationContext): ?object - { - if (null === $identifier) { - return null; - } - - try { - $item = $this->iriConverter->getResourceFromIri($identifier, $normalizationContext); - } catch (ItemNotFoundException) { - return null; - } - - return $item; - } - - private function getNormalizedFilters(array $args): array - { - $filters = $args; - - foreach ($filters as $name => $value) { - if (\is_array($value)) { - if (strpos($name, '_list')) { - $name = substr($name, 0, \strlen($name) - \strlen('_list')); - } - - // If the value contains arrays, we need to merge them for the filters to understand this syntax, proper to GraphQL to preserve the order of the arguments. - if ($this->isSequentialArrayOfArrays($value)) { - $value = array_merge(...$value); - } - $filters[$name] = $this->getNormalizedFilters($value); - } - - if (\is_string($name) && strpos($name, $this->nestingSeparator)) { - // Gives a chance to relations/nested fields. - $index = array_search($name, array_keys($filters), true); - $filters = - \array_slice($filters, 0, $index + 1) + - [str_replace($this->nestingSeparator, '.', $name) => $value] + - \array_slice($filters, $index + 1); - } - } - - return $filters; - } - - public function isSequentialArrayOfArrays(array $array): bool - { - if (!$this->isSequentialArray($array)) { - return false; - } - - return $this->arrayContainsOnly($array, 'array'); - } - - public function isSequentialArray(array $array): bool - { - if ([] === $array) { - return false; - } - - return array_is_list($array); - } - - public function arrayContainsOnly(array $array, string $type): bool - { - return $array === array_filter($array, static fn ($item): bool => $type === \gettype($item)); - } - - /** - * Get class name of the given object. - */ - private function getObjectClass(object $object): string - { - return $this->getRealClassName($object::class); - } - - /** - * Get the real class name of a class name that could be a proxy. - */ - private function getRealClassName(string $className): string - { - // __CG__: Doctrine Common Marker for Proxy (ODM < 2.0 and ORM < 3.0) - // __PM__: Ocramius Proxy Manager (ODM >= 2.0) - $positionCg = strrpos($className, '\\__CG__\\'); - $positionPm = strrpos($className, '\\__PM__\\'); - - if (false === $positionCg && false === $positionPm) { - return $className; - } - - if (false !== $positionCg) { - return substr($className, $positionCg + 8); - } - - $className = ltrim($className, '\\'); - - return substr( - $className, - 8 + $positionPm, - strrpos($className, '\\') - ($positionPm + 8) - ); - } -} diff --git a/src/GraphQl/Resolver/Stage/ReadStageInterface.php b/src/GraphQl/Resolver/Stage/ReadStageInterface.php deleted file mode 100644 index fa0e941ea20..00000000000 --- a/src/GraphQl/Resolver/Stage/ReadStageInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Read stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -interface ReadStageInterface -{ - public function __invoke(?string $resourceClass, ?string $rootClass, Operation $operation, array $context): object|array|null; -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php deleted file mode 100644 index f87d48ed545..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface as LegacyResourceAccessCheckerInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * Security post denormalize stage of GraphQL resolvers. - * - * @author Vincent Chalamon - */ -final class SecurityPostDenormalizeStage implements SecurityPostDenormalizeStageInterface -{ - /** - * @var LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface - */ - private $resourceAccessChecker; - - /** - * @param LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface|null $resourceAccessChecker - */ - public function __construct($resourceAccessChecker) - { - $this->resourceAccessChecker = $resourceAccessChecker; - } - - /** - * {@inheritdoc} - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void - { - $isGranted = $operation->getSecurityPostDenormalize(); - - if (null !== $isGranted && null === $this->resourceAccessChecker) { - throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".'); - } - - if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) { - return; - } - - throw new AccessDeniedHttpException($operation->getSecurityPostDenormalizeMessage() ?? 'Access Denied.'); - } -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php deleted file mode 100644 index a62ba52731d..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Security post deserialization stage of GraphQL resolvers. - * - * @author Vincent Chalamon - */ -interface SecurityPostDenormalizeStageInterface -{ - /** - * @throws Error - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostValidationStage.php b/src/GraphQl/Resolver/Stage/SecurityPostValidationStage.php deleted file mode 100644 index 4ab18fa8716..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostValidationStage.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface as LegacyResourceAccessCheckerInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * Security post validation stage of GraphQL resolvers. - * - * @deprecated use providers instead of stages - * - * @author Vincent Chalamon - * @author Grégoire Pineau - */ -final class SecurityPostValidationStage implements SecurityPostValidationStageInterface -{ - /** - * @var LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface - */ - private $resourceAccessChecker; - - /** - * @param LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface|null $resourceAccessChecker - */ - public function __construct($resourceAccessChecker) - { - $this->resourceAccessChecker = $resourceAccessChecker; - } - - /** - * {@inheritdoc} - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void - { - $isGranted = $operation->getSecurityPostValidation(); - - if (null !== $isGranted && null === $this->resourceAccessChecker) { - throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".'); - } - - if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) { - return; - } - - throw new AccessDeniedHttpException($operation->getSecurityPostValidationMessage() ?? 'Access Denied.'); - } -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php deleted file mode 100644 index bbbbf7d4d05..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Security post validation stage of GraphQL resolvers. - * - * @author Vincent Chalamon - * @author Grégoire Pineau - */ -interface SecurityPostValidationStageInterface -{ - /** - * @throws Error - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/SecurityStage.php b/src/GraphQl/Resolver/Stage/SecurityStage.php deleted file mode 100644 index b577fc1c8a0..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityStage.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface as LegacyResourceAccessCheckerInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * Security stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class SecurityStage implements SecurityStageInterface -{ - /** - * @var LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface - */ - private $resourceAccessChecker; - - /** - * @param LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface|null $resourceAccessChecker - */ - public function __construct($resourceAccessChecker) - { - $this->resourceAccessChecker = $resourceAccessChecker; - } - - /** - * {@inheritdoc} - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void - { - $isGranted = $operation->getSecurity(); - - if (null !== $isGranted && null === $this->resourceAccessChecker) { - throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".'); - } - - if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) { - return; - } - - throw new AccessDeniedHttpException($operation->getSecurityMessage() ?? 'Access Denied.'); - } -} diff --git a/src/GraphQl/Resolver/Stage/SecurityStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityStageInterface.php deleted file mode 100644 index a1f47fad9bf..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityStageInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Security stage of GraphQL resolvers. - * - * @author Alan Poulain - * @author Vincent Chalamon - */ -interface SecurityStageInterface -{ - /** - * @throws Error - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/SerializeStage.php b/src/GraphQl/Resolver/Stage/SerializeStage.php deleted file mode 100644 index e10298f0a1e..00000000000 --- a/src/GraphQl/Resolver/Stage/SerializeStage.php +++ /dev/null @@ -1,244 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Subscription; -use ApiPlatform\State\Pagination\HasNextPagePaginatorInterface; -use ApiPlatform\State\Pagination\Pagination; -use ApiPlatform\State\Pagination\PaginatorInterface; -use ApiPlatform\State\Pagination\PartialPaginatorInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Serialize stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class SerializeStage implements SerializeStageInterface -{ - use IdentifierTrait; - - public function __construct(private readonly NormalizerInterface $normalizer, private readonly SerializerContextBuilderInterface $serializerContextBuilder, private readonly Pagination $pagination) - { - } - - public function __invoke(object|array|null $itemOrCollection, string $resourceClass, Operation $operation, array $context): ?array - { - $isCollection = $operation instanceof CollectionOperationInterface; - $isMutation = $operation instanceof Mutation; - $isSubscription = $operation instanceof Subscription; - $shortName = $operation->getShortName(); - $operationName = $operation->getName(); - - if (!($operation->canSerialize() ?? true)) { - if ($isCollection) { - if ($this->pagination->isGraphQlEnabled($operation, $context)) { - return 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ? - $this->getDefaultCursorBasedPaginatedData() : - $this->getDefaultPageBasedPaginatedData(); - } - - return []; - } - - if ($isMutation) { - return $this->getDefaultMutationData($context); - } - - if ($isSubscription) { - return $this->getDefaultSubscriptionData($context); - } - - return null; - } - - $normalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, true); - - $data = null; - if (!$isCollection) { - if ($isMutation && 'delete' === $operationName) { - $data = ['id' => $this->getIdentifierFromContext($context)]; - } else { - $data = $this->normalizer->normalize($itemOrCollection, ItemNormalizer::FORMAT, $normalizationContext); - } - } - - if ($isCollection && is_iterable($itemOrCollection)) { - if (!$this->pagination->isGraphQlEnabled($operation, $context)) { - $data = []; - foreach ($itemOrCollection as $index => $object) { - $data[$index] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext); - } - } else { - $data = 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ? - $this->serializeCursorBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context) : - $this->serializePageBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context); - } - } - - if (null !== $data && !\is_array($data)) { - throw new \UnexpectedValueException('Expected serialized data to be a nullable array.'); - } - - if ($isMutation || $isSubscription) { - $wrapFieldName = lcfirst($shortName); - - return [$wrapFieldName => $data] + ($isMutation ? $this->getDefaultMutationData($context) : $this->getDefaultSubscriptionData($context)); - } - - return $data; - } - - /** - * @throws \LogicException - * @throws \UnexpectedValueException - */ - private function serializeCursorBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array - { - $args = $context['args']; - - if (!($collection instanceof PartialPaginatorInterface)) { - throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s or %s.', PaginatorInterface::class, PartialPaginatorInterface::class)); - } - - $selection = $context['info']->getFieldSelection(1); - - $offset = 0; - $totalItems = 1; // For partial pagination, always consider there is at least one item. - $data = ['edges' => []]; - if (isset($selection['pageInfo']) || isset($selection['totalCount']) || isset($selection['edges']['cursor'])) { - $nbPageItems = $collection->count(); - if (isset($args['after'])) { - $after = base64_decode($args['after'], true); - if (false === $after || '' === $args['after']) { - throw new \UnexpectedValueException('' === $args['after'] ? 'Empty cursor is invalid' : sprintf('Cursor %s is invalid', $args['after'])); - } - $offset = 1 + (int) $after; - } - - if ($collection instanceof PaginatorInterface && (isset($selection['pageInfo']) || isset($selection['totalCount']))) { - $totalItems = $collection->getTotalItems(); - if (isset($args['before'])) { - $before = base64_decode($args['before'], true); - if (false === $before || '' === $args['before']) { - throw new \UnexpectedValueException('' === $args['before'] ? 'Empty cursor is invalid' : sprintf('Cursor %s is invalid', $args['before'])); - } - $offset = (int) $before - $nbPageItems; - } - if (isset($args['last']) && !isset($args['before'])) { - $offset = $totalItems - $args['last']; - } - } - - $offset = max(0, $offset); - - $data = $this->getDefaultCursorBasedPaginatedData(); - if ((isset($selection['pageInfo']) || isset($selection['totalCount'])) && $totalItems > 0) { - isset($selection['pageInfo']['startCursor']) && $data['pageInfo']['startCursor'] = base64_encode((string) $offset); - $end = $offset + $nbPageItems - 1; - isset($selection['pageInfo']['endCursor']) && $data['pageInfo']['endCursor'] = base64_encode((string) max($end, 0)); - isset($selection['pageInfo']['hasPreviousPage']) && $data['pageInfo']['hasPreviousPage'] = $offset > 0; - if ($collection instanceof PaginatorInterface) { - isset($selection['totalCount']) && $data['totalCount'] = $totalItems; - - $itemsPerPage = $collection->getItemsPerPage(); - isset($selection['pageInfo']['hasNextPage']) && $data['pageInfo']['hasNextPage'] = (float) ($itemsPerPage > 0 ? $offset % $itemsPerPage : $offset) + $itemsPerPage * $collection->getCurrentPage() < $totalItems; - } - } - } - - $index = 0; - foreach ($collection as $object) { - $edge = [ - 'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext), - ]; - if (isset($selection['edges']['cursor'])) { - $edge['cursor'] = base64_encode((string) ($index + $offset)); - } - $data['edges'][$index] = $edge; - ++$index; - } - - return $data; - } - - /** - * @throws \LogicException - */ - private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array - { - $data = ['collection' => []]; - - $selection = $context['info']->getFieldSelection(1); - if (isset($selection['paginationInfo'])) { - $data['paginationInfo'] = []; - if (isset($selection['paginationInfo']['itemsPerPage'])) { - if (!($collection instanceof PartialPaginatorInterface)) { - throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return itemsPerPage field.', PartialPaginatorInterface::class)); - } - $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage(); - } - if (isset($selection['paginationInfo']['totalCount'])) { - if (!($collection instanceof PaginatorInterface)) { - throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return totalCount field.', PaginatorInterface::class)); - } - $data['paginationInfo']['totalCount'] = $collection->getTotalItems(); - } - if (isset($selection['paginationInfo']['lastPage'])) { - if (!($collection instanceof PaginatorInterface)) { - throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return lastPage field.', PaginatorInterface::class)); - } - $data['paginationInfo']['lastPage'] = $collection->getLastPage(); - } - if (isset($selection['paginationInfo']['hasNextPage'])) { - if (!($collection instanceof HasNextPagePaginatorInterface)) { - throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return hasNextPage field.', HasNextPagePaginatorInterface::class)); - } - $data['paginationInfo']['hasNextPage'] = $collection->hasNextPage(); - } - } - - foreach ($collection as $object) { - $data['collection'][] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext); - } - - return $data; - } - - private function getDefaultCursorBasedPaginatedData(): array - { - return ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]]; - } - - private function getDefaultPageBasedPaginatedData(): array - { - return ['collection' => [], 'paginationInfo' => ['itemsPerPage' => 0., 'totalCount' => 0., 'lastPage' => 0., 'hasNextPage' => false]]; - } - - private function getDefaultMutationData(array $context): array - { - return ['clientMutationId' => $context['args']['input']['clientMutationId'] ?? null]; - } - - private function getDefaultSubscriptionData(array $context): array - { - return ['clientSubscriptionId' => $context['args']['input']['clientSubscriptionId'] ?? null]; - } -} diff --git a/src/GraphQl/Resolver/Stage/SerializeStageInterface.php b/src/GraphQl/Resolver/Stage/SerializeStageInterface.php deleted file mode 100644 index ac67e9b97f0..00000000000 --- a/src/GraphQl/Resolver/Stage/SerializeStageInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Serialize stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -interface SerializeStageInterface -{ - public function __invoke(object|array|null $itemOrCollection, string $resourceClass, Operation $operation, array $context): ?array; -} diff --git a/src/GraphQl/Resolver/Stage/ValidateStage.php b/src/GraphQl/Resolver/Stage/ValidateStage.php deleted file mode 100644 index c7c2103b4b3..00000000000 --- a/src/GraphQl/Resolver/Stage/ValidateStage.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Validator\ValidatorInterface; - -/** - * Validate stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class ValidateStage implements ValidateStageInterface -{ - public function __construct(private readonly ValidatorInterface $validator) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(object $object, string $resourceClass, Operation $operation, array $context): void - { - if (!($operation->canValidate() ?? true)) { - return; - } - - $this->validator->validate($object, $operation->getValidationContext() ?? []); - } -} diff --git a/src/GraphQl/Resolver/Stage/ValidateStageInterface.php b/src/GraphQl/Resolver/Stage/ValidateStageInterface.php deleted file mode 100644 index 092a1d5aee4..00000000000 --- a/src/GraphQl/Resolver/Stage/ValidateStageInterface.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Validate stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -interface ValidateStageInterface -{ - /** - * @throws Error - */ - public function __invoke(object $object, string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/WriteStage.php b/src/GraphQl/Resolver/Stage/WriteStage.php deleted file mode 100644 index 35bd780154b..00000000000 --- a/src/GraphQl/Resolver/Stage/WriteStage.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\State\ProcessorInterface; - -/** - * Write stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class WriteStage implements WriteStageInterface -{ - public function __construct(private readonly ProcessorInterface $processor, private readonly SerializerContextBuilderInterface $serializerContextBuilder) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(?object $data, string $resourceClass, Operation $operation, array $context): ?object - { - if (null === $data || !($operation->canWrite() ?? true)) { - return $data; - } - - $denormalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, false); - - return $this->processor->process($data, $operation, [], ['operation' => $operation] + $denormalizationContext); - } -} diff --git a/src/GraphQl/Resolver/Stage/WriteStageInterface.php b/src/GraphQl/Resolver/Stage/WriteStageInterface.php deleted file mode 100644 index 9d090ce59c9..00000000000 --- a/src/GraphQl/Resolver/Stage/WriteStageInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Write stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -interface WriteStageInterface -{ - public function __invoke(?object $data, string $resourceClass, Operation $operation, array $context): ?object; -} diff --git a/src/GraphQl/Subscription/SubscriptionManager.php b/src/GraphQl/Subscription/SubscriptionManager.php index 2cd2c2c6873..702414d4d7b 100644 --- a/src/GraphQl/Subscription/SubscriptionManager.php +++ b/src/GraphQl/Subscription/SubscriptionManager.php @@ -37,7 +37,7 @@ final class SubscriptionManager implements OperationAwareSubscriptionManagerInte use ResourceClassInfoTrait; use SortTrait; - public function __construct(private readonly CacheItemPoolInterface $subscriptionsCache, private readonly SubscriptionIdentifierGeneratorInterface $subscriptionIdentifierGenerator, private readonly ?SerializeStageInterface $serializeStage, private readonly IriConverterInterface $iriConverter, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ?ProcessorInterface $normalizeProcessor = null) + public function __construct(private readonly CacheItemPoolInterface $subscriptionsCache, private readonly SubscriptionIdentifierGeneratorInterface $subscriptionIdentifierGenerator, private readonly ProcessorInterface $normalizeProcessor, private readonly IriConverterInterface $iriConverter, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) { } @@ -85,13 +85,7 @@ public function getPushPayloads(object $object): array $resolverContext = ['fields' => $subscriptionFields, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; /** @var Operation */ $operation = (new Subscription())->withName('update_subscription')->withShortName($shortName); - if ($this->normalizeProcessor) { - $data = $this->normalizeProcessor->process($object, $operation, [], $resolverContext); - } elseif ($this->serializeStage) { - $data = ($this->serializeStage)($object, $resourceClass, $operation, $resolverContext); - } else { - throw new \LogicException(); - } + $data = $this->normalizeProcessor->process($object, $operation, [], $resolverContext); unset($data['clientSubscriptionId']); diff --git a/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php deleted file mode 100644 index 2bb08db8033..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php +++ /dev/null @@ -1,269 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\CollectionResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Tests\Fixtures\Enum\GenderTypeEnum; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; - -/** - * @author Alan Poulain - * @author Kévin Dunglas - */ -class CollectionResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private CollectionResolverFactory $collectionResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $securityPostDenormalizeStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $queryResolverLocatorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->securityPostDenormalizeStageProphecy = $this->prophesize(SecurityPostDenormalizeStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->queryResolverLocatorProphecy = $this->prophesize(ContainerInterface::class); - - $this->collectionResolverFactory = new CollectionResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->securityPostDenormalizeStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->queryResolverLocatorProphecy->reveal(), - ); - } - - public function testResolve(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['testField' => 0]; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn(['testField' => true]); - $info = $infoProphecy->reveal(); - $info->fieldName = 'testField'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - 'previous_object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageCollection, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveEnumFieldFromSource(): void - { - $resourceClass = GenderTypeEnum::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['genders' => [GenderTypeEnum::MALE, GenderTypeEnum::FEMALE]]; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'genders'; - - $this->assertSame([GenderTypeEnum::MALE, GenderTypeEnum::FEMALE], ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveFieldNotInSource(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['source']; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn(['testField' => true]); - $info = $infoProphecy->reveal(); - $info->fieldName = 'testField'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldNotBeCalled(); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - 'previous_object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - - // Null should be returned if the field isn't in the source - as its lack of presence will be due to @ApiProperty security stripping unauthorized fields - $this->assertNull(($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullSource(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = null; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - 'previous_object' => $readStageCollection, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageCollection, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullResourceClass(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['source']; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - - $this->assertNull(($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullRootClass(): void - { - $resourceClass = \stdClass::class; - $rootClass = null; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['source']; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - - $this->assertNull(($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveBadReadStageCollection(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = null; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Collection from read stage should be iterable.'); - - ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveCustom(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withResolver('query_resolver_id')->withName($operationName); - $source = null; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $customCollection = [new \stdClass()]; - $customCollection[0]->field = 'foo'; - $this->queryResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): array => $customCollection); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customCollection, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customCollection, - 'previous_object' => $customCollection, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($customCollection, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } -} diff --git a/src/GraphQl/Tests/Resolver/Factory/ItemMutationResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ItemMutationResolverFactoryTest.php deleted file mode 100644 index c7220499409..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/ItemMutationResolverFactoryTest.php +++ /dev/null @@ -1,321 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\ItemMutationResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\DeserializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ValidateStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy; -use ApiPlatform\Metadata\GraphQl\Mutation; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; - -/** - * @author Alan Poulain - */ -class ItemMutationResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private ItemMutationResolverFactory $itemMutationResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $securityPostDenormalizeStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $deserializeStageProphecy; - private ObjectProphecy $writeStageProphecy; - private ObjectProphecy $validateStageProphecy; - private ObjectProphecy $mutationResolverLocatorProphecy; - private ObjectProphecy $securityPostValidationStageProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->securityPostDenormalizeStageProphecy = $this->prophesize(SecurityPostDenormalizeStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->deserializeStageProphecy = $this->prophesize(DeserializeStageInterface::class); - $this->writeStageProphecy = $this->prophesize(WriteStageInterface::class); - $this->validateStageProphecy = $this->prophesize(ValidateStageInterface::class); - $this->mutationResolverLocatorProphecy = $this->prophesize(ContainerInterface::class); - $this->securityPostValidationStageProphecy = $this->prophesize(SecurityPostValidationStageInterface::class); - - $this->itemMutationResolverFactory = new ItemMutationResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->securityPostDenormalizeStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->deserializeStageProphecy->reveal(), - $this->writeStageProphecy->reveal(), - $this->validateStageProphecy->reveal(), - $this->mutationResolverLocatorProphecy->reveal(), - $this->securityPostValidationStageProphecy->reveal() - ); - } - - public function testResolve(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = new \stdClass(); - $deserializeStageItem->field = 'deserialize'; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $deserializeStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke($deserializeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled(); - - $writeStageItem = new \stdClass(); - $writeStageItem->field = 'write'; - $this->writeStageProphecy->__invoke($deserializeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($writeStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($writeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullResourceClass(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operation = (new Mutation())->withName('create'); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullOperation(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemMutationResolverFactory)($resourceClass, $rootClass, null)($source, $args, null, $info)); - } - - public function testResolveBadReadStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = []; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Item from read stage should be a nullable object.'); - - ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveNullDeserializeStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = null; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $deserializeStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $this->writeStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $serializeStageData = null; - $this->serializeStageProphecy->__invoke($deserializeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertNull(($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveDelete(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'delete'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->deserializeStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $writeStageItem = new \stdClass(); - $writeStageItem->field = 'write'; - $this->writeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($writeStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($writeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveCustom(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withResolver('query_resolver_id')->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = new \stdClass(); - $deserializeStageItem->field = 'deserialize'; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $customItem = new \stdClass(); - $customItem->field = 'foo'; - $this->mutationResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): \stdClass => $customItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke($customItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled(); - - $writeStageItem = new \stdClass(); - $writeStageItem->field = 'write'; - $this->writeStageProphecy->__invoke($customItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($writeStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($writeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveCustomBadItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withResolver('query_resolver_id')->withName($operationName)->withShortName('shortName'); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = new \stdClass(); - $deserializeStageItem->field = 'deserialize'; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $customItem = new Dummy(); - $this->mutationResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): Dummy => $customItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Custom mutation resolver "query_resolver_id" has to return an item of class shortName but returned an item of class Dummy.'); - - ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } -} diff --git a/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php deleted file mode 100644 index 7ab86f63f5f..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php +++ /dev/null @@ -1,268 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\ItemResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ChildFoo; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ParentFoo; -use ApiPlatform\Metadata\GraphQl\Query; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; - -/** - * @author Alan Poulain - * @author Kévin Dunglas - */ -class ItemResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private ItemResolverFactory $itemResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $securityPostDenormalizeStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $queryResolverLocatorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->securityPostDenormalizeStageProphecy = $this->prophesize(SecurityPostDenormalizeStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->queryResolverLocatorProphecy = $this->prophesize(ContainerInterface::class); - - $this->itemResolverFactory = new ItemResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->securityPostDenormalizeStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->queryResolverLocatorProphecy->reveal() - ); - } - - /** - * @dataProvider itemResourceProvider - */ - public function testResolve(?string $resourceClass, string $determinedResourceClass, ?object $readStageItem): void - { - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->securityStageProphecy->__invoke($determinedResourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($determinedResourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $determinedResourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public static function itemResourceProvider(): array - { - return [ - 'nominal' => [\stdClass::class, \stdClass::class, new \stdClass()], - 'null item' => [\stdClass::class, \stdClass::class, null], - 'null resource class' => [null, \stdClass::class, new \stdClass()], - ]; - } - - public function testResolveNested(): void - { - $source = ['nested' => ['already_serialized']]; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'nested'; - - $this->assertEquals(['already_serialized'], ($this->itemResolverFactory)('resourceClass')($source, [], null, $info)); - } - - public function testResolveNestedNullValue(): void - { - $source = ['nestedNullValue' => null]; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'nestedNullValue'; - - $this->assertNull(($this->itemResolverFactory)('resourceClass')($source, [], null, $info)); - } - - public function testResolveBadReadStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = []; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Item from read stage should be a nullable object.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveNoResourceNoItem(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = null; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Resource class cannot be determined.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveBadItem(): void - { - $resourceClass = Dummy::class; - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Resolver only handles items of class Dummy but retrieved item is of class stdClass.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveCustom(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'custom_query'; - $operation = (new Query())->withResolver('query_resolver_id')->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $customItem = new \stdClass(); - $customItem->field = 'foo'; - $this->queryResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): \stdClass => $customItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customItem, - 'previous_object' => $customItem, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($customItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveCustomBadItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'custom_query'; - $operation = (new Query())->withResolver('query_resolver_id')->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $customItem = new Dummy(); - $this->queryResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): Dummy => $customItem); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Custom query resolver "query_resolver_id" has to return an item of class stdClass but returned an item of class Dummy.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveInheritedClass(): void - { - $resourceClass = ParentFoo::class; - $rootClass = $resourceClass; - $operationName = 'custom_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new ChildFoo(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } -} diff --git a/src/GraphQl/Tests/Resolver/Factory/ItemSubscriptionResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ItemSubscriptionResolverFactoryTest.php deleted file mode 100644 index 5d455a6df0f..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/ItemSubscriptionResolverFactoryTest.php +++ /dev/null @@ -1,197 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\ItemSubscriptionResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface; -use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Subscription; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; - -/** - * @author Alan Poulain - */ -class ItemSubscriptionResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private ItemSubscriptionResolverFactory $itemSubscriptionResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $subscriptionManagerProphecy; - private ObjectProphecy $mercureSubscriptionIriGeneratorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->subscriptionManagerProphecy = $this->prophesize(SubscriptionManagerInterface::class); - $this->mercureSubscriptionIriGeneratorProphecy = $this->prophesize(MercureSubscriptionIriGeneratorInterface::class); - - $this->itemSubscriptionResolverFactory = new ItemSubscriptionResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->subscriptionManagerProphecy->reveal(), - $this->mercureSubscriptionIriGeneratorProphecy->reveal() - ); - } - - public function testResolve(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withMercure(true)->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $subscriptionId = 'subscriptionId'; - $this->subscriptionManagerProphecy->retrieveSubscriptionId($resolverContext, $serializeStageData)->shouldBeCalled()->willReturn($subscriptionId); - - $mercureUrl = 'mercure-url'; - $this->mercureSubscriptionIriGeneratorProphecy->generateMercureUrl($subscriptionId, null)->shouldBeCalled()->willReturn($mercureUrl); - - $this->assertSame($serializeStageData + ['mercureUrl' => $mercureUrl], ($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullResourceClass(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullOperationName(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, null)($source, $args, null, $info)); - } - - public function testResolveBadReadStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $readStageItem = []; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Item from read stage should be a nullable object.'); - - ($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveNoSubscriptionId(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withName($operationName)->withMercure(true); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->willReturn($readStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->willReturn($serializeStageData); - - $this->subscriptionManagerProphecy->retrieveSubscriptionId($resolverContext, $serializeStageData)->willReturn(null); - - $this->mercureSubscriptionIriGeneratorProphecy->generateMercureUrl(Argument::any())->shouldNotBeCalled(); - - $this->assertSame($serializeStageData, ($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNoMercureSubscriptionIriGenerator(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - /** @var Operation $operation */ - $operation = (new Subscription())->withName($operationName)->withMercure(true); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->willReturn($readStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->willReturn($serializeStageData); - - $subscriptionId = 'subscriptionId'; - $this->subscriptionManagerProphecy->retrieveSubscriptionId($resolverContext, $serializeStageData)->willReturn($subscriptionId); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Cannot use Mercure for subscriptions when MercureBundle is not installed. Try running "composer require mercure".'); - - $itemSubscriptionResolverFactory = new ItemSubscriptionResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->subscriptionManagerProphecy->reveal(), - null - ); - - ($itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/DeserializeStageTest.php b/src/GraphQl/Tests/Resolver/Stage/DeserializeStageTest.php deleted file mode 100644 index 174dbe06f92..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/DeserializeStageTest.php +++ /dev/null @@ -1,92 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\DeserializeStage; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; - -/** - * @author Alan Poulain - */ -class DeserializeStageTest extends TestCase -{ - use ProphecyTrait; - - private DeserializeStage $deserializeStage; - private ObjectProphecy $denormalizerProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->denormalizerProphecy = $this->prophesize(DenormalizerInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $this->deserializeStage = new DeserializeStage( - $this->denormalizerProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal() - ); - } - - /** - * @dataProvider objectToPopulateProvider - */ - public function testApplyDisabled(?object $objectToPopulate): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query')->withClass($resourceClass)->withDeserialize(false); - $result = ($this->deserializeStage)($objectToPopulate, $resourceClass, $operation, []); - - $this->assertSame($objectToPopulate, $result); - } - - /** - * @dataProvider objectToPopulateProvider - */ - public function testApply(?object $objectToPopulate, array $denormalizationContext): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - $context = ['args' => ['input' => 'myInput']]; - - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, false)->shouldBeCalled()->willReturn($denormalizationContext); - - $denormalizedData = new \stdClass(); - $this->denormalizerProphecy->denormalize($context['args']['input'], $resourceClass, ItemNormalizer::FORMAT, $denormalizationContext)->shouldBeCalled()->willReturn($denormalizedData); - - $result = ($this->deserializeStage)($objectToPopulate, $resourceClass, $operation, $context); - - $this->assertSame($denormalizedData, $result); - } - - public static function objectToPopulateProvider(): array - { - return [ - 'null' => [null, ['denormalization' => true]], - 'object' => [$object = new \stdClass(), ['denormalization' => true, ItemNormalizer::OBJECT_TO_POPULATE => $object]], - ]; - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/ReadStageTest.php b/src/GraphQl/Tests/Resolver/Stage/ReadStageTest.php deleted file mode 100644 index 40c6c37a03f..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/ReadStageTest.php +++ /dev/null @@ -1,270 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\ReadStage; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\Exception\ItemNotFoundException; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\State\ProviderInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - -/** - * @author Alan Poulain - */ -class ReadStageTest extends TestCase -{ - use ProphecyTrait; - - private ReadStage $readStage; - private ObjectProphecy $iriConverterProphecy; - private ObjectProphecy $providerProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $this->providerProphecy = $this->prophesize(ProviderInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $this->readStage = new ReadStage( - $this->iriConverterProphecy->reveal(), - $this->providerProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal(), - '_' - ); - } - - /** - * @dataProvider contextProvider - */ - public function testApplyDisabled(array $context, object|array|null $expectedResult): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withRead(false)->withName('item_query')->withClass($resourceClass); - - $result = ($this->readStage)($resourceClass, null, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function contextProvider(): array - { - return [ - 'item context' => [['is_collection' => false], null], - 'collection context' => [['is_collection' => true], []], - ]; - } - - /** - * @dataProvider itemProvider - */ - public function testApplyItem(?string $identifier, ?object $item, bool $throwNotFound, ?object $expectedResult): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $context = [ - 'is_collection' => false, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => ['id' => $identifier], - 'info' => $info, - ]; - - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName); - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - if ($throwNotFound) { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willThrow(new ItemNotFoundException()); - } else { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willReturn($item); - } - - $result = ($this->readStage)($resourceClass, null, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function itemProvider(): array - { - $item = new \stdClass(); - - return [ - 'no identifier' => [null, $item, false, null], - 'identifier' => ['identifier', $item, false, $item], - 'identifier not found' => ['identifier_not_found', $item, true, null], - ]; - } - - /** - * @dataProvider itemMutationOrSubscriptionProvider - */ - public function testApplyMutationOrSubscription(bool $isMutation, bool $isSubscription, string $resourceClass, ?string $identifier, ?object $item, bool $throwNotFound, ?object $expectedResult, ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void - { - $operationName = 'create'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $context = [ - 'is_collection' => false, - 'is_mutation' => $isMutation, - 'is_subscription' => $isSubscription, - 'args' => ['input' => ['id' => $identifier]], - 'info' => $info, - ]; - - /** @var Operation $operation */ - $operation = (new Mutation())->withName($operationName)->withShortName('shortName'); - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - if ($throwNotFound) { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willThrow(new ItemNotFoundException()); - } else { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willReturn($item); - } - - if ($expectedExceptionClass) { - $this->expectException($expectedExceptionClass); - $this->expectExceptionMessage($expectedExceptionMessage); - } - - $result = ($this->readStage)($resourceClass, null, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function itemMutationOrSubscriptionProvider(): array - { - $item = new \stdClass(); - - return [ - 'no identifier' => [true, false, 'myResource', null, $item, false, null], - 'identifier' => [true, false, \stdClass::class, 'identifier', $item, false, $item], - 'identifier bad item' => [true, false, 'myResource', 'identifier', $item, false, $item, \UnexpectedValueException::class, 'Item "identifier" did not match expected type "shortName".'], - 'identifier not found' => [true, false, 'myResource', 'identifier_not_found', $item, true, null, NotFoundHttpException::class, 'Item "identifier_not_found" not found.'], - 'no identifier (subscription)' => [false, true, 'myResource', null, $item, false, null], - 'identifier (subscription)' => [false, true, \stdClass::class, 'identifier', $item, false, $item], - ]; - } - - /** - * @dataProvider collectionProvider - */ - public function testApplyCollection(array $args, ?string $rootClass, ?array $source, array $expectedFilters, iterable $expectedResult): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $fieldName = 'resource'; - $info->fieldName = $fieldName; - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => $args, - 'info' => $info, - 'source' => $source, - ]; - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withName($operationName); - $normalizationContext = ['normalization' => true, 'operation' => $operation]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->providerProphecy->provide($operation, [], $normalizationContext + ['filters' => $expectedFilters])->willReturn([]); - $this->providerProphecy->provide($operation, ['id' => 3], $normalizationContext + ['filters' => $expectedFilters, 'linkClass' => 'myResource', 'linkProperty' => 'resource'])->willReturn(['resource']); - - $result = ($this->readStage)($resourceClass, $rootClass, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public function testPreserveOrderOfOrderFiltersIfNested(): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $fieldName = 'resource'; - $info->fieldName = $fieldName; - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => [ - 'order' => [ - 'some_field' => 'ASC', - 'localField' => 'ASC', - ], - ], - 'info' => $info, - 'source' => null, - ]; - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withName($operationName); - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - ($this->readStage)($resourceClass, $resourceClass, $operation, $context); - - $this->providerProphecy->provide($operation, [], Argument::that(fn ($args): bool => // Prophecy does not check the order of items in associative arrays. Checking if some.field comes first manually -array_search('some.field', array_keys($args['filters']['order']), true) < - array_search('localField', array_keys($args['filters']['order']), true)))->shouldHaveBeenCalled(); - } - - public static function collectionProvider(): array - { - return [ - 'no root class' => [[], null, null, [], []], - 'nominal' => [ - ['filter_list' => 'filtered', 'filter_field_list' => ['filtered1', 'filtered2']], - 'myResource', - null, - ['filter_list' => 'filtered', 'filter_field_list' => ['filtered1', 'filtered2'], 'filter.list' => 'filtered', 'filter_field' => ['filtered1', 'filtered2'], 'filter.field' => ['filtered1', 'filtered2']], - [], - ], - 'with array filter syntax' => [ - ['filter' => [['filterArg1' => 'filterValue1'], ['filterArg2' => 'filterValue2']]], - 'myResource', - null, - ['filter' => ['filterArg1' => 'filterValue1', 'filterArg2' => 'filterValue2']], - [], - ], - 'with resource' => [ - [], - 'myResource', - ['resource' => [], ItemNormalizer::ITEM_IDENTIFIERS_KEY => ['id' => 3], ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => 'myResource'], - [], - ['resource'], - ], - ]; - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SecurityPostDenormalizeStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SecurityPostDenormalizeStageTest.php deleted file mode 100644 index 1b870ea2b57..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SecurityPostDenormalizeStageTest.php +++ /dev/null @@ -1,124 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * @author Alan Poulain - * @author Vincent Chalamon - */ -class SecurityPostDenormalizeStageTest extends TestCase -{ - use ProphecyTrait; - - private SecurityPostDenormalizeStage $securityPostDenormalizeStage; - private ObjectProphecy $resourceAccessCheckerProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - - $this->securityPostDenormalizeStage = new SecurityPostDenormalizeStage( - $this->resourceAccessCheckerProphecy->reveal() - ); - } - - public function testNoSecurity(): void - { - $resourceClass = 'myResource'; - $operation = new Query(); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::cetera())->shouldNotBeCalled(); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, []); - } - - public function testGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withSecurityPostDenormalize($isGranted)->withName($operationName); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(true); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, ['extra_variables' => $extraVariables]); - } - - public function testNotGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withSecurityPostDenormalize($isGranted)->withName($operationName); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(false); - - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->expectException(AccessDeniedHttpException::class); - $this->expectExceptionMessage('Access Denied.'); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, [ - 'info' => $info, - 'extra_variables' => $extraVariables, - ]); - } - - public function testNoSecurityBundleInstalled(): void - { - $this->securityPostDenormalizeStage = new SecurityPostDenormalizeStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - /** @var Operation $operation */ - $operation = (new Query())->withSecurityPostDenormalize($isGranted)->withName($operationName); - - $this->expectException(\LogicException::class); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, []); - } - - public function testNoSecurityBundleInstalledNoExpression(): void - { - $this->securityPostDenormalizeStage = new SecurityPostDenormalizeStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::any())->shouldNotBeCalled(); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, []); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SecurityPostValidationStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SecurityPostValidationStageTest.php deleted file mode 100644 index d824578b9d9..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SecurityPostValidationStageTest.php +++ /dev/null @@ -1,127 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * @author Alan Poulain - * @author Vincent Chalamon - */ -class SecurityPostValidationStageTest extends TestCase -{ - use ProphecyTrait; - - private SecurityPostValidationStage $securityPostValidationStage; - private ObjectProphecy $resourceAccessCheckerProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - - $this->securityPostValidationStage = new SecurityPostValidationStage( - $this->resourceAccessCheckerProphecy->reveal() - ); - } - - public function testNoSecurity(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::cetera())->shouldNotBeCalled(); - - ($this->securityPostValidationStage)($resourceClass, $operation, []); - } - - public function testGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurityPostValidation($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(true); - - ($this->securityPostValidationStage)($resourceClass, $operation, ['extra_variables' => $extraVariables]); - } - - public function testNotGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurityPostValidation($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(false); - - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->expectException(AccessDeniedHttpException::class); - $this->expectExceptionMessage('Access Denied.'); - - ($this->securityPostValidationStage)($resourceClass, $operation, [ - 'info' => $info, - 'extra_variables' => $extraVariables, - ]); - } - - public function testNoSecurityBundleInstalled(): void - { - $this->securityPostValidationStage = new SecurityPostValidationStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurityPostValidation($isGranted); - - $this->expectException(\LogicException::class); - - ($this->securityPostValidationStage)($resourceClass, $operation, []); - } - - public function testNoSecurityBundleInstalledNoExpression(): void - { - $this->securityPostValidationStage = new SecurityPostValidationStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::any())->shouldNotBeCalled(); - - ($this->securityPostValidationStage)($resourceClass, $operation, []); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SecurityStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SecurityStageTest.php deleted file mode 100644 index 1eb69316e4e..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SecurityStageTest.php +++ /dev/null @@ -1,122 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * @author Alan Poulain - * @author Vincent Chalamon - */ -class SecurityStageTest extends TestCase -{ - use ProphecyTrait; - - private SecurityStage $securityStage; - private ObjectProphecy $resourceAccessCheckerProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - - $this->securityStage = new SecurityStage( - $this->resourceAccessCheckerProphecy->reveal() - ); - } - - public function testNoSecurity(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::cetera())->shouldNotBeCalled(); - - ($this->securityStage)($resourceClass, $operation, []); - } - - public function testGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurity($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(true); - - ($this->securityStage)($resourceClass, $operation, ['extra_variables' => $extraVariables]); - } - - public function testNotGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurity($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(false); - - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->expectException(AccessDeniedHttpException::class); - $this->expectExceptionMessage('Access Denied.'); - - ($this->securityStage)($resourceClass, $operation, [ - 'info' => $info, - 'extra_variables' => $extraVariables, - ]); - } - - public function testNoSecurityBundleInstalled(): void - { - $this->securityStage = new SecurityStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurity($isGranted); - - $this->expectException(\LogicException::class); - - ($this->securityStage)($resourceClass, $operation, []); - } - - public function testNoSecurityBundleInstalledNoExpression(): void - { - $this->securityStage = new SecurityStage(null); - - $resourceClass = 'myResource'; - $this->resourceAccessCheckerProphecy->isGranted(Argument::any())->shouldNotBeCalled(); - - ($this->securityStage)($resourceClass, new Query(), []); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php deleted file mode 100644 index afcf85ffbef..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php +++ /dev/null @@ -1,287 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStage; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -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\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\State\Pagination\ArrayPaginator; -use ApiPlatform\State\Pagination\Pagination; -use ApiPlatform\State\Pagination\PaginatorInterface; -use ApiPlatform\State\Pagination\PartialPaginatorInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * @author Alan Poulain - */ -class SerializeStageTest extends TestCase -{ - use ProphecyTrait; - - private ObjectProphecy $normalizerProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - private ObjectProphecy $resolveInfoProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->normalizerProphecy = $this->prophesize(NormalizerInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $this->resolveInfoProphecy = $this->prophesize(ResolveInfo::class); - } - - /** - * @dataProvider applyDisabledProvider - */ - public function testApplyDisabled(Operation $operation, bool $paginationEnabled, ?array $expectedResult): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = $operation->withSerialize(false); - - $result = ($this->createSerializeStage($paginationEnabled))(null, $resourceClass, $operation, []); - - $this->assertSame($expectedResult, $result); - } - - public static function applyDisabledProvider(): array - { - return [ - 'item' => [new Query(), false, null], - 'collection with pagination' => [new QueryCollection(), true, ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]]], - 'collection without pagination' => [new QueryCollection(), false, []], - 'mutation' => [new Mutation(), false, ['clientMutationId' => null]], - 'subscription' => [new Subscription(), false, ['clientSubscriptionId' => null]], - ]; - } - - /** - * @dataProvider applyProvider - */ - public function testApply(object|array $itemOrCollection, string $operationName, callable $contextFactory, bool $paginationEnabled, ?array $expectedResult): void - { - $context = $contextFactory($this); - - $resourceClass = 'myResource'; - $operation = $context['is_mutation'] ? new Mutation() : new Query(); - if ($context['is_subscription']) { - $operation = new Subscription(); - } - - if ($context['is_collection'] ?? false) { - $operation = new QueryCollection(); - } - - /** @var Operation $operation */ - $operation = $operation->withShortName('shortName')->withName($operationName)->withClass($resourceClass); - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(['normalized_item']); - - $result = ($this->createSerializeStage($paginationEnabled))($itemOrCollection, $resourceClass, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function applyProvider(): iterable - { - $defaultContextFactory = fn (self $that): array => [ - 'args' => [], - 'info' => $that->resolveInfoProphecy->reveal(), - ]; - - yield 'item' => [new \stdClass(), 'item_query', fn (self $that): array => $defaultContextFactory($that) + ['is_collection' => false, 'is_mutation' => false, 'is_subscription' => false], false, ['normalized_item']]; - yield 'collection without pagination' => [[new \stdClass(), new \stdClass()], 'collection_query', fn (self $that): array => $defaultContextFactory($that) + ['is_collection' => true, 'is_mutation' => false, 'is_subscription' => false], false, [['normalized_item'], ['normalized_item']]]; - yield 'mutation' => [new \stdClass(), 'create', fn (self $that): array => array_merge($defaultContextFactory($that), ['args' => ['input' => ['clientMutationId' => 'clientMutationId']], 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]), false, ['shortName' => ['normalized_item'], 'clientMutationId' => 'clientMutationId']]; - yield 'delete mutation' => [new \stdClass(), 'delete', fn (self $that): array => array_merge($defaultContextFactory($that), ['args' => ['input' => ['id' => '/iri/4']], 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]), false, ['shortName' => ['id' => '/iri/4'], 'clientMutationId' => null]]; - yield 'subscription' => [new \stdClass(), 'update', fn (self $that): array => array_merge($defaultContextFactory($that), ['args' => ['input' => ['clientSubscriptionId' => 'clientSubscriptionId']], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]), false, ['shortName' => ['normalized_item'], 'clientSubscriptionId' => 'clientSubscriptionId']]; - } - - /** - * @dataProvider applyCollectionWithPaginationProvider - */ - public function testApplyCollectionWithPagination(iterable|callable $collection, array $args, ?array $expectedResult, bool $pageBasedPagination, array $getFieldSelection = [], ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $this->resolveInfoProphecy->getFieldSelection(1)->willReturn($getFieldSelection); - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => $args, - 'info' => $this->resolveInfoProphecy->reveal(), - ]; - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withShortName('shortName')->withName($operationName); - if ($pageBasedPagination) { - $operation = $operation->withPaginationType('page'); - } - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(['normalized_item']); - - if ($expectedExceptionClass) { - $this->expectException($expectedExceptionClass); - $this->expectExceptionMessage($expectedExceptionMessage); - } - - $result = ($this->createSerializeStage(true))(\is_callable($collection) ? $collection($this) : $collection, $resourceClass, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function applyCollectionWithPaginationProvider(): iterable - { - $partialPaginatorFactory = function (self $that): PartialPaginatorInterface { - $partialPaginatorProphecy = $that->prophesize(PartialPaginatorInterface::class); - $partialPaginatorProphecy->count()->willReturn(2); - $partialPaginatorProphecy->valid()->willReturn(false); - $partialPaginatorProphecy->getItemsPerPage()->willReturn(2.0); - $partialPaginatorProphecy->rewind(); - - return $partialPaginatorProphecy->reveal(); - }; - - yield 'cursor - not paginator' => [[], [], null, false, [], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface or ApiPlatform\State\Pagination\PartialPaginatorInterface.']; - yield 'cursor - empty paginator' => [new ArrayPaginator([], 0, 0), [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => [], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MA=='], ['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with after cursor' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 2), ['after' => 'MA=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with bad after cursor' => [new ArrayPaginator([], 0, 0), ['after' => '-'], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Cursor - is invalid']; - yield 'cursor - paginator with empty after cursor' => [new ArrayPaginator([], 0, 0), ['after' => ''], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Empty cursor is invalid']; - yield 'cursor - paginator with before cursor' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 1), ['before' => 'Mg=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with bad before cursor' => [new ArrayPaginator([], 0, 0), ['before' => '-'], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Cursor - is invalid']; - yield 'cursor - paginator with empty before cursor' => [new ArrayPaginator([], 0, 0), ['before' => ''], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Empty cursor is invalid']; - yield 'cursor - paginator with last' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 2), ['last' => 2], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - partial paginator' => [$partialPaginatorFactory, [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => false, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - partial paginator with after cursor' => [$partialPaginatorFactory, ['after' => 'MA=='], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - - yield 'page - not paginator, itemsPerPage requested' => [[], [], null, true, ['paginationInfo' => ['itemsPerPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PartialPaginatorInterface to return itemsPerPage field.']; - yield 'page - not paginator, lastPage requested' => [[], [], null, true, ['paginationInfo' => ['lastPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return lastPage field.']; - yield 'page - not paginator, totalCount requested' => [[], [], null, true, ['paginationInfo' => ['totalCount' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return totalCount field.']; - yield 'page - not paginator, hasNextPage requested' => [[], [], null, true, ['paginationInfo' => ['hasNextPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\HasNextPagePaginatorInterface to return hasNextPage field.']; - yield 'page - empty paginator - itemsPerPage requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['itemsPerPage' => .0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - empty paginator - lastPage requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['lastPage' => 1.0]], true, ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - empty paginator - totalCount requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['totalCount' => .0]], true, ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - empty paginator - hasNextPage requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['hasNextPage' => false]], true, ['paginationInfo' => ['hasNextPage' => true]]]; - yield 'page - paginator page 1 - itemsPerPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - paginator page 1 - lastPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], true, ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - paginator page 1 - totalCount requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], true, ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - paginator page 1 - hasNextPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['hasNextPage' => true]], true, ['paginationInfo' => ['hasNextPage' => true]]]; - yield 'page - paginator with page - itemsPerPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - paginator with page - lastPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], true, ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - paginator with page - totalCount requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], true, ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - paginator with page - hasNextPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['hasNextPage' => false]], true, ['paginationInfo' => ['hasNextPage' => true]]]; - } - - /** - * @dataProvider applyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequestedProvider - */ - public function testApplyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequested(bool $pageBasedPagination, array $getFieldSelection = [], bool $getTotalItemsCalled = false): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $this->resolveInfoProphecy->getFieldSelection(1)->willReturn($getFieldSelection); - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => [], - 'info' => $this->resolveInfoProphecy->reveal(), - ]; - $collectionProphecy = $this->prophesize(PaginatorInterface::class); - $collectionProphecy->getTotalItems()->willReturn(1); - $collectionProphecy->count()->willReturn(1); - $collectionProphecy->getItemsPerPage()->willReturn(20.0); - $collectionProphecy->valid()->willReturn(false); - $collectionProphecy->rewind(); - if ($getTotalItemsCalled) { - $collectionProphecy->getTotalItems()->shouldBeCalledOnce(); - } else { - $collectionProphecy->getTotalItems()->shouldNotBeCalled(); - } - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withShortName('shortName')->withName($operationName); - if ($pageBasedPagination) { - $operation = $operation->withPaginationType('page'); - } - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(['normalized_item']); - - ($this->createSerializeStage(true))($collectionProphecy->reveal(), $resourceClass, $operation, $context); - } - - public static function applyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequestedProvider(): iterable - { - yield 'cursor - totalCount requested' => [false, ['totalCount' => true], true]; - yield 'cursor - totalCount not requested' => [false, [], false]; - yield 'page - totalCount requested' => [true, ['paginationInfo' => ['totalCount' => true]], true]; - yield 'page - totalCount not requested' => [true, [], false]; - } - - public function testApplyBadNormalizedData(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $context = ['is_collection' => false, 'is_mutation' => false, 'is_subscription' => false, 'args' => [], 'info' => $this->prophesize(ResolveInfo::class)->reveal()]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName); - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(0); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Expected serialized data to be a nullable array.'); - - ($this->createSerializeStage(false))(new \stdClass(), $resourceClass, $operation, $context); - } - - private function createSerializeStage(bool $paginationEnabled): SerializeStage - { - $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactoryProphecy->create(Argument::type('string'))->willReturn(new ResourceMetadataCollection('')); - $pagination = new Pagination([], ['enabled' => $paginationEnabled]); - - return new SerializeStage( - $this->normalizerProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal(), - $pagination - ); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/ValidateStageTest.php b/src/GraphQl/Tests/Resolver/Stage/ValidateStageTest.php deleted file mode 100644 index 41c683f3d8c..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/ValidateStageTest.php +++ /dev/null @@ -1,90 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\ValidateStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Validator\Exception\ValidationException; -use ApiPlatform\Validator\ValidatorInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\Validator\ConstraintViolationList; - -/** - * @author Alan Poulain - */ -class ValidateStageTest extends TestCase -{ - use ProphecyTrait; - - private ValidateStage $validateStage; - private ObjectProphecy $validatorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->validatorProphecy = $this->prophesize(ValidatorInterface::class); - - $this->validateStage = new ValidateStage( - $this->validatorProphecy->reveal() - ); - } - - public function testApplyDisabled(): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withValidate(false)->withName('item_query'); - - $this->validatorProphecy->validate(Argument::cetera())->shouldNotBeCalled(); - - ($this->validateStage)(new \stdClass(), $resourceClass, $operation, []); - } - - public function testApply(): void - { - $resourceClass = 'myResource'; - $validationGroups = ['group']; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query')->withValidationContext(['groups' => $validationGroups]); - - $object = new \stdClass(); - $this->validatorProphecy->validate($object, ['groups' => $validationGroups])->shouldBeCalled(); - - ($this->validateStage)($object, $resourceClass, $operation, []); - } - - public function testApplyNotValidated(): void - { - $resourceClass = 'myResource'; - $validationGroups = ['group']; - /** @var Operation $operation */ - $operation = (new Query())->withValidationContext(['groups' => $validationGroups])->withName('item_query'); - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $context = ['info' => $info]; - - $object = new \stdClass(); - $this->validatorProphecy->validate($object, ['groups' => $validationGroups])->shouldBeCalled()->willThrow(new ValidationException(new ConstraintViolationList())); - - $this->expectException(ValidationException::class); - - ($this->validateStage)($object, $resourceClass, $operation, $context); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/WriteStageTest.php b/src/GraphQl/Tests/Resolver/Stage/WriteStageTest.php deleted file mode 100644 index 25544062faf..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/WriteStageTest.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\WriteStage; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\State\ProcessorInterface; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; - -/** - * @author Alan Poulain - */ -class WriteStageTest extends TestCase -{ - use ProphecyTrait; - - private WriteStage $writeStage; - private ObjectProphecy $processorProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->processorProphecy = $this->prophesize(ProcessorInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $this->writeStage = new WriteStage( - $this->processorProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal() - ); - } - - public function testNoData(): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query'); - - $result = ($this->writeStage)(null, $resourceClass, $operation, []); - - $this->assertNull($result); - } - - public function testApplyDisabled(): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query')->withWrite(false); - - $data = new \stdClass(); - $result = ($this->writeStage)($data, $resourceClass, $operation, []); - - $this->assertSame($data, $result); - } - - public function testApply(): void - { - $operationName = 'create'; - $resourceClass = 'myResource'; - $context = []; - /** @var Operation $operation */ - $operation = (new Mutation())->withName($operationName); - - $denormalizationContext = ['denormalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, false)->willReturn($denormalizationContext); - - $data = new \stdClass(); - $processedData = new \stdClass(); - $this->processorProphecy->process($data, $operation, [], ['operation' => $operation] + $denormalizationContext)->shouldBeCalled()->willReturn($processedData); - - $result = ($this->writeStage)($data, $resourceClass, $operation, $context); - - $this->assertSame($processedData, $result); - } -} diff --git a/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php b/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php index b8fcbd22e3f..7afeaeaef03 100644 --- a/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php +++ b/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php @@ -13,7 +13,6 @@ namespace ApiPlatform\GraphQl\Tests\Subscription; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; use ApiPlatform\GraphQl\Subscription\SubscriptionIdentifierGeneratorInterface; use ApiPlatform\GraphQl\Subscription\SubscriptionManager; use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy; @@ -24,6 +23,7 @@ use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\State\ProcessorInterface; use GraphQL\Type\Definition\ResolveInfo; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -40,7 +40,7 @@ class SubscriptionManagerTest extends TestCase private ObjectProphecy $subscriptionsCacheProphecy; private ObjectProphecy $subscriptionIdentifierGeneratorProphecy; - private ObjectProphecy $serializeStageProphecy; + private ObjectProphecy $normalizeProcessor; private ObjectProphecy $iriConverterProphecy; private SubscriptionManager $subscriptionManager; private ObjectProphecy $resourceMetadataCollectionFactory; @@ -52,10 +52,10 @@ protected function setUp(): void { $this->subscriptionsCacheProphecy = $this->prophesize(CacheItemPoolInterface::class); $this->subscriptionIdentifierGeneratorProphecy = $this->prophesize(SubscriptionIdentifierGeneratorInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); + $this->normalizeProcessor = $this->prophesize(ProcessorInterface::class); $this->iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $this->resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $this->subscriptionManager = new SubscriptionManager($this->subscriptionsCacheProphecy->reveal(), $this->subscriptionIdentifierGeneratorProphecy->reveal(), $this->serializeStageProphecy->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceMetadataCollectionFactory->reveal()); + $this->subscriptionManager = new SubscriptionManager($this->subscriptionsCacheProphecy->reveal(), $this->subscriptionIdentifierGeneratorProphecy->reveal(), $this->normalizeProcessor->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceMetadataCollectionFactory->reveal()); } public function testRetrieveSubscriptionIdNoIdentifier(): void @@ -206,8 +206,23 @@ public function testGetPushPayloadsHit(): void ]); $this->subscriptionsCacheProphecy->getItem('_dummies_2')->willReturn($cacheItemProphecy->reveal()); - $this->serializeStageProphecy->__invoke($object, Dummy::class, (new Subscription())->withName('update_subscription')->withShortName('Dummy'), ['fields' => ['fieldsFoo'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true])->willReturn(['newResultFoo', 'clientSubscriptionId' => 'client-subscription-id']); - $this->serializeStageProphecy->__invoke($object, Dummy::class, (new Subscription())->withName('update_subscription')->withShortName('Dummy'), ['fields' => ['fieldsBar'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true])->willReturn(['resultBar', 'clientSubscriptionId' => 'client-subscription-id']); + $this->normalizeProcessor->process( + $object, + (new Subscription())->withName('update_subscription')->withShortName('Dummy'), + [], + ['fields' => ['fieldsFoo'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true] + )->willReturn( + ['newResultFoo', 'clientSubscriptionId' => 'client-subscription-id'] + ); + + $this->normalizeProcessor->process( + $object, + (new Subscription())->withName('update_subscription')->withShortName('Dummy'), + [], + ['fields' => ['fieldsBar'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true] + )->willReturn( + ['resultBar', 'clientSubscriptionId' => 'client-subscription-id'] + ); $this->assertEquals([['subscriptionIdFoo', ['newResultFoo']]], $this->subscriptionManager->getPushPayloads($object)); }