├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── UPGRADE.md ├── composer.json ├── docs ├── class-reference.md ├── complementary-tools.md ├── concepts.md ├── data-fetching.md ├── error-handling.md ├── executing-queries.md ├── getting-started.md ├── index.md ├── schema-definition-language.md ├── schema-definition.md ├── security.md └── type-definitions │ ├── directives.md │ ├── enums.md │ ├── index.md │ ├── inputs.md │ ├── interfaces.md │ ├── lists-and-nonnulls.md │ ├── object-types.md │ ├── scalars.md │ └── unions.md └── src ├── Deferred.php ├── Error ├── ClientAware.php ├── CoercionError.php ├── DebugFlag.php ├── Error.php ├── FormattedError.php ├── InvariantViolation.php ├── ProvidesExtensions.php ├── SerializationError.php ├── SyntaxError.php ├── UserError.php └── Warning.php ├── Executor ├── ExecutionContext.php ├── ExecutionResult.php ├── Executor.php ├── ExecutorImplementation.php ├── Promise │ ├── Adapter │ │ ├── AmpPromiseAdapter.php │ │ ├── ReactPromiseAdapter.php │ │ ├── SyncPromise.php │ │ └── SyncPromiseAdapter.php │ ├── Promise.php │ └── PromiseAdapter.php ├── ReferenceExecutor.php ├── ScopedContext.php └── Values.php ├── GraphQL.php ├── Language ├── AST │ ├── ArgumentNode.php │ ├── BooleanValueNode.php │ ├── DefinitionNode.php │ ├── DirectiveDefinitionNode.php │ ├── DirectiveNode.php │ ├── DocumentNode.php │ ├── EnumTypeDefinitionNode.php │ ├── EnumTypeExtensionNode.php │ ├── EnumValueDefinitionNode.php │ ├── EnumValueNode.php │ ├── ExecutableDefinitionNode.php │ ├── FieldDefinitionNode.php │ ├── FieldNode.php │ ├── FloatValueNode.php │ ├── FragmentDefinitionNode.php │ ├── FragmentSpreadNode.php │ ├── HasSelectionSet.php │ ├── InlineFragmentNode.php │ ├── InputObjectTypeDefinitionNode.php │ ├── InputObjectTypeExtensionNode.php │ ├── InputValueDefinitionNode.php │ ├── IntValueNode.php │ ├── InterfaceTypeDefinitionNode.php │ ├── InterfaceTypeExtensionNode.php │ ├── ListTypeNode.php │ ├── ListValueNode.php │ ├── Location.php │ ├── NameNode.php │ ├── NamedTypeNode.php │ ├── Node.php │ ├── NodeKind.php │ ├── NodeList.php │ ├── NonNullTypeNode.php │ ├── NullValueNode.php │ ├── ObjectFieldNode.php │ ├── ObjectTypeDefinitionNode.php │ ├── ObjectTypeExtensionNode.php │ ├── ObjectValueNode.php │ ├── OperationDefinitionNode.php │ ├── OperationTypeDefinitionNode.php │ ├── ScalarTypeDefinitionNode.php │ ├── ScalarTypeExtensionNode.php │ ├── SchemaDefinitionNode.php │ ├── SchemaExtensionNode.php │ ├── SelectionNode.php │ ├── SelectionSetNode.php │ ├── StringValueNode.php │ ├── TypeDefinitionNode.php │ ├── TypeExtensionNode.php │ ├── TypeNode.php │ ├── TypeSystemDefinitionNode.php │ ├── TypeSystemExtensionNode.php │ ├── UnionTypeDefinitionNode.php │ ├── UnionTypeExtensionNode.php │ ├── ValueNode.php │ ├── VariableDefinitionNode.php │ └── VariableNode.php ├── BlockString.php ├── DirectiveLocation.php ├── Lexer.php ├── Parser.php ├── Printer.php ├── Source.php ├── SourceLocation.php ├── Token.php ├── Visitor.php ├── VisitorOperation.php ├── VisitorRemoveNode.php ├── VisitorSkipNode.php └── VisitorStop.php ├── Server ├── Exception │ ├── BatchedQueriesAreNotSupported.php │ ├── CannotParseJsonBody.php │ ├── CannotParseVariables.php │ ├── CannotReadBody.php │ ├── FailedToDetermineOperationType.php │ ├── GetMethodSupportsOnlyQueryOperation.php │ ├── HttpMethodNotSupported.php │ ├── InvalidOperationParameter.php │ ├── InvalidQueryIdParameter.php │ ├── InvalidQueryParameter.php │ ├── MissingContentTypeHeader.php │ ├── MissingQueryOrQueryIdParameter.php │ ├── PersistedQueriesAreNotSupported.php │ └── UnexpectedContentType.php ├── Helper.php ├── OperationParams.php ├── RequestError.php ├── ServerConfig.php └── StandardServer.php ├── Type ├── Definition │ ├── AbstractType.php │ ├── Argument.php │ ├── BooleanType.php │ ├── CompositeType.php │ ├── CustomScalarType.php │ ├── Deprecated.php │ ├── Description.php │ ├── Directive.php │ ├── EnumType.php │ ├── EnumValueDefinition.php │ ├── FieldDefinition.php │ ├── FloatType.php │ ├── HasFieldsType.php │ ├── HasFieldsTypeImplementation.php │ ├── IDType.php │ ├── ImplementingType.php │ ├── ImplementingTypeImplementation.php │ ├── InputObjectField.php │ ├── InputObjectType.php │ ├── InputType.php │ ├── IntType.php │ ├── InterfaceType.php │ ├── LeafType.php │ ├── ListOfType.php │ ├── NamedType.php │ ├── NamedTypeImplementation.php │ ├── NonNull.php │ ├── NullableType.php │ ├── ObjectType.php │ ├── OutputType.php │ ├── PhpEnumType.php │ ├── QueryPlan.php │ ├── ResolveInfo.php │ ├── ScalarType.php │ ├── StringType.php │ ├── Type.php │ ├── UnionType.php │ ├── UnmodifiedType.php │ ├── UnresolvedFieldDefinition.php │ └── WrappingType.php ├── Introspection.php ├── Schema.php ├── SchemaConfig.php ├── SchemaValidationContext.php ├── TypeKind.php └── Validation │ └── InputObjectCircularRefs.php ├── Utils ├── AST.php ├── ASTDefinitionBuilder.php ├── BreakingChangesFinder.php ├── BuildClientSchema.php ├── BuildSchema.php ├── InterfaceImplementations.php ├── LazyException.php ├── LexicalDistance.php ├── MixedStore.php ├── PairSet.php ├── PhpDoc.php ├── SchemaExtender.php ├── SchemaPrinter.php ├── TypeComparators.php ├── TypeInfo.php ├── Utils.php └── Value.php └── Validator ├── DocumentValidator.php ├── QueryValidationContext.php ├── Rules ├── CustomValidationRule.php ├── DisableIntrospection.php ├── ExecutableDefinitions.php ├── FieldsOnCorrectType.php ├── FragmentsOnCompositeTypes.php ├── KnownArgumentNames.php ├── KnownArgumentNamesOnDirectives.php ├── KnownDirectives.php ├── KnownFragmentNames.php ├── KnownTypeNames.php ├── LoneAnonymousOperation.php ├── LoneSchemaDefinition.php ├── NoFragmentCycles.php ├── NoUndefinedVariables.php ├── NoUnusedFragments.php ├── NoUnusedVariables.php ├── OverlappingFieldsCanBeMerged.php ├── PossibleFragmentSpreads.php ├── PossibleTypeExtensions.php ├── ProvidedRequiredArguments.php ├── ProvidedRequiredArgumentsOnDirectives.php ├── QueryComplexity.php ├── QueryDepth.php ├── QuerySecurityRule.php ├── ScalarLeafs.php ├── SingleFieldSubscription.php ├── UniqueArgumentDefinitionNames.php ├── UniqueArgumentNames.php ├── UniqueDirectiveNames.php ├── UniqueDirectivesPerLocation.php ├── UniqueEnumValueNames.php ├── UniqueFieldDefinitionNames.php ├── UniqueFragmentNames.php ├── UniqueInputFieldNames.php ├── UniqueOperationNames.php ├── UniqueOperationTypes.php ├── UniqueTypeNames.php ├── UniqueVariableNames.php ├── ValidationRule.php ├── ValuesOfCorrectType.php ├── VariablesAreInputTypes.php └── VariablesInAllowedPosition.php ├── SDLValidationContext.php └── ValidationContext.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-present, Webonyx, LLC. 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: it 2 | it: fix stan test docs ## Run the commonly used targets 3 | 4 | .PHONY: help 5 | help: ## Displays this list of targets with descriptions 6 | @grep --extended-regexp '^[a-zA-Z0-9_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' 7 | 8 | .PHONY: setup 9 | setup: vendor phpstan.neon ## Set up the project 10 | 11 | .PHONY: fix 12 | fix: rector php-cs-fixer prettier ## Automatic code fixes 13 | 14 | .PHONY: rector 15 | rector: vendor ## Automatic code fixes with Rector 16 | composer rector 17 | 18 | .PHONY: php-cs-fixer 19 | php-cs-fixer: vendor ## Fix code style 20 | composer php-cs-fixer 21 | 22 | .PHONY: prettier 23 | prettier: ## Format code with prettier 24 | prettier --write --tab-width=2 *.md **/*.md 25 | 26 | phpstan.neon: 27 | printf "includes:\n - phpstan.neon.dist" > phpstan.neon 28 | 29 | .PHONY: stan 30 | stan: ## Runs static analysis with phpstan 31 | composer stan 32 | 33 | .PHONY: test 34 | test: ## Runs tests with phpunit 35 | composer test 36 | 37 | .PHONY: bench 38 | bench: ## Runs benchmarks with phpbench 39 | composer bench 40 | 41 | .PHONY: docs 42 | docs: ## Generate the class-reference docs 43 | php generate-class-reference.php 44 | prettier --write docs/class-reference.md 45 | 46 | vendor: composer.json composer.lock 47 | composer install 48 | composer validate 49 | composer normalize 50 | 51 | composer.lock: composer.json 52 | composer update 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webonyx/graphql-php", 3 | "description": "A PHP port of GraphQL reference implementation", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "graphql", 8 | "API" 9 | ], 10 | "homepage": "https://github.com/webonyx/graphql-php", 11 | "require": { 12 | "php": "^7.4 || ^8", 13 | "ext-json": "*", 14 | "ext-mbstring": "*" 15 | }, 16 | "require-dev": { 17 | "amphp/amp": "^2.6", 18 | "amphp/http-server": "^2.1", 19 | "dms/phpunit-arraysubset-asserts": "dev-master", 20 | "ergebnis/composer-normalize": "^2.28", 21 | "friendsofphp/php-cs-fixer": "3.75.0", 22 | "mll-lab/php-cs-fixer-config": "5.11.0", 23 | "nyholm/psr7": "^1.5", 24 | "phpbench/phpbench": "^1.2", 25 | "phpstan/extension-installer": "^1.1", 26 | "phpstan/phpstan": "2.1.17", 27 | "phpstan/phpstan-phpunit": "2.0.6", 28 | "phpstan/phpstan-strict-rules": "2.0.4", 29 | "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", 30 | "psr/http-message": "^1 || ^2", 31 | "react/http": "^1.6", 32 | "react/promise": "^2.0 || ^3.0", 33 | "rector/rector": "^2.0", 34 | "symfony/polyfill-php81": "^1.23", 35 | "symfony/var-exporter": "^5 || ^6 || ^7", 36 | "thecodingmachine/safe": "^1.3 || ^2 || ^3" 37 | }, 38 | "suggest": { 39 | "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", 40 | "psr/http-message": "To use standard GraphQL server", 41 | "react/promise": "To leverage async resolving on React PHP platform" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "GraphQL\\": "src/" 46 | } 47 | }, 48 | "autoload-dev": { 49 | "psr-4": { 50 | "GraphQL\\Benchmarks\\": "benchmarks/", 51 | "GraphQL\\Examples\\Blog\\": "examples/01-blog/Blog/", 52 | "GraphQL\\Tests\\": "tests/" 53 | } 54 | }, 55 | "config": { 56 | "allow-plugins": { 57 | "composer/package-versions-deprecated": true, 58 | "ergebnis/composer-normalize": true, 59 | "phpstan/extension-installer": true 60 | }, 61 | "preferred-install": "dist", 62 | "sort-packages": true 63 | }, 64 | "scripts": { 65 | "baseline": "phpstan --generate-baseline", 66 | "bench": "phpbench run", 67 | "check": [ 68 | "@fix", 69 | "@stan", 70 | "@test" 71 | ], 72 | "docs": "php generate-class-reference.php", 73 | "fix": [ 74 | "@rector", 75 | "@php-cs-fixer" 76 | ], 77 | "php-cs-fixer": "php-cs-fixer fix", 78 | "rector": "rector process", 79 | "stan": "phpstan --verbose", 80 | "test": "php -d zend.exception_ignore_args=Off -d zend.assertions=On -d assert.active=On -d assert.exception=On vendor/bin/phpunit" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/complementary-tools.md: -------------------------------------------------------------------------------- 1 | ## Server Integrations 2 | 3 | - [Standard Server](executing-queries.md#using-server) – Out of the box integration with any PSR-7 compatible framework (like [Slim](https://slimframework.com) or [Laminas Mezzio](https://docs.mezzio.dev/mezzio/)) 4 | - [Lighthouse](https://github.com/nuwave/lighthouse) – Laravel package, SDL-first 5 | - [Laravel GraphQL](https://github.com/rebing/graphql-laravel) - Laravel package, code-first 6 | - [OverblogGraphQLBundle](https://github.com/overblog/GraphQLBundle) – Symfony bundle 7 | - [WP-GraphQL](https://github.com/wp-graphql/wp-graphql) - WordPress plugin 8 | - [Siler](https://github.com/leocavalcante/siler) - Flat files and plain-old PHP functions, supports Swoole 9 | - [API Platform](https://api-platform.com/docs/core/graphql) - Creates a GraphQL API from PHP models 10 | - [LDOG Stack](https://ldog.apiskeletons.dev) - Laravel, Doctrine ORM, and GraphQL application template 11 | 12 | ## Server Utilities 13 | 14 | - [GraphQLite](https://graphqlite.thecodingmachine.io) – Use PHP Annotations to define your schema 15 | - [GraphQL Doctrine](https://github.com/Ecodev/graphql-doctrine) – Define types with Doctrine ORM annotations 16 | - [GraphQL Type Driver for Doctrine ORM](https://github.com/api-skeletons/doctrine-orm-graphql) – Includes events, pagination with the [Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model), and support for all default [Doctrine Types](https://doctrine-orm-graphql.apiskeletons.dev/en/latest/types.html#data-type-mappings) 17 | - [DataLoaderPHP](https://github.com/overblog/dataloader-php) – Implements [deferred resolvers](data-fetching.md#solving-n1-problem) 18 | - [GraphQL Upload](https://github.com/Ecodev/graphql-upload) – PSR-15 middleware to support file uploads in GraphQL 19 | - [GraphQL Batch Processor](https://github.com/vasily-kartashov/graphql-batch-processing) – Provides a builder interface for defining collection, querying, filtering, and post-processing logic of batched data fetches 20 | - [GraphQL Utils](https://github.com/simPod/GraphQL-Utils) – Objective schema definition builders (no need for arrays anymore) 21 | - [Relay Library](https://github.com/ivome/graphql-relay-php) – Helps construct Relay related schema definitions 22 | - [Resonance/GraphQL](https://resonance.distantmagic.com/docs/features/graphql/) – Integrates with Swoole for parallelism. Define your schema code-first with annotations. 23 | - [GraphQL PHP Validation Toolkit](https://github.com/shmax/graphql-php-validation-toolkit) - Small library for validation of user input 24 | - [MLL Scalars](https://github.com/mll-lab/graphql-php-scalars) - Collection of custom scalar types 25 | - [X GraphQL](https://github.com/x-graphql) - Provides set of libraries for building http schema, schema gateway, transforming schema, generating PHP code from execution definition, and more. 26 | 27 | ## GraphQL Clients 28 | 29 | - [GraphiQL](https://github.com/graphql/graphiql) – Graphical interactive in-browser GraphQL IDE 30 | - [GraphQL Playground](https://github.com/graphql/graphql-playground) – GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration) 31 | - [Altair GraphQL Client](https://altair.sirmuel.design) - Beautiful feature-rich GraphQL Client for all platforms 32 | - [Sailor](https://github.com/spawnia/sailor) - Typesafe GraphQL client for PHP 33 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # graphql-php 2 | 3 | [![GitHub stars](https://img.shields.io/github/stars/webonyx/graphql-php.svg?style=social&label=Star)](https://github.com/webonyx/graphql-php) 4 | [![CI](https://github.com/webonyx/graphql-php/workflows/CI/badge.svg)](https://github.com/webonyx/graphql-php/actions?query=workflow:CI+branch:master) 5 | [![Coverage Status](https://codecov.io/gh/webonyx/graphql-php/branch/master/graph/badge.svg)](https://codecov.io/gh/webonyx/graphql-php/branch/master) 6 | [![Latest Stable Version](https://poser.pugx.org/webonyx/graphql-php/version)](https://packagist.org/packages/webonyx/graphql-php) 7 | [![License](https://poser.pugx.org/webonyx/graphql-php/license)](https://packagist.org/packages/webonyx/graphql-php) 8 | 9 | ## About GraphQL 10 | 11 | GraphQL is a modern way to build HTTP APIs consumed by the web and mobile clients. 12 | It is intended to be an alternative to REST and SOAP APIs (even for **existing applications**). 13 | 14 | GraphQL itself is a [specification](https://github.com/graphql/graphql-spec) designed by Facebook 15 | engineers. Various implementations of this specification were written 16 | [in different languages and environments](https://graphql.org/code/). 17 | 18 | Great overview of GraphQL features and benefits is presented on [the official website](https://graphql.org/). 19 | All of them equally apply to this PHP implementation. 20 | 21 | ## About graphql-php 22 | 23 | **graphql-php** is a feature-complete implementation of GraphQL specification in PHP. 24 | It was originally inspired by [reference JavaScript implementation](https://github.com/graphql/graphql-js) 25 | published by Facebook. 26 | 27 | This library is a thin wrapper around your existing data layer and business logic. 28 | It doesn't dictate how these layers are implemented or which storage engines 29 | are used. Instead, it provides tools for creating rich API for your existing app. 30 | 31 | Library features include: 32 | 33 | - Primitives to express your app as a [Type System](type-definitions/index.md) 34 | - Validation and introspection of this Type System (for compatibility with [tools like GraphiQL](complementary-tools.md#general-graphql-tools)) 35 | - Parsing, validating and [executing GraphQL queries](executing-queries.md) against this Type System 36 | - Rich [error reporting](error-handling.md), including query validation and execution errors 37 | - Optional tools for [parsing schema definition language](schema-definition-language.md) 38 | - Tools for [batching requests](data-fetching.md#solving-n1-problem) to backend storage 39 | - [Async PHP platforms support](data-fetching.md#async-php) via promises 40 | - [Standard HTTP server](executing-queries.md#using-server) 41 | 42 | Also, several [complementary tools](complementary-tools.md) are available which provide integrations with 43 | existing PHP frameworks, add support for Relay, etc. 44 | 45 | ### Current Status 46 | 47 | The first version of this library (v0.1) was released on August 10th 2015. 48 | 49 | The current version supports all features described by GraphQL specification 50 | as well as some experimental features like 51 | [schema definition language](schema-definition-language.md) and 52 | [schema printer](class-reference.md#graphqlutilsschemaprinter). 53 | 54 | Ready for real-world usage. 55 | 56 | ### GitHub 57 | 58 | Project source code is [hosted on GitHub](https://github.com/webonyx/graphql-php). 59 | -------------------------------------------------------------------------------- /docs/schema-definition-language.md: -------------------------------------------------------------------------------- 1 | # Schema Definition Language 2 | 3 | The [schema definition language](https://graphql.org/learn/schema/#type-language) is a convenient way to define your schema, 4 | especially with IDE autocompletion and syntax validation. 5 | 6 | You can define this separately from your PHP code. 7 | An example for a **schema.graphql** file might look like this: 8 | 9 | ```graphql 10 | type Query { 11 | greetings(input: HelloInput!): String! 12 | } 13 | 14 | input HelloInput { 15 | firstName: String! 16 | lastName: String 17 | } 18 | ``` 19 | 20 | To create an executable schema instance from this file, use [`GraphQL\Utils\BuildSchema`](class-reference.md#graphqlutilsbuildschema): 21 | 22 | ```php 23 | use GraphQL\Utils\BuildSchema; 24 | 25 | $contents = file_get_contents('schema.graphql'); 26 | $schema = BuildSchema::build($contents); 27 | ``` 28 | 29 | By default, such a schema is created without any resolvers. 30 | We have to rely on [the default field resolver](data-fetching.md#default-field-resolver) 31 | and the **root value** to execute queries against this schema. 32 | 33 | ## Defining resolvers 34 | 35 | To enable **Interfaces**, **Unions**, and custom field resolvers, 36 | you can pass the second argument **callable $typeConfigDecorator** to **BuildSchema::build()**. 37 | 38 | It accepts a callable that receives the default type config produced by the builder and is expected to add missing options like 39 | [**resolveType**](type-definitions/interfaces.md#configuration-options) for interface types or 40 | [**resolveField**](type-definitions/object-types.md#configuration-options) for object types. 41 | 42 | ```php 43 | use GraphQL\Utils\BuildSchema; 44 | use GraphQL\Language\AST\TypeDefinitionNode; 45 | 46 | $typeConfigDecorator = function (array $typeConfig, TypeDefinitionNode $typeDefinitionNode): array { 47 | $name = $typeConfig['name']; 48 | // ... add missing options to $typeConfig based on type $name 49 | return $typeConfig; 50 | }; 51 | 52 | $contents = file_get_contents('schema.graphql'); 53 | $schema = BuildSchema::build($contents, $typeConfigDecorator); 54 | ``` 55 | 56 | You can learn more about using `$typeConfigDecorator` in [examples/05-type-config-decorator](https://github.com/webonyx/graphql-php/blob/master/examples/05-type-config-decorator). 57 | 58 | ## Performance considerations 59 | 60 | Method **BuildSchema::build()** produces a [lazy schema](schema-definition.md#lazy-loading-of-types) automatically, 61 | so it works efficiently even with huge schemas. 62 | 63 | However, parsing the schema definition file on each request is suboptimal. 64 | It is recommended to cache the intermediate parsed representation of the schema for the production environment: 65 | 66 | ```php 67 | use GraphQL\Language\Parser; 68 | use GraphQL\Utils\BuildSchema; 69 | use GraphQL\Utils\AST; 70 | 71 | $cacheFilename = 'cached_schema.php'; 72 | 73 | if (!file_exists($cacheFilename)) { 74 | $document = Parser::parse(file_get_contents('./schema.graphql')); 75 | file_put_contents($cacheFilename, " 'track', 47 | 'description' => 'Instruction to record usage of the field by client', 48 | 'locations' => [ 49 | DirectiveLocation::FIELD, 50 | ], 51 | 'args' => [ 52 | 'details' => [ 53 | 'type' => Type::string(), 54 | 'description' => 'String with additional details of field usage scenario', 55 | 'defaultValue' => '' 56 | ] 57 | ] 58 | ]); 59 | ``` 60 | 61 | See possible directive locations in 62 | [`GraphQL\Language\DirectiveLocation`](../class-reference.md#graphqllanguagedirectivelocation). 63 | -------------------------------------------------------------------------------- /docs/type-definitions/index.md: -------------------------------------------------------------------------------- 1 | # Type Definitions 2 | 3 | graphql-php represents a **type** as a class instance from the `GraphQL\Type\Definition` namespace: 4 | 5 | - [`ObjectType`](object-types.md) 6 | - [`InterfaceType`](interfaces.md) 7 | - [`UnionType`](unions.md) 8 | - [`InputObjectType`](inputs.md) 9 | - [`ScalarType`](scalars.md) 10 | - [`EnumType`](enums.md) 11 | 12 | ## Input vs. Output Types 13 | 14 | All types in GraphQL are of two categories: **input** and **output**. 15 | 16 | - **Output** types (or field types) are: [Scalar](scalars.md), [Enum](enums.md), [Object](object-types.md), 17 | [Interface](interfaces.md), [Union](unions.md) 18 | 19 | - **Input** types (or argument types) are: [Scalar](scalars.md), [Enum](enums.md), [Inputs](inputs.md) 20 | 21 | Obviously, [NonNull and List](lists-and-nonnulls.md) types belong to both categories depending on their 22 | inner type. 23 | 24 | ## Definition styles 25 | 26 | Several styles of type definitions are supported depending on your preferences. 27 | 28 | ### Inline definitions 29 | 30 | ```php 31 | use GraphQL\Type\Definition\ObjectType; 32 | use GraphQL\Type\Definition\Type; 33 | 34 | $myType = new ObjectType([ 35 | 'name' => 'MyType', 36 | 'fields' => [ 37 | 'id' => Type::id() 38 | ] 39 | ]); 40 | ``` 41 | 42 | ### Class per type 43 | 44 | ```php 45 | use GraphQL\Type\Definition\ObjectType; 46 | use GraphQL\Type\Definition\Type; 47 | 48 | class MyType extends ObjectType 49 | { 50 | public function __construct() 51 | { 52 | $config = [ 53 | // Note: 'name' is not needed in this form: 54 | // it will be inferred from class name by omitting namespace and dropping "Type" suffix 55 | 'fields' => [ 56 | 'id' => Type::id() 57 | ] 58 | ]; 59 | parent::__construct($config); 60 | } 61 | } 62 | ``` 63 | 64 | ### Schema definition language 65 | 66 | ```graphql 67 | schema { 68 | query: Query 69 | mutation: Mutation 70 | } 71 | 72 | type Query { 73 | greetings(input: HelloInput!): String! 74 | } 75 | 76 | input HelloInput { 77 | firstName: String! 78 | lastName: String 79 | } 80 | ``` 81 | 82 | Read more about [building an executable schema using schema definition language](../schema-definition-language.md). 83 | -------------------------------------------------------------------------------- /docs/type-definitions/lists-and-nonnulls.md: -------------------------------------------------------------------------------- 1 | ## Lists 2 | 3 | **graphql-php** provides built-in support for lists. In order to create list type - wrap 4 | existing type with `GraphQL\Type\Definition\Type::listOf()` modifier: 5 | 6 | ```php 7 | use GraphQL\Type\Definition\Type; 8 | use GraphQL\Type\Definition\ObjectType; 9 | 10 | $userType = new ObjectType([ 11 | 'name' => 'User', 12 | 'fields' => [ 13 | 'emails' => [ 14 | 'type' => Type::listOf(Type::string()), 15 | 'resolve' => fn (): array => [ 16 | 'jon@example.com', 17 | 'jonny@example.com' 18 | ], 19 | ] 20 | ] 21 | ]); 22 | ``` 23 | 24 | Resolvers for such fields are expected to return **array** or instance of PHP's built-in **Traversable** 25 | interface (**null** is allowed by default too). 26 | 27 | If returned value is not of one of these types - **graphql-php** will add an error to result 28 | and set the field value to **null** (only if the field is nullable, see below for non-null fields). 29 | 30 | ## Non-Nulls 31 | 32 | By default, every field or argument can have a **null** value. 33 | To indicate the value must be **non-null** use the `GraphQL\Type\Definition\Type::nonNull()` modifier: 34 | 35 | ```php 36 | use GraphQL\Type\Definition\Type; 37 | use GraphQL\Type\Definition\ObjectType; 38 | 39 | $humanType = new ObjectType([ 40 | 'name' => 'User', 41 | 'fields' => [ 42 | 'id' => [ 43 | 'type' => Type::nonNull(Type::id()), 44 | 'resolve' => fn (): string => uniqid(), 45 | ], 46 | 'emails' => [ 47 | 'type' => Type::nonNull(Type::listOf(Type::string())), 48 | 'resolve' => fn (): array => [ 49 | 'jon@example.com', 50 | 'jonny@example.com' 51 | ], 52 | ] 53 | ] 54 | ]); 55 | ``` 56 | 57 | If resolver of non-null field returns **null**, graphql-php will add an error to 58 | result and exclude the whole object from the output (an error will bubble to first 59 | nullable parent field which will be set to **null**). 60 | 61 | Read the section on [Data Fetching](../data-fetching.md) for details. 62 | -------------------------------------------------------------------------------- /docs/type-definitions/unions.md: -------------------------------------------------------------------------------- 1 | # Union Type Definition 2 | 3 | A Union is an abstract type that simply enumerates other Object Types. 4 | The value of Union Type is actually a value of one of included Object Types. 5 | 6 | ## Writing Union Types 7 | 8 | In **graphql-php** union type is an instance of `GraphQL\Type\Definition\UnionType` 9 | (or one of its subclasses) which accepts configuration array in a constructor: 10 | 11 | ```php 12 | use GraphQL\Type\Definition\ObjectType; 13 | use GraphQL\Type\Definition\UnionType; 14 | 15 | $searchResultType = new UnionType([ 16 | 'name' => 'SearchResult', 17 | 'types' => [ 18 | MyTypes::story(), 19 | MyTypes::user() 20 | ], 21 | 'resolveType' => function ($value): ObjectType { 22 | switch ($value->type ?? null) { 23 | case 'story': return MyTypes::story(); 24 | case 'user': return MyTypes::user(); 25 | default: throw new Exception("Unexpected SearchResult type: {$value->type ?? null}"); 26 | } 27 | }, 28 | ]); 29 | ``` 30 | 31 | This example uses **inline** style for Union definition, but you can also use 32 | [inheritance or schema definition language](index.md#definition-styles). 33 | 34 | ## Configuration options 35 | 36 | The constructor of UnionType accepts an array. Below is a full list of allowed options: 37 | 38 | | Option | Type | Notes | 39 | | ----------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 40 | | name | `string` | **Required.** Unique name of this interface type within Schema | 41 | | types | `array` | **Required.** List of Object Types included in this Union. Note that you can't create a Union type out of Interfaces or other Unions. | 42 | | description | `string` | Plain-text description of this type for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation) | 43 | | resolveType | `callback` | **function ($value, $context, [ResolveInfo](../class-reference.md#graphqltypedefinitionresolveinfo) $info): ObjectType**
Receives **$value** from resolver of the parent field and returns concrete Object Type for this **$value**. | 44 | -------------------------------------------------------------------------------- /src/Deferred.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class CoercionError extends Error 11 | { 12 | /** @var InputPath|null */ 13 | public ?array $inputPath; 14 | 15 | /** @var mixed whatever invalid value was passed */ 16 | public $invalidValue; 17 | 18 | /** 19 | * @param InputPath|null $inputPath 20 | * @param mixed $invalidValue whatever invalid value was passed 21 | * 22 | * @return static 23 | */ 24 | public static function make( 25 | string $message, 26 | ?array $inputPath, 27 | $invalidValue, 28 | ?\Throwable $previous = null 29 | ): self { 30 | $instance = new static($message, null, null, [], null, $previous); 31 | $instance->inputPath = $inputPath; 32 | $instance->invalidValue = $invalidValue; 33 | 34 | return $instance; 35 | } 36 | 37 | public function printInputPath(): ?string 38 | { 39 | if ($this->inputPath === null) { 40 | return null; 41 | } 42 | 43 | $path = ''; 44 | foreach ($this->inputPath as $segment) { 45 | $path .= is_int($segment) 46 | ? "[{$segment}]" 47 | : ".{$segment}"; 48 | } 49 | 50 | return $path; 51 | } 52 | 53 | public function printInvalidValue(): string 54 | { 55 | return Utils::printSafeJson($this->invalidValue); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Error/DebugFlag.php: -------------------------------------------------------------------------------- 1 | |null 14 | */ 15 | public function getExtensions(): ?array; 16 | } 17 | -------------------------------------------------------------------------------- /src/Error/SerializationError.php: -------------------------------------------------------------------------------- 1 | */ 25 | public array $fragments; 26 | 27 | /** @var mixed */ 28 | public $rootValue; 29 | 30 | /** @var mixed */ 31 | public $contextValue; 32 | 33 | public OperationDefinitionNode $operation; 34 | 35 | /** @var array */ 36 | public array $variableValues; 37 | 38 | /** 39 | * @var callable 40 | * 41 | * @phpstan-var FieldResolver 42 | */ 43 | public $fieldResolver; 44 | 45 | /** 46 | * @var callable 47 | * 48 | * @phpstan-var ArgsMapper 49 | */ 50 | public $argsMapper; 51 | 52 | /** @var list */ 53 | public array $errors; 54 | 55 | public PromiseAdapter $promiseAdapter; 56 | 57 | /** 58 | * @param array $fragments 59 | * @param mixed $rootValue 60 | * @param mixed $contextValue 61 | * @param array $variableValues 62 | * @param list $errors 63 | * 64 | * @phpstan-param FieldResolver $fieldResolver 65 | */ 66 | public function __construct( 67 | Schema $schema, 68 | array $fragments, 69 | $rootValue, 70 | $contextValue, 71 | OperationDefinitionNode $operation, 72 | array $variableValues, 73 | array $errors, 74 | callable $fieldResolver, 75 | callable $argsMapper, 76 | PromiseAdapter $promiseAdapter 77 | ) { 78 | $this->schema = $schema; 79 | $this->fragments = $fragments; 80 | $this->rootValue = $rootValue; 81 | $this->contextValue = $contextValue; 82 | $this->operation = $operation; 83 | $this->variableValues = $variableValues; 84 | $this->errors = $errors; 85 | $this->fieldResolver = $fieldResolver; 86 | $this->argsMapper = $argsMapper; 87 | $this->promiseAdapter = $promiseAdapter; 88 | } 89 | 90 | public function addError(Error $error): void 91 | { 92 | $this->errors[] = $error; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Executor/ExecutorImplementation.php: -------------------------------------------------------------------------------- 1 | adoptedPromise; 32 | assert($adoptedPromise instanceof ReactPromiseInterface); 33 | 34 | return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this); 35 | } 36 | 37 | /** @throws InvariantViolation */ 38 | public function create(callable $resolver): Promise 39 | { 40 | $promise = new ReactPromise($resolver); 41 | 42 | return new Promise($promise, $this); 43 | } 44 | 45 | /** @throws InvariantViolation */ 46 | public function createFulfilled($value = null): Promise 47 | { 48 | $promise = resolve($value); 49 | 50 | return new Promise($promise, $this); 51 | } 52 | 53 | /** @throws InvariantViolation */ 54 | public function createRejected(\Throwable $reason): Promise 55 | { 56 | $promise = reject($reason); 57 | 58 | return new Promise($promise, $this); 59 | } 60 | 61 | /** @throws InvariantViolation */ 62 | public function all(iterable $promisesOrValues): Promise 63 | { 64 | foreach ($promisesOrValues as &$promiseOrValue) { 65 | if ($promiseOrValue instanceof Promise) { 66 | $promiseOrValue = $promiseOrValue->adoptedPromise; 67 | } 68 | } 69 | 70 | $promisesOrValuesArray = is_array($promisesOrValues) 71 | ? $promisesOrValues 72 | : iterator_to_array($promisesOrValues); 73 | $promise = all($promisesOrValuesArray)->then(static fn ($values): array => array_map( 74 | static fn ($key) => $values[$key], 75 | array_keys($promisesOrValuesArray), 76 | )); 77 | 78 | return new Promise($promise, $this); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Executor/Promise/Promise.php: -------------------------------------------------------------------------------- 1 | |AmpPromise */ 16 | public $adoptedPromise; 17 | 18 | private PromiseAdapter $adapter; 19 | 20 | /** 21 | * @param mixed $adoptedPromise 22 | * 23 | * @throws InvariantViolation 24 | */ 25 | public function __construct($adoptedPromise, PromiseAdapter $adapter) 26 | { 27 | if ($adoptedPromise instanceof self) { 28 | throw new InvariantViolation('Expecting promise from adapted system, got ' . self::class); 29 | } 30 | 31 | $this->adoptedPromise = $adoptedPromise; 32 | $this->adapter = $adapter; 33 | } 34 | 35 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise 36 | { 37 | return $this->adapter->then($this, $onFulfilled, $onRejected); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Executor/Promise/PromiseAdapter.php: -------------------------------------------------------------------------------- 1 | $promisesOrValues 68 | * 69 | * @api 70 | */ 71 | public function all(iterable $promisesOrValues): Promise; 72 | } 73 | -------------------------------------------------------------------------------- /src/Executor/ScopedContext.php: -------------------------------------------------------------------------------- 1 | */ 14 | public NodeList $arguments; 15 | 16 | public bool $repeatable; 17 | 18 | /** @var NodeList */ 19 | public NodeList $locations; 20 | 21 | public function __construct(array $vars) 22 | { 23 | parent::__construct($vars); 24 | $this->arguments ??= new NodeList([]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Language/AST/DirectiveNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $arguments; 13 | 14 | public function __construct(array $vars) 15 | { 16 | parent::__construct($vars); 17 | $this->arguments ??= new NodeList([]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Language/AST/DocumentNode.php: -------------------------------------------------------------------------------- 1 | */ 10 | public NodeList $definitions; 11 | } 12 | -------------------------------------------------------------------------------- /src/Language/AST/EnumTypeDefinitionNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | /** @var NodeList */ 15 | public NodeList $values; 16 | 17 | public ?StringValueNode $description = null; 18 | 19 | public function getName(): NameNode 20 | { 21 | return $this->name; 22 | } 23 | 24 | public function __construct(array $vars) 25 | { 26 | parent::__construct($vars); 27 | $this->directives ??= new NodeList([]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Language/AST/EnumTypeExtensionNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | /** @var NodeList */ 15 | public NodeList $values; 16 | 17 | public function __construct(array $vars) 18 | { 19 | parent::__construct($vars); 20 | $this->directives ??= new NodeList([]); 21 | } 22 | 23 | public function getName(): NameNode 24 | { 25 | return $this->name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Language/AST/EnumValueDefinitionNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | public ?StringValueNode $description = null; 15 | } 16 | -------------------------------------------------------------------------------- /src/Language/AST/EnumValueNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $arguments; 13 | 14 | /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ 15 | public TypeNode $type; 16 | 17 | /** @var NodeList */ 18 | public NodeList $directives; 19 | 20 | public ?StringValueNode $description = null; 21 | } 22 | -------------------------------------------------------------------------------- /src/Language/AST/FieldNode.php: -------------------------------------------------------------------------------- 1 | */ 14 | public NodeList $arguments; 15 | 16 | /** @var NodeList */ 17 | public NodeList $directives; 18 | 19 | public ?SelectionSetNode $selectionSet = null; 20 | 21 | public function __construct(array $vars) 22 | { 23 | parent::__construct($vars); 24 | $this->directives ??= new NodeList([]); 25 | $this->arguments ??= new NodeList([]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Language/AST/FloatValueNode.php: -------------------------------------------------------------------------------- 1 | |null 18 | */ 19 | public ?NodeList $variableDefinitions = null; 20 | 21 | public NamedTypeNode $typeCondition; 22 | 23 | /** @var NodeList */ 24 | public NodeList $directives; 25 | 26 | public SelectionSetNode $selectionSet; 27 | 28 | public function __construct(array $vars) 29 | { 30 | parent::__construct($vars); 31 | $this->directives ??= new NodeList([]); 32 | } 33 | 34 | public function getSelectionSet(): SelectionSetNode 35 | { 36 | return $this->selectionSet; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Language/AST/FragmentSpreadNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | public function __construct(array $vars) 15 | { 16 | parent::__construct($vars); 17 | $this->directives ??= new NodeList([]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Language/AST/HasSelectionSet.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | public SelectionSetNode $selectionSet; 15 | 16 | public function __construct(array $vars) 17 | { 18 | parent::__construct($vars); 19 | $this->directives ??= new NodeList([]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Language/AST/InputObjectTypeDefinitionNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | /** @var NodeList */ 15 | public NodeList $fields; 16 | 17 | public ?StringValueNode $description = null; 18 | 19 | public function getName(): NameNode 20 | { 21 | return $this->name; 22 | } 23 | 24 | public function __construct(array $vars) 25 | { 26 | parent::__construct($vars); 27 | $this->directives ??= new NodeList([]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Language/AST/InputObjectTypeExtensionNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | /** @var NodeList */ 15 | public NodeList $fields; 16 | 17 | public function getName(): NameNode 18 | { 19 | return $this->name; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Language/AST/InputValueDefinitionNode.php: -------------------------------------------------------------------------------- 1 | */ 18 | public NodeList $directives; 19 | 20 | public ?StringValueNode $description = null; 21 | } 22 | -------------------------------------------------------------------------------- /src/Language/AST/IntValueNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | /** @var NodeList */ 15 | public NodeList $interfaces; 16 | 17 | /** @var NodeList */ 18 | public NodeList $fields; 19 | 20 | public ?StringValueNode $description = null; 21 | 22 | public function getName(): NameNode 23 | { 24 | return $this->name; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Language/AST/InterfaceTypeExtensionNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | /** @var NodeList */ 15 | public NodeList $interfaces; 16 | 17 | /** @var NodeList */ 18 | public NodeList $fields; 19 | 20 | public function getName(): NameNode 21 | { 22 | return $this->name; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Language/AST/ListTypeNode.php: -------------------------------------------------------------------------------- 1 | */ 10 | public NodeList $values; 11 | } 12 | -------------------------------------------------------------------------------- /src/Language/AST/Location.php: -------------------------------------------------------------------------------- 1 | start = $start; 36 | $tmp->end = $end; 37 | 38 | return $tmp; 39 | } 40 | 41 | public function __construct(?Token $startToken = null, ?Token $endToken = null, ?Source $source = null) 42 | { 43 | $this->startToken = $startToken; 44 | $this->endToken = $endToken; 45 | $this->source = $source; 46 | 47 | if ($startToken === null || $endToken === null) { 48 | return; 49 | } 50 | 51 | $this->start = $startToken->start; 52 | $this->end = $endToken->end; 53 | } 54 | 55 | /** @return LocationArray */ 56 | public function toArray(): array 57 | { 58 | return [ 59 | 'start' => $this->start, 60 | 'end' => $this->end, 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Language/AST/NameNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $interfaces; 13 | 14 | /** @var NodeList */ 15 | public NodeList $directives; 16 | 17 | /** @var NodeList */ 18 | public NodeList $fields; 19 | 20 | public ?StringValueNode $description = null; 21 | 22 | public function getName(): NameNode 23 | { 24 | return $this->name; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Language/AST/ObjectTypeExtensionNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $interfaces; 13 | 14 | /** @var NodeList */ 15 | public NodeList $directives; 16 | 17 | /** @var NodeList */ 18 | public NodeList $fields; 19 | 20 | public function getName(): NameNode 21 | { 22 | return $this->name; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Language/AST/ObjectValueNode.php: -------------------------------------------------------------------------------- 1 | */ 10 | public NodeList $fields; 11 | } 12 | -------------------------------------------------------------------------------- /src/Language/AST/OperationDefinitionNode.php: -------------------------------------------------------------------------------- 1 | */ 18 | public NodeList $variableDefinitions; 19 | 20 | /** @var NodeList */ 21 | public NodeList $directives; 22 | 23 | public SelectionSetNode $selectionSet; 24 | 25 | public function __construct(array $vars) 26 | { 27 | parent::__construct($vars); 28 | $this->directives ??= new NodeList([]); 29 | $this->variableDefinitions ??= new NodeList([]); 30 | } 31 | 32 | public function getSelectionSet(): SelectionSetNode 33 | { 34 | return $this->selectionSet; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Language/AST/OperationTypeDefinitionNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | public ?StringValueNode $description = null; 15 | 16 | public function getName(): NameNode 17 | { 18 | return $this->name; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Language/AST/ScalarTypeExtensionNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | public function getName(): NameNode 15 | { 16 | return $this->name; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Language/AST/SchemaDefinitionNode.php: -------------------------------------------------------------------------------- 1 | */ 10 | public NodeList $directives; 11 | 12 | /** @var NodeList */ 13 | public NodeList $operationTypes; 14 | } 15 | -------------------------------------------------------------------------------- /src/Language/AST/SchemaExtensionNode.php: -------------------------------------------------------------------------------- 1 | */ 10 | public NodeList $directives; 11 | 12 | /** @var NodeList */ 13 | public NodeList $operationTypes; 14 | } 15 | -------------------------------------------------------------------------------- /src/Language/AST/SelectionNode.php: -------------------------------------------------------------------------------- 1 | */ 10 | public NodeList $selections; 11 | } 12 | -------------------------------------------------------------------------------- /src/Language/AST/StringValueNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | /** @var NodeList */ 15 | public NodeList $types; 16 | 17 | public ?StringValueNode $description = null; 18 | 19 | public function getName(): NameNode 20 | { 21 | return $this->name; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Language/AST/UnionTypeExtensionNode.php: -------------------------------------------------------------------------------- 1 | */ 12 | public NodeList $directives; 13 | 14 | /** @var NodeList */ 15 | public NodeList $types; 16 | 17 | public function getName(): NameNode 18 | { 19 | return $this->name; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Language/AST/ValueNode.php: -------------------------------------------------------------------------------- 1 | */ 18 | public NodeList $directives; 19 | 20 | public function __construct(array $vars) 21 | { 22 | parent::__construct($vars); 23 | $this->directives ??= new NodeList([]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Language/AST/VariableNode.php: -------------------------------------------------------------------------------- 1 | self::QUERY, 21 | self::MUTATION => self::MUTATION, 22 | self::SUBSCRIPTION => self::SUBSCRIPTION, 23 | self::FIELD => self::FIELD, 24 | self::FRAGMENT_DEFINITION => self::FRAGMENT_DEFINITION, 25 | self::FRAGMENT_SPREAD => self::FRAGMENT_SPREAD, 26 | self::INLINE_FRAGMENT => self::INLINE_FRAGMENT, 27 | self::VARIABLE_DEFINITION => self::VARIABLE_DEFINITION, 28 | ]; 29 | 30 | public const SCHEMA = 'SCHEMA'; 31 | public const SCALAR = 'SCALAR'; 32 | public const OBJECT = 'OBJECT'; 33 | public const FIELD_DEFINITION = 'FIELD_DEFINITION'; 34 | public const ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION'; 35 | public const IFACE = 'INTERFACE'; 36 | public const UNION = 'UNION'; 37 | public const ENUM = 'ENUM'; 38 | public const ENUM_VALUE = 'ENUM_VALUE'; 39 | public const INPUT_OBJECT = 'INPUT_OBJECT'; 40 | public const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION'; 41 | 42 | public const TYPE_SYSTEM_LOCATIONS = [ 43 | self::SCHEMA => self::SCHEMA, 44 | self::SCALAR => self::SCALAR, 45 | self::OBJECT => self::OBJECT, 46 | self::FIELD_DEFINITION => self::FIELD_DEFINITION, 47 | self::ARGUMENT_DEFINITION => self::ARGUMENT_DEFINITION, 48 | self::IFACE => self::IFACE, 49 | self::UNION => self::UNION, 50 | self::ENUM => self::ENUM, 51 | self::ENUM_VALUE => self::ENUM_VALUE, 52 | self::INPUT_OBJECT => self::INPUT_OBJECT, 53 | self::INPUT_FIELD_DEFINITION => self::INPUT_FIELD_DEFINITION, 54 | ]; 55 | 56 | public const LOCATIONS = self::EXECUTABLE_LOCATIONS + self::TYPE_SYSTEM_LOCATIONS; 57 | 58 | public static function has(string $name): bool 59 | { 60 | return isset(self::LOCATIONS[$name]); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Language/Source.php: -------------------------------------------------------------------------------- 1 | body = $body; 27 | $this->length = mb_strlen($body, 'UTF-8'); 28 | $this->name = $name === '' || $name === null 29 | ? 'GraphQL request' 30 | : $name; 31 | $this->locationOffset = $location ?? new SourceLocation(1, 1); 32 | } 33 | 34 | public function getLocation(int $position): SourceLocation 35 | { 36 | $line = 1; 37 | $column = $position + 1; 38 | 39 | $utfChars = json_decode('"\u2028\u2029"'); 40 | $lineRegexp = '/\r\n|[\n\r' . $utfChars . ']/su'; 41 | $matches = []; 42 | preg_match_all($lineRegexp, mb_substr($this->body, 0, $position, 'UTF-8'), $matches, \PREG_OFFSET_CAPTURE); 43 | 44 | foreach ($matches[0] as $match) { 45 | ++$line; 46 | 47 | $column = $position + 1 - ($match[1] + mb_strlen($match[0], 'UTF-8')); 48 | } 49 | 50 | return new SourceLocation($line, $column); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Language/SourceLocation.php: -------------------------------------------------------------------------------- 1 | line = $line; 14 | $this->column = $col; 15 | } 16 | 17 | /** @return array{line: int, column: int} */ 18 | public function toArray(): array 19 | { 20 | return [ 21 | 'line' => $this->line, 22 | 'column' => $this->column, 23 | ]; 24 | } 25 | 26 | /** @return array{line: int, column: int} */ 27 | public function toSerializableArray(): array 28 | { 29 | return $this->toArray(); 30 | } 31 | 32 | /** @return array{line: int, column: int} */ 33 | #[\ReturnTypeWillChange] 34 | public function jsonSerialize(): array 35 | { 36 | return $this->toArray(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Language/Token.php: -------------------------------------------------------------------------------- 1 | '; 15 | public const EOF = ''; 16 | public const BANG = '!'; 17 | public const DOLLAR = '$'; 18 | public const AMP = '&'; 19 | public const PAREN_L = '('; 20 | public const PAREN_R = ')'; 21 | public const SPREAD = '...'; 22 | public const COLON = ':'; 23 | public const EQUALS = '='; 24 | public const AT = '@'; 25 | public const BRACKET_L = '['; 26 | public const BRACKET_R = ']'; 27 | public const BRACE_L = '{'; 28 | public const PIPE = '|'; 29 | public const BRACE_R = '}'; 30 | public const NAME = 'Name'; 31 | public const INT = 'Int'; 32 | public const FLOAT = 'Float'; 33 | public const STRING = 'String'; 34 | public const BLOCK_STRING = 'BlockString'; 35 | public const COMMENT = 'Comment'; 36 | 37 | /** The kind of Token (see one of constants above). */ 38 | public string $kind; 39 | 40 | /** The character offset at which this Node begins. */ 41 | public int $start; 42 | 43 | /** The character offset at which this Node ends. */ 44 | public int $end; 45 | 46 | /** The 1-indexed line number on which this Token appears. */ 47 | public int $line; 48 | 49 | /** The 1-indexed column number at which this Token begins. */ 50 | public int $column; 51 | 52 | public ?string $value; 53 | 54 | /** 55 | * Tokens exist as nodes in a double-linked-list amongst all tokens 56 | * including ignored tokens. is always the first node and 57 | * the last. 58 | */ 59 | public ?Token $prev; 60 | 61 | public ?Token $next = null; 62 | 63 | public function __construct(string $kind, int $start, int $end, int $line, int $column, ?Token $previous = null, ?string $value = null) 64 | { 65 | $this->kind = $kind; 66 | $this->start = $start; 67 | $this->end = $end; 68 | $this->line = $line; 69 | $this->column = $column; 70 | $this->prev = $previous; 71 | $this->value = $value; 72 | } 73 | 74 | public function getDescription(): string 75 | { 76 | return $this->kind 77 | . ($this->value === null 78 | ? '' 79 | : " \"{$this->value}\""); 80 | } 81 | 82 | /** 83 | * @return array{ 84 | * kind: string, 85 | * value: string|null, 86 | * line: int, 87 | * column: int, 88 | * } 89 | */ 90 | public function toArray(): array 91 | { 92 | return [ 93 | 'kind' => $this->kind, 94 | 'value' => $this->value, 95 | 'line' => $this->line, 96 | 'column' => $this->column, 97 | ]; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Language/VisitorOperation.php: -------------------------------------------------------------------------------- 1 | 52 | */ 53 | public $variables; 54 | 55 | /** 56 | * Reserved for implementors to extend the protocol however they see fit. 57 | * 58 | * @api 59 | * 60 | * @var mixed should be array 61 | */ 62 | public $extensions; 63 | 64 | /** 65 | * Executed in read-only context (e.g. via HTTP GET request)? 66 | * 67 | * @api 68 | */ 69 | public bool $readOnly; 70 | 71 | /** 72 | * The raw params used to construct this instance. 73 | * 74 | * @api 75 | * 76 | * @var array 77 | */ 78 | public array $originalInput; 79 | 80 | /** 81 | * Creates an instance from given array. 82 | * 83 | * @param array $params 84 | * 85 | * @api 86 | */ 87 | public static function create(array $params, bool $readonly = false): OperationParams 88 | { 89 | $instance = new static(); 90 | 91 | $params = array_change_key_case($params, \CASE_LOWER); 92 | $instance->originalInput = $params; 93 | 94 | $params += [ 95 | 'query' => null, 96 | 'queryid' => null, 97 | 'documentid' => null, // alias to queryid 98 | 'id' => null, // alias to queryid 99 | 'operationname' => null, 100 | 'variables' => null, 101 | 'extensions' => null, 102 | ]; 103 | 104 | foreach ($params as &$value) { 105 | if ($value === '') { 106 | $value = null; 107 | } 108 | } 109 | 110 | $instance->query = $params['query']; 111 | $instance->queryId = $params['queryid'] ?? $params['documentid'] ?? $params['id']; 112 | $instance->operation = $params['operationname']; 113 | $instance->variables = static::decodeIfJSON($params['variables']); 114 | $instance->extensions = static::decodeIfJSON($params['extensions']); 115 | $instance->readOnly = $readonly; 116 | 117 | // Apollo server/client compatibility 118 | if ( 119 | isset($instance->extensions['persistedQuery']['sha256Hash']) 120 | && $instance->queryId === null 121 | ) { 122 | $instance->queryId = $instance->extensions['persistedQuery']['sha256Hash']; 123 | } 124 | 125 | return $instance; 126 | } 127 | 128 | /** 129 | * Decodes the value if it is JSON, otherwise returns it unchanged. 130 | * 131 | * @param mixed $value 132 | * 133 | * @return mixed 134 | */ 135 | protected static function decodeIfJSON($value) 136 | { 137 | if (! is_string($value)) { 138 | return $value; 139 | } 140 | 141 | $decoded = json_decode($value, true); 142 | if (json_last_error() === \JSON_ERROR_NONE) { 143 | return $decoded; 144 | } 145 | 146 | return $value; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Server/RequestError.php: -------------------------------------------------------------------------------- 1 | value; 43 | } 44 | 45 | $notBoolean = Printer::doPrint($valueNode); 46 | throw new Error("Boolean cannot represent a non boolean value: {$notBoolean}", $valueNode); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Type/Definition/CompositeType.php: -------------------------------------------------------------------------------- 1 | name = $config['name']; 36 | $this->value = $config['value'] ?? null; 37 | $this->deprecationReason = $config['deprecationReason'] ?? null; 38 | $this->description = $config['description'] ?? null; 39 | $this->astNode = $config['astNode'] ?? null; 40 | 41 | $this->config = $config; 42 | } 43 | 44 | public function isDeprecated(): bool 45 | { 46 | return (bool) $this->deprecationReason; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Type/Definition/FloatType.php: -------------------------------------------------------------------------------- 1 | value; 56 | } 57 | 58 | $notFloat = Printer::doPrint($valueNode); 59 | throw new Error("Float cannot represent non numeric value: {$notFloat}", $valueNode); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Type/Definition/HasFieldsType.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function getFields(): array; 22 | 23 | /** 24 | * @throws InvariantViolation 25 | * 26 | * @return array 27 | */ 28 | public function getVisibleFields(): array; 29 | 30 | /** 31 | * Get all field names, including only visible fields. 32 | * 33 | * @throws InvariantViolation 34 | * 35 | * @return array 36 | */ 37 | public function getFieldNames(): array; 38 | } 39 | -------------------------------------------------------------------------------- /src/Type/Definition/HasFieldsTypeImplementation.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | private array $fields; 18 | 19 | /** @throws InvariantViolation */ 20 | private function initializeFields(): void 21 | { 22 | if (isset($this->fields)) { 23 | return; 24 | } 25 | 26 | $this->fields = FieldDefinition::defineFieldMap($this, $this->config['fields']); 27 | } 28 | 29 | /** @throws InvariantViolation */ 30 | public function getField(string $name): FieldDefinition 31 | { 32 | $field = $this->findField($name); 33 | 34 | if ($field === null) { 35 | throw new InvariantViolation("Field \"{$name}\" is not defined for type \"{$this->name}\""); 36 | } 37 | 38 | return $field; 39 | } 40 | 41 | /** @throws InvariantViolation */ 42 | public function findField(string $name): ?FieldDefinition 43 | { 44 | $this->initializeFields(); 45 | 46 | if (! isset($this->fields[$name])) { 47 | return null; 48 | } 49 | 50 | $field = $this->fields[$name]; 51 | if ($field instanceof UnresolvedFieldDefinition) { 52 | return $this->fields[$name] = $field->resolve(); 53 | } 54 | 55 | return $field; 56 | } 57 | 58 | /** @throws InvariantViolation */ 59 | public function hasField(string $name): bool 60 | { 61 | $this->initializeFields(); 62 | 63 | return isset($this->fields[$name]); 64 | } 65 | 66 | /** @throws InvariantViolation */ 67 | public function getFields(): array 68 | { 69 | $this->initializeFields(); 70 | 71 | foreach ($this->fields as $name => $field) { 72 | if ($field instanceof UnresolvedFieldDefinition) { 73 | $this->fields[$name] = $field->resolve(); 74 | } 75 | } 76 | 77 | // @phpstan-ignore-next-line all field definitions are now resolved 78 | return $this->fields; 79 | } 80 | 81 | public function getVisibleFields(): array 82 | { 83 | return array_filter( 84 | $this->getFields(), 85 | fn (FieldDefinition $fieldDefinition): bool => $fieldDefinition->isVisible() 86 | ); 87 | } 88 | 89 | /** @throws InvariantViolation */ 90 | public function getFieldNames(): array 91 | { 92 | $this->initializeFields(); 93 | 94 | $visibleFieldNames = array_map( 95 | fn (FieldDefinition $fieldDefinition): string => $fieldDefinition->getName(), 96 | $this->getVisibleFields() 97 | ); 98 | 99 | return array_values($visibleFieldNames); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Type/Definition/IDType.php: -------------------------------------------------------------------------------- 1 | value; 54 | } 55 | 56 | $notID = Printer::doPrint($valueNode); 57 | throw new Error("ID cannot represent a non-string and non-integer value: {$notID}", $valueNode); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Type/Definition/ImplementingType.php: -------------------------------------------------------------------------------- 1 | */ 15 | public function getInterfaces(): array; 16 | } 17 | -------------------------------------------------------------------------------- /src/Type/Definition/ImplementingTypeImplementation.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | private array $interfaces; 19 | 20 | public function implementsInterface(InterfaceType $interfaceType): bool 21 | { 22 | if (! isset($this->interfaces)) { 23 | $this->initializeInterfaces(); 24 | } 25 | 26 | foreach ($this->interfaces as $interface) { 27 | if ($interfaceType->name === $interface->name) { 28 | return true; 29 | } 30 | } 31 | 32 | return false; 33 | } 34 | 35 | /** @return array */ 36 | public function getInterfaces(): array 37 | { 38 | if (! isset($this->interfaces)) { 39 | $this->initializeInterfaces(); 40 | } 41 | 42 | return $this->interfaces; 43 | } 44 | 45 | private function initializeInterfaces(): void 46 | { 47 | $this->interfaces = []; 48 | 49 | if (! isset($this->config['interfaces'])) { 50 | return; 51 | } 52 | 53 | $interfaces = $this->config['interfaces']; 54 | if (is_callable($interfaces)) { 55 | $interfaces = $interfaces(); 56 | } 57 | 58 | foreach ($interfaces as $interface) { 59 | $this->interfaces[] = Schema::resolveType($interface); 60 | } 61 | } 62 | 63 | /** @throws InvariantViolation */ 64 | protected function assertValidInterfaces(): void 65 | { 66 | if (! isset($this->config['interfaces'])) { 67 | return; 68 | } 69 | 70 | $interfaces = $this->config['interfaces']; 71 | if (is_callable($interfaces)) { 72 | $interfaces = $interfaces(); 73 | } 74 | 75 | // @phpstan-ignore-next-line should not happen if used correctly 76 | if (! is_iterable($interfaces)) { 77 | throw new InvariantViolation("{$this->name} interfaces must be an iterable or a callable which returns an iterable."); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Type/Definition/InputObjectField.php: -------------------------------------------------------------------------------- 1 | name = $config['name']; 52 | $this->defaultValue = $config['defaultValue'] ?? null; 53 | $this->description = $config['description'] ?? null; 54 | $this->deprecationReason = $config['deprecationReason'] ?? null; 55 | // Do nothing for type, it is lazy loaded in getType() 56 | $this->astNode = $config['astNode'] ?? null; 57 | 58 | $this->config = $config; 59 | } 60 | 61 | /** @return Type&InputType */ 62 | public function getType(): Type 63 | { 64 | if (! isset($this->type)) { 65 | $this->type = Schema::resolveType($this->config['type']); 66 | } 67 | 68 | return $this->type; 69 | } 70 | 71 | public function defaultValueExists(): bool 72 | { 73 | return array_key_exists('defaultValue', $this->config); 74 | } 75 | 76 | public function isRequired(): bool 77 | { 78 | return $this->getType() instanceof NonNull 79 | && ! $this->defaultValueExists(); 80 | } 81 | 82 | public function isDeprecated(): bool 83 | { 84 | return (bool) $this->deprecationReason; 85 | } 86 | 87 | /** 88 | * @param Type&NamedType $parentType 89 | * 90 | * @throws InvariantViolation 91 | */ 92 | public function assertValid(Type $parentType): void 93 | { 94 | $error = Utils::isValidNameError($this->name); 95 | if ($error !== null) { 96 | throw new InvariantViolation("{$parentType->name}.{$this->name}: {$error->getMessage()}"); 97 | } 98 | 99 | $type = Type::getNamedType($this->getType()); 100 | 101 | if (! $type instanceof InputType) { 102 | $notInputType = Utils::printSafe($this->type); 103 | throw new InvariantViolation("{$parentType->name}.{$this->name} field type must be Input Type but got: {$notInputType}"); 104 | } 105 | 106 | // @phpstan-ignore-next-line should not happen if used properly 107 | if (array_key_exists('resolve', $this->config)) { 108 | throw new InvariantViolation("{$parentType->name}.{$this->name} field has a resolve property, but Input Types cannot define resolvers."); 109 | } 110 | 111 | if ($this->isRequired() && $this->isDeprecated()) { 112 | throw new InvariantViolation("Required input field {$parentType->name}.{$this->name} cannot be deprecated."); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Type/Definition/InputType.php: -------------------------------------------------------------------------------- 1 | 11 | * | NonNull< 12 | * | ScalarType 13 | * | EnumType 14 | * | InputObjectType 15 | * | ListOfType, 16 | * >;. 17 | */ 18 | interface InputType {} 19 | -------------------------------------------------------------------------------- /src/Type/Definition/IntType.php: -------------------------------------------------------------------------------- 1 | = self::MIN_INT) { 33 | return $value; 34 | } 35 | 36 | $float = is_numeric($value) || is_bool($value) 37 | ? (float) $value 38 | : null; 39 | 40 | if ($float === null || floor($float) !== $float) { 41 | $notInt = Utils::printSafe($value); 42 | throw new SerializationError("Int cannot represent non-integer value: {$notInt}"); 43 | } 44 | 45 | if ($float > self::MAX_INT || $float < self::MIN_INT) { 46 | $outOfRangeInt = Utils::printSafe($value); 47 | throw new SerializationError("Int cannot represent non 32-bit signed integer value: {$outOfRangeInt}"); 48 | } 49 | 50 | return (int) $float; 51 | } 52 | 53 | /** @throws Error */ 54 | public function parseValue($value): int 55 | { 56 | $isInt = is_int($value) 57 | || (is_float($value) && floor($value) === $value); 58 | 59 | if (! $isInt) { 60 | $notInt = Utils::printSafeJson($value); 61 | throw new Error("Int cannot represent non-integer value: {$notInt}"); 62 | } 63 | 64 | if ($value > self::MAX_INT || $value < self::MIN_INT) { 65 | $outOfRangeInt = Utils::printSafeJson($value); 66 | throw new Error("Int cannot represent non 32-bit signed integer value: {$outOfRangeInt}"); 67 | } 68 | 69 | return (int) $value; 70 | } 71 | 72 | public function parseLiteral(Node $valueNode, ?array $variables = null): int 73 | { 74 | if ($valueNode instanceof IntValueNode) { 75 | $val = (int) $valueNode->value; 76 | if ($valueNode->value === (string) $val && $val >= self::MIN_INT && $val <= self::MAX_INT) { 77 | return $val; 78 | } 79 | } 80 | 81 | $notInt = Printer::doPrint($valueNode); 82 | throw new Error("Int cannot represent non-integer value: {$notInt}", $valueNode); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Type/Definition/InterfaceType.php: -------------------------------------------------------------------------------- 1 | |callable(): iterable, 21 | * resolveType?: ResolveType|null, 22 | * astNode?: InterfaceTypeDefinitionNode|null, 23 | * extensionASTNodes?: array|null 24 | * } 25 | */ 26 | class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NullableType, HasFieldsType, NamedType, ImplementingType 27 | { 28 | use HasFieldsTypeImplementation; 29 | use NamedTypeImplementation; 30 | use ImplementingTypeImplementation; 31 | 32 | public ?InterfaceTypeDefinitionNode $astNode; 33 | 34 | /** @var array */ 35 | public array $extensionASTNodes; 36 | 37 | /** @phpstan-var InterfaceConfig */ 38 | public array $config; 39 | 40 | /** 41 | * @throws InvariantViolation 42 | * 43 | * @phpstan-param InterfaceConfig $config 44 | */ 45 | public function __construct(array $config) 46 | { 47 | $this->name = $config['name'] ?? $this->inferName(); 48 | $this->description = $config['description'] ?? null; 49 | $this->astNode = $config['astNode'] ?? null; 50 | $this->extensionASTNodes = $config['extensionASTNodes'] ?? []; 51 | 52 | $this->config = $config; 53 | } 54 | 55 | /** 56 | * @param mixed $type 57 | * 58 | * @throws InvariantViolation 59 | */ 60 | public static function assertInterfaceType($type): self 61 | { 62 | if (! ($type instanceof self)) { 63 | $notInterfaceType = Utils::printSafe($type); 64 | throw new InvariantViolation("Expected {$notInterfaceType} to be a GraphQL Interface type."); 65 | } 66 | 67 | return $type; 68 | } 69 | 70 | public function resolveType($objectValue, $context, ResolveInfo $info) 71 | { 72 | if (isset($this->config['resolveType'])) { 73 | return ($this->config['resolveType'])($objectValue, $context, $info); 74 | } 75 | 76 | return null; 77 | } 78 | 79 | /** 80 | * @throws Error 81 | * @throws InvariantViolation 82 | */ 83 | public function assertValid(): void 84 | { 85 | Utils::assertValidName($this->name); 86 | 87 | $resolveType = $this->config['resolveType'] ?? null; 88 | // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime 89 | if ($resolveType !== null && ! is_callable($resolveType)) { 90 | $notCallable = Utils::printSafe($resolveType); 91 | throw new InvariantViolation("{$this->name} must provide \"resolveType\" as null or a callable, but got: {$notCallable}."); 92 | } 93 | 94 | $this->assertValidInterfaces(); 95 | } 96 | 97 | public function astNode(): ?InterfaceTypeDefinitionNode 98 | { 99 | return $this->astNode; 100 | } 101 | 102 | /** @return array */ 103 | public function extensionASTNodes(): array 104 | { 105 | return $this->extensionASTNodes; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Type/Definition/LeafType.php: -------------------------------------------------------------------------------- 1 | |null $variables 51 | * 52 | * @throws Error 53 | * 54 | * @return mixed 55 | */ 56 | public function parseLiteral(Node $valueNode, ?array $variables = null); 57 | } 58 | -------------------------------------------------------------------------------- /src/Type/Definition/ListOfType.php: -------------------------------------------------------------------------------- 1 | wrappedType = $type; 27 | } 28 | 29 | public function toString(): string 30 | { 31 | return '[' . $this->getWrappedType()->toString() . ']'; 32 | } 33 | 34 | /** @phpstan-return OfType */ 35 | public function getWrappedType(): Type 36 | { 37 | return Schema::resolveType($this->wrappedType); 38 | } 39 | 40 | public function getInnermostType(): NamedType 41 | { 42 | $type = $this->getWrappedType(); 43 | while ($type instanceof WrappingType) { 44 | $type = $type->getWrappedType(); 45 | } 46 | 47 | assert($type instanceof NamedType, 'known because we unwrapped all the way down'); 48 | 49 | return $type; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Type/Definition/NamedType.php: -------------------------------------------------------------------------------- 1 | $extensionASTNodes 23 | */ 24 | interface NamedType 25 | { 26 | /** @throws Error */ 27 | public function assertValid(): void; 28 | 29 | /** Is this type a built-in type? */ 30 | public function isBuiltInType(): bool; 31 | 32 | public function name(): string; 33 | 34 | public function description(): ?string; 35 | 36 | /** @return (Node&TypeDefinitionNode)|null */ 37 | public function astNode(): ?Node; 38 | 39 | /** @return array */ 40 | public function extensionASTNodes(): array; 41 | } 42 | -------------------------------------------------------------------------------- /src/Type/Definition/NamedTypeImplementation.php: -------------------------------------------------------------------------------- 1 | name; 17 | } 18 | 19 | /** @throws InvariantViolation */ 20 | protected function inferName(): string 21 | { 22 | if (isset($this->name)) { // @phpstan-ignore-line property might be uninitialized 23 | return $this->name; 24 | } 25 | 26 | // If class is extended - infer name from className 27 | // QueryType -> Type 28 | // SomeOtherType -> SomeOther 29 | $reflection = new \ReflectionClass($this); 30 | $name = $reflection->getShortName(); 31 | 32 | if ($reflection->getNamespaceName() !== __NAMESPACE__) { 33 | $withoutPrefixType = preg_replace('~Type$~', '', $name); 34 | assert(is_string($withoutPrefixType), 'regex is statically known to be correct'); 35 | 36 | return $withoutPrefixType; 37 | } 38 | 39 | throw new InvariantViolation('Must provide name for Type.'); 40 | } 41 | 42 | public function isBuiltInType(): bool 43 | { 44 | return in_array($this->name, Type::BUILT_IN_TYPE_NAMES, true); 45 | } 46 | 47 | public function name(): string 48 | { 49 | return $this->name; 50 | } 51 | 52 | public function description(): ?string 53 | { 54 | return $this->description; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Type/Definition/NonNull.php: -------------------------------------------------------------------------------- 1 | wrappedType = $type; 27 | } 28 | 29 | public function toString(): string 30 | { 31 | return $this->getWrappedType()->toString() . '!'; 32 | } 33 | 34 | /** @return NullableType&Type */ 35 | public function getWrappedType(): Type 36 | { 37 | return Schema::resolveType($this->wrappedType); 38 | } 39 | 40 | public function getInnermostType(): NamedType 41 | { 42 | $type = $this->getWrappedType(); 43 | while ($type instanceof WrappingType) { 44 | $type = $type->getWrappedType(); 45 | } 46 | 47 | assert($type instanceof NamedType, 'known because we unwrapped all the way down'); 48 | 49 | return $type; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Type/Definition/NullableType.php: -------------------------------------------------------------------------------- 1 | ; 14 | */ 15 | 16 | interface NullableType {} 17 | -------------------------------------------------------------------------------- /src/Type/Definition/OutputType.php: -------------------------------------------------------------------------------- 1 | |null 33 | * } 34 | */ 35 | abstract class ScalarType extends Type implements OutputType, InputType, LeafType, NullableType, NamedType 36 | { 37 | use NamedTypeImplementation; 38 | 39 | public ?ScalarTypeDefinitionNode $astNode; 40 | 41 | /** @var array */ 42 | public array $extensionASTNodes; 43 | 44 | /** @phpstan-var ScalarConfig */ 45 | public array $config; 46 | 47 | /** 48 | * @throws InvariantViolation 49 | * 50 | * @phpstan-param ScalarConfig $config 51 | */ 52 | public function __construct(array $config = []) 53 | { 54 | $this->name = $config['name'] ?? $this->inferName(); 55 | $this->description = $config['description'] ?? $this->description ?? null; 56 | $this->astNode = $config['astNode'] ?? null; 57 | $this->extensionASTNodes = $config['extensionASTNodes'] ?? []; 58 | 59 | $this->config = $config; 60 | } 61 | 62 | public function assertValid(): void 63 | { 64 | Utils::assertValidName($this->name); 65 | } 66 | 67 | public function astNode(): ?ScalarTypeDefinitionNode 68 | { 69 | return $this->astNode; 70 | } 71 | 72 | /** @return array */ 73 | public function extensionASTNodes(): array 74 | { 75 | return $this->extensionASTNodes; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Type/Definition/StringType.php: -------------------------------------------------------------------------------- 1 | value; 51 | } 52 | 53 | $notString = Printer::doPrint($valueNode); 54 | throw new Error("String cannot represent a non string value: {$notString}", $valueNode); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Type/Definition/UnmodifiedType.php: -------------------------------------------------------------------------------- 1 | name = $name; 25 | $this->definitionResolver = $definitionResolver; 26 | } 27 | 28 | public function getName(): string 29 | { 30 | return $this->name; 31 | } 32 | 33 | public function resolve(): FieldDefinition 34 | { 35 | $field = ($this->definitionResolver)(); 36 | 37 | if ($field instanceof FieldDefinition) { 38 | return $field; 39 | } 40 | 41 | if ($field instanceof Type) { 42 | return new FieldDefinition([ 43 | 'name' => $this->name, 44 | 'type' => $field, 45 | ]); 46 | } 47 | 48 | return new FieldDefinition($field + ['name' => $this->name]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Type/Definition/WrappingType.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | private array $visitedTypes = []; 23 | 24 | /** @var array */ 25 | private array $fieldPath = []; 26 | 27 | /** 28 | * Position in the type path. 29 | * 30 | * @var array 31 | */ 32 | private array $fieldPathIndexByTypeName = []; 33 | 34 | public function __construct(SchemaValidationContext $schemaValidationContext) 35 | { 36 | $this->schemaValidationContext = $schemaValidationContext; 37 | } 38 | 39 | /** 40 | * This does a straight-forward DFS to find cycles. 41 | * It does not terminate when a cycle was found but continues to explore 42 | * the graph to find all possible cycles. 43 | * 44 | * @throws InvariantViolation 45 | */ 46 | public function validate(InputObjectType $inputObj): void 47 | { 48 | if (isset($this->visitedTypes[$inputObj->name])) { 49 | return; 50 | } 51 | 52 | $this->visitedTypes[$inputObj->name] = true; 53 | $this->fieldPathIndexByTypeName[$inputObj->name] = count($this->fieldPath); 54 | 55 | $fieldMap = $inputObj->getFields(); 56 | foreach ($fieldMap as $field) { 57 | $type = $field->getType(); 58 | 59 | if ($type instanceof NonNull) { 60 | $fieldType = $type->getWrappedType(); 61 | 62 | // If the type of the field is anything else then a non-nullable input object, 63 | // there is no chance of an unbreakable cycle 64 | if ($fieldType instanceof InputObjectType) { 65 | $this->fieldPath[] = $field; 66 | 67 | if (! isset($this->fieldPathIndexByTypeName[$fieldType->name])) { 68 | $this->validate($fieldType); 69 | } else { 70 | $cycleIndex = $this->fieldPathIndexByTypeName[$fieldType->name]; 71 | $cyclePath = array_slice($this->fieldPath, $cycleIndex); 72 | $fieldNames = implode( 73 | '.', 74 | array_map( 75 | static fn (InputObjectField $field): string => $field->name, 76 | $cyclePath 77 | ) 78 | ); 79 | $fieldNodes = array_map( 80 | static fn (InputObjectField $field): ?InputValueDefinitionNode => $field->astNode, 81 | $cyclePath 82 | ); 83 | 84 | $this->schemaValidationContext->reportError( 85 | "Cannot reference Input Object \"{$fieldType->name}\" within itself through a series of non-null fields: \"{$fieldNames}\".", 86 | $fieldNodes 87 | ); 88 | } 89 | } 90 | } 91 | 92 | array_pop($this->fieldPath); 93 | } 94 | 95 | unset($this->fieldPathIndexByTypeName[$inputObj->name]); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Utils/InterfaceImplementations.php: -------------------------------------------------------------------------------- 1 | */ 16 | private $objects; 17 | 18 | /** @var array */ 19 | private $interfaces; 20 | 21 | /** 22 | * @param array $objects 23 | * @param array $interfaces 24 | */ 25 | public function __construct(array $objects, array $interfaces) 26 | { 27 | $this->objects = $objects; 28 | $this->interfaces = $interfaces; 29 | } 30 | 31 | /** @return array */ 32 | public function objects(): array 33 | { 34 | return $this->objects; 35 | } 36 | 37 | /** @return array */ 38 | public function interfaces(): array 39 | { 40 | return $this->interfaces; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Utils/LazyException.php: -------------------------------------------------------------------------------- 1 | > */ 12 | private array $data = []; 13 | 14 | public function has(string $a, string $b, bool $areMutuallyExclusive): bool 15 | { 16 | $first = $this->data[$a] ?? null; 17 | $result = $first !== null && isset($first[$b]) ? $first[$b] : null; 18 | if ($result === null) { 19 | return false; 20 | } 21 | 22 | // areMutuallyExclusive being false is a superset of being true, 23 | // hence if we want to know if this PairSet "has" these two with no 24 | // exclusivity, we have to ensure it was added as such. 25 | if ($areMutuallyExclusive === false) { 26 | return $result === false; 27 | } 28 | 29 | return true; 30 | } 31 | 32 | public function add(string $a, string $b, bool $areMutuallyExclusive): void 33 | { 34 | $this->pairSetAdd($a, $b, $areMutuallyExclusive); 35 | $this->pairSetAdd($b, $a, $areMutuallyExclusive); 36 | } 37 | 38 | private function pairSetAdd(string $a, string $b, bool $areMutuallyExclusive): void 39 | { 40 | $this->data[$a] ??= []; 41 | $this->data[$a][$b] = $areMutuallyExclusive; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Utils/PhpDoc.php: -------------------------------------------------------------------------------- 1 | ' ' . trim($line), 36 | $lines 37 | ); 38 | 39 | $content = implode("\n", $lines); 40 | 41 | return static::nonEmptyOrNull($content); 42 | } 43 | 44 | protected static function nonEmptyOrNull(string $maybeEmptyString): ?string 45 | { 46 | $trimmed = trim($maybeEmptyString); 47 | 48 | return $trimmed === '' 49 | ? null 50 | : $trimmed; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Utils/TypeComparators.php: -------------------------------------------------------------------------------- 1 | getWrappedType(), $typeB->getWrappedType()); 25 | } 26 | 27 | // If either type is a list, the other must also be a list. 28 | if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) { 29 | return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); 30 | } 31 | 32 | // Otherwise the types are not equal. 33 | return false; 34 | } 35 | 36 | /** 37 | * Provided a type and a super type, return true if the first type is either 38 | * equal or a subset of the second super type (covariant). 39 | * 40 | * @throws InvariantViolation 41 | */ 42 | public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType): bool 43 | { 44 | // Equivalent type is a valid subtype 45 | if ($maybeSubType === $superType) { 46 | return true; 47 | } 48 | 49 | // If superType is non-null, maybeSubType must also be nullable. 50 | if ($superType instanceof NonNull) { 51 | if ($maybeSubType instanceof NonNull) { 52 | return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType()); 53 | } 54 | 55 | return false; 56 | } 57 | 58 | if ($maybeSubType instanceof NonNull) { 59 | // If superType is nullable, maybeSubType may be non-null. 60 | return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType); 61 | } 62 | 63 | // If superType type is a list, maybeSubType type must also be a list. 64 | if ($superType instanceof ListOfType) { 65 | if ($maybeSubType instanceof ListOfType) { 66 | return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType()); 67 | } 68 | 69 | return false; 70 | } 71 | 72 | if ($maybeSubType instanceof ListOfType) { 73 | // If superType is not a list, maybeSubType must also be not a list. 74 | return false; 75 | } 76 | 77 | if (Type::isAbstractType($superType)) { 78 | // If superType type is an abstract type, maybeSubType type may be a currently 79 | // possible object or interface type. 80 | 81 | return $maybeSubType instanceof ImplementingType 82 | && $schema->isSubType($superType, $maybeSubType); 83 | } 84 | 85 | return false; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Validator/Rules/CustomValidationRule.php: -------------------------------------------------------------------------------- 1 | |array> 14 | * @phpstan-type VisitorFn callable(ValidationContext): VisitorFnResult 15 | */ 16 | class CustomValidationRule extends ValidationRule 17 | { 18 | /** 19 | * @var callable 20 | * 21 | * @phpstan-var VisitorFn 22 | */ 23 | protected $visitorFn; 24 | 25 | /** @phpstan-param VisitorFn $visitorFn */ 26 | public function __construct(string $name, callable $visitorFn) 27 | { 28 | $this->name = $name; 29 | $this->visitorFn = $visitorFn; 30 | } 31 | 32 | public function getVisitor(ValidationContext $context): array 33 | { 34 | return ($this->visitorFn)($context); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Validator/Rules/DisableIntrospection.php: -------------------------------------------------------------------------------- 1 | setEnabled($enabled); 19 | } 20 | 21 | public function setEnabled(int $enabled): void 22 | { 23 | $this->isEnabled = $enabled; 24 | } 25 | 26 | public function getVisitor(QueryValidationContext $context): array 27 | { 28 | return $this->invokeIfNeeded( 29 | $context, 30 | [ 31 | NodeKind::FIELD => static function (FieldNode $node) use ($context): void { 32 | if ($node->name->value !== '__type' && $node->name->value !== '__schema') { 33 | return; 34 | } 35 | 36 | $context->reportError(new Error( 37 | static::introspectionDisabledMessage(), 38 | [$node] 39 | )); 40 | }, 41 | ] 42 | ); 43 | } 44 | 45 | public static function introspectionDisabledMessage(): string 46 | { 47 | return 'GraphQL introspection is not allowed, but the query contained __schema or __type'; 48 | } 49 | 50 | protected function isEnabled(): bool 51 | { 52 | return $this->isEnabled !== self::DISABLED; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Validator/Rules/ExecutableDefinitions.php: -------------------------------------------------------------------------------- 1 | static function (DocumentNode $node) use ($context): VisitorOperation { 29 | foreach ($node->definitions as $definition) { 30 | if (! $definition instanceof ExecutableDefinitionNode) { 31 | if ($definition instanceof SchemaDefinitionNode || $definition instanceof SchemaExtensionNode) { 32 | $defName = 'schema'; 33 | } else { 34 | assert( 35 | $definition instanceof TypeDefinitionNode || $definition instanceof TypeExtensionNode, 36 | 'only other option' 37 | ); 38 | $defName = "\"{$definition->getName()->value}\""; 39 | } 40 | 41 | $context->reportError(new Error( 42 | static::nonExecutableDefinitionMessage($defName), 43 | [$definition] 44 | )); 45 | } 46 | } 47 | 48 | return Visitor::skipNode(); 49 | }, 50 | ]; 51 | } 52 | 53 | public static function nonExecutableDefinitionMessage(string $defName): string 54 | { 55 | return "The {$defName} definition is not executable."; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Validator/Rules/FragmentsOnCompositeTypes.php: -------------------------------------------------------------------------------- 1 | static function (InlineFragmentNode $node) use ($context): void { 20 | if ($node->typeCondition === null) { 21 | return; 22 | } 23 | 24 | $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->typeCondition); 25 | if ($type === null || Type::isCompositeType($type)) { 26 | return; 27 | } 28 | 29 | $context->reportError(new Error( 30 | static::inlineFragmentOnNonCompositeErrorMessage($type->toString()), 31 | [$node->typeCondition] 32 | )); 33 | }, 34 | NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) use ($context): void { 35 | $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->typeCondition); 36 | 37 | if ($type === null || Type::isCompositeType($type)) { 38 | return; 39 | } 40 | 41 | $context->reportError(new Error( 42 | static::fragmentOnNonCompositeErrorMessage( 43 | $node->name->value, 44 | Printer::doPrint($node->typeCondition) 45 | ), 46 | [$node->typeCondition] 47 | )); 48 | }, 49 | ]; 50 | } 51 | 52 | public static function inlineFragmentOnNonCompositeErrorMessage(string $type): string 53 | { 54 | return "Fragment cannot condition on non composite type \"{$type}\"."; 55 | } 56 | 57 | public static function fragmentOnNonCompositeErrorMessage(string $fragName, string $type): string 58 | { 59 | return "Fragment \"{$fragName}\" cannot condition on non composite type \"{$type}\"."; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Validator/Rules/KnownArgumentNames.php: -------------------------------------------------------------------------------- 1 | getVisitor($context) + [ 28 | NodeKind::ARGUMENT => static function (ArgumentNode $node) use ($context): void { 29 | $argDef = $context->getArgument(); 30 | if ($argDef !== null) { 31 | return; 32 | } 33 | 34 | $fieldDef = $context->getFieldDef(); 35 | if ($fieldDef === null) { 36 | return; 37 | } 38 | 39 | $parentType = $context->getParentType(); 40 | if (! $parentType instanceof NamedType) { 41 | return; 42 | } 43 | 44 | $context->reportError(new Error( 45 | static::unknownArgMessage( 46 | $node->name->value, 47 | $fieldDef->name, 48 | $parentType->name, 49 | Utils::suggestionList( 50 | $node->name->value, 51 | array_map( 52 | static fn (Argument $arg): string => $arg->name, 53 | $fieldDef->args 54 | ) 55 | ) 56 | ), 57 | [$node] 58 | )); 59 | }, 60 | ]; 61 | } 62 | 63 | /** @param array $suggestedArgs */ 64 | public static function unknownArgMessage(string $argName, string $fieldName, string $typeName, array $suggestedArgs): string 65 | { 66 | $message = "Unknown argument \"{$argName}\" on field \"{$fieldName}\" of type \"{$typeName}\"."; 67 | 68 | if ($suggestedArgs !== []) { 69 | $suggestions = Utils::quotedOrList($suggestedArgs); 70 | $message .= " Did you mean {$suggestions}?"; 71 | } 72 | 73 | return $message; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Validator/Rules/KnownArgumentNamesOnDirectives.php: -------------------------------------------------------------------------------- 1 | $suggestedArgs */ 30 | public static function unknownDirectiveArgMessage(string $argName, string $directiveName, array $suggestedArgs): string 31 | { 32 | $message = "Unknown argument \"{$argName}\" on directive \"@{$directiveName}\"."; 33 | 34 | if (isset($suggestedArgs[0])) { 35 | $suggestions = Utils::quotedOrList($suggestedArgs); 36 | $message .= " Did you mean {$suggestions}?"; 37 | } 38 | 39 | return $message; 40 | } 41 | 42 | /** @throws InvariantViolation */ 43 | public function getSDLVisitor(SDLValidationContext $context): array 44 | { 45 | return $this->getASTVisitor($context); 46 | } 47 | 48 | /** @throws InvariantViolation */ 49 | public function getVisitor(QueryValidationContext $context): array 50 | { 51 | return $this->getASTVisitor($context); 52 | } 53 | 54 | /** 55 | * @phpstan-return VisitorArray 56 | * 57 | * @throws InvariantViolation 58 | */ 59 | public function getASTVisitor(ValidationContext $context): array 60 | { 61 | $directiveArgs = []; 62 | $schema = $context->getSchema(); 63 | $definedDirectives = $schema !== null 64 | ? $schema->getDirectives() 65 | : Directive::getInternalDirectives(); 66 | 67 | foreach ($definedDirectives as $directive) { 68 | $directiveArgs[$directive->name] = array_map( 69 | static fn (Argument $arg): string => $arg->name, 70 | $directive->args 71 | ); 72 | } 73 | 74 | $astDefinitions = $context->getDocument()->definitions; 75 | foreach ($astDefinitions as $def) { 76 | if ($def instanceof DirectiveDefinitionNode) { 77 | $argNames = []; 78 | foreach ($def->arguments as $arg) { 79 | $argNames[] = $arg->name->value; 80 | } 81 | 82 | $directiveArgs[$def->name->value] = $argNames; 83 | } 84 | } 85 | 86 | return [ 87 | NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($directiveArgs, $context): VisitorOperation { 88 | $directiveName = $directiveNode->name->value; 89 | 90 | if (! isset($directiveArgs[$directiveName])) { 91 | return Visitor::skipNode(); 92 | } 93 | $knownArgs = $directiveArgs[$directiveName]; 94 | 95 | foreach ($directiveNode->arguments as $argNode) { 96 | $argName = $argNode->name->value; 97 | if (! in_array($argName, $knownArgs, true)) { 98 | $suggestions = Utils::suggestionList($argName, $knownArgs); 99 | $context->reportError(new Error( 100 | static::unknownDirectiveArgMessage($argName, $directiveName, $suggestions), 101 | [$argNode] 102 | )); 103 | } 104 | } 105 | 106 | return Visitor::skipNode(); 107 | }, 108 | ]; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Validator/Rules/KnownFragmentNames.php: -------------------------------------------------------------------------------- 1 | static function (FragmentSpreadNode $node) use ($context): void { 16 | $fragmentName = $node->name->value; 17 | $fragment = $context->getFragment($fragmentName); 18 | if ($fragment !== null) { 19 | return; 20 | } 21 | 22 | $context->reportError(new Error( 23 | static::unknownFragmentMessage($fragmentName), 24 | [$node->name] 25 | )); 26 | }, 27 | ]; 28 | } 29 | 30 | public static function unknownFragmentMessage(string $fragName): string 31 | { 32 | return "Unknown fragment \"{$fragName}\"."; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Validator/Rules/KnownTypeNames.php: -------------------------------------------------------------------------------- 1 | getASTVisitor($context); 30 | } 31 | 32 | public function getSDLVisitor(SDLValidationContext $context): array 33 | { 34 | return $this->getASTVisitor($context); 35 | } 36 | 37 | /** @phpstan-return VisitorArray */ 38 | public function getASTVisitor(ValidationContext $context): array 39 | { 40 | /** @var array $definedTypes */ 41 | $definedTypes = []; 42 | foreach ($context->getDocument()->definitions as $def) { 43 | if ($def instanceof TypeDefinitionNode) { 44 | $definedTypes[] = $def->getName()->value; 45 | } 46 | } 47 | 48 | return [ 49 | NodeKind::NAMED_TYPE => static function (NamedTypeNode $node, $_1, $parent, $_2, $ancestors) use ($context, $definedTypes): void { 50 | $typeName = $node->name->value; 51 | $schema = $context->getSchema(); 52 | 53 | if (in_array($typeName, $definedTypes, true)) { 54 | return; 55 | } 56 | 57 | if ($schema !== null && $schema->hasType($typeName)) { 58 | return; 59 | } 60 | 61 | $definitionNode = $ancestors[2] ?? $parent; 62 | $isSDL = $definitionNode instanceof TypeSystemDefinitionNode || $definitionNode instanceof TypeSystemExtensionNode; 63 | if ($isSDL && in_array($typeName, Type::BUILT_IN_TYPE_NAMES, true)) { 64 | return; 65 | } 66 | 67 | $existingTypesMap = $schema !== null 68 | ? $schema->getTypeMap() 69 | : []; 70 | $typeNames = [ 71 | ...array_keys($existingTypesMap), 72 | ...$definedTypes, 73 | ]; 74 | $context->reportError(new Error( 75 | static::unknownTypeMessage( 76 | $typeName, 77 | Utils::suggestionList( 78 | $typeName, 79 | $isSDL 80 | ? [...Type::BUILT_IN_TYPE_NAMES, ...$typeNames] 81 | : $typeNames 82 | ) 83 | ), 84 | [$node] 85 | )); 86 | }, 87 | ]; 88 | } 89 | 90 | /** @param array $suggestedTypes */ 91 | public static function unknownTypeMessage(string $type, array $suggestedTypes): string 92 | { 93 | $message = "Unknown type \"{$type}\"."; 94 | 95 | if ($suggestedTypes !== []) { 96 | $suggestionList = Utils::quotedOrList($suggestedTypes); 97 | $message .= " Did you mean {$suggestionList}?"; 98 | } 99 | 100 | return $message; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Validator/Rules/LoneAnonymousOperation.php: -------------------------------------------------------------------------------- 1 | static function (DocumentNode $node) use (&$operationCount): void { 25 | $operationCount = 0; 26 | foreach ($node->definitions as $definition) { 27 | if ($definition instanceof OperationDefinitionNode) { 28 | ++$operationCount; 29 | } 30 | } 31 | }, 32 | NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use (&$operationCount, $context): void { 33 | if ($node->name !== null || $operationCount <= 1) { 34 | return; 35 | } 36 | 37 | $context->reportError( 38 | new Error(static::anonOperationNotAloneMessage(), [$node]) 39 | ); 40 | }, 41 | ]; 42 | } 43 | 44 | public static function anonOperationNotAloneMessage(): string 45 | { 46 | return 'This anonymous operation must be the only defined operation.'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Validator/Rules/LoneSchemaDefinition.php: -------------------------------------------------------------------------------- 1 | getSchema(); 30 | $alreadyDefined = $oldSchema === null 31 | ? false 32 | : ( 33 | $oldSchema->astNode !== null 34 | || $oldSchema->getQueryType() !== null 35 | || $oldSchema->getMutationType() !== null 36 | || $oldSchema->getSubscriptionType() !== null 37 | ); 38 | 39 | $schemaDefinitionsCount = 0; 40 | 41 | return [ 42 | NodeKind::SCHEMA_DEFINITION => static function (SchemaDefinitionNode $node) use ($alreadyDefined, $context, &$schemaDefinitionsCount): void { 43 | if ($alreadyDefined) { 44 | $context->reportError(new Error(static::canNotDefineSchemaWithinExtensionMessage(), $node)); 45 | 46 | return; 47 | } 48 | 49 | if ($schemaDefinitionsCount > 0) { 50 | $context->reportError(new Error(static::schemaDefinitionNotAloneMessage(), $node)); 51 | } 52 | 53 | ++$schemaDefinitionsCount; 54 | }, 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Validator/Rules/NoFragmentCycles.php: -------------------------------------------------------------------------------- 1 | */ 16 | protected array $visitedFrags; 17 | 18 | /** @var array */ 19 | protected array $spreadPath; 20 | 21 | /** @var array */ 22 | protected array $spreadPathIndexByName; 23 | 24 | public function getVisitor(QueryValidationContext $context): array 25 | { 26 | // Tracks already visited fragments to maintain O(N) and to ensure that cycles 27 | // are not redundantly reported. 28 | $this->visitedFrags = []; 29 | 30 | // Array of AST nodes used to produce meaningful errors 31 | $this->spreadPath = []; 32 | 33 | // Position in the spread path 34 | $this->spreadPathIndexByName = []; 35 | 36 | return [ 37 | NodeKind::OPERATION_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(), 38 | NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context): VisitorOperation { 39 | $this->detectCycleRecursive($node, $context); 40 | 41 | return Visitor::skipNode(); 42 | }, 43 | ]; 44 | } 45 | 46 | protected function detectCycleRecursive(FragmentDefinitionNode $fragment, QueryValidationContext $context): void 47 | { 48 | if (isset($this->visitedFrags[$fragment->name->value])) { 49 | return; 50 | } 51 | 52 | $fragmentName = $fragment->name->value; 53 | $this->visitedFrags[$fragmentName] = true; 54 | 55 | $spreadNodes = $context->getFragmentSpreads($fragment); 56 | 57 | if ($spreadNodes === []) { 58 | return; 59 | } 60 | 61 | $this->spreadPathIndexByName[$fragmentName] = count($this->spreadPath); 62 | 63 | foreach ($spreadNodes as $spreadNode) { 64 | $spreadName = $spreadNode->name->value; 65 | $cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null; 66 | 67 | $this->spreadPath[] = $spreadNode; 68 | if ($cycleIndex === null) { 69 | $spreadFragment = $context->getFragment($spreadName); 70 | if ($spreadFragment !== null) { 71 | $this->detectCycleRecursive($spreadFragment, $context); 72 | } 73 | } else { 74 | $cyclePath = array_slice($this->spreadPath, $cycleIndex); 75 | $fragmentNames = []; 76 | foreach (array_slice($cyclePath, 0, -1) as $frag) { 77 | $fragmentNames[] = $frag->name->value; 78 | } 79 | 80 | $context->reportError(new Error( 81 | static::cycleErrorMessage($spreadName, $fragmentNames), 82 | $cyclePath 83 | )); 84 | } 85 | 86 | array_pop($this->spreadPath); 87 | } 88 | 89 | $this->spreadPathIndexByName[$fragmentName] = null; 90 | } 91 | 92 | /** @param array $spreadNames */ 93 | public static function cycleErrorMessage(string $fragName, array $spreadNames = []): string 94 | { 95 | $via = $spreadNames === [] 96 | ? '' 97 | : ' via ' . implode(', ', $spreadNames); 98 | 99 | return "Cannot spread fragment \"{$fragName}\" within itself{$via}."; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Validator/Rules/NoUndefinedVariables.php: -------------------------------------------------------------------------------- 1 | $variableNameDefined */ 20 | $variableNameDefined = []; 21 | 22 | return [ 23 | NodeKind::OPERATION_DEFINITION => [ 24 | 'enter' => static function () use (&$variableNameDefined): void { 25 | $variableNameDefined = []; 26 | }, 27 | 'leave' => static function (OperationDefinitionNode $operation) use (&$variableNameDefined, $context): void { 28 | $usages = $context->getRecursiveVariableUsages($operation); 29 | 30 | foreach ($usages as $usage) { 31 | $node = $usage['node']; 32 | $varName = $node->name->value; 33 | 34 | if (! isset($variableNameDefined[$varName])) { 35 | $context->reportError(new Error( 36 | static::undefinedVarMessage( 37 | $varName, 38 | $operation->name !== null 39 | ? $operation->name->value 40 | : null 41 | ), 42 | [$node, $operation] 43 | )); 44 | } 45 | } 46 | }, 47 | ], 48 | NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $def) use (&$variableNameDefined): void { 49 | $variableNameDefined[$def->variable->name->value] = true; 50 | }, 51 | ]; 52 | } 53 | 54 | public static function undefinedVarMessage(string $varName, ?string $opName): string 55 | { 56 | return $opName === null 57 | ? "Variable \"\${$varName}\" is not defined by operation \"{$opName}\"." 58 | : "Variable \"\${$varName}\" is not defined."; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Validator/Rules/NoUnusedFragments.php: -------------------------------------------------------------------------------- 1 | */ 16 | protected array $operationDefs; 17 | 18 | /** @var array */ 19 | protected array $fragmentDefs; 20 | 21 | public function getVisitor(QueryValidationContext $context): array 22 | { 23 | $this->operationDefs = []; 24 | $this->fragmentDefs = []; 25 | 26 | return [ 27 | NodeKind::OPERATION_DEFINITION => function ($node): VisitorOperation { 28 | $this->operationDefs[] = $node; 29 | 30 | return Visitor::skipNode(); 31 | }, 32 | NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $def): VisitorOperation { 33 | $this->fragmentDefs[] = $def; 34 | 35 | return Visitor::skipNode(); 36 | }, 37 | NodeKind::DOCUMENT => [ 38 | 'leave' => function () use ($context): void { 39 | $fragmentNameUsed = []; 40 | 41 | foreach ($this->operationDefs as $operation) { 42 | foreach ($context->getRecursivelyReferencedFragments($operation) as $fragment) { 43 | $fragmentNameUsed[$fragment->name->value] = true; 44 | } 45 | } 46 | 47 | foreach ($this->fragmentDefs as $fragmentDef) { 48 | $fragName = $fragmentDef->name->value; 49 | 50 | if (! isset($fragmentNameUsed[$fragName])) { 51 | $context->reportError(new Error( 52 | static::unusedFragMessage($fragName), 53 | [$fragmentDef] 54 | )); 55 | } 56 | } 57 | }, 58 | ], 59 | ]; 60 | } 61 | 62 | public static function unusedFragMessage(string $fragName): string 63 | { 64 | return "Fragment \"{$fragName}\" is never used."; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Validator/Rules/NoUnusedVariables.php: -------------------------------------------------------------------------------- 1 | */ 14 | protected array $variableDefs; 15 | 16 | public function getVisitor(QueryValidationContext $context): array 17 | { 18 | $this->variableDefs = []; 19 | 20 | return [ 21 | NodeKind::OPERATION_DEFINITION => [ 22 | 'enter' => function (): void { 23 | $this->variableDefs = []; 24 | }, 25 | 'leave' => function (OperationDefinitionNode $operation) use ($context): void { 26 | $variableNameUsed = []; 27 | $usages = $context->getRecursiveVariableUsages($operation); 28 | $opName = $operation->name !== null 29 | ? $operation->name->value 30 | : null; 31 | 32 | foreach ($usages as $usage) { 33 | $node = $usage['node']; 34 | $variableNameUsed[$node->name->value] = true; 35 | } 36 | 37 | foreach ($this->variableDefs as $variableDef) { 38 | $variableName = $variableDef->variable->name->value; 39 | 40 | if (! isset($variableNameUsed[$variableName])) { 41 | $context->reportError(new Error( 42 | static::unusedVariableMessage($variableName, $opName), 43 | [$variableDef] 44 | )); 45 | } 46 | } 47 | }, 48 | ], 49 | NodeKind::VARIABLE_DEFINITION => function ($def): void { 50 | $this->variableDefs[] = $def; 51 | }, 52 | ]; 53 | } 54 | 55 | public static function unusedVariableMessage(string $varName, ?string $opName = null): string 56 | { 57 | return $opName !== null 58 | ? "Variable \"\${$varName}\" is never used in operation \"{$opName}\"." 59 | : "Variable \"\${$varName}\" is never used."; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Validator/Rules/ProvidedRequiredArguments.php: -------------------------------------------------------------------------------- 1 | getVisitor($context) + [ 20 | NodeKind::FIELD => [ 21 | 'leave' => static function (FieldNode $fieldNode) use ($context): ?VisitorOperation { 22 | $fieldDef = $context->getFieldDef(); 23 | 24 | if ($fieldDef === null) { 25 | return Visitor::skipNode(); 26 | } 27 | 28 | $argNodes = $fieldNode->arguments; 29 | 30 | $argNodeMap = []; 31 | foreach ($argNodes as $argNode) { 32 | $argNodeMap[$argNode->name->value] = $argNode; 33 | } 34 | 35 | foreach ($fieldDef->args as $argDef) { 36 | $argNode = $argNodeMap[$argDef->name] ?? null; 37 | if ($argNode === null && $argDef->isRequired()) { 38 | $context->reportError(new Error( 39 | static::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()->toString()), 40 | [$fieldNode] 41 | )); 42 | } 43 | } 44 | 45 | return null; 46 | }, 47 | ], 48 | ]; 49 | } 50 | 51 | public static function missingFieldArgMessage(string $fieldName, string $argName, string $type): string 52 | { 53 | return "Field \"{$fieldName}\" argument \"{$argName}\" of type \"{$type}\" is required but not provided."; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Validator/Rules/ScalarLeafs.php: -------------------------------------------------------------------------------- 1 | static function (FieldNode $node) use ($context): void { 17 | $type = $context->getType(); 18 | if ($type === null) { 19 | return; 20 | } 21 | 22 | if (Type::isLeafType(Type::getNamedType($type))) { 23 | if ($node->selectionSet !== null) { 24 | $context->reportError(new Error( 25 | static::noSubselectionAllowedMessage($node->name->value, $type->toString()), 26 | [$node->selectionSet] 27 | )); 28 | } 29 | } elseif ($node->selectionSet === null) { 30 | $context->reportError(new Error( 31 | static::requiredSubselectionMessage($node->name->value, $type->toString()), 32 | [$node] 33 | )); 34 | } 35 | }, 36 | ]; 37 | } 38 | 39 | public static function noSubselectionAllowedMessage(string $field, string $type): string 40 | { 41 | return "Field \"{$field}\" of type \"{$type}\" must not have a sub selection."; 42 | } 43 | 44 | public static function requiredSubselectionMessage(string $field, string $type): string 45 | { 46 | return "Field \"{$field}\" of type \"{$type}\" must have a sub selection."; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Validator/Rules/SingleFieldSubscription.php: -------------------------------------------------------------------------------- 1 | static function (OperationDefinitionNode $node) use ($context): VisitorOperation { 18 | if ($node->operation === 'subscription') { 19 | $selections = $node->selectionSet->selections; 20 | 21 | if (count($selections) > 1) { 22 | $offendingSelections = $selections->splice(1, count($selections)); 23 | 24 | $context->reportError(new Error( 25 | static::multipleFieldsInOperation($node->name->value ?? null), 26 | $offendingSelections 27 | )); 28 | } 29 | } 30 | 31 | return Visitor::skipNode(); 32 | }, 33 | ]; 34 | } 35 | 36 | public static function multipleFieldsInOperation(?string $operationName): string 37 | { 38 | if ($operationName === null) { 39 | return 'Anonymous Subscription must select only one top level field.'; 40 | } 41 | 42 | return "Subscription \"{$operationName}\" must select only one top level field."; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueArgumentDefinitionNames.php: -------------------------------------------------------------------------------- 1 | fields as $fieldDef) { 37 | self::checkArgUniqueness("{$node->name->value}.{$fieldDef->name->value}", $fieldDef->arguments, $context); 38 | } 39 | 40 | return Visitor::skipNode(); 41 | }; 42 | 43 | return [ 44 | NodeKind::DIRECTIVE_DEFINITION => static fn (DirectiveDefinitionNode $node): VisitorOperation => self::checkArgUniqueness("@{$node->name->value}", $node->arguments, $context), 45 | NodeKind::INTERFACE_TYPE_DEFINITION => $checkArgUniquenessPerField, 46 | NodeKind::INTERFACE_TYPE_EXTENSION => $checkArgUniquenessPerField, 47 | NodeKind::OBJECT_TYPE_DEFINITION => $checkArgUniquenessPerField, 48 | NodeKind::OBJECT_TYPE_EXTENSION => $checkArgUniquenessPerField, 49 | ]; 50 | } 51 | 52 | /** @param NodeList $arguments */ 53 | private static function checkArgUniqueness(string $parentName, NodeList $arguments, SDLValidationContext $context): VisitorOperation 54 | { 55 | $seenArgs = []; 56 | foreach ($arguments as $argument) { 57 | $seenArgs[$argument->name->value][] = $argument; 58 | } 59 | 60 | foreach ($seenArgs as $argName => $argNodes) { 61 | if (count($argNodes) > 1) { 62 | $context->reportError( 63 | new Error( 64 | "Argument \"{$parentName}({$argName}:)\" can only be defined once.", 65 | $argNodes, 66 | ), 67 | ); 68 | } 69 | } 70 | 71 | return Visitor::skipNode(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueArgumentNames.php: -------------------------------------------------------------------------------- 1 | */ 21 | protected array $knownArgNames; 22 | 23 | public function getSDLVisitor(SDLValidationContext $context): array 24 | { 25 | return $this->getASTVisitor($context); 26 | } 27 | 28 | public function getVisitor(QueryValidationContext $context): array 29 | { 30 | return $this->getASTVisitor($context); 31 | } 32 | 33 | /** @phpstan-return VisitorArray */ 34 | public function getASTVisitor(ValidationContext $context): array 35 | { 36 | $this->knownArgNames = []; 37 | 38 | return [ 39 | NodeKind::FIELD => function (): void { 40 | $this->knownArgNames = []; 41 | }, 42 | NodeKind::DIRECTIVE => function (): void { 43 | $this->knownArgNames = []; 44 | }, 45 | NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context): VisitorOperation { 46 | $argName = $node->name->value; 47 | if (isset($this->knownArgNames[$argName])) { 48 | $context->reportError(new Error( 49 | static::duplicateArgMessage($argName), 50 | [$this->knownArgNames[$argName], $node->name] 51 | )); 52 | } else { 53 | $this->knownArgNames[$argName] = $node->name; 54 | } 55 | 56 | return Visitor::skipNode(); 57 | }, 58 | ]; 59 | } 60 | 61 | public static function duplicateArgMessage(string $argName): string 62 | { 63 | return "There can be only one argument named \"{$argName}\"."; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueDirectiveNames.php: -------------------------------------------------------------------------------- 1 | getSchema(); 22 | 23 | /** @var array $knownDirectiveNames */ 24 | $knownDirectiveNames = []; 25 | 26 | return [ 27 | NodeKind::DIRECTIVE_DEFINITION => static function ($node) use ($context, $schema, &$knownDirectiveNames): ?VisitorOperation { 28 | $directiveName = $node->name->value; 29 | 30 | if ($schema !== null && $schema->getDirective($directiveName) !== null) { 31 | $context->reportError( 32 | new Error( 33 | 'Directive "@' . $directiveName . '" already exists in the schema. It cannot be redefined.', 34 | $node->name, 35 | ), 36 | ); 37 | 38 | return null; 39 | } 40 | 41 | if (isset($knownDirectiveNames[$directiveName])) { 42 | $context->reportError( 43 | new Error( 44 | 'There can be only one directive named "@' . $directiveName . '".', 45 | [ 46 | $knownDirectiveNames[$directiveName], 47 | $node->name, 48 | ] 49 | ), 50 | ); 51 | } else { 52 | $knownDirectiveNames[$directiveName] = $node->name; 53 | } 54 | 55 | return Visitor::skipNode(); 56 | }, 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueDirectivesPerLocation.php: -------------------------------------------------------------------------------- 1 | getASTVisitor($context); 29 | } 30 | 31 | /** @throws InvariantViolation */ 32 | public function getSDLVisitor(SDLValidationContext $context): array 33 | { 34 | return $this->getASTVisitor($context); 35 | } 36 | 37 | /** 38 | * @throws InvariantViolation 39 | * 40 | * @phpstan-return VisitorArray 41 | */ 42 | public function getASTVisitor(ValidationContext $context): array 43 | { 44 | /** @var array $uniqueDirectiveMap */ 45 | $uniqueDirectiveMap = []; 46 | 47 | $schema = $context->getSchema(); 48 | $definedDirectives = $schema !== null 49 | ? $schema->getDirectives() 50 | : Directive::getInternalDirectives(); 51 | foreach ($definedDirectives as $directive) { 52 | if (! $directive->isRepeatable) { 53 | $uniqueDirectiveMap[$directive->name] = true; 54 | } 55 | } 56 | 57 | $astDefinitions = $context->getDocument()->definitions; 58 | foreach ($astDefinitions as $definition) { 59 | if ($definition instanceof DirectiveDefinitionNode 60 | && ! $definition->repeatable 61 | ) { 62 | $uniqueDirectiveMap[$definition->name->value] = true; 63 | } 64 | } 65 | 66 | return [ 67 | 'enter' => static function (Node $node) use ($uniqueDirectiveMap, $context): void { 68 | if (! property_exists($node, 'directives')) { 69 | return; 70 | } 71 | 72 | $knownDirectives = []; 73 | 74 | foreach ($node->directives as $directive) { 75 | $directiveName = $directive->name->value; 76 | 77 | if (isset($uniqueDirectiveMap[$directiveName])) { 78 | if (isset($knownDirectives[$directiveName])) { 79 | $context->reportError(new Error( 80 | static::duplicateDirectiveMessage($directiveName), 81 | [$knownDirectives[$directiveName], $directive] 82 | )); 83 | } else { 84 | $knownDirectives[$directiveName] = $directive; 85 | } 86 | } 87 | } 88 | }, 89 | ]; 90 | } 91 | 92 | public static function duplicateDirectiveMessage(string $directiveName): string 93 | { 94 | return "The directive \"{$directiveName}\" can only be used once at this location."; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueEnumValueNames.php: -------------------------------------------------------------------------------- 1 | > $knownValueNames */ 20 | $knownValueNames = []; 21 | 22 | /** 23 | * @param EnumTypeDefinitionNode|EnumTypeExtensionNode $enum 24 | */ 25 | $checkValueUniqueness = static function ($enum) use ($context, &$knownValueNames): VisitorOperation { 26 | $typeName = $enum->name->value; 27 | 28 | $schema = $context->getSchema(); 29 | $existingType = $schema !== null 30 | ? $schema->getType($typeName) 31 | : null; 32 | 33 | $valueNodes = $enum->values; 34 | 35 | if (! isset($knownValueNames[$typeName])) { 36 | $knownValueNames[$typeName] = []; 37 | } 38 | 39 | $valueNames = &$knownValueNames[$typeName]; 40 | 41 | foreach ($valueNodes as $valueDef) { 42 | $valueNameNode = $valueDef->name; 43 | $valueName = $valueNameNode->value; 44 | 45 | if ($existingType instanceof EnumType && $existingType->getValue($valueName) !== null) { 46 | $context->reportError(new Error( 47 | "Enum value \"{$typeName}.{$valueName}\" already exists in the schema. It cannot also be defined in this type extension.", 48 | $valueNameNode 49 | )); 50 | } elseif (isset($valueNames[$valueName])) { 51 | $context->reportError(new Error( 52 | "Enum value \"{$typeName}.{$valueName}\" can only be defined once.", 53 | [$valueNames[$valueName], $valueNameNode] 54 | )); 55 | } else { 56 | $valueNames[$valueName] = $valueNameNode; 57 | } 58 | } 59 | 60 | return Visitor::skipNode(); 61 | }; 62 | 63 | return [ 64 | NodeKind::ENUM_TYPE_DEFINITION => $checkValueUniqueness, 65 | NodeKind::ENUM_TYPE_EXTENSION => $checkValueUniqueness, 66 | ]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueFragmentNames.php: -------------------------------------------------------------------------------- 1 | */ 16 | protected array $knownFragmentNames; 17 | 18 | public function getVisitor(QueryValidationContext $context): array 19 | { 20 | $this->knownFragmentNames = []; 21 | 22 | return [ 23 | NodeKind::OPERATION_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(), 24 | NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context): VisitorOperation { 25 | $fragmentName = $node->name->value; 26 | if (! isset($this->knownFragmentNames[$fragmentName])) { 27 | $this->knownFragmentNames[$fragmentName] = $node->name; 28 | } else { 29 | $context->reportError(new Error( 30 | static::duplicateFragmentNameMessage($fragmentName), 31 | [$this->knownFragmentNames[$fragmentName], $node->name] 32 | )); 33 | } 34 | 35 | return Visitor::skipNode(); 36 | }, 37 | ]; 38 | } 39 | 40 | public static function duplicateFragmentNameMessage(string $fragName): string 41 | { 42 | return "There can be only one fragment named \"{$fragName}\"."; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueInputFieldNames.php: -------------------------------------------------------------------------------- 1 | */ 21 | protected array $knownNames; 22 | 23 | /** @var array> */ 24 | protected array $knownNameStack; 25 | 26 | public function getVisitor(QueryValidationContext $context): array 27 | { 28 | return $this->getASTVisitor($context); 29 | } 30 | 31 | public function getSDLVisitor(SDLValidationContext $context): array 32 | { 33 | return $this->getASTVisitor($context); 34 | } 35 | 36 | /** @phpstan-return VisitorArray */ 37 | public function getASTVisitor(ValidationContext $context): array 38 | { 39 | $this->knownNames = []; 40 | $this->knownNameStack = []; 41 | 42 | return [ 43 | NodeKind::OBJECT => [ 44 | 'enter' => function (): void { 45 | $this->knownNameStack[] = $this->knownNames; 46 | $this->knownNames = []; 47 | }, 48 | 'leave' => function (): void { 49 | $knownNames = array_pop($this->knownNameStack); 50 | assert(is_array($knownNames), 'should not happen if the visitor works correctly'); 51 | 52 | $this->knownNames = $knownNames; 53 | }, 54 | ], 55 | NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context): VisitorOperation { 56 | $fieldName = $node->name->value; 57 | 58 | if (isset($this->knownNames[$fieldName])) { 59 | $context->reportError(new Error( 60 | static::duplicateInputFieldMessage($fieldName), 61 | [$this->knownNames[$fieldName], $node->name] 62 | )); 63 | } else { 64 | $this->knownNames[$fieldName] = $node->name; 65 | } 66 | 67 | return Visitor::skipNode(); 68 | }, 69 | ]; 70 | } 71 | 72 | public static function duplicateInputFieldMessage(string $fieldName): string 73 | { 74 | return "There can be only one input field named \"{$fieldName}\"."; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueOperationNames.php: -------------------------------------------------------------------------------- 1 | */ 16 | protected array $knownOperationNames; 17 | 18 | public function getVisitor(QueryValidationContext $context): array 19 | { 20 | $this->knownOperationNames = []; 21 | 22 | return [ 23 | NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ($context): VisitorOperation { 24 | $operationName = $node->name; 25 | 26 | if ($operationName !== null) { 27 | if (! isset($this->knownOperationNames[$operationName->value])) { 28 | $this->knownOperationNames[$operationName->value] = $operationName; 29 | } else { 30 | $context->reportError(new Error( 31 | static::duplicateOperationNameMessage($operationName->value), 32 | [$this->knownOperationNames[$operationName->value], $operationName] 33 | )); 34 | } 35 | } 36 | 37 | return Visitor::skipNode(); 38 | }, 39 | NodeKind::FRAGMENT_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(), 40 | ]; 41 | } 42 | 43 | public static function duplicateOperationNameMessage(string $operationName): string 44 | { 45 | return "There can be only one operation named \"{$operationName}\"."; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueOperationTypes.php: -------------------------------------------------------------------------------- 1 | getSchema(); 23 | $definedOperationTypes = []; 24 | $existingOperationTypes = $schema !== null 25 | ? [ 26 | 'query' => $schema->getQueryType(), 27 | 'mutation' => $schema->getMutationType(), 28 | 'subscription' => $schema->getSubscriptionType(), 29 | ] 30 | : []; 31 | 32 | /** 33 | * @param SchemaDefinitionNode|SchemaExtensionNode $node 34 | */ 35 | $checkOperationTypes = static function ($node) use ($context, &$definedOperationTypes, $existingOperationTypes): VisitorOperation { 36 | foreach ($node->operationTypes as $operationType) { 37 | $operation = $operationType->operation; 38 | $alreadyDefinedOperationType = $definedOperationTypes[$operation] ?? null; 39 | 40 | if (isset($existingOperationTypes[$operation])) { 41 | $context->reportError( 42 | new Error( 43 | "Type for {$operation} already defined in the schema. It cannot be redefined.", 44 | $operationType, 45 | ), 46 | ); 47 | } elseif ($alreadyDefinedOperationType !== null) { 48 | $context->reportError( 49 | new Error( 50 | "There can be only one {$operation} type in schema.", 51 | [$alreadyDefinedOperationType, $operationType], 52 | ), 53 | ); 54 | } else { 55 | $definedOperationTypes[$operation] = $operationType; 56 | } 57 | } 58 | 59 | return Visitor::skipNode(); 60 | }; 61 | 62 | return [ 63 | NodeKind::SCHEMA_DEFINITION => $checkOperationTypes, 64 | NodeKind::SCHEMA_EXTENSION => $checkOperationTypes, 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueTypeNames.php: -------------------------------------------------------------------------------- 1 | getSchema(); 22 | /** @var array $knownTypeNames */ 23 | $knownTypeNames = []; 24 | $checkTypeName = static function ($node) use ($context, $schema, &$knownTypeNames): ?VisitorOperation { 25 | $typeName = $node->name->value; 26 | 27 | if ($schema !== null && $schema->getType($typeName) !== null) { 28 | $context->reportError( 29 | new Error( 30 | "Type \"{$typeName}\" already exists in the schema. It cannot also be defined in this type definition.", 31 | $node->name, 32 | ), 33 | ); 34 | 35 | return null; 36 | } 37 | 38 | if (array_key_exists($typeName, $knownTypeNames)) { 39 | $context->reportError( 40 | new Error( 41 | "There can be only one type named \"{$typeName}\".", 42 | [ 43 | $knownTypeNames[$typeName], 44 | $node->name, 45 | ] 46 | ), 47 | ); 48 | } else { 49 | $knownTypeNames[$typeName] = $node->name; 50 | } 51 | 52 | return Visitor::skipNode(); 53 | }; 54 | 55 | return [ 56 | NodeKind::SCALAR_TYPE_DEFINITION => $checkTypeName, 57 | NodeKind::OBJECT_TYPE_DEFINITION => $checkTypeName, 58 | NodeKind::INTERFACE_TYPE_DEFINITION => $checkTypeName, 59 | NodeKind::UNION_TYPE_DEFINITION => $checkTypeName, 60 | NodeKind::ENUM_TYPE_DEFINITION => $checkTypeName, 61 | NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $checkTypeName, 62 | ]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueVariableNames.php: -------------------------------------------------------------------------------- 1 | */ 14 | protected array $knownVariableNames; 15 | 16 | public function getVisitor(QueryValidationContext $context): array 17 | { 18 | $this->knownVariableNames = []; 19 | 20 | return [ 21 | NodeKind::OPERATION_DEFINITION => function (): void { 22 | $this->knownVariableNames = []; 23 | }, 24 | NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context): void { 25 | $variableName = $node->variable->name->value; 26 | if (! isset($this->knownVariableNames[$variableName])) { 27 | $this->knownVariableNames[$variableName] = $node->variable->name; 28 | } else { 29 | $context->reportError(new Error( 30 | static::duplicateVariableMessage($variableName), 31 | [$this->knownVariableNames[$variableName], $node->variable->name] 32 | )); 33 | } 34 | }, 35 | ]; 36 | } 37 | 38 | public static function duplicateVariableMessage(string $variableName): string 39 | { 40 | return "There can be only one variable named \"{$variableName}\"."; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Validator/Rules/ValidationRule.php: -------------------------------------------------------------------------------- 1 | name ?? static::class; 19 | } 20 | 21 | /** 22 | * Returns structure suitable for @see \GraphQL\Language\Visitor. 23 | * 24 | * @phpstan-return VisitorArray 25 | */ 26 | public function getVisitor(QueryValidationContext $context): array 27 | { 28 | return []; 29 | } 30 | 31 | /** 32 | * Returns structure suitable for @see \GraphQL\Language\Visitor. 33 | * 34 | * @phpstan-return VisitorArray 35 | */ 36 | public function getSDLVisitor(SDLValidationContext $context): array 37 | { 38 | return []; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Validator/Rules/VariablesAreInputTypes.php: -------------------------------------------------------------------------------- 1 | static function (VariableDefinitionNode $node) use ($context): void { 19 | $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->type); 20 | 21 | // If the variable type is not an input type, return an error. 22 | if ($type === null || Type::isInputType($type)) { 23 | return; 24 | } 25 | 26 | $variableName = $node->variable->name->value; 27 | $context->reportError(new Error( 28 | static::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), 29 | [$node->type] 30 | )); 31 | }, 32 | ]; 33 | } 34 | 35 | public static function nonInputTypeOnVarMessage(string $variableName, string $typeName): string 36 | { 37 | return "Variable \"\${$variableName}\" cannot be non-input type \"{$typeName}\"."; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Validator/SDLValidationContext.php: -------------------------------------------------------------------------------- 1 | */ 16 | protected array $errors = []; 17 | 18 | public function __construct(DocumentNode $ast, ?Schema $schema) 19 | { 20 | $this->ast = $ast; 21 | $this->schema = $schema; 22 | } 23 | 24 | public function reportError(Error $error): void 25 | { 26 | $this->errors[] = $error; 27 | } 28 | 29 | public function getErrors(): array 30 | { 31 | return $this->errors; 32 | } 33 | 34 | public function getDocument(): DocumentNode 35 | { 36 | return $this->ast; 37 | } 38 | 39 | public function getSchema(): ?Schema 40 | { 41 | return $this->schema; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Validator/ValidationContext.php: -------------------------------------------------------------------------------- 1 | */ 14 | public function getErrors(): array; 15 | 16 | public function getDocument(): DocumentNode; 17 | 18 | public function getSchema(): ?Schema; 19 | } 20 | --------------------------------------------------------------------------------