├── 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 | [](https://github.com/webonyx/graphql-php)
4 | [](https://github.com/webonyx/graphql-php/actions?query=workflow:CI+branch:master)
5 | [](https://codecov.io/gh/webonyx/graphql-php/branch/master)
6 | [](https://packagist.org/packages/webonyx/graphql-php)
7 | [](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 |
--------------------------------------------------------------------------------