├── LICENSE ├── README.md ├── composer.json ├── examples └── no-framework │ ├── README.md │ ├── composer.json │ ├── index.php │ └── src │ └── Controllers │ └── MyController.php └── src ├── AggregateControllerQueryProvider.php ├── AggregateControllerQueryProviderFactory.php ├── AggregateQueryProvider.php ├── AnnotationReader.php ├── Annotations ├── AbstractRequest.php ├── Autowire.php ├── Cost.php ├── Decorate.php ├── EnumType.php ├── Exceptions │ ├── ClassNotFoundException.php │ ├── IncompatibleAnnotationsException.php │ └── InvalidParameterException.php ├── ExtendType.php ├── Factory.php ├── FailWith.php ├── Field.php ├── HideIfUnauthorized.php ├── HideParameter.php ├── InjectUser.php ├── Input.php ├── Logged.php ├── MagicField.php ├── MiddlewareAnnotationInterface.php ├── MiddlewareAnnotations.php ├── Mutation.php ├── ParameterAnnotationInterface.php ├── ParameterAnnotations.php ├── Prefetch.php ├── Query.php ├── Right.php ├── Security.php ├── SourceField.php ├── SourceFieldInterface.php ├── Subscription.php ├── TooManyAnnotationsException.php ├── Type.php ├── TypeInterface.php └── UseInputType.php ├── Cache ├── ClassBoundCache.php ├── FilesSnapshot.php └── SnapshotClassBoundCache.php ├── Containers ├── BasicAutoWiringContainer.php ├── EmptyContainer.php ├── LazyContainer.php └── NotFoundException.php ├── Context ├── Context.php ├── ContextInterface.php └── ResetableContextInterface.php ├── Discovery ├── Cache │ ├── ClassFinderComputedCache.php │ ├── HardClassFinderComputedCache.php │ └── SnapshotClassFinderComputedCache.php ├── ClassFinder.php ├── KcsClassFinder.php └── StaticClassFinder.php ├── Exceptions ├── GraphQLAggregateException.php ├── GraphQLAggregateExceptionInterface.php ├── GraphQLException.php ├── GraphQLExceptionInterface.php └── WebonyxErrorHandler.php ├── FactoryContext.php ├── FailedAssertionException.php ├── FailedResolvingInputType.php ├── FieldNotFoundException.php ├── FieldsBuilder.php ├── FromSourceFieldsInterface.php ├── GlobControllerQueryProvider.php ├── GraphQLRuntimeException.php ├── Http ├── HttpCodeDecider.php ├── HttpCodeDeciderInterface.php ├── Psr15GraphQLMiddlewareBuilder.php └── WebonyxGraphqlMiddleware.php ├── InputField.php ├── InputFieldDescriptor.php ├── InputTypeGenerator.php ├── InputTypeUtils.php ├── InvalidCallableRuntimeException.php ├── InvalidDocBlockRuntimeException.php ├── InvalidPrefetchMethodRuntimeException.php ├── Mappers ├── CannotMapTypeException.php ├── CannotMapTypeExceptionInterface.php ├── CannotMapTypeTrait.php ├── ClassFinderTypeMapper.php ├── CompositeTypeMapper.php ├── DuplicateMappingException.php ├── GlobAnnotationsCache.php ├── GlobExtendAnnotationsCache.php ├── GlobExtendTypeMapperCache.php ├── GlobTypeMapperCache.php ├── MappedClass.php ├── Parameters │ ├── CannotHideParameterRuntimeException.php │ ├── ContainerParameterHandler.php │ ├── InjectUserParameterHandler.php │ ├── MissingAutowireTypeException.php │ ├── Next.php │ ├── ParameterHandlerInterface.php │ ├── ParameterMiddlewareInterface.php │ ├── ParameterMiddlewarePipe.php │ ├── PrefetchParameterMiddleware.php │ ├── ResolveInfoParameterHandler.php │ └── TypeHandler.php ├── PorpaginasMissingParameterException.php ├── PorpaginasTypeMapper.php ├── Proxys │ ├── MutableAdapterTrait.php │ ├── MutableInterfaceTypeAdapter.php │ └── MutableObjectTypeAdapter.php ├── RecursiveTypeMapper.php ├── RecursiveTypeMapperInterface.php ├── Root │ ├── BaseTypeMapper.php │ ├── CallableTypeMapper.php │ ├── CompoundTypeMapper.php │ ├── EnumTypeMapper.php │ ├── FinalRootTypeMapper.php │ ├── IteratorTypeMapper.php │ ├── LastDelegatingTypeMapper.php │ ├── MyCLabsEnumTypeMapper.php │ ├── NullableTypeMapperAdapter.php │ ├── RootTypeMapperFactoryContext.php │ ├── RootTypeMapperFactoryInterface.php │ ├── RootTypeMapperInterface.php │ └── VoidTypeMapper.php ├── StaticClassListTypeMapperFactory.php ├── StaticTypeMapper.php ├── TypeMapperFactoryInterface.php ├── TypeMapperInterface.php └── TypeNotFoundException.php ├── Middlewares ├── AuthorizationFieldMiddleware.php ├── AuthorizationInputFieldMiddleware.php ├── BadExpressionInSecurityException.php ├── CostFieldMiddleware.php ├── FieldHandlerInterface.php ├── FieldMiddlewareInterface.php ├── FieldMiddlewarePipe.php ├── InputFieldHandlerInterface.php ├── InputFieldMiddlewareInterface.php ├── InputFieldMiddlewarePipe.php ├── InputNext.php ├── MagicPropertyResolver.php ├── MissingAuthorizationException.php ├── MissingMagicGetException.php ├── Next.php ├── ResolverInterface.php ├── SecurityFieldMiddleware.php ├── SecurityInputFieldMiddleware.php ├── ServiceResolver.php ├── SourceConstructorParameterResolver.php ├── SourceInputPropertyResolver.php ├── SourceMethodResolver.php └── SourcePropertyResolver.php ├── MissingAnnotationException.php ├── MissingTypeHintRuntimeException.php ├── NamingStrategy.php ├── NamingStrategyInterface.php ├── ParameterizedCallableResolver.php ├── Parameters ├── ContainerParameter.php ├── DefaultValueParameter.php ├── ExpandsInputTypeParameters.php ├── InjectUserParameter.php ├── InputTypeParameter.php ├── InputTypeParameterInterface.php ├── InputTypeProperty.php ├── MissingArgumentException.php ├── ParameterInterface.php ├── PrefetchDataParameter.php ├── ResolveInfoParameter.php └── SourceParameter.php ├── PrefetchBuffer.php ├── QueryField.php ├── QueryFieldDescriptor.php ├── QueryProviderFactoryInterface.php ├── QueryProviderInterface.php ├── Reflection ├── DocBlock │ ├── CachedDocBlockFactory.php │ ├── DocBlockFactory.php │ └── PhpDocumentorDocBlockFactory.php └── ReflectionInterfaceUtils.php ├── ResolveUtils.php ├── Schema.php ├── SchemaFactory.php ├── Security ├── AuthenticationServiceInterface.php ├── AuthorizationServiceInterface.php ├── FailAuthenticationService.php ├── FailAuthorizationService.php ├── SecurityExpressionLanguageProvider.php ├── SecurityNotImplementedException.php ├── VoidAuthenticationService.php └── VoidAuthorizationService.php ├── Server └── PersistedQuery │ ├── CachePersistedQueryLoader.php │ ├── NotSupportedPersistedQueryLoader.php │ ├── PersistedQueryException.php │ ├── PersistedQueryIdInvalidException.php │ ├── PersistedQueryNotFoundException.php │ └── PersistedQueryNotSupportedException.php ├── TypeGenerator.php ├── TypeMismatchRuntimeException.php ├── TypeRegistry.php ├── Types ├── ArgumentResolver.php ├── DateTimeType.php ├── EnumType.php ├── ID.php ├── InputType.php ├── InputTypeValidatorInterface.php ├── InterfaceFromObjectType.php ├── InvalidTypesInUnionException.php ├── MutableInputInterface.php ├── MutableInputObjectType.php ├── MutableInterface.php ├── MutableInterfaceType.php ├── MutableObjectType.php ├── MutableTrait.php ├── MyCLabsEnumType.php ├── NoFieldsException.php ├── ObjectFromInterfaceType.php ├── ResolvableMutableInputInterface.php ├── ResolvableMutableInputObjectType.php ├── TypeAnnotatedInterfaceType.php ├── TypeAnnotatedObjectType.php ├── TypeResolver.php ├── UnionType.php └── VoidType.php └── Utils ├── AccessPropertyException.php ├── Cloneable.php ├── NamespacedCache.php └── PropertyAccessor.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 TheCodingMachine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thecodingmachine/graphqlite", 3 | "description": "Write your GraphQL queries in simple to write controllers (using webonyx/graphql-php).", 4 | "license": "MIT", 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "David Négrier", 9 | "email": "d.negrier@thecodingmachine.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=8.1", 14 | "ext-json": "*", 15 | "composer/package-versions-deprecated": "^1.8", 16 | "phpdocumentor/reflection-docblock": "^5.4", 17 | "phpdocumentor/type-resolver": "^1.7", 18 | "psr/container": "^1.1 || ^2", 19 | "psr/http-factory": "^1", 20 | "psr/http-message": "^1.0.1 || ^2.0", 21 | "psr/http-server-handler": "^1", 22 | "psr/http-server-middleware": "^1", 23 | "psr/simple-cache": "^1.0.1 || ^2 || ^3", 24 | "symfony/cache": "^4.3 || ^5 || ^6 || ^7", 25 | "symfony/expression-language": "^4 || ^5 || ^6 || ^7", 26 | "webonyx/graphql-php": "^v15.0", 27 | "kcs/class-finder": "^0.6.0" 28 | }, 29 | "require-dev": { 30 | "beberlei/porpaginas": "^2.0", 31 | "doctrine/coding-standard": "^12.0 || ^13.0", 32 | "ecodev/graphql-upload": "^7.0", 33 | "laminas/laminas-diactoros": "^3.5", 34 | "myclabs/php-enum": "^1.6.6", 35 | "php-coveralls/php-coveralls": "^2.7", 36 | "phpstan/extension-installer": "^1.4", 37 | "phpstan/phpstan": "^2.0", 38 | "phpunit/phpunit": "^10.5 || ^11.0", 39 | "symfony/var-dumper": "^6.4" 40 | }, 41 | "suggest": { 42 | "beberlei/porpaginas": "If you want automatic pagination in your GraphQL types", 43 | "ecodev/graphql-upload": "If you want to support file upload inside GraphQL input types" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "TheCodingMachine\\GraphQLite\\": "src/" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "TheCodingMachine\\GraphQLite\\": "tests/" 53 | } 54 | }, 55 | "scripts": { 56 | "phpstan": "phpstan analyse -c phpstan.neon --no-progress -vvv --memory-limit=1G", 57 | "cs-check": "phpcs", 58 | "cs-fix": "phpcbf", 59 | "test": ["@cs-check", "@phpstan", "phpunit"] 60 | }, 61 | "extra": { 62 | "branch-alias": { 63 | "dev-master": "5.0.x-dev" 64 | } 65 | }, 66 | "config": { 67 | "allow-plugins": { 68 | "composer/package-versions-deprecated": true, 69 | "dealerdirect/phpcodesniffer-composer-installer": true, 70 | "phpstan/extension-installer": true 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/no-framework/README.md: -------------------------------------------------------------------------------- 1 | No-Framework Integration Example 2 | ================================ 3 | 4 | ``` 5 | composer install 6 | php -S 127.0.0.1:8080 7 | ``` 8 | 9 | ``` 10 | curl -X POST -d '{"query":"{ hello(name: \"World\") }"}' -H "Content-Type: application/json" http://localhost:8080/ 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/no-framework/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoload": { 3 | "psr-4": { 4 | "App\\": "src/" 5 | } 6 | }, 7 | "require": { 8 | "thecodingmachine/graphqlite": "@dev", 9 | "mouf/picotainer": "^1.1", 10 | "symfony/cache": "^4.3", 11 | "psr/simple-cache": "^1.0" 12 | }, 13 | "repositories": [ 14 | { 15 | "type": "path", 16 | "url": "tmp-graphqlite", 17 | "options": { 18 | "symlink": true 19 | } 20 | } 21 | ], 22 | "scripts": { 23 | "symlink-package": [ 24 | "rm -rf tmp-graphqlite && ln -s -f ../../ tmp-graphqlite" 25 | ], 26 | "pre-install-cmd": "@symlink-package", 27 | "pre-update-cmd": "@symlink-package" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/no-framework/index.php: -------------------------------------------------------------------------------- 1 | function() { 22 | return new MyController(); 23 | }, 24 | ]); 25 | 26 | $factory = new SchemaFactory($cache, $container); 27 | $factory->addNamespace('App'); 28 | 29 | $schema = $factory->createSchema(); 30 | 31 | $rawInput = file_get_contents('php://input'); 32 | $input = json_decode($rawInput, true); 33 | $query = $input['query']; 34 | $variableValues = isset($input['variables']) ? $input['variables'] : null; 35 | 36 | $result = GraphQL::executeQuery($schema, $query, null, new Context(), $variableValues); 37 | $output = $result->toArray(); 38 | 39 | header('Content-Type: application/json'); 40 | echo json_encode($output) . "\n"; 41 | 42 | -------------------------------------------------------------------------------- /examples/no-framework/src/Controllers/MyController.php: -------------------------------------------------------------------------------- 1 | $controllers A list of controllers name in the container. 16 | * @param ContainerInterface $controllersContainer The container we will fetch controllers from. 17 | */ 18 | public function __construct( 19 | private readonly iterable $controllers, 20 | private readonly ContainerInterface $controllersContainer, 21 | ) { 22 | } 23 | 24 | public function create(FactoryContext $context): QueryProviderInterface 25 | { 26 | return new AggregateControllerQueryProvider( 27 | $this->controllers, 28 | $context->getFieldsBuilder(), 29 | $this->controllersContainer, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/AggregateQueryProvider.php: -------------------------------------------------------------------------------- 1 | queryProviders = is_array($queryProviders) 24 | ? $queryProviders 25 | : iterator_to_array($queryProviders); 26 | } 27 | 28 | /** @return QueryField[] */ 29 | public function getQueries(): array 30 | { 31 | $queriesArray = array_map(static function (QueryProviderInterface $queryProvider) { 32 | return $queryProvider->getQueries(); 33 | }, $this->queryProviders); 34 | if ($queriesArray === []) { 35 | return []; 36 | } 37 | 38 | return array_merge(...$queriesArray); 39 | } 40 | 41 | /** @return QueryField[] */ 42 | public function getMutations(): array 43 | { 44 | $mutationsArray = array_map(static function (QueryProviderInterface $queryProvider) { 45 | return $queryProvider->getMutations(); 46 | }, $this->queryProviders); 47 | if ($mutationsArray === []) { 48 | return []; 49 | } 50 | 51 | return array_merge(...$mutationsArray); 52 | } 53 | 54 | /** @return QueryField[] */ 55 | public function getSubscriptions(): array 56 | { 57 | $subscriptionsArray = array_map(static function (QueryProviderInterface $queryProvider) { 58 | return $queryProvider->getSubscriptions(); 59 | }, $this->queryProviders); 60 | if ($subscriptionsArray === []) { 61 | return []; 62 | } 63 | 64 | return array_merge(...$subscriptionsArray); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Annotations/AbstractRequest.php: -------------------------------------------------------------------------------- 1 | outputType = $outputType ?? $attributes['outputType'] ?? null; 17 | $this->name = $name ?? $attributes['name'] ?? null; 18 | } 19 | 20 | /** 21 | * Returns the GraphQL return type of the request (as a string). 22 | * The string can represent the FQCN of the type or an entry in the container resolving to the GraphQL type. 23 | */ 24 | public function getOutputType(): string|null 25 | { 26 | return $this->outputType; 27 | } 28 | 29 | /** 30 | * Returns the name of the GraphQL query/mutation/field. 31 | * If not specified, the name of the method should be used instead. 32 | */ 33 | public function getName(): string|null 34 | { 35 | return $this->name; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Annotations/Autowire.php: -------------------------------------------------------------------------------- 1 | |string $params */ 23 | public function __construct( 24 | array|string $params = [], 25 | string|null $for = null, 26 | string|null $identifier = null, 27 | ) 28 | { 29 | $values = $params; 30 | if (is_string($values)) { 31 | $this->identifier = $values; 32 | } else { 33 | $this->identifier = $identifier ?? $values['identifier'] ?? $values['value'] ?? null; 34 | if (isset($values['for']) || $for !== null) { 35 | $this->for = ltrim($for ?? $values['for'], '$'); 36 | } 37 | } 38 | } 39 | 40 | public function getTarget(): string 41 | { 42 | if ($this->for === null) { 43 | throw new BadMethodCallException('The #[Autowire] attribute must be passed a target. For instance: "#[Autowire(for: "$myService")]"'); 44 | } 45 | return $this->for; 46 | } 47 | 48 | public function getIdentifier(): string|null 49 | { 50 | return $this->identifier; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Annotations/Cost.php: -------------------------------------------------------------------------------- 1 | |string $inputTypeName 23 | * 24 | * @throws BadMethodCallException 25 | */ 26 | public function __construct(array|string $inputTypeName = []) 27 | { 28 | $values = $inputTypeName; 29 | if (is_string($values)) { 30 | $this->inputTypeName = $values; 31 | } elseif (! isset($values['value']) && ! isset($values['inputTypeName'])) { 32 | throw new BadMethodCallException('The #[Decorate] attribute must be passed an input type. For instance: "#[Decorate("MyInputType")]"'); 33 | } else { 34 | $this->inputTypeName = $values['value'] ?? $values['inputTypeName']; 35 | } 36 | } 37 | 38 | public function getInputTypeName(): string 39 | { 40 | return $this->inputTypeName; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Annotations/EnumType.php: -------------------------------------------------------------------------------- 1 | name = $name ?? $attributes['name'] ?? null; 30 | $this->useValues = $useValues ?? $attributes['useValues'] ?? false; 31 | } 32 | 33 | /** 34 | * Returns the GraphQL name for this type. 35 | */ 36 | public function getName(): string|null 37 | { 38 | return $this->name; 39 | } 40 | 41 | /** 42 | * Returns true if the enum type should expose backed values instead of case names. 43 | */ 44 | public function useValues(): bool 45 | { 46 | return $this->useValues; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Annotations/Exceptions/ClassNotFoundException.php: -------------------------------------------------------------------------------- 1 | getMessage() . " defined in #[Type] attribute of class '" . $className . "'"); 21 | } 22 | 23 | public static function wrapExceptionForExtendTag(self $e, string $className): self 24 | { 25 | return new self($e->getMessage() . " defined in #[ExtendType] attribute of class '" . $className . "'"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Annotations/Exceptions/IncompatibleAnnotationsException.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass()->getName(), $reflectionMethod->getName())); 17 | } 18 | 19 | public static function parameterNotFoundFromSourceField(string $parameter, string $annotationClass, ReflectionMethod $reflectionMethod): self 20 | { 21 | return new self(sprintf('Could not find parameter "%s" declared in annotation "%s". This annotation is itself declared in a SourceField attribute targeting resolver "%s::%s()".', $parameter, $annotationClass, $reflectionMethod->getDeclaringClass()->getName(), $reflectionMethod->getName())); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Annotations/ExtendType.php: -------------------------------------------------------------------------------- 1 | |null */ 22 | private string|null $class; 23 | private string|null $name; 24 | 25 | /** @param mixed[] $attributes */ 26 | public function __construct( 27 | array $attributes = [], 28 | string|null $class = null, 29 | string|null $name = null, 30 | ) { 31 | $className = isset($attributes['class']) ? ltrim($attributes['class'], '\\') : null; 32 | $className = $className ?? $class; 33 | if ($className !== null && ! class_exists($className) && ! interface_exists($className)) { 34 | throw ClassNotFoundException::couldNotFindClass($className); 35 | } 36 | $this->name = $name ?? $attributes['name'] ?? null; 37 | $this->class = $className; 38 | if (! $this->class && ! $this->name) { 39 | throw new BadMethodCallException('In attribute #[ExtendType], missing one of the compulsory parameter "class" or "name".'); 40 | } 41 | } 42 | 43 | /** 44 | * Returns the name of the GraphQL query/mutation/field. 45 | * If not specified, the name of the method should be used instead. 46 | * 47 | * @return class-string|null 48 | */ 49 | public function getClass(): string|null 50 | { 51 | return $this->class; 52 | } 53 | 54 | public function getName(): string|null 55 | { 56 | return $this->name; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Annotations/Factory.php: -------------------------------------------------------------------------------- 1 | name = $name ?? $attributes['name'] ?? null; 23 | // This IS the default if no name is set and no "default" attribute is passed. 24 | $this->default = $default ?? $attributes['default'] ?? ! isset($attributes['name']); 25 | 26 | if ($this->name === null && $this->default === false) { 27 | throw new GraphQLRuntimeException('A #[Factory] that has "default=false" attribute must be given a name (i.e. add a name="FooBarInput" attribute).'); 28 | } 29 | } 30 | 31 | /** 32 | * Returns the name of the GraphQL input type. 33 | * If not specified, the name of the method should be used instead. 34 | */ 35 | public function getName(): string|null 36 | { 37 | return $this->name; 38 | } 39 | 40 | /** 41 | * Returns true if this factory should map the return type of the factory by default. 42 | */ 43 | public function isDefault(): bool 44 | { 45 | return $this->default; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Annotations/FailWith.php: -------------------------------------------------------------------------------- 1 | value = $value; 26 | } elseif (is_array($values) && array_key_exists('value', $values)) { 27 | $this->value = $values['value']; 28 | } elseif (! is_array($values)) { 29 | $this->value = $values; 30 | } else { 31 | throw new BadMethodCallException('The #[FailWith] attribute must be passed a defaultValue. For instance: "#[FailWith(null)]"'); 32 | } 33 | } 34 | 35 | /** 36 | * Returns the default value to use if the right is not enforced. 37 | */ 38 | public function getValue(): mixed 39 | { 40 | return $this->value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Annotations/Field.php: -------------------------------------------------------------------------------- 1 | prefetchMethod = $prefetchMethod ?? $attributes['prefetchMethod'] ?? null; 44 | $this->description = $description ?? $attributes['description'] ?? null; 45 | $this->inputType = $inputType ?? $attributes['inputType'] ?? null; 46 | 47 | $forValue = $for ?? $attributes['for'] ?? null; 48 | if (! $forValue) { 49 | return; 50 | } 51 | 52 | $this->for = (array) $forValue; 53 | 54 | if (! $this->prefetchMethod) { 55 | return; 56 | } 57 | 58 | trigger_error( 59 | "Using #[Field(prefetchMethod='" . $this->prefetchMethod . "')] on fields is deprecated in favor " . 60 | "of #[Prefetch('" . $this->prefetchMethod . "')] \$data attribute on the parameter itself.", 61 | E_USER_DEPRECATED, 62 | ); 63 | } 64 | 65 | /** 66 | * Returns the prefetch method name (the method that will be called to fetch many records at once) 67 | */ 68 | public function getPrefetchMethod(): string|null 69 | { 70 | return $this->prefetchMethod; 71 | } 72 | 73 | /** @return string[]|null */ 74 | public function getFor(): array|null 75 | { 76 | return $this->for; 77 | } 78 | 79 | public function getDescription(): string|null 80 | { 81 | return $this->description; 82 | } 83 | 84 | public function getInputType(): string|null 85 | { 86 | return $this->inputType; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Annotations/HideIfUnauthorized.php: -------------------------------------------------------------------------------- 1 | $values */ 22 | public function __construct(array $values = [], string|null $for = null) 23 | { 24 | if (! isset($values['for']) && $for === null) { 25 | return; 26 | } 27 | 28 | $this->for = ltrim($for ?? $values['for'], '$'); 29 | } 30 | 31 | public function getTarget(): string 32 | { 33 | if ($this->for === null) { 34 | throw new BadMethodCallException('The #[HideParameter] attribute must be passed a target. For instance: "#[HideParameter(for: "$myParameterToHide")]"'); 35 | } 36 | return $this->for; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Annotations/InjectUser.php: -------------------------------------------------------------------------------- 1 | $values */ 22 | public function __construct(array $values = []) 23 | { 24 | if (! isset($values['for'])) { 25 | return; 26 | } 27 | 28 | $this->for = ltrim($values['for'], '$'); 29 | } 30 | 31 | public function getTarget(): string 32 | { 33 | if ($this->for === null) { 34 | throw new BadMethodCallException('The #[InjectUser] attribute must be passed a target. For instance: "#[InjectUser(for: "$user")]"'); 35 | } 36 | return $this->for; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Annotations/Logged.php: -------------------------------------------------------------------------------- 1 | $annotations */ 17 | public function __construct(private array $annotations) 18 | { 19 | } 20 | 21 | /** 22 | * Return annotations of the $className type 23 | * 24 | * @param class-string $className 25 | * 26 | * @return array 27 | * 28 | * @template TAnnotation of MiddlewareAnnotationInterface 29 | */ 30 | public function getAnnotationsByType(string $className): array 31 | { 32 | return array_filter($this->annotations, static function (MiddlewareAnnotationInterface $annotation) use ($className) { 33 | return $annotation instanceof $className; 34 | }); 35 | } 36 | 37 | /** 38 | * Returns at most 1 annotation of the $className type. 39 | * 40 | * @param class-string $className 41 | * 42 | * @return TAnnotation|null 43 | * 44 | * @template TAnnotation of MiddlewareAnnotationInterface 45 | */ 46 | public function getAnnotationByType(string $className): MiddlewareAnnotationInterface|null 47 | { 48 | $annotations = $this->getAnnotationsByType($className); 49 | $count = count($annotations); 50 | if ($count > 1) { 51 | throw TooManyAnnotationsException::forClass($className); 52 | } 53 | 54 | if ($count === 0) { 55 | return null; 56 | } 57 | 58 | return array_pop($annotations); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Annotations/Mutation.php: -------------------------------------------------------------------------------- 1 | $annotations */ 17 | public function __construct(private array $annotations) 18 | { 19 | } 20 | 21 | /** 22 | * Return annotations of the $className type 23 | * 24 | * @param class-string $className 25 | * 26 | * @return array 27 | * 28 | * @template T of ParameterAnnotationInterface 29 | */ 30 | public function getAnnotationsByType(string $className): array 31 | { 32 | return array_filter($this->annotations, static function (ParameterAnnotationInterface $annotation) use ($className) { 33 | return $annotation instanceof $className; 34 | }); 35 | } 36 | 37 | /** 38 | * Returns at most 1 annotation of the $className type. 39 | * 40 | * @param class-string $className 41 | * 42 | * @return T|null 43 | * 44 | * @template T of ParameterAnnotationInterface 45 | */ 46 | public function getAnnotationByType(string $className): ParameterAnnotationInterface|null 47 | { 48 | $annotations = $this->getAnnotationsByType($className); 49 | $count = count($annotations); 50 | if ($count > 1) { 51 | throw TooManyAnnotationsException::forClass($className); 52 | } 53 | 54 | if ($count === 0) { 55 | return null; 56 | } 57 | 58 | return array_pop($annotations); 59 | } 60 | 61 | /** @return array */ 62 | public function getAllAnnotations(): array 63 | { 64 | return $this->annotations; 65 | } 66 | 67 | public function merge(ParameterAnnotations $parameterAnnotations): void 68 | { 69 | $this->annotations = [...$this->annotations, ...$parameterAnnotations->annotations]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Annotations/Prefetch.php: -------------------------------------------------------------------------------- 1 | |string $name 19 | * 20 | * @throws BadMethodCallException 21 | */ 22 | public function __construct(array|string $name = []) 23 | { 24 | $data = $name; 25 | if (is_string($data)) { 26 | $data = ['name' => $data]; 27 | } 28 | if (! isset($data['value']) && ! isset($data['name'])) { 29 | throw new BadMethodCallException('The #[Right] attribute must be passed a right name. For instance: "#[Right(\'my_right\')]"'); 30 | } 31 | $this->name = $data['value'] ?? $data['name']; 32 | } 33 | 34 | public function getName(): string 35 | { 36 | return $this->name; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Annotations/Security.php: -------------------------------------------------------------------------------- 1 | |string $data data array managed by the Doctrine Annotations library or the expression 24 | * 25 | * @throws BadMethodCallException 26 | */ 27 | public function __construct( 28 | array|string $data = [], 29 | string|null $expression = null, 30 | mixed $failWith = '__fail__with__magic__key__', 31 | string|null $message = null, 32 | int|null $statusCode = null, 33 | ) { 34 | if (is_string($data)) { 35 | $data = ['expression' => $data]; 36 | } 37 | 38 | $expression = $data['value'] ?? $data['expression'] ?? $expression; 39 | if (! $expression) { 40 | throw new BadMethodCallException('The #[Security] attribute must be passed an expression. For instance: "#[Security("is_granted(\'CAN_EDIT_STUFF\')")]"'); 41 | } 42 | 43 | $this->expression = $expression; 44 | 45 | if (array_key_exists('failWith', $data)) { 46 | $this->failWith = $data['failWith']; 47 | $this->failWithIsSet = true; 48 | } elseif ($failWith !== '__fail__with__magic__key__') { 49 | $this->failWith = $failWith; 50 | $this->failWithIsSet = true; 51 | } 52 | $this->message = $message ?? $data['message'] ?? 'Access denied.'; 53 | $this->statusCode = $statusCode ?? $data['statusCode'] ?? 403; 54 | if ($this->failWithIsSet === true && (($message || isset($data['message'])) || ($statusCode || isset($data['statusCode'])))) { 55 | throw new BadMethodCallException('A #[Security] attribute that has "failWith" attribute set cannot have a message or a statusCode attribute.'); 56 | } 57 | } 58 | 59 | public function getExpression(): string 60 | { 61 | return $this->expression; 62 | } 63 | 64 | public function isFailWithSet(): bool 65 | { 66 | return $this->failWithIsSet; 67 | } 68 | 69 | public function getFailWith(): mixed 70 | { 71 | return $this->failWith; 72 | } 73 | 74 | public function getStatusCode(): int 75 | { 76 | return $this->statusCode; 77 | } 78 | 79 | public function getMessage(): string 80 | { 81 | return $this->message; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Annotations/SourceFieldInterface.php: -------------------------------------------------------------------------------- 1 | Key: the name of the attribute */ 42 | public function getParameterAnnotations(): array; 43 | 44 | /** 45 | * If true, this source field should be fetched from a magic property (rather than from a getter) 46 | * In this case, getOutputType MUST NOT return null. 47 | */ 48 | public function shouldFetchFromMagicProperty(): bool; 49 | } 50 | -------------------------------------------------------------------------------- /src/Annotations/Subscription.php: -------------------------------------------------------------------------------- 1 | */ 20 | public function getClass(): string; 21 | 22 | public function isDefault(): bool; 23 | 24 | public function getName(): string|null; 25 | } 26 | -------------------------------------------------------------------------------- /src/Annotations/UseInputType.php: -------------------------------------------------------------------------------- 1 | |string $inputType 24 | * 25 | * @throws BadMethodCallException 26 | */ 27 | public function __construct(array|string $inputType = [], string|null $for = null) 28 | { 29 | $values = $inputType; 30 | if (is_string($values)) { 31 | $values = ['inputType' => $values]; 32 | } 33 | if (is_string($for) && $for !== '') { 34 | $values['for'] = $for; 35 | } 36 | if (! isset($values['inputType'])) { 37 | throw new BadMethodCallException('The #[UseInputType] attribute must be passed an input type. For instance: #[UseInputType("MyInputType")]'); 38 | } 39 | $this->inputType = $values['inputType']; 40 | if (! isset($values['for'])) { 41 | return; 42 | } 43 | 44 | $this->for = ltrim($values['for'], '$'); 45 | } 46 | 47 | public function getTarget(): string 48 | { 49 | if ($this->for === null) { 50 | throw new BadMethodCallException('The #[UseInputType] attribute must be passed a target and an input type. For instance: #[UseInputType("MyInputType")]'); 51 | } 52 | return $this->for; 53 | } 54 | 55 | public function getInputType(): string 56 | { 57 | return $this->inputType; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Cache/ClassBoundCache.php: -------------------------------------------------------------------------------- 1 | $dependencies */ 15 | private function __construct( 16 | private readonly array $dependencies, 17 | ) 18 | { 19 | } 20 | 21 | /** @param list $files */ 22 | public static function for(array $files): self 23 | { 24 | $dependencies = []; 25 | 26 | foreach (array_unique($files) as $file) { 27 | $dependencies[$file] = filemtime($file); 28 | } 29 | 30 | return new self($dependencies); 31 | } 32 | 33 | public static function forClass(ReflectionClass $class, bool $withInheritance = false): self 34 | { 35 | return self::for( 36 | self::dependencies($class, $withInheritance), 37 | ); 38 | } 39 | 40 | public static function alwaysUnchanged(): self 41 | { 42 | return new self([]); 43 | } 44 | 45 | /** @return list */ 46 | private static function dependencies(ReflectionClass $class, bool $withInheritance = false): array 47 | { 48 | $filename = $class->getFileName(); 49 | 50 | // Internal classes are treated as always the same, e.g. you'll have to drop the cache between PHP versions. 51 | if ($filename === false) { 52 | return []; 53 | } 54 | 55 | $files = [$filename]; 56 | 57 | if (! $withInheritance) { 58 | return $files; 59 | } 60 | 61 | if ($class->getParentClass() !== false) { 62 | $files = [...$files, ...self::dependencies($class->getParentClass(), $withInheritance)]; 63 | } 64 | 65 | foreach ($class->getTraits() as $trait) { 66 | $files = [...$files, ...self::dependencies($trait, $withInheritance)]; 67 | } 68 | 69 | foreach ($class->getInterfaces() as $interface) { 70 | $files = [...$files, ...self::dependencies($interface, $withInheritance)]; 71 | } 72 | 73 | return $files; 74 | } 75 | 76 | public function changed(): bool 77 | { 78 | foreach ($this->dependencies as $filename => $modificationTime) { 79 | if ($modificationTime !== filemtime($filename)) { 80 | return true; 81 | } 82 | } 83 | 84 | return false; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Cache/SnapshotClassBoundCache.php: -------------------------------------------------------------------------------- 1 | getName() . '__' . $key; 24 | $cacheKey = str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $cacheKey); 25 | 26 | $item = $this->cache->get($cacheKey); 27 | 28 | if ($item !== null && ! $item['snapshot']->changed()) { 29 | return $item['data']; 30 | } 31 | 32 | $item = [ 33 | 'data' => $resolver(), 34 | 'snapshot' => ($this->filesSnapshotFactory)($reflectionClass, $withInheritance), 35 | ]; 36 | 37 | $this->cache->set($cacheKey, $item); 38 | 39 | return $item['data']; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Containers/EmptyContainer.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected array $objects; 22 | private ContainerInterface $delegateLookupContainer; 23 | 24 | /** 25 | * Instantiate the container. 26 | * 27 | * @param array $entries The array of closures defining each entry of the container. Entries must be passed as an array of anonymous functions. 28 | * @param ContainerInterface|null $delegateLookupContainer Optional delegate lookup container. 29 | */ 30 | public function __construct(private array $entries, ContainerInterface|null $delegateLookupContainer = null) 31 | { 32 | $this->delegateLookupContainer = $delegateLookupContainer ?: $this; 33 | } 34 | 35 | public function get(string $id): mixed 36 | { 37 | if (isset($this->objects[$id])) { 38 | return $this->objects[$id]; 39 | } 40 | if (! isset($this->entries[$id])) { 41 | throw NotFoundException::notFoundInContainer($id); 42 | } 43 | 44 | return $this->objects[$id] = $this->entries[$id]($this->delegateLookupContainer); 45 | } 46 | 47 | public function has(string $id): bool 48 | { 49 | return isset($this->entries[$id]) || isset($this->objects[$id]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Containers/NotFoundException.php: -------------------------------------------------------------------------------- 1 | prefetchBuffers = new WeakMap(); 21 | } 22 | 23 | /** 24 | * Returns the prefetch buffer associated to the field $field. 25 | * (the buffer is created on the fly if it does not exist yet). 26 | */ 27 | public function getPrefetchBuffer(ParameterInterface $field): PrefetchBuffer 28 | { 29 | if ($this->prefetchBuffers->offsetExists($field)) { 30 | $prefetchBuffer = $this->prefetchBuffers->offsetGet($field); 31 | } else { 32 | $prefetchBuffer = new PrefetchBuffer(); 33 | $this->prefetchBuffers->offsetSet($field, $prefetchBuffer); 34 | } 35 | 36 | return $prefetchBuffer; 37 | } 38 | 39 | public function reset(): void 40 | { 41 | $this->prefetchBuffers = new WeakMap(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Context/ContextInterface.php: -------------------------------------------------------------------------------- 1 | . Once all classes are iterated, 21 | * $reduce will then be called with that map, and it's final result is returned. 22 | * 23 | * Now the point of this is now whenever file A changes, we can automatically remove entries generated for it 24 | * and simply call $map only for classes from file A, leaving all other entries untouched and not having to 25 | * waste resources on the rest of them. We then only need to call the cheap $reduce and have the final result :) 26 | * 27 | * @param callable(ReflectionClass): TEntry $map 28 | * @param callable(array): TReturn $reduce 29 | * 30 | * @return TReturn 31 | * 32 | * @template TEntry of mixed 33 | * @template TReturn of mixed 34 | */ 35 | public function compute( 36 | ClassFinder $classFinder, 37 | string $key, 38 | callable $map, 39 | callable $reduce, 40 | ): mixed; 41 | } 42 | -------------------------------------------------------------------------------- /src/Discovery/Cache/HardClassFinderComputedCache.php: -------------------------------------------------------------------------------- 1 | ): TEntry $map 22 | * @param callable(array): TReturn $reduce 23 | * 24 | * @return TReturn 25 | * 26 | * @template TEntry of mixed 27 | * @template TReturn of mixed 28 | */ 29 | public function compute( 30 | ClassFinder $classFinder, 31 | string $key, 32 | callable $map, 33 | callable $reduce, 34 | ): mixed 35 | { 36 | $key = sprintf('%s.%s', $key, $classFinder->hash()); 37 | $result = $this->cache->get($key); 38 | 39 | if ($result !== null) { 40 | return $result; 41 | } 42 | 43 | $result = $reduce($this->entries($classFinder, $map)); 44 | 45 | $this->cache->set($key, $result); 46 | 47 | return $result; 48 | } 49 | 50 | /** 51 | * @param callable(ReflectionClass): TEntry $map 52 | * 53 | * @return array 54 | * 55 | * @template TEntry of mixed 56 | */ 57 | private function entries( 58 | ClassFinder $classFinder, 59 | callable $map, 60 | ): mixed 61 | { 62 | $entries = []; 63 | 64 | foreach ($classFinder as $classReflection) { 65 | $entries[$classReflection->getFileName()] = $map($classReflection); 66 | } 67 | 68 | /** @phpstan-ignore return.type */ 69 | return $entries; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Discovery/ClassFinder.php: -------------------------------------------------------------------------------- 1 | > */ 11 | interface ClassFinder extends IteratorAggregate 12 | { 13 | public function withPathFilter(callable $filter): self; 14 | 15 | /** 16 | * Path filter does not affect the hash. 17 | */ 18 | public function hash(): string; 19 | } 20 | -------------------------------------------------------------------------------- /src/Discovery/KcsClassFinder.php: -------------------------------------------------------------------------------- 1 | finder = (clone $that->finder)->pathFilter($filter); 23 | 24 | return $that; 25 | } 26 | 27 | /** @return Traversable */ 28 | public function getIterator(): Traversable 29 | { 30 | return $this->finder->getIterator(); 31 | } 32 | 33 | public function hash(): string 34 | { 35 | return $this->hash; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Discovery/StaticClassFinder.php: -------------------------------------------------------------------------------- 1 | $classes */ 21 | public function __construct( 22 | private readonly array $classes, 23 | ) 24 | { 25 | } 26 | 27 | public function withPathFilter(callable $filter): ClassFinder 28 | { 29 | $that = clone $this; 30 | $that->pathFilter = $filter; 31 | 32 | return $that; 33 | } 34 | 35 | /** @return Traversable */ 36 | public function getIterator(): Traversable 37 | { 38 | foreach ($this->classes as $class) { 39 | $classReflection = new ReflectionClass($class); 40 | 41 | /** @phpstan-ignore-next-line */ 42 | if ($this->pathFilter && ! ($this->pathFilter)($classReflection->getFileName())) { 43 | continue; 44 | } 45 | 46 | yield $class => $classReflection; 47 | } 48 | } 49 | 50 | public function hash(): string 51 | { 52 | return $this->hash ??= md5(serialize($this->classes)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Exceptions/GraphQLAggregateException.php: -------------------------------------------------------------------------------- 1 | add($exception); 30 | } 31 | } 32 | 33 | /** @param ClientAware&Throwable $exception */ 34 | public function add(ClientAware $exception): void 35 | { 36 | $this->exceptions[] = $exception; 37 | $this->message .= "\n" . $exception->getMessage(); 38 | $this->updateCode(); 39 | } 40 | 41 | /** @return (ClientAware&Throwable)[] */ 42 | public function getExceptions(): array 43 | { 44 | return $this->exceptions; 45 | } 46 | 47 | public function hasExceptions(): bool 48 | { 49 | return ! empty($this->exceptions); 50 | } 51 | 52 | /** 53 | * By convention, the aggregated code is the highest code of all exceptions 54 | */ 55 | private function updateCode(): void 56 | { 57 | $codes = array_map(static function (Throwable $t) { 58 | return $t->getCode(); 59 | }, $this->exceptions); 60 | 61 | if (count($codes) === 0) { 62 | throw new RuntimeException('Unable to determine code for exception'); 63 | } 64 | 65 | $this->code = max($codes); 66 | } 67 | 68 | /** 69 | * Throw the exceptions passed in parameter. 70 | * If only one exception is passed, it is thrown. 71 | * If many exceptions are passed, they are bundled in the GraphQLAggregateException 72 | * 73 | * @param (ClientAware&Throwable)[] $exceptions 74 | * 75 | * @throws GraphQLAggregateException|Throwable 76 | */ 77 | public static function throwExceptions(array $exceptions): void 78 | { 79 | $count = count($exceptions); 80 | if ($count === 0) { 81 | return; 82 | } 83 | if ($count === 1) { 84 | $exception = reset($exceptions); 85 | assert($exception instanceof Throwable); 86 | throw $exception; 87 | } 88 | throw new self($exceptions); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Exceptions/GraphQLAggregateExceptionInterface.php: -------------------------------------------------------------------------------- 1 | $extensions */ 13 | public function __construct( 14 | string $message, 15 | int $code = 0, 16 | Throwable|null $previous = null, 17 | protected array $extensions = [], 18 | ) { 19 | parent::__construct($message, $code, $previous); 20 | } 21 | 22 | /** 23 | * Returns true when exception message is safe to be displayed to a client. 24 | */ 25 | public function isClientSafe(): bool 26 | { 27 | return true; 28 | } 29 | 30 | /** 31 | * Returns the "extensions" object attached to the GraphQL error. 32 | * 33 | * @return array 34 | */ 35 | public function getExtensions(): array 36 | { 37 | return $this->extensions; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Exceptions/GraphQLExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function getExtensions(): array; 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/WebonyxErrorHandler.php: -------------------------------------------------------------------------------- 1 | getPrevious(); 28 | if ($previous instanceof GraphQLExceptionInterface && ! empty($previous->getExtensions())) { 29 | $formattedError['extensions'] = isset($formattedError['extensions']) ? $previous->getExtensions() + $formattedError['extensions'] : $previous->getExtensions(); 30 | } 31 | 32 | return $formattedError; 33 | } 34 | 35 | /** 36 | * @param Error[] $errors 37 | * 38 | * @return mixed[] 39 | */ 40 | public static function errorHandler(array $errors, callable $formatter): array 41 | { 42 | $formattedErrors = []; 43 | foreach ($errors as $error) { 44 | $previous = $error->getPrevious(); 45 | if ($previous instanceof GraphQLAggregateExceptionInterface) { 46 | $exceptions = $previous->getExceptions(); 47 | $innerErrors = array_map(static function (ClientAware $clientAware) use ($error) { 48 | // Let's build a new error at the same spot than the aggregated one, but for the wrapped exception. 49 | $extensions = $clientAware instanceof GraphQLExceptionInterface ? $clientAware->getExtensions() : []; 50 | 51 | return new Error($clientAware->getMessage(), $error->getNodes(), $error->getSource(), $error->getPositions(), $error->getPath(), $clientAware, $extensions); 52 | }, $exceptions); 53 | 54 | $formattedInnerErrors = self::errorHandler($innerErrors, $formatter); 55 | 56 | $formattedErrors = [...$formattedErrors, ...$formattedInnerErrors]; 57 | } else { 58 | $formattedErrors[] = $formatter($error); 59 | } 60 | } 61 | 62 | return $formattedErrors; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/FailedAssertionException.php: -------------------------------------------------------------------------------- 1 | getMessage()), previous: $original); 17 | } 18 | 19 | public static function createForDecorator(string $class): self 20 | { 21 | return new self(sprintf("Input type '%s' cannot be a decorator.", $class)); 22 | } 23 | 24 | public static function createForNotInstantiableClass(string $class): self 25 | { 26 | return new self(sprintf("Class '%s' annotated with @Input must be instantiable.", $class)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/FieldNotFoundException.php: -------------------------------------------------------------------------------- 1 | getMessage(), 34 | ), 0, $e); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/FromSourceFieldsInterface.php: -------------------------------------------------------------------------------- 1 | data !== null && empty($result->errors)) { 23 | return 200; 24 | } 25 | $status = 0; 26 | // There might be many errors. Let's return the highest code we encounter. 27 | foreach ($result->errors as $error) { 28 | $wrappedException = $error->getPrevious(); 29 | if ($wrappedException !== null) { 30 | $code = $wrappedException->getCode(); 31 | if ($code < 400 || $code >= 600) { 32 | if (! ($wrappedException instanceof ClientAware) || $wrappedException->isClientSafe() !== true) { 33 | // The exception code is not a valid HTTP code. Let's ignore it 34 | continue; 35 | } 36 | 37 | // A "client aware" exception is almost certainly targeting the client (there is 38 | // no need to pass a server exception error message to the client). 39 | // So a ClientAware exception is almost certainly a HTTP 400 code 40 | $code = 400; 41 | } 42 | } else { 43 | $code = 400; 44 | } 45 | $status = (int) max($status, $code); 46 | } 47 | 48 | // If exceptions have been thrown, and they have not an "HTTP like code", let's throw a 500. 49 | if ($status < 200) { 50 | $status = 500; 51 | } 52 | 53 | return $status; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Http/HttpCodeDeciderInterface.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass()->getName() . '::' . $refMethod->getName() . ' has several @return annotations.'); 15 | } 16 | 17 | /** 18 | * Creates an exception for property to have multiple var tags. 19 | */ 20 | public static function tooManyVarTags(ReflectionProperty $refProperty): self 21 | { 22 | throw new self('Property ' . $refProperty->getDeclaringClass()->getName() . '::' . $refProperty->getName() . ' has several @var annotations.'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/InvalidPrefetchMethodRuntimeException.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass()->getName() . '::' . $reflector->getName() . ' specifies a "prefetch method" that could not be found. Unable to find method ' . $reflectionClass->getName() . '::' . $methodName . '.', 0, $previous); 18 | } 19 | 20 | public static function fromInvalidCallable( 21 | ReflectionMethod $reflector, 22 | string $parameterName, 23 | InvalidCallableRuntimeException $e, 24 | ): self 25 | { 26 | return new self( 27 | '#[Prefetch] attribute on parameter $' . $parameterName . ' in ' . $reflector->getDeclaringClass()->getName() . '::' . $reflector->getName() . 28 | ' specifies a callable that is invalid: ' . $e->getMessage(), 29 | previous: $e, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Mappers/CannotMapTypeExceptionInterface.php: -------------------------------------------------------------------------------- 1 | $class */ 22 | public function addSourceFieldInfo(ReflectionClass $class, SourceFieldInterface $sourceField): void; 23 | 24 | /** @param ReflectionClass $class */ 25 | public function addExtendTypeInfo(ReflectionClass $class, ExtendType $extendType): void; 26 | } 27 | -------------------------------------------------------------------------------- /src/Mappers/GlobExtendAnnotationsCache.php: -------------------------------------------------------------------------------- 1 | extendTypeClassName; 26 | } 27 | 28 | public function getExtendTypeName(): string 29 | { 30 | return $this->extendTypeName; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Mappers/GlobExtendTypeMapperCache.php: -------------------------------------------------------------------------------- 1 | > Maps a domain class to one or many type extenders (with the @ExtendType annotation) The array of type extenders has a key and value equals to FQCN */ 15 | private array $mapClassToExtendTypeArray = []; 16 | /** @var array> Maps a GraphQL type name to one or many type extenders (with the @ExtendType annotation) The array of type extenders has a key and value equals to FQCN */ 17 | private array $mapNameToExtendType = []; 18 | 19 | /** 20 | * Merges annotations of a given class in the global cache. 21 | * 22 | * @param ReflectionClass|class-string $sourceClass 23 | */ 24 | public function registerAnnotations(ReflectionClass|string $sourceClass, GlobExtendAnnotationsCache $globExtendAnnotationsCache): void 25 | { 26 | $className = $sourceClass instanceof ReflectionClass ? $sourceClass->getName() : $sourceClass; 27 | 28 | $typeClassName = $globExtendAnnotationsCache->getExtendTypeClassName(); 29 | if ($typeClassName !== null) { 30 | $this->mapClassToExtendTypeArray[$typeClassName][$className] = $className; 31 | } 32 | 33 | $typeName = $globExtendAnnotationsCache->getExtendTypeName(); 34 | $this->mapNameToExtendType[$typeName][$className] = $className; 35 | } 36 | 37 | /** @return array|null An array of classes with the @ExtendType annotation (key and value = FQCN) */ 38 | public function getExtendTypesByObjectClass(string $className): array|null 39 | { 40 | return $this->mapClassToExtendTypeArray[$className] ?? null; 41 | } 42 | 43 | /** @return array|null An array of classes with the @ExtendType annotation (key and value = FQCN) */ 44 | public function getExtendTypesByGraphQLTypeName(string $graphqlTypeName): array|null 45 | { 46 | return $this->mapNameToExtendType[$graphqlTypeName] ?? null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Mappers/MappedClass.php: -------------------------------------------------------------------------------- 1 | className = $className; 21 | }*/ 22 | 23 | /** @return string */ 24 | /*public function getClassName(): string 25 | { 26 | return $this->className; 27 | }*/ 28 | 29 | /** @return MappedClass|null */ 30 | /*public function getParent(): ?MappedClass 31 | { 32 | return $this->parent; 33 | }*/ 34 | 35 | /** @param MappedClass|null $parent */ 36 | /*public function setParent(?MappedClass $parent): void 37 | { 38 | $this->parent = $parent; 39 | }*/ 40 | 41 | /** @return MappedClass[] */ 42 | public function getChildren(): array 43 | { 44 | return $this->children; 45 | } 46 | 47 | public function addChild(MappedClass $child): void 48 | { 49 | $this->children[] = $child; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Mappers/Parameters/CannotHideParameterRuntimeException.php: -------------------------------------------------------------------------------- 1 | getDeclaringFunction(); 18 | assert($method instanceof ReflectionMethod); 19 | return new self('For parameter $' . $parameter->getName() . ' of method ' . $method->getDeclaringClass()->getName() . '::' . $method->getName() . '(), cannot use the @HideParameter annotation. The parameter needs to provide a default value.'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Mappers/Parameters/ContainerParameterHandler.php: -------------------------------------------------------------------------------- 1 | getAnnotationByType(Autowire::class); 31 | 32 | if ($autowire === null) { 33 | return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations); 34 | } 35 | 36 | $id = $autowire->getIdentifier(); 37 | if ($id === null) { 38 | $type = $parameter->getType(); 39 | if ($type === null) { 40 | throw MissingAutowireTypeException::create($parameter); 41 | } 42 | assert($type instanceof ReflectionNamedType); 43 | $id = $type->getName(); 44 | } 45 | 46 | return new ContainerParameter($this->container, $id); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Mappers/Parameters/InjectUserParameterHandler.php: -------------------------------------------------------------------------------- 1 | getAnnotationByType(InjectUser::class); 28 | 29 | if ($injectUser === null) { 30 | return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations); 31 | } 32 | 33 | // Now we need to know if authentication is optional. If type isn't nullable we'll assume the user 34 | // is required for that parameter. If type is missing, it's also assumed optional. 35 | $optional = $parameter->getType()?->allowsNull() ?? true; 36 | 37 | return new InjectUserParameter($this->authenticationService, $optional); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Mappers/Parameters/MissingAutowireTypeException.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass(); 16 | if ($declaringClass === null) { 17 | throw new InvalidArgumentException('Parameter passed must be a parameter of a method, not a parameter of a function.'); 18 | } 19 | 20 | return new self('For parameter $' . $refParameter->getName() . ' in ' . $declaringClass->getName() . '::' . $refParameter->getDeclaringFunction()->getName() . ', annotated with annotation @Autowire, you must either provide a type-hint or specify the container identifier with @Autowire(identifier="my_service")'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Mappers/Parameters/Next.php: -------------------------------------------------------------------------------- 1 | queue = clone $queue; 32 | } 33 | 34 | public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, Type|null $paramTagType, ParameterAnnotations $parameterAnnotations): ParameterInterface 35 | { 36 | if ($this->queue->isEmpty()) { 37 | return $this->fallbackHandler->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations); 38 | } 39 | 40 | $middleware = $this->queue->dequeue(); 41 | assert($middleware instanceof ParameterMiddlewareInterface); 42 | 43 | return $middleware->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations, $this); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Mappers/Parameters/ParameterHandlerInterface.php: -------------------------------------------------------------------------------- 1 | */ 17 | private SplQueue $pipeline; 18 | 19 | /** 20 | * Initializes the queue. 21 | */ 22 | public function __construct() 23 | { 24 | $this->pipeline = new SplQueue(); 25 | } 26 | 27 | /** 28 | * PSR-15 middleware invocation. 29 | * 30 | * Executes the internal pipeline, passing $handler as the "final 31 | * handler" in cases when the pipeline exhausts itself. 32 | */ 33 | public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, Type|null $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $parameterMapper): ParameterInterface 34 | { 35 | return (new Next($this->pipeline, $parameterMapper))->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations); 36 | } 37 | 38 | /** 39 | * Attach middleware to the pipeline. 40 | */ 41 | public function pipe(ParameterMiddlewareInterface $middleware): void 42 | { 43 | $this->pipeline->enqueue($middleware); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Mappers/Parameters/PrefetchParameterMiddleware.php: -------------------------------------------------------------------------------- 1 | getAnnotationByType(Prefetch::class); 35 | 36 | if ($prefetch === null) { 37 | return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations); 38 | } 39 | 40 | $method = $parameter->getDeclaringFunction(); 41 | 42 | assert($method instanceof ReflectionMethod); 43 | 44 | // Map callable specified by #[Prefetch] into a real callable and parse all of the GraphQL parameters. 45 | try { 46 | [$resolver, $parameters] = $this->parameterizedCallableResolver->resolve($prefetch->callable, $method->getDeclaringClass(), 1); 47 | } catch (InvalidCallableRuntimeException $e) { 48 | throw InvalidPrefetchMethodRuntimeException::fromInvalidCallable($method, $parameter->getName(), $e); 49 | } 50 | 51 | return new PrefetchDataParameter( 52 | fieldName: $method->getName(), 53 | resolver: $resolver, 54 | parameters: $parameters, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Mappers/Parameters/ResolveInfoParameterHandler.php: -------------------------------------------------------------------------------- 1 | getType(); 23 | assert($type === null || $type instanceof ReflectionNamedType); 24 | if ($type !== null && $type->getName() === ResolveInfo::class) { 25 | return new ResolveInfoParameter(); 26 | } 27 | 28 | return $parameterMapper->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Mappers/PorpaginasMissingParameterException.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->className = $className; 23 | $this->name = $type->name; 24 | $this->description = $type->description; 25 | $this->config = $type->config; 26 | $this->astNode = $type->astNode; 27 | $this->extensionASTNodes = $type->extensionASTNodes; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Mappers/Proxys/MutableObjectTypeAdapter.php: -------------------------------------------------------------------------------- 1 | type = $type; 25 | $this->className = $className; 26 | $this->name = $type->name; 27 | $this->description = $type->description; 28 | $this->config = $type->config; 29 | $this->astNode = $type->astNode; 30 | $this->extensionASTNodes = $type->extensionASTNodes; 31 | $this->resolveFieldFn = $type->resolveFieldFn; 32 | } 33 | 34 | /** 35 | * @return InterfaceType[] 36 | */ 37 | public function getInterfaces(): array 38 | { 39 | $type = $this->type; 40 | assert($type instanceof ObjectType); 41 | return $type->getInterfaces(); 42 | } 43 | 44 | /** 45 | * @param mixed[] $value 46 | * @param mixed[]|null $context 47 | * 48 | * @return bool|\GraphQL\Deferred|null 49 | */ 50 | public function isTypeOf($value, $context, ResolveInfo $info) 51 | { 52 | $type = $this->type; 53 | assert($type instanceof ObjectType); 54 | return $type->isTypeOf($value, $context, $info); 55 | } 56 | 57 | /** 58 | * @param InterfaceType $iface 59 | * 60 | * @return bool 61 | */ 62 | public function implementsInterface($iface): bool 63 | { 64 | $type = $this->type; 65 | assert($type instanceof ObjectType); 66 | return $type->implementsInterface($iface); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Mappers/Root/CallableTypeMapper.php: -------------------------------------------------------------------------------- 1 | next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); 33 | } 34 | 35 | if ($type->getParameters()) { 36 | throw CannotMapTypeException::createForUnexpectedCallableParameters(); 37 | } 38 | 39 | $returnType = $type->getReturnType(); 40 | 41 | if (! $returnType) { 42 | throw CannotMapTypeException::createForMissingCallableReturnType(); 43 | } 44 | 45 | return $this->topRootTypeMapper->toGraphQLOutputType($returnType, null, $reflector, $docBlockObj); 46 | } 47 | 48 | public function toGraphQLInputType(Type $type, InputType|null $subType, string $argumentName, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): InputType&GraphQLType 49 | { 50 | if (! $type instanceof Callable_) { 51 | return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); 52 | } 53 | 54 | throw CannotMapTypeException::createForCallableAsInput(); 55 | } 56 | 57 | public function mapNameToType(string $typeName): NamedType&GraphQLType 58 | { 59 | return $this->next->mapNameToType($typeName); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Mappers/Root/FinalRootTypeMapper.php: -------------------------------------------------------------------------------- 1 | recursiveTypeMapper->mapNameToType($typeName); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Mappers/Root/LastDelegatingTypeMapper.php: -------------------------------------------------------------------------------- 1 | next = $next; 26 | } 27 | 28 | public function toGraphQLOutputType(Type $type, OutputType|null $subType, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): OutputType&GraphQLType 29 | { 30 | return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); 31 | } 32 | 33 | public function toGraphQLInputType(Type $type, InputType|null $subType, string $argumentName, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): InputType&GraphQLType 34 | { 35 | return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); 36 | } 37 | 38 | public function mapNameToType(string $typeName): NamedType&GraphQLType 39 | { 40 | return $this->next->mapNameToType($typeName); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Mappers/Root/RootTypeMapperFactoryContext.php: -------------------------------------------------------------------------------- 1 | annotationReader; 41 | } 42 | 43 | public function getTypeResolver(): TypeResolver 44 | { 45 | return $this->typeResolver; 46 | } 47 | 48 | public function getNamingStrategy(): NamingStrategyInterface 49 | { 50 | return $this->namingStrategy; 51 | } 52 | 53 | public function getTypeRegistry(): TypeRegistry 54 | { 55 | return $this->typeRegistry; 56 | } 57 | 58 | public function getRecursiveTypeMapper(): RecursiveTypeMapperInterface 59 | { 60 | return $this->recursiveTypeMapper; 61 | } 62 | 63 | public function getContainer(): ContainerInterface 64 | { 65 | return $this->container; 66 | } 67 | 68 | public function getCache(): CacheInterface 69 | { 70 | return $this->cache; 71 | } 72 | 73 | public function getClassFinder(): ClassFinder 74 | { 75 | return $this->classFinder; 76 | } 77 | 78 | public function getClassFinderComputedCache(): ClassFinderComputedCache 79 | { 80 | return $this->classFinderComputedCache; 81 | } 82 | 83 | public function getClassBoundCache(): ClassBoundCache 84 | { 85 | return $this->classBoundCache; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Mappers/Root/RootTypeMapperFactoryInterface.php: -------------------------------------------------------------------------------- 1 | next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); 33 | } 34 | 35 | return self::getVoidType(); 36 | } 37 | 38 | public function toGraphQLInputType(Type $type, InputType|null $subType, string $argumentName, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): InputType&GraphQLType 39 | { 40 | if (! $type instanceof Void_) { 41 | return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); 42 | } 43 | 44 | throw CannotMapTypeException::mustBeOutputType(self::getVoidType()->name); 45 | } 46 | 47 | public function mapNameToType(string $typeName): NamedType&GraphQLType 48 | { 49 | return match ($typeName) { 50 | self::getVoidType()->name => self::getVoidType(), 51 | default => $this->next->mapNameToType($typeName), 52 | }; 53 | } 54 | 55 | private static function getVoidType(): VoidType 56 | { 57 | return self::$voidType ??= new VoidType(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Mappers/StaticClassListTypeMapperFactory.php: -------------------------------------------------------------------------------- 1 | $classList The list of classes to be scanned. 20 | */ 21 | public function __construct( 22 | private array $classList, 23 | ) { 24 | } 25 | 26 | public function create(FactoryContext $context): TypeMapperInterface 27 | { 28 | $inputTypeUtils = new InputTypeUtils($context->getAnnotationReader(), $context->getNamingStrategy()); 29 | 30 | return new ClassFinderTypeMapper( 31 | new StaticClassFinder($this->classList), 32 | $context->getTypeGenerator(), 33 | $context->getInputTypeGenerator(), 34 | $inputTypeUtils, 35 | $context->getContainer(), 36 | $context->getAnnotationReader(), 37 | $context->getNamingStrategy(), 38 | $context->getRecursiveTypeMapper(), 39 | $context->getClassFinderComputedCache(), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Mappers/TypeMapperFactoryInterface.php: -------------------------------------------------------------------------------- 1 | getMiddlewareAnnotations(); 31 | 32 | $loggedAnnotation = $annotations->getAnnotationByType(Logged::class); 33 | $rightAnnotation = $annotations->getAnnotationByType(Right::class); 34 | 35 | // Avoid wrapping resolver callback when no annotations are specified. 36 | if (! $loggedAnnotation && ! $rightAnnotation) { 37 | return $inputFieldHandler->handle($inputFieldDescriptor); 38 | } 39 | 40 | $hideIfUnauthorized = $annotations->getAnnotationByType(HideIfUnauthorized::class); 41 | 42 | if ($hideIfUnauthorized !== null && ! $this->isAuthorized($loggedAnnotation, $rightAnnotation)) { 43 | return null; 44 | } 45 | 46 | $resolver = $inputFieldDescriptor->getResolver(); 47 | 48 | $inputFieldDescriptor = $inputFieldDescriptor->withResolver(function (...$args) use ($rightAnnotation, $loggedAnnotation, $resolver) { 49 | if ($this->isAuthorized($loggedAnnotation, $rightAnnotation)) { 50 | return $resolver(...$args); 51 | } 52 | 53 | if ($loggedAnnotation !== null && ! $this->authenticationService->isLogged()) { 54 | throw MissingAuthorizationException::unauthorized(); 55 | } 56 | 57 | throw MissingAuthorizationException::forbidden(); 58 | }); 59 | 60 | return $inputFieldHandler->handle($inputFieldDescriptor); 61 | } 62 | 63 | /** 64 | * Checks the @Logged and @Right annotations. 65 | */ 66 | private function isAuthorized(Logged|null $loggedAnnotation, Right|null $rightAnnotation): bool 67 | { 68 | if ($loggedAnnotation !== null && ! $this->authenticationService->isLogged()) { 69 | return false; 70 | } 71 | 72 | return $rightAnnotation === null || $this->authorizationService->isAllowed($rightAnnotation->getName()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Middlewares/BadExpressionInSecurityException.php: -------------------------------------------------------------------------------- 1 | getOriginalResolver(); 20 | $message = 'An error occurred while evaluating expression in @Security annotation of method "' . $originalResolver->toString() . '": ' . $e->getMessage(); 21 | 22 | return new self($message, $e->getCode(), $e); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Middlewares/CostFieldMiddleware.php: -------------------------------------------------------------------------------- 1 | getMiddlewareAnnotations()->getAnnotationByType(Cost::class); 22 | 23 | if (! $costAttribute) { 24 | return $fieldHandler->handle($queryFieldDescriptor); 25 | } 26 | 27 | $field = $fieldHandler->handle( 28 | $queryFieldDescriptor->withAddedCommentLines($this->buildQueryComment($costAttribute)), 29 | ); 30 | 31 | if (! $field) { 32 | return $field; 33 | } 34 | 35 | $field->complexityFn = static function (int $childrenComplexity, array $fieldArguments) use ($costAttribute): int { 36 | if (! $costAttribute->multipliers) { 37 | return $costAttribute->complexity + $childrenComplexity; 38 | } 39 | 40 | $cost = $costAttribute->complexity + $childrenComplexity; 41 | $needsDefaultMultiplier = true; 42 | 43 | foreach ($costAttribute->multipliers as $multiplier) { 44 | $value = $fieldArguments[$multiplier] ?? null; 45 | 46 | if (! is_int($value)) { 47 | continue; 48 | } 49 | 50 | $cost *= $value; 51 | $needsDefaultMultiplier = false; 52 | } 53 | 54 | if ($needsDefaultMultiplier && $costAttribute->defaultMultiplier !== null) { 55 | $cost *= $costAttribute->defaultMultiplier; 56 | } 57 | 58 | return $cost; 59 | }; 60 | 61 | return $field; 62 | } 63 | 64 | private function buildQueryComment(Cost $costAttribute): string 65 | { 66 | return "\nCost: " . 67 | implode(', ', [ 68 | 'complexity = ' . $costAttribute->complexity, 69 | 'multipliers = [' . implode(', ', $costAttribute->multipliers) . ']', 70 | 'defaultMultiplier = ' . ($costAttribute->defaultMultiplier ?? 'null'), 71 | ]); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Middlewares/FieldHandlerInterface.php: -------------------------------------------------------------------------------- 1 | pipeline = new SplQueue(); 21 | } 22 | 23 | /** 24 | * PSR-15 middleware invocation. 25 | * 26 | * Executes the internal pipeline, passing $handler as the "final 27 | * handler" in cases when the pipeline exhausts itself. 28 | */ 29 | public function process(QueryFieldDescriptor $queryFieldDescriptor, FieldHandlerInterface $fieldHandler): FieldDefinition|null 30 | { 31 | return (new Next($this->pipeline, $fieldHandler))->handle($queryFieldDescriptor); 32 | } 33 | 34 | /** 35 | * Attach middleware to the pipeline. 36 | */ 37 | public function pipe(FieldMiddlewareInterface $middleware): void 38 | { 39 | $this->pipeline->enqueue($middleware); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Middlewares/InputFieldHandlerInterface.php: -------------------------------------------------------------------------------- 1 | pipeline = new SplQueue(); 21 | } 22 | 23 | /** 24 | * PSR-15 middleware invocation. 25 | * 26 | * Executes the internal pipeline, passing $handler as the "final 27 | * handler" in cases when the pipeline exhausts itself. 28 | */ 29 | public function process(InputFieldDescriptor $inputFieldDescriptor, InputFieldHandlerInterface $inputFieldHandler): InputField|null 30 | { 31 | return (new InputNext($this->pipeline, $inputFieldHandler))->handle($inputFieldDescriptor); 32 | } 33 | 34 | /** 35 | * Attach middleware to the pipeline. 36 | */ 37 | public function pipe(InputFieldMiddlewareInterface $middleware): void 38 | { 39 | $this->pipeline->enqueue($middleware); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Middlewares/InputNext.php: -------------------------------------------------------------------------------- 1 | queue = clone $queue; 29 | } 30 | 31 | public function handle(InputFieldDescriptor $inputFieldDescriptor): InputField|null 32 | { 33 | if ($this->queue->isEmpty()) { 34 | return $this->fallbackHandler->handle($inputFieldDescriptor); 35 | } 36 | 37 | $middleware = $this->queue->dequeue(); 38 | assert($middleware instanceof InputFieldMiddlewareInterface); 39 | return $middleware->process($inputFieldDescriptor, $this); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Middlewares/MagicPropertyResolver.php: -------------------------------------------------------------------------------- 1 | className; 34 | } 35 | 36 | public function propertyName(): string 37 | { 38 | return $this->propertyName; 39 | } 40 | 41 | public function executionSource(object|null $source): object|null 42 | { 43 | return $source; 44 | } 45 | 46 | public function __invoke(object|null $source, mixed ...$args): mixed 47 | { 48 | if ($source === null) { 49 | throw new GraphQLRuntimeException('You must provide a source for MagicPropertyResolver.'); 50 | } 51 | 52 | if (! method_exists($source, '__get')) { 53 | throw MissingMagicGetException::cannotFindMagicGet($source::class); 54 | } 55 | 56 | return $source->__get($this->propertyName); 57 | } 58 | 59 | public function toString(): string 60 | { 61 | return $this->className . '::__get(\'' . $this->propertyName . '\')'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Middlewares/MissingAuthorizationException.php: -------------------------------------------------------------------------------- 1 | queue = clone $queue; 27 | } 28 | 29 | public function handle(QueryFieldDescriptor $fieldDescriptor): FieldDefinition|null 30 | { 31 | if ($this->queue->isEmpty()) { 32 | return $this->fallbackHandler->handle($fieldDescriptor); 33 | } 34 | 35 | return $this->queue->dequeue()->process($fieldDescriptor, $this); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Middlewares/ResolverInterface.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 23 | } 24 | 25 | /** @return callable&array{0:object, 1:string} */ 26 | public function callable(): callable 27 | { 28 | return $this->callable; 29 | } 30 | 31 | public function executionSource(object|null $source): object 32 | { 33 | return $this->callable[0]; 34 | } 35 | 36 | public function __invoke(object|null $source, mixed ...$args): mixed 37 | { 38 | $callable = $this->callable; 39 | 40 | return $callable(...$args); 41 | } 42 | 43 | public function toString(): string 44 | { 45 | $class = get_class($this->callable[0]); 46 | 47 | return $class . '::' . $this->callable[1] . '()'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Middlewares/SourceConstructorParameterResolver.php: -------------------------------------------------------------------------------- 1 | className; 29 | } 30 | 31 | public function parameterName(): string 32 | { 33 | return $this->parameterName; 34 | } 35 | 36 | public function executionSource(object|null $source): object|null 37 | { 38 | return $source; 39 | } 40 | 41 | public function __invoke(object|null $source, mixed ...$args): mixed 42 | { 43 | return $args[0]; 44 | } 45 | 46 | public function toString(): string 47 | { 48 | return $this->className . '::__construct($' . $this->parameterName . ')'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Middlewares/SourceInputPropertyResolver.php: -------------------------------------------------------------------------------- 1 | propertyReflection; 27 | } 28 | 29 | public function executionSource(object|null $source): object|null 30 | { 31 | return $source; 32 | } 33 | 34 | public function __invoke(object|null $source, mixed ...$args): mixed 35 | { 36 | if ($source === null) { 37 | throw new GraphQLRuntimeException('You must provide a source for SourceInputPropertyResolver.'); 38 | } 39 | 40 | PropertyAccessor::setValue($source, $this->propertyReflection->getName(), ...$args); 41 | 42 | return $args[0]; 43 | } 44 | 45 | public function toString(): string 46 | { 47 | return $this->propertyReflection->getDeclaringClass()->getName() . '::' . $this->propertyReflection->getName(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Middlewares/SourceMethodResolver.php: -------------------------------------------------------------------------------- 1 | methodReflection; 29 | } 30 | 31 | public function executionSource(object|null $source): object|null 32 | { 33 | return $source; 34 | } 35 | 36 | public function __invoke(object|null $source, mixed ...$args): mixed 37 | { 38 | if ($source === null) { 39 | throw new GraphQLRuntimeException('You must provide a source for SourceMethodResolver.'); 40 | } 41 | 42 | $callable = [$source, $this->methodReflection->getName()]; 43 | assert(is_callable($callable)); 44 | 45 | return $callable(...$args); 46 | } 47 | 48 | public function toString(): string 49 | { 50 | return $this->methodReflection->getDeclaringClass()->getName() . '::' . $this->methodReflection->getName() . '()'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Middlewares/SourcePropertyResolver.php: -------------------------------------------------------------------------------- 1 | propertyReflection; 27 | } 28 | 29 | public function executionSource(object|null $source): object|null 30 | { 31 | return $source; 32 | } 33 | 34 | public function __invoke(object|null $source, mixed ...$args): mixed 35 | { 36 | if ($source === null) { 37 | throw new GraphQLRuntimeException('You must provide a source for SourcePropertyResolver.'); 38 | } 39 | 40 | return PropertyAccessor::getValue($source, $this->propertyReflection->getName(), ...$args); 41 | } 42 | 43 | public function toString(): string 44 | { 45 | return $this->propertyReflection->getDeclaringClass()->getName() . '::' . $this->propertyReflection->getName(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/MissingAnnotationException.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass()->getName(), $method->getName())); 18 | } 19 | 20 | public static function invalidReturnType(ReflectionMethod $method): self 21 | { 22 | $returnType = $method->getReturnType(); 23 | assert($returnType === null || $returnType instanceof ReflectionNamedType); 24 | return new self(sprintf('The return type of factory "%s::%s" must be an object, "%s" passed instead.', $method->getDeclaringClass()->getName(), $method->getName(), $returnType ? $returnType->getName() : 'mixed')); 25 | } 26 | 27 | public static function nullableReturnType(ReflectionMethod $method): self 28 | { 29 | return new self(sprintf('Factory "%s::%s" must have a non nullable return type.', $method->getDeclaringClass()->getName(), $method->getName())); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/NamingStrategyInterface.php: -------------------------------------------------------------------------------- 1 | $className 35 | */ 36 | public function getInputTypeName(string $className, Input|Factory $input): string; 37 | 38 | /** 39 | * Returns the name of a GraphQL field from the name of the annotated method. 40 | */ 41 | public function getFieldNameFromMethodName(string $methodName): string; 42 | 43 | /** 44 | * Returns the name of a GraphQL input field from the name of the annotated method. 45 | */ 46 | public function getInputFieldNameFromMethodName(string $methodName): string; 47 | 48 | /** 49 | * Returns the name of a GraphQL union type based on the included types. 50 | * 51 | * @param string[] $typeNames The list of GraphQL type names 52 | */ 53 | public function getUnionTypeName(array $typeNames): string; 54 | } 55 | -------------------------------------------------------------------------------- /src/ParameterizedCallableResolver.php: -------------------------------------------------------------------------------- 1 | } 30 | */ 31 | public function resolve(string|array $callable, string|ReflectionClass $classContext, int $skip = 0): array 32 | { 33 | if ($classContext instanceof ReflectionClass) { 34 | $classContext = $classContext->getName(); 35 | } 36 | 37 | // If string method is given, it's equivalent to [self::class, 'method'] 38 | if (is_string($callable)) { 39 | $callable = [$classContext, $callable]; 40 | } 41 | 42 | try { 43 | $refMethod = new ReflectionMethod($callable[0], $callable[1]); 44 | } catch (ReflectionException $e) { 45 | throw InvalidCallableRuntimeException::methodNotFound($callable[0], $callable[1], $e); 46 | } 47 | 48 | // If method isn't static, then we should try to resolve the class name through the container. 49 | if (! $refMethod->isStatic()) { 50 | $callable = fn (...$args) => $this->container->get($callable[0])->{$callable[1]}(...$args); 51 | } 52 | 53 | assert(is_callable($callable)); 54 | 55 | // Map all parameters of the callable. 56 | $parameters = $this->fieldsBuilder->getParameters($refMethod, $skip); 57 | 58 | return [$callable, $parameters]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Parameters/ContainerParameter.php: -------------------------------------------------------------------------------- 1 | $args */ 20 | public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): mixed 21 | { 22 | return $this->container->get($this->identifier); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Parameters/DefaultValueParameter.php: -------------------------------------------------------------------------------- 1 | $args */ 19 | public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): mixed 20 | { 21 | return $this->defaultValue; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Parameters/ExpandsInputTypeParameters.php: -------------------------------------------------------------------------------- 1 | */ 10 | public function toInputTypeParameters(): array; 11 | } 12 | -------------------------------------------------------------------------------- /src/Parameters/InjectUserParameter.php: -------------------------------------------------------------------------------- 1 | $args */ 24 | public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): object|null 25 | { 26 | $user = $this->authenticationService->getUser(); 27 | 28 | // If user is required but wasn't provided, we'll throw unauthorized error the same way #[Logged] does. 29 | if (! $user && ! $this->optional) { 30 | throw MissingAuthorizationException::unauthorized(); 31 | } 32 | 33 | return $user; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Parameters/InputTypeParameter.php: -------------------------------------------------------------------------------- 1 | $args */ 27 | public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): mixed 28 | { 29 | if (isset($args[$this->name])) { 30 | return $this->argumentResolver->resolve($source, $args[$this->name], $context, $info, $this->type); 31 | } 32 | 33 | if ($this->hasDefaultValue) { 34 | return $this->defaultValue; 35 | } 36 | 37 | // Special case: if an argument is not provided for a factory BUT the factory can be instantiated without 38 | // passing any argument. Let's resolve that. 39 | if ($this->type instanceof ResolvableMutableInputObjectType && $this->type->isInstantiableWithoutParameters()) { 40 | return $this->argumentResolver->resolve($source, [], $context, $info, $this->type); 41 | } 42 | 43 | throw MissingArgumentException::create($this->name); 44 | } 45 | 46 | public function getName(): string 47 | { 48 | return $this->name; 49 | } 50 | 51 | public function getType(): InputType&Type 52 | { 53 | return $this->type; 54 | } 55 | 56 | public function hasDefaultValue(): bool 57 | { 58 | return $this->hasDefaultValue; 59 | } 60 | 61 | public function getDefaultValue(): mixed 62 | { 63 | return $this->defaultValue; 64 | } 65 | 66 | public function getDescription(): string 67 | { 68 | return $this->description ?? ''; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Parameters/InputTypeParameterInterface.php: -------------------------------------------------------------------------------- 1 | $args */ 15 | public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): mixed; 16 | 17 | public function getType(): InputType&Type; 18 | 19 | public function hasDefaultValue(): bool; 20 | 21 | public function getDefaultValue(): mixed; 22 | 23 | public function getName(): string; 24 | 25 | public function getDescription(): string; 26 | } 27 | -------------------------------------------------------------------------------- /src/Parameters/InputTypeProperty.php: -------------------------------------------------------------------------------- 1 | propertyName; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Parameters/MissingArgumentException.php: -------------------------------------------------------------------------------- 1 | getMessage(), 28 | $inputType, 29 | self::toLocation($callable), 30 | ); 31 | 32 | return new self($message, 0, $previous); 33 | } 34 | 35 | public static function wrapWithDecoratorContext(self $previous, string $inputType, callable $callable): self 36 | { 37 | $message = sprintf( 38 | '%s in GraphQL input type \'%s\' used in decorator \'%s\'', 39 | $previous->getMessage(), 40 | $inputType, 41 | self::toLocation($callable), 42 | ); 43 | 44 | return new self($message, 0, $previous); 45 | } 46 | 47 | public static function wrapWithFieldContext(self $previous, string $name, callable $callable): self 48 | { 49 | $message = sprintf( 50 | '%s in GraphQL query/mutation/field \'%s\' used in method \'%s\'', 51 | $previous->getMessage(), 52 | $name, 53 | self::toLocation($callable), 54 | ); 55 | 56 | return new self($message, 0, $previous); 57 | } 58 | 59 | private static function toLocation(callable $callable): string 60 | { 61 | if ($callable instanceof ResolverInterface) { 62 | return $callable->toString(); 63 | } 64 | if (! is_array($callable)) { 65 | return ''; 66 | } 67 | if (is_string($callable[0])) { 68 | $factoryName = $callable[0]; 69 | } else { 70 | $factoryName = get_class($callable[0]); 71 | } 72 | 73 | return $factoryName . '::' . $callable[1] . '()'; 74 | } 75 | 76 | /** 77 | * Returns true when exception message is safe to be displayed to a client. 78 | */ 79 | public function isClientSafe(): bool 80 | { 81 | return true; 82 | } 83 | 84 | /** 85 | * Returns the "extensions" object attached to the GraphQL error. 86 | * 87 | * @return array 88 | */ 89 | public function getExtensions(): array 90 | { 91 | return []; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Parameters/ParameterInterface.php: -------------------------------------------------------------------------------- 1 | $args */ 17 | public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): mixed; 18 | } 19 | -------------------------------------------------------------------------------- /src/Parameters/ResolveInfoParameter.php: -------------------------------------------------------------------------------- 1 | $args */ 15 | public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): ResolveInfo 16 | { 17 | return $info; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Parameters/SourceParameter.php: -------------------------------------------------------------------------------- 1 | $args */ 15 | public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): object|null 16 | { 17 | return $source; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/PrefetchBuffer.php: -------------------------------------------------------------------------------- 1 | > An array of buffered, indexed by hash of arguments. */ 19 | private array $objects = []; 20 | 21 | /** @var WeakMap A Storage of prefetch method results, holds source to resolved values. */ 22 | private WeakMap $results; 23 | 24 | public function __construct() 25 | { 26 | $this->results = new WeakMap(); 27 | } 28 | 29 | /** @param array $arguments The input arguments passed from GraphQL to the field. */ 30 | public function register( 31 | object $object, 32 | array $arguments, 33 | ResolveInfo|null $info = null, 34 | ): void { 35 | $this->objects[$this->computeHash($arguments, $info)][] = $object; 36 | } 37 | 38 | /** @param array $arguments The input arguments passed from GraphQL to the field. */ 39 | private function computeHash( 40 | array $arguments, 41 | ResolveInfo|null $info, 42 | ): string { 43 | if ( 44 | $info instanceof ResolveInfo 45 | && isset($info->operation) 46 | && $info->operation->loc?->source?->body !== null 47 | ) { 48 | return md5(serialize($arguments) . $info->operation->loc->source->body); 49 | } 50 | 51 | return md5(serialize($arguments)); 52 | } 53 | 54 | /** 55 | * @param array $arguments The input arguments passed from GraphQL to the field. 56 | * 57 | * @return array 58 | */ 59 | public function getObjectsByArguments( 60 | array $arguments, 61 | ResolveInfo|null $info = null, 62 | ): array { 63 | return $this->objects[$this->computeHash($arguments, $info)] ?? []; 64 | } 65 | 66 | /** @param array $arguments The input arguments passed from GraphQL to the field. */ 67 | public function purge( 68 | array $arguments, 69 | ResolveInfo|null $info = null, 70 | ): void { 71 | unset($this->objects[$this->computeHash($arguments, $info)]); 72 | } 73 | 74 | public function storeResult( 75 | object $source, 76 | mixed $result, 77 | ): void { 78 | $this->results->offsetSet($source, $result); 79 | } 80 | 81 | public function hasResult( 82 | object $source, 83 | ): bool { 84 | return $this->results->offsetExists($source); 85 | } 86 | 87 | public function getResult( 88 | object $source, 89 | ): mixed { 90 | return $this->results->offsetGet($source); 91 | } 92 | 93 | public function purgeResult( 94 | object $source, 95 | ): void { 96 | $this->results->offsetUnset($source); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/QueryProviderFactoryInterface.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass(); 34 | 35 | return $this->classBoundCache->get( 36 | $class, 37 | fn () => $this->docBlockFactory->create($reflector, $context ?? $this->createContext($class)), 38 | 'reflection.docBlock.' . md5($reflector::class . '.' . $reflector->getName()), 39 | ); 40 | } 41 | 42 | public function createContext(ReflectionClass|ReflectionMethod|ReflectionProperty|ReflectionClassConstant $reflector): Context 43 | { 44 | $reflector = $reflector instanceof ReflectionClass ? $reflector : $reflector->getDeclaringClass(); 45 | 46 | return $this->classBoundCache->get( 47 | $reflector, 48 | fn () => $this->docBlockFactory->createContext($reflector), 49 | 'reflection.docBlockContext', 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/DocBlockFactory.php: -------------------------------------------------------------------------------- 1 | getDocComment() ?: '/** */'; 40 | 41 | return $this->docBlockFactory->create( 42 | $docblock, 43 | $context ?? $this->createContext($reflector), 44 | ); 45 | } 46 | 47 | public function createContext(ReflectionClass|ReflectionMethod|ReflectionProperty|ReflectionClassConstant $reflector): Context 48 | { 49 | return $this->contextFactory->createFromReflector($reflector); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Reflection/ReflectionInterfaceUtils.php: -------------------------------------------------------------------------------- 1 | $reflectionClass 18 | * 19 | * @return array> Interfaces indexed by FQCN 20 | * 21 | * @template T of object 22 | */ 23 | public static function getDirectlyImplementedInterfaces(ReflectionClass $reflectionClass): array 24 | { 25 | $interfaces = $reflectionClass->getInterfaces(); 26 | 27 | $subInterfaces = []; 28 | foreach ($interfaces as $interface) { 29 | $subInterfaces += $interface->getInterfaces(); 30 | } 31 | 32 | return array_diff_key($interfaces, $subInterfaces); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Security/AuthenticationServiceInterface.php: -------------------------------------------------------------------------------- 1 | isAllowed(%s, %s)', $rightName, $object); 23 | }, static function (array $variables, string $rightName, $object = null): bool { 24 | return $variables['authorizationService']->isAllowed($rightName, $object); 25 | }), 26 | 27 | new ExpressionFunction('is_logged', static function (): string { 28 | return '$authenticationService->isLogged()'; 29 | }, static function (array $variables): bool { 30 | return $variables['authenticationService']->isLogged(); 31 | }), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Security/SecurityNotImplementedException.php: -------------------------------------------------------------------------------- 1 | cache->get($queryId); 30 | 31 | if ($query) { 32 | return $query; 33 | } 34 | 35 | $query = $operation->query; 36 | 37 | if (! $query) { 38 | throw new PersistedQueryNotFoundException(); 39 | } 40 | 41 | if (! $this->queryMatchesId($queryId, $query)) { 42 | throw new PersistedQueryIdInvalidException(); 43 | } 44 | 45 | $this->cache->set($queryId, $query, $this->ttl); 46 | 47 | return $query; 48 | } 49 | 50 | private function queryMatchesId(string $queryId, string $query): bool 51 | { 52 | return $queryId === hash('sha256', $query); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Server/PersistedQuery/NotSupportedPersistedQueryLoader.php: -------------------------------------------------------------------------------- 1 | code = 'PERSISTED_QUERY_ID_INVALID'; 20 | } 21 | 22 | /** @return array */ 23 | public function getExtensions(): array 24 | { 25 | return [ 26 | 'code' => $this->code, 27 | ]; 28 | } 29 | 30 | public function isClientSafe(): bool 31 | { 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Server/PersistedQuery/PersistedQueryNotFoundException.php: -------------------------------------------------------------------------------- 1 | code = 'PERSISTED_QUERY_NOT_FOUND'; 20 | } 21 | 22 | /** @return array */ 23 | public function getExtensions(): array 24 | { 25 | return [ 26 | 'code' => $this->code, 27 | ]; 28 | } 29 | 30 | public function isClientSafe(): bool 31 | { 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Server/PersistedQuery/PersistedQueryNotSupportedException.php: -------------------------------------------------------------------------------- 1 | code = 'PERSISTED_QUERY_NOT_SUPPORTED'; 20 | } 21 | 22 | /** @return array */ 23 | public function getExtensions(): array 24 | { 25 | return [ 26 | 'code' => $this->code, 27 | ]; 28 | } 29 | 30 | public function isClientSafe(): bool 31 | { 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/TypeMismatchRuntimeException.php: -------------------------------------------------------------------------------- 1 | message = 'In ' . $location . ' (declaring field "' . $fieldName . '"): ' . $this->message; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Types/ArgumentResolver.php: -------------------------------------------------------------------------------- 1 | stripNonNullType($type); 36 | if ($type instanceof ListOfType) { 37 | if (! is_array($val)) { 38 | throw new InvalidArgumentException('Expected GraphQL List but value passed is not an array.'); 39 | } 40 | 41 | return array_map(function ($item) use ($type, $source, $context, $resolveInfo) { 42 | $wrappedType = $type->getWrappedType(); 43 | assert($wrappedType instanceof InputType); 44 | return $this->resolve($source, $item, $context, $resolveInfo, $wrappedType); 45 | }, $val); 46 | } 47 | 48 | if ($type instanceof IDType) { 49 | return new ID($val); 50 | } 51 | 52 | // For some reason, the enum type behaves differently as the LeafType. 53 | // If seems to be already resolved. 54 | if ($type instanceof EnumType) { 55 | return $val; 56 | } 57 | 58 | if ($type instanceof LeafType) { 59 | return $type->parseValue($val); 60 | } 61 | 62 | if ($type instanceof ResolvableMutableInputInterface) { 63 | return $type->resolve($source, $val, $context, $resolveInfo); 64 | } 65 | 66 | throw new RuntimeException('Unexpected type: ' . $type::class); 67 | } 68 | 69 | private function stripNonNullType(InputType&Type $type): InputType&Type 70 | { 71 | if ($type instanceof NonNull) { 72 | $wrapped = $type->getWrappedType(); 73 | assert($wrapped instanceof InputType); 74 | return $this->stripNonNullType($wrapped); 75 | } 76 | 77 | return $type; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Types/DateTimeType.php: -------------------------------------------------------------------------------- 1 | format(DateTimeInterface::ATOM); 29 | } 30 | 31 | public function parseValue(mixed $value): DateTimeImmutable|null 32 | { 33 | if ($value === null) { 34 | return null; 35 | } 36 | 37 | if ($value instanceof DateTimeImmutable) { 38 | return $value; 39 | } 40 | 41 | return new DateTimeImmutable($value); 42 | } 43 | 44 | /** 45 | * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input 46 | * 47 | * In the case of an invalid node or value this method must throw an Exception 48 | * 49 | * @param mixed $valueNode 50 | * @param array|null $variables 51 | * 52 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint 53 | */ 54 | public function parseLiteral($valueNode, array|null $variables = null): string 55 | { 56 | if ($valueNode instanceof StringValueNode) { 57 | return $valueNode->value; 58 | } 59 | 60 | // Intentionally without message, as all information already in wrapped Exception 61 | throw new GraphQLRuntimeException(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Types/EnumType.php: -------------------------------------------------------------------------------- 1 | $enumName 22 | * @param array $caseDescriptions 23 | * @param array $caseDeprecationReasons 24 | */ 25 | public function __construct( 26 | string $enumName, 27 | string $typeName, 28 | string|null $description, 29 | array $caseDescriptions, 30 | array $caseDeprecationReasons, 31 | private readonly bool $useValues = false, 32 | ) { 33 | $typeValues = []; 34 | foreach ($enumName::cases() as $case) { 35 | $key = $this->serialize($case); 36 | $typeValues[$key] = [ 37 | 'name' => $key, 38 | 'value' => $case, 39 | 'description' => $caseDescriptions[$case->name] ?? null, 40 | 'deprecationReason' => $caseDeprecationReasons[$case->name] ?? null, 41 | ]; 42 | } 43 | 44 | parent::__construct([ 45 | 'name' => $typeName, 46 | 'values' => $typeValues, 47 | 'description' => $description, 48 | ]); 49 | } 50 | 51 | // phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint 52 | 53 | /** @param mixed $value */ 54 | public function serialize($value): string 55 | { 56 | if (! $value instanceof UnitEnum) { 57 | throw new InvalidArgumentException('Expected a UnitEnum instance'); 58 | } 59 | 60 | if (! $this->useValues) { 61 | return $value->name; 62 | } 63 | 64 | assert($value instanceof BackedEnum); 65 | assert(is_string($value->value)); 66 | 67 | return $value->value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Types/ID.php: -------------------------------------------------------------------------------- 1 | value; 34 | } 35 | 36 | public function __toString(): string 37 | { 38 | if (is_bool($this->value)) { 39 | return $this->value === true ? '1' : '0'; 40 | } 41 | 42 | if (is_scalar($this->value)) { 43 | return (string) $this->value; 44 | } 45 | 46 | return $this->value->__toString(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Types/InputTypeValidatorInterface.php: -------------------------------------------------------------------------------- 1 | $name, 27 | 'fields' => static function () use ($type) { 28 | return $type->getFields(); 29 | }, 30 | 'description' => $type->description, 31 | 'resolveType' => static function ($value) use ($typeMapper, $subType) { 32 | if (! is_object($value)) { 33 | throw new InvalidArgumentException('Expected object for resolveType. Got: "' . gettype($value) . '"'); 34 | } 35 | 36 | $className = $value::class; 37 | 38 | return $typeMapper->mapClassToType($className, $subType); 39 | }, 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Types/InvalidTypesInUnionException.php: -------------------------------------------------------------------------------- 1 | 29 | * 30 | * @throws InvariantViolation 31 | */ 32 | public function getFields(): array; 33 | } 34 | -------------------------------------------------------------------------------- /src/Types/MutableInterfaceType.php: -------------------------------------------------------------------------------- 1 | |null $className 21 | */ 22 | public function __construct(array $config, string|null $className = null) 23 | { 24 | $this->status = self::STATUS_PENDING; 25 | 26 | parent::__construct($config); 27 | 28 | $this->className = $className; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Types/MutableObjectType.php: -------------------------------------------------------------------------------- 1 | |null $className 21 | */ 22 | public function __construct(array $config, string|null $className = null) 23 | { 24 | $this->status = self::STATUS_PENDING; 25 | 26 | parent::__construct($config); 27 | 28 | $this->className = $className; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Types/MyCLabsEnumType.php: -------------------------------------------------------------------------------- 1 | $value) { 24 | $constInstances[$key] = ['value' => $enumClassName::$key()]; 25 | } 26 | 27 | parent::__construct([ 28 | 'name' => $typeName, 29 | 'values' => $constInstances, 30 | ]); 31 | } 32 | 33 | public function serialize(mixed $value): mixed 34 | { 35 | if (! $value instanceof Enum) { 36 | throw new InvalidArgumentException('Expected a Myclabs Enum instance'); 37 | } 38 | return $value->getKey(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Types/NoFieldsException.php: -------------------------------------------------------------------------------- 1 | $name, 16 | 'fields' => static function () use ($type) { 17 | return $type->getFields(); 18 | }, 19 | 'interfaces' => [$type], 20 | 'description' => $type->description, 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Types/ResolvableMutableInputInterface.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 32 | } 33 | 34 | /** @throws CannotMapTypeExceptionInterface|JsonException */ 35 | public function mapNameToType(string $typeName): Type 36 | { 37 | if ($this->schema === null) { 38 | throw new RuntimeException('You must register a schema first before resolving types.'); 39 | } 40 | 41 | try { 42 | $parsedOutputType = Parser::parseType($typeName); 43 | 44 | $type = AST::typeFromAST([$this->schema, 'getType'], $parsedOutputType); 45 | } catch (Error $e) { 46 | throw CannotMapTypeException::createForParseError($e); 47 | } 48 | 49 | if ($type === null) { 50 | throw CannotMapTypeException::createForName($typeName); 51 | } 52 | 53 | return $type; 54 | } 55 | 56 | public function mapNameToOutputType(string $typeName): OutputType&Type 57 | { 58 | $type = $this->mapNameToType($typeName); 59 | if (! $type instanceof OutputType || ($type instanceof WrappingType && ! $type->getWrappedType() instanceof OutputType)) { 60 | throw CannotMapTypeException::mustBeOutputType($typeName); 61 | } 62 | 63 | return $type; 64 | } 65 | 66 | public function mapNameToInputType(string $typeName): InputType&Type 67 | { 68 | $type = $this->mapNameToType($typeName); 69 | if (! $type instanceof InputType || ($type instanceof WrappingType && ! $type->getWrappedType() instanceof InputType)) { 70 | throw CannotMapTypeException::mustBeInputType($typeName); 71 | } 72 | 73 | return $type; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Types/UnionType.php: -------------------------------------------------------------------------------- 1 | $types */ 21 | public function __construct( 22 | array $types, 23 | RecursiveTypeMapperInterface $typeMapper, 24 | NamingStrategyInterface $namingStrategy, 25 | ) 26 | { 27 | // Make sure all types are object types 28 | foreach ($types as $type) { 29 | if (! $type instanceof ObjectType) { 30 | throw InvalidTypesInUnionException::notObjectType(); 31 | } 32 | } 33 | 34 | $typeNames = array_map(static fn (ObjectType $type) => $type->name(), $types); 35 | $name = $namingStrategy->getUnionTypeName($typeNames); 36 | 37 | parent::__construct([ 38 | 'name' => $name, 39 | 'types' => $types, 40 | 'resolveType' => 41 | static function (mixed $value) use ($typeMapper): ObjectType { 42 | if (! is_object($value)) { 43 | throw new InvalidArgumentException('Expected object for resolveType. Got: "' . gettype($value) . '"'); 44 | } 45 | 46 | $className = $value::class; 47 | 48 | $result = $typeMapper->mapClassToInterfaceOrType($className, null); 49 | assert($result instanceof ObjectType); 50 | return $result; 51 | }, 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Types/VoidType.php: -------------------------------------------------------------------------------- 1 | ...$values 19 | * 20 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint 21 | */ 22 | public function with(...$values): static 23 | { 24 | $refClass = new ReflectionClass(static::class); 25 | $clone = $refClass->newInstanceWithoutConstructor(); 26 | 27 | foreach ($refClass->getProperties() as $refProperty) { 28 | if ($refProperty->isStatic()) { 29 | continue; 30 | } 31 | 32 | $objectField = $refProperty->getName(); 33 | 34 | if (! array_key_exists($objectField, $values) && ! $refProperty->isInitialized($this)) { 35 | continue; 36 | } 37 | 38 | $objectValue = array_key_exists($objectField, $values) ? $values[$objectField] : $refProperty->getValue($this); 39 | 40 | $refProperty->setValue($clone, $objectValue); 41 | } 42 | 43 | return $clone; 44 | } 45 | } 46 | --------------------------------------------------------------------------------