├── examples ├── 02-shorthand │ ├── schema.graphqls │ ├── README.md │ ├── rootvalue.php │ └── graphql.php ├── 01-blog │ ├── Blog │ │ ├── Data │ │ │ ├── User.php │ │ │ ├── Story.php │ │ │ ├── Comment.php │ │ │ └── Image.php │ │ ├── AppContext.php │ │ └── Type │ │ │ ├── Enum │ │ │ ├── ContentFormatEnum.php │ │ │ └── ImageSizeEnumType.php │ │ │ ├── SearchResultType.php │ │ │ ├── NodeType.php │ │ │ ├── Field │ │ │ └── HtmlField.php │ │ │ ├── ImageType.php │ │ │ ├── Scalar │ │ │ ├── UrlType.php │ │ │ └── EmailType.php │ │ │ ├── UserType.php │ │ │ ├── CommentType.php │ │ │ └── QueryType.php │ ├── graphql.php │ └── README.md ├── 00-hello-world │ ├── README.md │ └── graphql.php └── 03-server │ ├── README.md │ └── graphql.php ├── src ├── Language │ ├── AST │ │ ├── NullValueNode.php │ │ ├── TypeNode.php │ │ ├── SelectionNode.php │ │ ├── NameNode.php │ │ ├── IntValueNode.php │ │ ├── EnumValueNode.php │ │ ├── FloatValueNode.php │ │ ├── HasSelectionSet.php │ │ ├── ListTypeNode.php │ │ ├── VariableNode.php │ │ ├── BooleanValueNode.php │ │ ├── DocumentNode.php │ │ ├── NamedTypeNode.php │ │ ├── SelectionSetNode.php │ │ ├── DefinitionNode.php │ │ ├── ListValueNode.php │ │ ├── ExecutableDefinitionNode.php │ │ ├── NonNullTypeNode.php │ │ ├── ObjectValueNode.php │ │ ├── ArgumentNode.php │ │ ├── ObjectFieldNode.php │ │ ├── DirectiveNode.php │ │ ├── StringValueNode.php │ │ ├── ValueNode.php │ │ ├── TypeSystemDefinitionNode.php │ │ ├── FragmentSpreadNode.php │ │ ├── ScalarTypeExtensionNode.php │ │ ├── SchemaDefinitionNode.php │ │ ├── TypeDefinitionNode.php │ │ ├── TypeExtensionNode.php │ │ ├── SchemaTypeExtensionNode.php │ │ ├── EnumValueDefinitionNode.php │ │ ├── VariableDefinitionNode.php │ │ ├── OperationTypeDefinitionNode.php │ │ ├── InlineFragmentNode.php │ │ ├── UnionTypeExtensionNode.php │ │ ├── EnumTypeExtensionNode.php │ │ ├── ScalarTypeDefinitionNode.php │ │ ├── InterfaceTypeExtensionNode.php │ │ ├── InputObjectTypeExtensionNode.php │ │ ├── DirectiveDefinitionNode.php │ │ ├── ObjectTypeExtensionNode.php │ │ ├── UnionTypeDefinitionNode.php │ │ ├── EnumTypeDefinitionNode.php │ │ ├── InterfaceTypeDefinitionNode.php │ │ ├── FieldNode.php │ │ ├── InputValueDefinitionNode.php │ │ ├── InputObjectTypeDefinitionNode.php │ │ ├── FieldDefinitionNode.php │ │ ├── ObjectTypeDefinitionNode.php │ │ ├── OperationDefinitionNode.php │ │ ├── FragmentDefinitionNode.php │ │ ├── Location.php │ │ └── NodeList.php │ ├── VisitorOperation.php │ ├── SourceLocation.php │ ├── DirectiveLocation.php │ ├── Source.php │ └── Token.php ├── Type │ ├── Definition │ │ ├── CompositeType.php │ │ ├── OutputType.php │ │ ├── NamedType.php │ │ ├── UnmodifiedType.php │ │ ├── WrappingType.php │ │ ├── InputType.php │ │ ├── AbstractType.php │ │ ├── ListOfType.php │ │ ├── EnumValueDefinition.php │ │ ├── LeafType.php │ │ ├── ScalarType.php │ │ ├── BooleanType.php │ │ ├── NonNull.php │ │ ├── FloatType.php │ │ ├── CustomScalarType.php │ │ ├── IDType.php │ │ ├── StringType.php │ │ ├── InputObjectField.php │ │ └── IntType.php │ └── TypeKind.php ├── Executor │ ├── ExecutorImplementation.php │ ├── Promise │ │ ├── Promise.php │ │ ├── PromiseAdapter.php │ │ └── Adapter │ │ │ └── ReactPromiseAdapter.php │ └── ExecutionContext.php ├── Error │ ├── InvariantViolation.php │ ├── Debug.php │ ├── SyntaxError.php │ ├── UserError.php │ ├── ClientAware.php │ └── Warning.php ├── Experimental │ └── Executor │ │ ├── Runtime.php │ │ ├── Strand.php │ │ ├── CoroutineContext.php │ │ └── CoroutineContextShared.php ├── Schema.php ├── Validator │ └── Rules │ │ ├── CustomValidationRule.php │ │ ├── ValidationRule.php │ │ ├── KnownFragmentNames.php │ │ ├── VariablesAreInputTypes.php │ │ ├── LoneSchemaDefinition.php │ │ ├── UniqueDirectivesPerLocation.php │ │ ├── UniqueVariableNames.php │ │ ├── DisableIntrospection.php │ │ ├── UniqueFragmentNames.php │ │ ├── UniqueArgumentNames.php │ │ ├── ExecutableDefinitions.php │ │ ├── ScalarLeafs.php │ │ ├── UniqueOperationNames.php │ │ ├── LoneAnonymousOperation.php │ │ ├── UniqueInputFieldNames.php │ │ ├── FragmentsOnCompositeTypes.php │ │ ├── NoUnusedFragments.php │ │ ├── NoUnusedVariables.php │ │ ├── VariablesDefaultValueAllowed.php │ │ ├── NoUndefinedVariables.php │ │ ├── KnownTypeNames.php │ │ └── KnownArgumentNamesOnDirectives.php ├── Server │ ├── RequestError.php │ └── OperationParams.php ├── Deferred.php └── Utils │ ├── PairSet.php │ └── BlockString.php ├── phpstan.neon.dist ├── docs ├── best-practices.md ├── how-it-works.md ├── type-system │ ├── unions.md │ ├── lists-and-nonnulls.md │ ├── directives.md │ ├── type-language.md │ └── index.md ├── complementary-tools.md ├── index.md └── security.md ├── .scrutinizer.yml ├── LICENSE ├── composer.json └── README.md /examples/02-shorthand/schema.graphqls: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Calc 4 | } 5 | 6 | type Calc { 7 | sum(x: Int, y: Int): Int 8 | } 9 | 10 | type Query { 11 | echo(message: String): String 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/Language/AST/NullValueNode.php: -------------------------------------------------------------------------------- 1 | 13 | | GraphQLNonNull< 14 | | GraphQLScalarType 15 | | GraphQLEnumType 16 | | GraphQLInputObjectType 17 | | GraphQLList, 18 | >; 19 | */ 20 | 21 | interface InputType 22 | { 23 | } 24 | -------------------------------------------------------------------------------- /src/Language/AST/InterfaceTypeExtensionNode.php: -------------------------------------------------------------------------------- 1 | 'ContentFormatEnum', 15 | 'values' => [self::FORMAT_TEXT, self::FORMAT_HTML] 16 | ]; 17 | parent::__construct($config); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 1 3 | 4 | paths: 5 | - %currentWorkingDirectory%/src 6 | - %currentWorkingDirectory%/tests 7 | 8 | ignoreErrors: 9 | - "~Construct empty\\(\\) is not allowed\\. Use more strict comparison~" 10 | - "~(Method|Property) .+::.+(\\(\\))? (has parameter \\$\\w+ with no|has no return|has no) typehint specified~" 11 | 12 | includes: 13 | - vendor/phpstan/phpstan-phpunit/extension.neon 14 | - vendor/phpstan/phpstan-phpunit/rules.neon 15 | - vendor/phpstan/phpstan-strict-rules/rules.neon 16 | -------------------------------------------------------------------------------- /src/Type/Definition/AbstractType.php: -------------------------------------------------------------------------------- 1 | current = $coroutine; 32 | $this->stack = []; 33 | $this->depth = 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Language/AST/OperationDefinitionNode.php: -------------------------------------------------------------------------------- 1 | name = $name; 18 | $this->visitorFn = $visitorFn; 19 | } 20 | 21 | /** 22 | * @return Error[] 23 | */ 24 | public function getVisitor(ValidationContext $context) 25 | { 26 | $fn = $this->visitorFn; 27 | 28 | return $fn($context); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/01-blog/Blog/Type/Enum/ImageSizeEnumType.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'ICON' => Image::SIZE_ICON, 15 | 'SMALL' => Image::SIZE_SMALL, 16 | 'MEDIUM' => Image::SIZE_MEDIUM, 17 | 'ORIGINAL' => Image::SIZE_ORIGINAL 18 | ] 19 | ]; 20 | 21 | parent::__construct($config); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Server/RequestError.php: -------------------------------------------------------------------------------- 1 | function($root, $args, $context) { 25 | $sum = new Addition(); 26 | 27 | return $sum->resolve($root, $args, $context); 28 | }, 29 | 'echo' => function($root, $args, $context) { 30 | $echo = new Echoer(); 31 | 32 | return $echo->resolve($root, $args, $context); 33 | }, 34 | 'prefix' => 'You said: ', 35 | ]; 36 | -------------------------------------------------------------------------------- /src/Validator/Rules/ValidationRule.php: -------------------------------------------------------------------------------- 1 | name ?: static::class; 18 | } 19 | 20 | public function __invoke(ValidationContext $context) 21 | { 22 | return $this->getVisitor($context); 23 | } 24 | 25 | /** 26 | * Returns structure suitable for GraphQL\Language\Visitor 27 | * 28 | * @see \GraphQL\Language\Visitor 29 | * 30 | * @return mixed[] 31 | */ 32 | abstract public function getVisitor(ValidationContext $context); 33 | } 34 | 35 | class_alias(ValidationRule::class, 'GraphQL\Validator\Rules\AbstractValidationRule'); 36 | -------------------------------------------------------------------------------- /examples/01-blog/Blog/Type/SearchResultType.php: -------------------------------------------------------------------------------- 1 | 'SearchResultType', 15 | 'types' => function() { 16 | return [ 17 | Types::story(), 18 | Types::user() 19 | ]; 20 | }, 21 | 'resolveType' => function($value) { 22 | if ($value instanceof Story) { 23 | return Types::story(); 24 | } else if ($value instanceof User) { 25 | return Types::user(); 26 | } 27 | } 28 | ]; 29 | parent::__construct($config); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Error/ClientAware.php: -------------------------------------------------------------------------------- 1 | 'Node', 16 | 'fields' => [ 17 | 'id' => Types::id() 18 | ], 19 | 'resolveType' => [$this, 'resolveNodeType'] 20 | ]; 21 | parent::__construct($config); 22 | } 23 | 24 | public function resolveNodeType($object) 25 | { 26 | if ($object instanceof User) { 27 | return Types::user(); 28 | } else if ($object instanceof Image) { 29 | return Types::image(); 30 | } else if ($object instanceof Story) { 31 | return Types::story(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/best-practices.md: -------------------------------------------------------------------------------- 1 | # Config Validation 2 | Defining types using arrays may be error-prone, but **graphql-php** provides config validation 3 | tool to report when config has unexpected structure. 4 | 5 | This validation tool is **disabled by default** because it is time-consuming operation which only 6 | makes sense during development. 7 | 8 | To enable validation - call: `GraphQL\Type\Definition\Config::enableValidation();` in your bootstrap 9 | but make sure to restrict it to debug/development mode only. 10 | 11 | # Type Registry 12 | **graphql-php** expects that each type in Schema is presented by single instance. Therefore 13 | if you define your types as separate PHP classes you need to ensure that each type is referenced only once. 14 | 15 | Technically you can create several instances of your type (for example for tests), but `GraphQL\Type\Schema` 16 | will throw on attempt to add different instances with the same name. 17 | 18 | There are several ways to achieve this depending on your preferences. We provide reference 19 | implementation below that introduces TypeRegistry class: -------------------------------------------------------------------------------- /src/Language/SourceLocation.php: -------------------------------------------------------------------------------- 1 | line = $line; 24 | $this->column = $col; 25 | } 26 | 27 | /** 28 | * @return int[] 29 | */ 30 | public function toArray() 31 | { 32 | return [ 33 | 'line' => $this->line, 34 | 'column' => $this->column, 35 | ]; 36 | } 37 | 38 | /** 39 | * @return int[] 40 | */ 41 | public function toSerializableArray() 42 | { 43 | return $this->toArray(); 44 | } 45 | 46 | /** 47 | * @return int[] 48 | */ 49 | public function jsonSerialize() 50 | { 51 | return $this->toSerializableArray(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | nodes: 3 | analysis: 4 | environment: 5 | php: 6 | version: 7.1 7 | cache: 8 | disabled: false 9 | directories: 10 | - ~/.composer/cache 11 | project_setup: 12 | override: true 13 | tests: 14 | override: 15 | - php-scrutinizer-run 16 | 17 | dependencies: 18 | override: 19 | - composer install --ignore-platform-reqs --no-interaction 20 | 21 | tools: 22 | external_code_coverage: 23 | timeout: 600 24 | 25 | build_failure_conditions: 26 | - 'elements.rating(<= C).new.exists' # No new classes/methods with a rating of C or worse allowed 27 | - 'issues.label("coding-style").new.exists' # No new coding style issues allowed 28 | - 'issues.severity(>= MAJOR).new.exists' # New issues of major or higher severity 29 | - 'project.metric_change("scrutinizer.test_coverage", < 0)' # Code Coverage decreased from previous inspection 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/02-shorthand/graphql.php: -------------------------------------------------------------------------------- 1 | [ 25 | 'message' => $e->getMessage() 26 | ] 27 | ]; 28 | } 29 | header('Content-Type: application/json; charset=UTF-8'); 30 | echo json_encode($result); 31 | 32 | -------------------------------------------------------------------------------- /src/Executor/Promise/Promise.php: -------------------------------------------------------------------------------- 1 | adapter = $adapter; 30 | $this->adoptedPromise = $adoptedPromise; 31 | } 32 | 33 | /** 34 | * @return Promise 35 | */ 36 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null) 37 | { 38 | return $this->adapter->then($this, $onFulfilled, $onRejected); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Type/Definition/ListOfType.php: -------------------------------------------------------------------------------- 1 | ofType = Type::assertType($type); 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function toString() 27 | { 28 | $type = $this->ofType; 29 | $str = $type instanceof Type ? $type->toString() : (string) $type; 30 | 31 | return '[' . $str . ']'; 32 | } 33 | 34 | /** 35 | * @param bool $recurse 36 | * 37 | * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType 38 | */ 39 | public function getWrappedType($recurse = false) 40 | { 41 | $type = $this->ofType; 42 | 43 | return $recurse && $type instanceof WrappingType ? $type->getWrappedType($recurse) : $type; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Validator/Rules/KnownFragmentNames.php: -------------------------------------------------------------------------------- 1 | static function (FragmentSpreadNode $node) use ($context) { 19 | $fragmentName = $node->name->value; 20 | $fragment = $context->getFragment($fragmentName); 21 | if ($fragment) { 22 | return; 23 | } 24 | 25 | $context->reportError(new Error( 26 | self::unknownFragmentMessage($fragmentName), 27 | [$node->name] 28 | )); 29 | }, 30 | ]; 31 | } 32 | 33 | /** 34 | * @param string $fragName 35 | */ 36 | public static function unknownFragmentMessage($fragName) 37 | { 38 | return sprintf('Unknown fragment "%s".', $fragName); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Type/Definition/EnumValueDefinition.php: -------------------------------------------------------------------------------- 1 | name = $config['name'] ?? null; 38 | $this->value = $config['value'] ?? null; 39 | $this->deprecationReason = $config['deprecationReason'] ?? null; 40 | $this->description = $config['description'] ?? null; 41 | $this->astNode = $config['astNode'] ?? null; 42 | 43 | $this->config = $config; 44 | } 45 | 46 | /** 47 | * @return bool 48 | */ 49 | public function isDeprecated() 50 | { 51 | return (bool) $this->deprecationReason; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Experimental/Executor/CoroutineContext.php: -------------------------------------------------------------------------------- 1 | shared = $shared; 51 | $this->type = $type; 52 | $this->value = $value; 53 | $this->result = $result; 54 | $this->path = $path; 55 | $this->nullFence = $nullFence; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Type/Definition/LeafType.php: -------------------------------------------------------------------------------- 1 | static function (VariableDefinitionNode $node) use ($context) { 22 | $type = TypeInfo::typeFromAST($context->getSchema(), $node->type); 23 | 24 | // If the variable type is not an input type, return an error. 25 | if (! $type || Type::isInputType($type)) { 26 | return; 27 | } 28 | 29 | $variableName = $node->variable->name->value; 30 | $context->reportError(new Error( 31 | self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), 32 | [$node->type] 33 | )); 34 | }, 35 | ]; 36 | } 37 | 38 | public static function nonInputTypeOnVarMessage($variableName, $typeName) 39 | { 40 | return sprintf('Variable "$%s" cannot be non-input type "%s".', $variableName, $typeName); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Deferred.php: -------------------------------------------------------------------------------- 1 | isEmpty()) { 32 | /** @var self $dfd */ 33 | $dfd = $q->dequeue(); 34 | $dfd->run(); 35 | } 36 | } 37 | 38 | public function __construct(callable $callback) 39 | { 40 | $this->callback = $callback; 41 | $this->promise = new SyncPromise(); 42 | self::getQueue()->enqueue($this); 43 | } 44 | 45 | public function then($onFulfilled = null, $onRejected = null) 46 | { 47 | return $this->promise->then($onFulfilled, $onRejected); 48 | } 49 | 50 | public function run() : void 51 | { 52 | try { 53 | $cb = $this->callback; 54 | $this->promise->resolve($cb()); 55 | } catch (Exception $e) { 56 | $this->promise->reject($e); 57 | } catch (Throwable $e) { 58 | $this->promise->reject($e); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Validator/Rules/LoneSchemaDefinition.php: -------------------------------------------------------------------------------- 1 | getSchema(); 22 | $alreadyDefined = $oldSchema !== null ? ( 23 | $oldSchema->getAstNode() || 24 | $oldSchema->getQueryType() || 25 | $oldSchema->getMutationType() || 26 | $oldSchema->getSubscriptionType() 27 | ) : false; 28 | 29 | $schemaDefinitionsCount = 0; 30 | 31 | return [ 32 | NodeKind::SCHEMA_DEFINITION => static function (SchemaDefinitionNode $node) use ($alreadyDefined, $context, &$schemaDefinitionsCount) { 33 | if ($alreadyDefined !== false) { 34 | $context->reportError(new Error('Cannot define a new schema within a schema extension.', $node)); 35 | return; 36 | } 37 | 38 | if ($schemaDefinitionsCount > 0) { 39 | $context->reportError(new Error('Must provide only one schema definition.', $node)); 40 | } 41 | 42 | ++$schemaDefinitionsCount; 43 | }, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueDirectivesPerLocation.php: -------------------------------------------------------------------------------- 1 | static function (Node $node) use ($context) { 19 | if (! isset($node->directives)) { 20 | return; 21 | } 22 | 23 | $knownDirectives = []; 24 | foreach ($node->directives as $directive) { 25 | /** @var DirectiveNode $directive */ 26 | $directiveName = $directive->name->value; 27 | if (isset($knownDirectives[$directiveName])) { 28 | $context->reportError(new Error( 29 | self::duplicateDirectiveMessage($directiveName), 30 | [$knownDirectives[$directiveName], $directive] 31 | )); 32 | } else { 33 | $knownDirectives[$directiveName] = $directive; 34 | } 35 | } 36 | }, 37 | ]; 38 | } 39 | 40 | public static function duplicateDirectiveMessage($directiveName) 41 | { 42 | return sprintf('The directive "%s" can only be used once at this location.', $directiveName); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Type/Definition/ScalarType.php: -------------------------------------------------------------------------------- 1 | name = $config['name'] ?? $this->tryInferName(); 44 | $this->description = $config['description'] ?? $this->description; 45 | $this->astNode = $config['astNode'] ?? null; 46 | $this->extensionASTNodes = $config['extensionASTNodes'] ?? null; 47 | $this->config = $config; 48 | 49 | Utils::invariant(is_string($this->name), 'Must provide name.'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueVariableNames.php: -------------------------------------------------------------------------------- 1 | knownVariableNames = []; 22 | 23 | return [ 24 | NodeKind::OPERATION_DEFINITION => function () { 25 | $this->knownVariableNames = []; 26 | }, 27 | NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) { 28 | $variableName = $node->variable->name->value; 29 | if (empty($this->knownVariableNames[$variableName])) { 30 | $this->knownVariableNames[$variableName] = $node->variable->name; 31 | } else { 32 | $context->reportError(new Error( 33 | self::duplicateVariableMessage($variableName), 34 | [$this->knownVariableNames[$variableName], $node->variable->name] 35 | )); 36 | } 37 | }, 38 | ]; 39 | } 40 | 41 | public static function duplicateVariableMessage($variableName) 42 | { 43 | return sprintf('There can be only one variable named "%s".', $variableName); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Validator/Rules/DisableIntrospection.php: -------------------------------------------------------------------------------- 1 | setEnabled($enabled); 22 | } 23 | 24 | public function setEnabled($enabled) 25 | { 26 | $this->isEnabled = $enabled; 27 | } 28 | 29 | public function getVisitor(ValidationContext $context) 30 | { 31 | return $this->invokeIfNeeded( 32 | $context, 33 | [ 34 | NodeKind::FIELD => static function (FieldNode $node) use ($context) { 35 | if ($node->name->value !== '__type' && $node->name->value !== '__schema') { 36 | return; 37 | } 38 | 39 | $context->reportError(new Error( 40 | static::introspectionDisabledMessage(), 41 | [$node] 42 | )); 43 | }, 44 | ] 45 | ); 46 | } 47 | 48 | public static function introspectionDisabledMessage() 49 | { 50 | return 'GraphQL introspection is not allowed, but the query contained __schema or __type'; 51 | } 52 | 53 | protected function isEnabled() 54 | { 55 | return $this->isEnabled !== static::DISABLED; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Experimental/Executor/CoroutineContextShared.php: -------------------------------------------------------------------------------- 1 | fieldNodes = $fieldNodes; 58 | $this->fieldName = $fieldName; 59 | $this->resultName = $resultName; 60 | $this->argumentValueMap = $argumentValueMap; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueFragmentNames.php: -------------------------------------------------------------------------------- 1 | knownFragmentNames = []; 23 | 24 | return [ 25 | NodeKind::OPERATION_DEFINITION => static function () { 26 | return Visitor::skipNode(); 27 | }, 28 | NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { 29 | $fragmentName = $node->name->value; 30 | if (empty($this->knownFragmentNames[$fragmentName])) { 31 | $this->knownFragmentNames[$fragmentName] = $node->name; 32 | } else { 33 | $context->reportError(new Error( 34 | self::duplicateFragmentNameMessage($fragmentName), 35 | [$this->knownFragmentNames[$fragmentName], $node->name] 36 | )); 37 | } 38 | 39 | return Visitor::skipNode(); 40 | }, 41 | ]; 42 | } 43 | 44 | public static function duplicateFragmentNameMessage($fragName) 45 | { 46 | return sprintf('There can be only one fragment named "%s".', $fragName); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueArgumentNames.php: -------------------------------------------------------------------------------- 1 | knownArgNames = []; 23 | 24 | return [ 25 | NodeKind::FIELD => function () { 26 | $this->knownArgNames = []; 27 | }, 28 | NodeKind::DIRECTIVE => function () { 29 | $this->knownArgNames = []; 30 | }, 31 | NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) { 32 | $argName = $node->name->value; 33 | if (! empty($this->knownArgNames[$argName])) { 34 | $context->reportError(new Error( 35 | self::duplicateArgMessage($argName), 36 | [$this->knownArgNames[$argName], $node->name] 37 | )); 38 | } else { 39 | $this->knownArgNames[$argName] = $node->name; 40 | } 41 | 42 | return Visitor::skipNode(); 43 | }, 44 | ]; 45 | } 46 | 47 | public static function duplicateArgMessage($argName) 48 | { 49 | return sprintf('There can be only one argument named "%s".', $argName); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Type/Definition/BooleanType.php: -------------------------------------------------------------------------------- 1 | value; 63 | } 64 | 65 | // Intentionally without message, as all information already in wrapped Exception 66 | throw new Exception(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webonyx/graphql-php", 3 | "description": "A PHP port of GraphQL reference implementation", 4 | "type": "library", 5 | "license": "MIT", 6 | "homepage": "https://github.com/webonyx/graphql-php", 7 | "keywords": [ 8 | "graphql", 9 | "API" 10 | ], 11 | "require": { 12 | "php": "^7.1", 13 | "ext-json": "*", 14 | "ext-mbstring": "*" 15 | }, 16 | "require-dev": { 17 | "doctrine/coding-standard": "^5.0", 18 | "phpbench/phpbench": "^0.14.0", 19 | "phpstan/phpstan": "0.10.5", 20 | "phpstan/phpstan-phpunit": "0.10.0", 21 | "phpstan/phpstan-strict-rules": "0.10.1", 22 | "phpunit/phpunit": "^7.2", 23 | "psr/http-message": "^1.0", 24 | "react/promise": "2.*" 25 | }, 26 | "config": { 27 | "preferred-install": "dist", 28 | "sort-packages": true 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "GraphQL\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "GraphQL\\Tests\\": "tests/", 38 | "GraphQL\\Benchmarks\\": "benchmarks/", 39 | "GraphQL\\Examples\\Blog\\": "examples/01-blog/Blog/" 40 | } 41 | }, 42 | "suggest": { 43 | "react/promise": "To leverage async resolving on React PHP platform", 44 | "psr/http-message": "To use standard GraphQL server" 45 | }, 46 | "scripts": { 47 | "api-docs": "php tools/gendocs.php", 48 | "bench": "phpbench run .", 49 | "test": "phpunit", 50 | "lint" : "phpcs", 51 | "fix-style" : "phpcbf", 52 | "static-analysis": "phpstan analyse --ansi --memory-limit 256M", 53 | "check-all": "composer lint && composer static-analysis && composer test" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-php 2 | [![Build Status](https://travis-ci.org/webonyx/graphql-php.svg?branch=master)](https://travis-ci.org/webonyx/graphql-php) 3 | [![Code Coverage](https://scrutinizer-ci.com/g/webonyx/graphql-php/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/webonyx/graphql-php) 4 | [![Latest Stable Version](https://poser.pugx.org/webonyx/graphql-php/version)](https://packagist.org/packages/webonyx/graphql-php) 5 | [![License](https://poser.pugx.org/webonyx/graphql-php/license)](https://packagist.org/packages/webonyx/graphql-php) 6 | 7 | This is a PHP implementation of the GraphQL [specification](https://github.com/facebook/graphql) 8 | based on the [reference implementation in JavaScript](https://github.com/graphql/graphql-js). 9 | 10 | ## Installation 11 | Via composer: 12 | ``` 13 | composer require webonyx/graphql-php 14 | ``` 15 | 16 | ## Documentation 17 | Full documentation is available on the [Documentation site](http://webonyx.github.io/graphql-php/) as well 18 | as in the [docs](docs/) folder of the distribution. 19 | 20 | If you don't know what GraphQL is, visit this [official website](http://graphql.org) 21 | by the Facebook engineering team. 22 | 23 | ## Examples 24 | There are several ready examples in the [examples](examples/) folder of the distribution with specific 25 | README file per example. 26 | 27 | ## Contribute 28 | Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for information on how to contribute. 29 | 30 | ## Old README.md 31 | Here is a [link to the old README.md](https://github.com/webonyx/graphql-php/blob/v0.9.14/README.md). 32 | 33 | Keep in mind that it relates to the version 0.9.x. It may contain outdated information for 34 | newer versions (even though we try to preserve backwards compatibility). 35 | -------------------------------------------------------------------------------- /src/Validator/Rules/ExecutableDefinitions.php: -------------------------------------------------------------------------------- 1 | static function (DocumentNode $node) use ($context) { 29 | /** @var Node $definition */ 30 | foreach ($node->definitions as $definition) { 31 | if ($definition instanceof OperationDefinitionNode || 32 | $definition instanceof FragmentDefinitionNode 33 | ) { 34 | continue; 35 | } 36 | 37 | $context->reportError(new Error( 38 | self::nonExecutableDefinitionMessage($definition->name->value), 39 | [$definition->name] 40 | )); 41 | } 42 | 43 | return Visitor::skipNode(); 44 | }, 45 | ]; 46 | } 47 | 48 | public static function nonExecutableDefinitionMessage($defName) 49 | { 50 | return sprintf('The "%s" definition is not executable.', $defName); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Validator/Rules/ScalarLeafs.php: -------------------------------------------------------------------------------- 1 | static function (FieldNode $node) use ($context) { 20 | $type = $context->getType(); 21 | if (! $type) { 22 | return; 23 | } 24 | 25 | if (Type::isLeafType(Type::getNamedType($type))) { 26 | if ($node->selectionSet) { 27 | $context->reportError(new Error( 28 | self::noSubselectionAllowedMessage($node->name->value, $type), 29 | [$node->selectionSet] 30 | )); 31 | } 32 | } elseif (! $node->selectionSet) { 33 | $context->reportError(new Error( 34 | self::requiredSubselectionMessage($node->name->value, $type), 35 | [$node] 36 | )); 37 | } 38 | }, 39 | ]; 40 | } 41 | 42 | public static function noSubselectionAllowedMessage($field, $type) 43 | { 44 | return sprintf('Field "%s" of type "%s" must not have a sub selection.', $field, $type); 45 | } 46 | 47 | public static function requiredSubselectionMessage($field, $type) 48 | { 49 | return sprintf('Field "%s" of type "%s" must have a sub selection.', $field, $type); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/type-system/unions.md: -------------------------------------------------------------------------------- 1 | # Union Type Definition 2 | A Union is an abstract type that simply enumerates other Object Types. 3 | The value of Union Type is actually a value of one of included Object Types. 4 | 5 | In **graphql-php** union type is an instance of `GraphQL\Type\Definition\UnionType` 6 | (or one of its subclasses) which accepts configuration array in a constructor: 7 | 8 | ```php 9 | 'SearchResult', 14 | 'types' => [ 15 | MyTypes::story(), 16 | MyTypes::user() 17 | ], 18 | 'resolveType' => function($value) { 19 | if ($value->type === 'story') { 20 | return MyTypes::story(); 21 | } else { 22 | return MyTypes::user(); 23 | } 24 | } 25 | ]); 26 | ``` 27 | 28 | This example uses **inline** style for Union definition, but you can also use 29 | [inheritance or type language](index.md#type-definition-styles). 30 | 31 | # Configuration options 32 | The constructor of UnionType accepts an array. Below is a full list of allowed options: 33 | 34 | Option | Type | Notes 35 | ------ | ---- | ----- 36 | name | `string` | **Required.** Unique name of this interface type within Schema 37 | 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. 38 | description | `string` | Plain-text description of this type for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation) 39 | resolveType | `callback` | **function($value, $context, [ResolveInfo](../reference.md#graphqltypedefinitionresolveinfo) $info)**
Receives **$value** from resolver of the parent field and returns concrete Object Type for this **$value**. 40 | -------------------------------------------------------------------------------- /src/Language/AST/Location.php: -------------------------------------------------------------------------------- 1 | start = $start; 61 | $tmp->end = $end; 62 | return $tmp; 63 | } 64 | 65 | public function __construct(?Token $startToken = null, ?Token $endToken = null, ?Source $source = null) 66 | { 67 | $this->startToken = $startToken; 68 | $this->endToken = $endToken; 69 | $this->source = $source; 70 | 71 | if (! $startToken || ! $endToken) { 72 | return; 73 | } 74 | 75 | $this->start = $startToken->start; 76 | $this->end = $endToken->end; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueOperationNames.php: -------------------------------------------------------------------------------- 1 | knownOperationNames = []; 23 | 24 | return [ 25 | NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ($context) { 26 | $operationName = $node->name; 27 | 28 | if ($operationName) { 29 | if (empty($this->knownOperationNames[$operationName->value])) { 30 | $this->knownOperationNames[$operationName->value] = $operationName; 31 | } else { 32 | $context->reportError(new Error( 33 | self::duplicateOperationNameMessage($operationName->value), 34 | [$this->knownOperationNames[$operationName->value], $operationName] 35 | )); 36 | } 37 | } 38 | 39 | return Visitor::skipNode(); 40 | }, 41 | NodeKind::FRAGMENT_DEFINITION => static function () { 42 | return Visitor::skipNode(); 43 | }, 44 | ]; 45 | } 46 | 47 | public static function duplicateOperationNameMessage($operationName) 48 | { 49 | return sprintf('There can be only one operation named "%s".', $operationName); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Utils/PairSet.php: -------------------------------------------------------------------------------- 1 | data = []; 19 | } 20 | 21 | /** 22 | * @param string $a 23 | * @param string $b 24 | * @param bool $areMutuallyExclusive 25 | * 26 | * @return bool 27 | */ 28 | public function has($a, $b, $areMutuallyExclusive) 29 | { 30 | $first = $this->data[$a] ?? null; 31 | $result = $first && isset($first[$b]) ? $first[$b] : null; 32 | if ($result === null) { 33 | return false; 34 | } 35 | // areMutuallyExclusive being false is a superset of being true, 36 | // hence if we want to know if this PairSet "has" these two with no 37 | // exclusivity, we have to ensure it was added as such. 38 | if ($areMutuallyExclusive === false) { 39 | return $result === false; 40 | } 41 | 42 | return true; 43 | } 44 | 45 | /** 46 | * @param string $a 47 | * @param string $b 48 | * @param bool $areMutuallyExclusive 49 | */ 50 | public function add($a, $b, $areMutuallyExclusive) 51 | { 52 | $this->pairSetAdd($a, $b, $areMutuallyExclusive); 53 | $this->pairSetAdd($b, $a, $areMutuallyExclusive); 54 | } 55 | 56 | /** 57 | * @param string $a 58 | * @param string $b 59 | * @param bool $areMutuallyExclusive 60 | */ 61 | private function pairSetAdd($a, $b, $areMutuallyExclusive) 62 | { 63 | $this->data[$a] = $this->data[$a] ?? []; 64 | $this->data[$a][$b] = $areMutuallyExclusive; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/01-blog/Blog/Type/Field/HtmlField.php: -------------------------------------------------------------------------------- 1 | $name, 18 | 'type' => Types::string(), 19 | 'args' => [ 20 | 'format' => [ 21 | 'type' => Types::contentFormatEnum(), 22 | 'defaultValue' => ContentFormatEnum::FORMAT_HTML 23 | ], 24 | 'maxLength' => Types::int() 25 | ], 26 | 'resolve' => function($object, $args) use ($objectKey) { 27 | $html = $object->{$objectKey}; 28 | $text = strip_tags($html); 29 | 30 | if (!empty($args['maxLength'])) { 31 | $safeText = mb_substr($text, 0, $args['maxLength']); 32 | } else { 33 | $safeText = $text; 34 | } 35 | 36 | switch ($args['format']) { 37 | case ContentFormatEnum::FORMAT_HTML: 38 | if ($safeText !== $text) { 39 | // Text was truncated, so just show what's safe: 40 | return nl2br($safeText); 41 | } else { 42 | return $html; 43 | } 44 | 45 | case ContentFormatEnum::FORMAT_TEXT: 46 | default: 47 | return $safeText; 48 | } 49 | } 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Validator/Rules/LoneAnonymousOperation.php: -------------------------------------------------------------------------------- 1 | static function (DocumentNode $node) use (&$operationCount) { 30 | $tmp = Utils::filter( 31 | $node->definitions, 32 | static function (Node $definition) { 33 | return $definition->kind === NodeKind::OPERATION_DEFINITION; 34 | } 35 | ); 36 | 37 | $operationCount = count($tmp); 38 | }, 39 | NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use ( 40 | &$operationCount, 41 | $context 42 | ) { 43 | if ($node->name || $operationCount <= 1) { 44 | return; 45 | } 46 | 47 | $context->reportError( 48 | new Error(self::anonOperationNotAloneMessage(), [$node]) 49 | ); 50 | }, 51 | ]; 52 | } 53 | 54 | public static function anonOperationNotAloneMessage() 55 | { 56 | return 'This anonymous operation must be the only defined operation.'; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Validator/Rules/UniqueInputFieldNames.php: -------------------------------------------------------------------------------- 1 | knownNames = []; 26 | $this->knownNameStack = []; 27 | 28 | return [ 29 | NodeKind::OBJECT => [ 30 | 'enter' => function () { 31 | $this->knownNameStack[] = $this->knownNames; 32 | $this->knownNames = []; 33 | }, 34 | 'leave' => function () { 35 | $this->knownNames = array_pop($this->knownNameStack); 36 | }, 37 | ], 38 | NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context) { 39 | $fieldName = $node->name->value; 40 | 41 | if (! empty($this->knownNames[$fieldName])) { 42 | $context->reportError(new Error( 43 | self::duplicateInputFieldMessage($fieldName), 44 | [$this->knownNames[$fieldName], $node->name] 45 | )); 46 | } else { 47 | $this->knownNames[$fieldName] = $node->name; 48 | } 49 | 50 | return Visitor::skipNode(); 51 | }, 52 | ]; 53 | } 54 | 55 | public static function duplicateInputFieldMessage($fieldName) 56 | { 57 | return sprintf('There can be only one input field named "%s".', $fieldName); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Executor/ExecutionContext.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 62 | $this->fragments = $fragments; 63 | $this->rootValue = $root; 64 | $this->contextValue = $contextValue; 65 | $this->operation = $operation; 66 | $this->variableValues = $variables; 67 | $this->errors = $errors ?: []; 68 | $this->fieldResolver = $fieldResolver; 69 | $this->promises = $promiseAdapter; 70 | } 71 | 72 | public function addError(Error $error) 73 | { 74 | $this->errors[] = $error; 75 | 76 | return $this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/03-server/graphql.php: -------------------------------------------------------------------------------- 1 | 'Query', 16 | 'fields' => [ 17 | 'echo' => [ 18 | 'type' => Type::string(), 19 | 'args' => [ 20 | 'message' => ['type' => Type::string()], 21 | ], 22 | 'resolve' => function ($root, $args) { 23 | return $root['prefix'] . $args['message']; 24 | } 25 | ], 26 | ], 27 | ]); 28 | 29 | $mutationType = new ObjectType([ 30 | 'name' => 'Calc', 31 | 'fields' => [ 32 | 'sum' => [ 33 | 'type' => Type::int(), 34 | 'args' => [ 35 | 'x' => ['type' => Type::int()], 36 | 'y' => ['type' => Type::int()], 37 | ], 38 | 'resolve' => function ($root, $args) { 39 | return $args['x'] + $args['y']; 40 | }, 41 | ], 42 | ], 43 | ]); 44 | 45 | // See docs on schema options: 46 | // http://webonyx.github.io/graphql-php/type-system/schema/#configuration-options 47 | $schema = new Schema([ 48 | 'query' => $queryType, 49 | 'mutation' => $mutationType, 50 | ]); 51 | 52 | // See docs on server options: 53 | // http://webonyx.github.io/graphql-php/executing-queries/#server-configuration-options 54 | $server = new StandardServer([ 55 | 'schema' => $schema 56 | ]); 57 | 58 | $server->handleRequest(); 59 | } catch (\Exception $e) { 60 | StandardServer::send500Error($e); 61 | } 62 | -------------------------------------------------------------------------------- /src/Type/Definition/NonNull.php: -------------------------------------------------------------------------------- 1 | ofType = self::assertNullableType($type); 27 | } 28 | 29 | /** 30 | * @param mixed $type 31 | * 32 | * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType 33 | */ 34 | public static function assertNullableType($type) 35 | { 36 | Utils::invariant( 37 | Type::isType($type) && ! $type instanceof self, 38 | 'Expected ' . Utils::printSafe($type) . ' to be a GraphQL nullable type.' 39 | ); 40 | 41 | return $type; 42 | } 43 | 44 | /** 45 | * @param mixed $type 46 | * 47 | * @return self 48 | */ 49 | public static function assertNullType($type) 50 | { 51 | Utils::invariant( 52 | $type instanceof self, 53 | 'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Non-Null type.' 54 | ); 55 | 56 | return $type; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function toString() 63 | { 64 | return $this->getWrappedType()->toString() . '!'; 65 | } 66 | 67 | /** 68 | * @param bool $recurse 69 | * 70 | * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType 71 | * 72 | * @throws InvariantViolation 73 | */ 74 | public function getWrappedType($recurse = false) 75 | { 76 | $type = $this->ofType; 77 | 78 | return $recurse && $type instanceof WrappingType ? $type->getWrappedType($recurse) : $type; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /docs/type-system/lists-and-nonnulls.md: -------------------------------------------------------------------------------- 1 | # Lists 2 | **graphql-php** provides built-in support for lists. In order to create list type - wrap 3 | existing type with `GraphQL\Type\Definition\Type::listOf()` modifier: 4 | ```php 5 | 'User', 13 | 'fields' => [ 14 | 'emails' => [ 15 | 'type' => Type::listOf(Type::string()), 16 | 'resolve' => function() { 17 | return ['jon@example.com', '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-Null fields 31 | By default in GraphQL, every field can have a **null** value. To indicate that some field always 32 | returns **non-null** value - use `GraphQL\Type\Definition\Type::nonNull()` modifier: 33 | 34 | ```php 35 | 'User', 41 | 'fields' => [ 42 | 'id' => [ 43 | 'type' => Type::nonNull(Type::id()), 44 | 'resolve' => function() { 45 | return uniqid(); 46 | } 47 | ], 48 | 'emails' => [ 49 | 'type' => Type::nonNull(Type::listOf(Type::string())), 50 | 'resolve' => function() { 51 | return ['jon@example.com', 'jonny@example.com']; 52 | } 53 | ] 54 | ] 55 | ]); 56 | ``` 57 | 58 | If resolver of non-null field returns **null**, graphql-php will add an error to 59 | result and exclude the whole object from the output (an error will bubble to first 60 | nullable parent field which will be set to **null**). 61 | 62 | Read the section on [Data Fetching](../data-fetching.md) for details. 63 | -------------------------------------------------------------------------------- /examples/01-blog/Blog/Type/ImageType.php: -------------------------------------------------------------------------------- 1 | 'ImageType', 16 | 'fields' => [ 17 | 'id' => Types::id(), 18 | 'type' => new EnumType([ 19 | 'name' => 'ImageTypeEnum', 20 | 'values' => [ 21 | 'USERPIC' => Image::TYPE_USERPIC 22 | ] 23 | ]), 24 | 'size' => Types::imageSizeEnum(), 25 | 'width' => Types::int(), 26 | 'height' => Types::int(), 27 | 'url' => [ 28 | 'type' => Types::url(), 29 | 'resolve' => [$this, 'resolveUrl'] 30 | ], 31 | 32 | // Just for the sake of example 33 | 'fieldWithError' => [ 34 | 'type' => Types::string(), 35 | 'resolve' => function() { 36 | throw new \Exception("Field with exception"); 37 | } 38 | ], 39 | 'nonNullFieldWithError' => [ 40 | 'type' => Types::nonNull(Types::string()), 41 | 'resolve' => function() { 42 | throw new \Exception("Non-null field with exception"); 43 | } 44 | ] 45 | ] 46 | ]; 47 | 48 | parent::__construct($config); 49 | } 50 | 51 | public function resolveUrl(Image $value, $args, AppContext $context) 52 | { 53 | switch ($value->type) { 54 | case Image::TYPE_USERPIC: 55 | $path = "/images/user/{$value->id}-{$value->size}.jpg"; 56 | break; 57 | default: 58 | throw new \UnexpectedValueException("Unexpected image type: " . $value->type); 59 | } 60 | return $context->rootUrl . $path; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docs/complementary-tools.md: -------------------------------------------------------------------------------- 1 | # Integrations 2 | 3 | * [Standard Server](executing-queries.md/#using-server) – Out of the box integration with any PSR-7 compatible framework (like [Slim](http://slimframework.com) or [Zend Expressive](http://zendframework.github.io/zend-expressive/)). 4 | * [Relay Library for graphql-php](https://github.com/ivome/graphql-relay-php) – Helps construct Relay related schema definitions. 5 | * Laravel 6 | - [Laravel GraphQL](https://github.com/Folkloreatelier/laravel-graphql) – Integration with Laravel 5 7 | - [laravel-graphql-relay](https://github.com/nuwave/laravel-graphql-relay) – Relay Helpers for Laravel 8 | - [Lighthouse](https://github.com/nuwave/lighthouse) – GraphQL Server for Laravel 9 | * [OverblogGraphQLBundle](https://github.com/overblog/GraphQLBundle) – Bundle for Symfony 10 | 11 | # GraphQL PHP Tools 12 | 13 | * [GraphQL Doctrine](https://github.com/Ecodev/graphql-doctrine) – Define types with Doctrine ORM annotations 14 | * [DataLoaderPHP](https://github.com/overblog/dataloader-php) – as a ready implementation for [deferred resolvers](data-fetching.md#solving-n1-problem) 15 | * [GraphQL Uploads](https://github.com/Ecodev/graphql-upload) – A PSR-15 middleware to support file uploads in GraphQL. 16 | * [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. 17 | * [GraphQL Utils](https://github.com/simPod/GraphQL-Utils) – Objective schema definition builders (no need for arrays anymore) and `DateTime` scalar 18 | * [PSR 15 compliant middleware](https://github.com/phps-cans/psr7-middleware-graphql) for the Standard Server _(experimental)_ 19 | 20 | # General GraphQL Tools 21 | 22 | * [GraphQL Playground](https://github.com/prismagraphql/graphql-playground) – GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration). 23 | * [GraphiQL](https://github.com/graphql/graphiql) – An in-browser IDE for exploring GraphQL 24 | * [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij) 25 | or [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp) – 26 | GraphiQL as Google Chrome extension 27 | -------------------------------------------------------------------------------- /examples/01-blog/Blog/Type/Scalar/UrlType.php: -------------------------------------------------------------------------------- 1 | parseValue($value); 26 | } 27 | 28 | /** 29 | * Parses an externally provided value (query variable) to use as an input 30 | * 31 | * @param mixed $value 32 | * @return mixed 33 | * @throws Error 34 | */ 35 | public function parseValue($value) 36 | { 37 | if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_URL)) { // quite naive, but after all this is example 38 | throw new Error("Cannot represent value as URL: " . Utils::printSafe($value)); 39 | } 40 | return $value; 41 | } 42 | 43 | /** 44 | * Parses an externally provided literal value to use as an input (e.g. in Query AST) 45 | * 46 | * @param Node $valueNode 47 | * @param array|null $variables 48 | * @return null|string 49 | * @throws Error 50 | */ 51 | public function parseLiteral($valueNode, array $variables = null) 52 | { 53 | // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL 54 | // error location in query: 55 | if (!($valueNode instanceof StringValueNode)) { 56 | throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]); 57 | } 58 | if (!is_string($valueNode->value) || !filter_var($valueNode->value, FILTER_VALIDATE_URL)) { 59 | throw new Error('Query error: Not a valid URL', [$valueNode]); 60 | } 61 | return $valueNode->value; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Type/Definition/FloatType.php: -------------------------------------------------------------------------------- 1 | coerceFloat($value); 39 | } 40 | 41 | private function coerceFloat($value) 42 | { 43 | if ($value === '') { 44 | throw new Error( 45 | 'Float cannot represent non numeric value: (empty string)' 46 | ); 47 | } 48 | 49 | if (! is_numeric($value) && $value !== true && $value !== false) { 50 | throw new Error( 51 | 'Float cannot represent non numeric value: ' . 52 | Utils::printSafe($value) 53 | ); 54 | } 55 | 56 | return (float) $value; 57 | } 58 | 59 | /** 60 | * @param mixed $value 61 | * 62 | * @return float|null 63 | * 64 | * @throws Error 65 | */ 66 | public function parseValue($value) 67 | { 68 | return $this->coerceFloat($value); 69 | } 70 | 71 | /** 72 | * @param Node $valueNode 73 | * @param mixed[]|null $variables 74 | * 75 | * @return float|null 76 | * 77 | * @throws Exception 78 | */ 79 | public function parseLiteral($valueNode, ?array $variables = null) 80 | { 81 | if ($valueNode instanceof FloatValueNode || $valueNode instanceof IntValueNode) { 82 | return (float) $valueNode->value; 83 | } 84 | 85 | // Intentionally without message, as all information already in wrapped Exception 86 | throw new Exception(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /examples/00-hello-world/graphql.php: -------------------------------------------------------------------------------- 1 | 'Query', 16 | 'fields' => [ 17 | 'echo' => [ 18 | 'type' => Type::string(), 19 | 'args' => [ 20 | 'message' => ['type' => Type::string()], 21 | ], 22 | 'resolve' => function ($root, $args) { 23 | return $root['prefix'] . $args['message']; 24 | } 25 | ], 26 | ], 27 | ]); 28 | 29 | $mutationType = new ObjectType([ 30 | 'name' => 'Calc', 31 | 'fields' => [ 32 | 'sum' => [ 33 | 'type' => Type::int(), 34 | 'args' => [ 35 | 'x' => ['type' => Type::int()], 36 | 'y' => ['type' => Type::int()], 37 | ], 38 | 'resolve' => function ($root, $args) { 39 | return $args['x'] + $args['y']; 40 | }, 41 | ], 42 | ], 43 | ]); 44 | 45 | // See docs on schema options: 46 | // http://webonyx.github.io/graphql-php/type-system/schema/#configuration-options 47 | $schema = new Schema([ 48 | 'query' => $queryType, 49 | 'mutation' => $mutationType, 50 | ]); 51 | 52 | $rawInput = file_get_contents('php://input'); 53 | $input = json_decode($rawInput, true); 54 | $query = $input['query']; 55 | $variableValues = isset($input['variables']) ? $input['variables'] : null; 56 | 57 | $rootValue = ['prefix' => 'You said: ']; 58 | $result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues); 59 | $output = $result->toArray(); 60 | } catch (\Exception $e) { 61 | $output = [ 62 | 'error' => [ 63 | 'message' => $e->getMessage() 64 | ] 65 | ]; 66 | } 67 | header('Content-Type: application/json; charset=UTF-8'); 68 | echo json_encode($output); 69 | 70 | -------------------------------------------------------------------------------- /src/Utils/BlockString.php: -------------------------------------------------------------------------------- 1 | = mb_strlen($line) || 38 | ($commonIndent !== null && $indent >= $commonIndent) 39 | ) { 40 | continue; 41 | } 42 | 43 | $commonIndent = $indent; 44 | if ($commonIndent === 0) { 45 | break; 46 | } 47 | } 48 | 49 | if ($commonIndent) { 50 | for ($i = 1; $i < $linesLength; $i++) { 51 | $line = $lines[$i]; 52 | $lines[$i] = mb_substr($line, $commonIndent); 53 | } 54 | } 55 | 56 | // Remove leading and trailing blank lines. 57 | while (count($lines) > 0 && trim($lines[0], " \t") === '') { 58 | array_shift($lines); 59 | } 60 | while (count($lines) > 0 && trim($lines[count($lines) - 1], " \t") === '') { 61 | array_pop($lines); 62 | } 63 | 64 | // Return a string of the lines joined with U+000A. 65 | return implode("\n", $lines); 66 | } 67 | 68 | private static function leadingWhitespace($str) 69 | { 70 | $i = 0; 71 | while ($i < mb_strlen($str) && ($str[$i] === ' ' || $str[$i] === '\t')) { 72 | $i++; 73 | } 74 | 75 | return $i; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Language/DirectiveLocation.php: -------------------------------------------------------------------------------- 1 | self::QUERY, 37 | self::MUTATION => self::MUTATION, 38 | self::SUBSCRIPTION => self::SUBSCRIPTION, 39 | self::FIELD => self::FIELD, 40 | self::FRAGMENT_DEFINITION => self::FRAGMENT_DEFINITION, 41 | self::FRAGMENT_SPREAD => self::FRAGMENT_SPREAD, 42 | self::INLINE_FRAGMENT => self::INLINE_FRAGMENT, 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 | /** 57 | * @param string $name 58 | * 59 | * @return bool 60 | */ 61 | public static function has($name) 62 | { 63 | return isset(self::$locations[$name]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Validator/Rules/FragmentsOnCompositeTypes.php: -------------------------------------------------------------------------------- 1 | static function (InlineFragmentNode $node) use ($context) { 23 | if (! $node->typeCondition) { 24 | return; 25 | } 26 | 27 | $type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition); 28 | if (! $type || Type::isCompositeType($type)) { 29 | return; 30 | } 31 | 32 | $context->reportError(new Error( 33 | static::inlineFragmentOnNonCompositeErrorMessage($type), 34 | [$node->typeCondition] 35 | )); 36 | }, 37 | NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) use ($context) { 38 | $type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition); 39 | 40 | if (! $type || Type::isCompositeType($type)) { 41 | return; 42 | } 43 | 44 | $context->reportError(new Error( 45 | static::fragmentOnNonCompositeErrorMessage( 46 | $node->name->value, 47 | Printer::doPrint($node->typeCondition) 48 | ), 49 | [$node->typeCondition] 50 | )); 51 | }, 52 | ]; 53 | } 54 | 55 | public static function inlineFragmentOnNonCompositeErrorMessage($type) 56 | { 57 | return sprintf('Fragment cannot condition on non composite type "%s".', $type); 58 | } 59 | 60 | public static function fragmentOnNonCompositeErrorMessage($fragName, $type) 61 | { 62 | return sprintf('Fragment "%s" cannot condition on non composite type "%s".', $fragName, $type); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Validator/Rules/NoUnusedFragments.php: -------------------------------------------------------------------------------- 1 | operationDefs = []; 26 | $this->fragmentDefs = []; 27 | 28 | return [ 29 | NodeKind::OPERATION_DEFINITION => function ($node) { 30 | $this->operationDefs[] = $node; 31 | 32 | return Visitor::skipNode(); 33 | }, 34 | NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $def) { 35 | $this->fragmentDefs[] = $def; 36 | 37 | return Visitor::skipNode(); 38 | }, 39 | NodeKind::DOCUMENT => [ 40 | 'leave' => function () use ($context) { 41 | $fragmentNameUsed = []; 42 | 43 | foreach ($this->operationDefs as $operation) { 44 | foreach ($context->getRecursivelyReferencedFragments($operation) as $fragment) { 45 | $fragmentNameUsed[$fragment->name->value] = true; 46 | } 47 | } 48 | 49 | foreach ($this->fragmentDefs as $fragmentDef) { 50 | $fragName = $fragmentDef->name->value; 51 | if (! empty($fragmentNameUsed[$fragName])) { 52 | continue; 53 | } 54 | 55 | $context->reportError(new Error( 56 | self::unusedFragMessage($fragName), 57 | [$fragmentDef] 58 | )); 59 | } 60 | }, 61 | ], 62 | ]; 63 | } 64 | 65 | public static function unusedFragMessage($fragName) 66 | { 67 | return sprintf('Fragment "%s" is never used.', $fragName); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/01-blog/graphql.php: -------------------------------------------------------------------------------- 1 | viewer = DataSource::findUser('1'); // simulated "currently logged-in user" 32 | $appContext->rootUrl = 'http://localhost:8080'; 33 | $appContext->request = $_REQUEST; 34 | 35 | // Parse incoming query and variables 36 | if (isset($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false) { 37 | $raw = file_get_contents('php://input') ?: ''; 38 | $data = json_decode($raw, true) ?: []; 39 | } else { 40 | $data = $_REQUEST; 41 | } 42 | 43 | $data += ['query' => null, 'variables' => null]; 44 | 45 | if (null === $data['query']) { 46 | $data['query'] = '{hello}'; 47 | } 48 | 49 | // GraphQL schema to be passed to query executor: 50 | $schema = new Schema([ 51 | 'query' => Types::query() 52 | ]); 53 | 54 | $result = GraphQL::executeQuery( 55 | $schema, 56 | $data['query'], 57 | null, 58 | $appContext, 59 | (array) $data['variables'] 60 | ); 61 | $output = $result->toArray($debug); 62 | $httpStatus = 200; 63 | } catch (\Exception $error) { 64 | $httpStatus = 500; 65 | $output['errors'] = [ 66 | FormattedError::createFromException($error, $debug) 67 | ]; 68 | } 69 | 70 | header('Content-Type: application/json', true, $httpStatus); 71 | echo json_encode($output); 72 | -------------------------------------------------------------------------------- /src/Validator/Rules/NoUnusedVariables.php: -------------------------------------------------------------------------------- 1 | variableDefs = []; 22 | 23 | return [ 24 | NodeKind::OPERATION_DEFINITION => [ 25 | 'enter' => function () { 26 | $this->variableDefs = []; 27 | }, 28 | 'leave' => function (OperationDefinitionNode $operation) use ($context) { 29 | $variableNameUsed = []; 30 | $usages = $context->getRecursiveVariableUsages($operation); 31 | $opName = $operation->name ? $operation->name->value : null; 32 | 33 | foreach ($usages as $usage) { 34 | $node = $usage['node']; 35 | $variableNameUsed[$node->name->value] = true; 36 | } 37 | 38 | foreach ($this->variableDefs as $variableDef) { 39 | $variableName = $variableDef->variable->name->value; 40 | 41 | if (! empty($variableNameUsed[$variableName])) { 42 | continue; 43 | } 44 | 45 | $context->reportError(new Error( 46 | self::unusedVariableMessage($variableName, $opName), 47 | [$variableDef] 48 | )); 49 | } 50 | }, 51 | ], 52 | NodeKind::VARIABLE_DEFINITION => function ($def) { 53 | $this->variableDefs[] = $def; 54 | }, 55 | ]; 56 | } 57 | 58 | public static function unusedVariableMessage($varName, $opName = null) 59 | { 60 | return $opName 61 | ? sprintf('Variable "$%s" is never used in operation "%s".', $varName, $opName) 62 | : sprintf('Variable "$%s" is never used.', $varName); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Validator/Rules/VariablesDefaultValueAllowed.php: -------------------------------------------------------------------------------- 1 | static function (VariableDefinitionNode $node) use ($context) { 29 | $name = $node->variable->name->value; 30 | $defaultValue = $node->defaultValue; 31 | $type = $context->getInputType(); 32 | if ($type instanceof NonNull && $defaultValue) { 33 | $context->reportError( 34 | new Error( 35 | self::defaultForRequiredVarMessage( 36 | $name, 37 | $type, 38 | $type->getWrappedType() 39 | ), 40 | [$defaultValue] 41 | ) 42 | ); 43 | } 44 | 45 | return Visitor::skipNode(); 46 | }, 47 | NodeKind::SELECTION_SET => static function (SelectionSetNode $node) { 48 | return Visitor::skipNode(); 49 | }, 50 | NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) { 51 | return Visitor::skipNode(); 52 | }, 53 | ]; 54 | } 55 | 56 | public static function defaultForRequiredVarMessage($varName, $type, $guessType) 57 | { 58 | return sprintf( 59 | 'Variable "$%s" of type "%s" is required and will not use the default value. Perhaps you meant to use type "%s".', 60 | $varName, 61 | $type, 62 | $guessType 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/01-blog/Blog/Type/Scalar/EmailType.php: -------------------------------------------------------------------------------- 1 | 'Email', 15 | 'serialize' => [__CLASS__, 'serialize'], 16 | 'parseValue' => [__CLASS__, 'parseValue'], 17 | 'parseLiteral' => [__CLASS__, 'parseLiteral'], 18 | ]); 19 | } 20 | 21 | /** 22 | * Serializes an internal value to include in a response. 23 | * 24 | * @param string $value 25 | * @return string 26 | */ 27 | public static function serialize($value) 28 | { 29 | // Assuming internal representation of email is always correct: 30 | return $value; 31 | 32 | // If it might be incorrect and you want to make sure that only correct values are included in response - 33 | // use following line instead: 34 | // return $this->parseValue($value); 35 | } 36 | 37 | /** 38 | * Parses an externally provided value (query variable) to use as an input 39 | * 40 | * @param mixed $value 41 | * @return mixed 42 | */ 43 | public static function parseValue($value) 44 | { 45 | if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { 46 | throw new \UnexpectedValueException("Cannot represent value as email: " . Utils::printSafe($value)); 47 | } 48 | return $value; 49 | } 50 | 51 | /** 52 | * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input 53 | * 54 | * @param \GraphQL\Language\AST\Node $valueNode 55 | * @return string 56 | * @throws Error 57 | */ 58 | public static function parseLiteral($valueNode) 59 | { 60 | // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL 61 | // error location in query: 62 | if (!$valueNode instanceof StringValueNode) { 63 | throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]); 64 | } 65 | if (!filter_var($valueNode->value, FILTER_VALIDATE_EMAIL)) { 66 | throw new Error("Not a valid email", [$valueNode]); 67 | } 68 | return $valueNode->value; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Validator/Rules/NoUndefinedVariables.php: -------------------------------------------------------------------------------- 1 | [ 28 | 'enter' => static function () use (&$variableNameDefined) { 29 | $variableNameDefined = []; 30 | }, 31 | 'leave' => static function (OperationDefinitionNode $operation) use (&$variableNameDefined, $context) { 32 | $usages = $context->getRecursiveVariableUsages($operation); 33 | 34 | foreach ($usages as $usage) { 35 | $node = $usage['node']; 36 | $varName = $node->name->value; 37 | 38 | if (! empty($variableNameDefined[$varName])) { 39 | continue; 40 | } 41 | 42 | $context->reportError(new Error( 43 | self::undefinedVarMessage( 44 | $varName, 45 | $operation->name ? $operation->name->value : null 46 | ), 47 | [$node, $operation] 48 | )); 49 | } 50 | }, 51 | ], 52 | NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $def) use (&$variableNameDefined) { 53 | $variableNameDefined[$def->variable->name->value] = true; 54 | }, 55 | ]; 56 | } 57 | 58 | public static function undefinedVarMessage($varName, $opName = null) 59 | { 60 | return $opName 61 | ? sprintf('Variable "$%s" is not defined by operation "%s".', $varName, $opName) 62 | : sprintf('Variable "$%s" is not defined.', $varName); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docs/type-system/directives.md: -------------------------------------------------------------------------------- 1 | # Built-in directives 2 | The directive is a way for a client to give GraphQL server additional context and hints on how to execute 3 | the query. The directive can be attached to a field or fragment and can affect the execution of the 4 | query in any way the server desires. 5 | 6 | GraphQL specification includes two built-in directives: 7 | 8 | * **@include(if: Boolean)** Only include this field or fragment in the result if the argument is **true** 9 | * **@skip(if: Boolean)** Skip this field or fragment if the argument is **true** 10 | 11 | For example: 12 | ```graphql 13 | query Hero($episode: Episode, $withFriends: Boolean!) { 14 | hero(episode: $episode) { 15 | name 16 | friends @include(if: $withFriends) { 17 | name 18 | } 19 | } 20 | } 21 | ``` 22 | Here if **$withFriends** variable is set to **false** - friends section will be ignored and excluded 23 | from the response. Important implementation detail: those fields will never be executed 24 | (not just removed from response after execution). 25 | 26 | # Custom directives 27 | **graphql-php** supports custom directives even though their presence does not affect the execution of fields. 28 | But you can use [`GraphQL\Type\Definition\ResolveInfo`](../reference.md#graphqltypedefinitionresolveinfo) 29 | in field resolvers to modify the output depending on those directives or perform statistics collection. 30 | 31 | Other use case is your own query validation rules relying on custom directives. 32 | 33 | In **graphql-php** custom directive is an instance of `GraphQL\Type\Definition\Directive` 34 | (or one of its subclasses) which accepts an array of following options: 35 | 36 | ```php 37 | 'track', 45 | 'description' => 'Instruction to record usage of the field by client', 46 | 'locations' => [ 47 | DirectiveLocation::FIELD, 48 | ], 49 | 'args' => [ 50 | new FieldArgument([ 51 | 'name' => 'details', 52 | 'type' => Type::string(), 53 | 'description' => 'String with additional details of field usage scenario', 54 | 'defaultValue' => '' 55 | ]) 56 | ] 57 | ]); 58 | ``` 59 | 60 | See possible directive locations in 61 | [`GraphQL\Language\DirectiveLocation`](../reference.md#graphqllanguagedirectivelocation). 62 | -------------------------------------------------------------------------------- /src/Type/Definition/CustomScalarType.php: -------------------------------------------------------------------------------- 1 | config['serialize'], $value); 28 | } 29 | 30 | /** 31 | * @param mixed $value 32 | * 33 | * @return mixed 34 | */ 35 | public function parseValue($value) 36 | { 37 | if (isset($this->config['parseValue'])) { 38 | return call_user_func($this->config['parseValue'], $value); 39 | } 40 | 41 | return $value; 42 | } 43 | 44 | /** 45 | * @param Node $valueNode 46 | * @param mixed[]|null $variables 47 | * 48 | * @return mixed 49 | * 50 | * @throws Exception 51 | */ 52 | public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ 53 | $valueNode, 54 | ?array $variables = null 55 | ) { 56 | if (isset($this->config['parseLiteral'])) { 57 | return call_user_func($this->config['parseLiteral'], $valueNode, $variables); 58 | } 59 | 60 | return AST::valueFromASTUntyped($valueNode, $variables); 61 | } 62 | 63 | public function assertValid() 64 | { 65 | parent::assertValid(); 66 | 67 | Utils::invariant( 68 | isset($this->config['serialize']) && is_callable($this->config['serialize']), 69 | sprintf('%s must provide "serialize" function. If this custom Scalar ', $this->name) . 70 | 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' . 71 | 'functions are also provided.' 72 | ); 73 | if (! isset($this->config['parseValue']) && ! isset($this->config['parseLiteral'])) { 74 | return; 75 | } 76 | 77 | Utils::invariant( 78 | isset($this->config['parseValue']) && isset($this->config['parseLiteral']) && 79 | is_callable($this->config['parseValue']) && is_callable($this->config['parseLiteral']), 80 | sprintf('%s must provide both "parseValue" and "parseLiteral" functions.', $this->name) 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/01-blog/Blog/Type/UserType.php: -------------------------------------------------------------------------------- 1 | 'User', 17 | 'description' => 'Our blog authors', 18 | 'fields' => function() { 19 | return [ 20 | 'id' => Types::id(), 21 | 'email' => Types::email(), 22 | 'photo' => [ 23 | 'type' => Types::image(), 24 | 'description' => 'User photo URL', 25 | 'args' => [ 26 | 'size' => Types::nonNull(Types::imageSizeEnum()), 27 | ] 28 | ], 29 | 'firstName' => [ 30 | 'type' => Types::string(), 31 | ], 32 | 'lastName' => [ 33 | 'type' => Types::string(), 34 | ], 35 | 'lastStoryPosted' => Types::story(), 36 | 'fieldWithError' => [ 37 | 'type' => Types::string(), 38 | 'resolve' => function() { 39 | throw new \Exception("This is error field"); 40 | } 41 | ] 42 | ]; 43 | }, 44 | 'interfaces' => [ 45 | Types::node() 46 | ], 47 | 'resolveField' => function($value, $args, $context, ResolveInfo $info) { 48 | $method = 'resolve' . ucfirst($info->fieldName); 49 | if (method_exists($this, $method)) { 50 | return $this->{$method}($value, $args, $context, $info); 51 | } else { 52 | return $value->{$info->fieldName}; 53 | } 54 | } 55 | ]; 56 | parent::__construct($config); 57 | } 58 | 59 | public function resolvePhoto(User $user, $args) 60 | { 61 | return DataSource::getUserPhoto($user->id, $args['size']); 62 | } 63 | 64 | public function resolveLastStoryPosted(User $user) 65 | { 66 | return DataSource::findLastStoryFor($user->id); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Validator/Rules/KnownTypeNames.php: -------------------------------------------------------------------------------- 1 | $skip, 35 | NodeKind::INTERFACE_TYPE_DEFINITION => $skip, 36 | NodeKind::UNION_TYPE_DEFINITION => $skip, 37 | NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip, 38 | NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) use ($context) { 39 | $schema = $context->getSchema(); 40 | $typeName = $node->name->value; 41 | $type = $schema->getType($typeName); 42 | if ($type !== null) { 43 | return; 44 | } 45 | 46 | $context->reportError(new Error( 47 | self::unknownTypeMessage( 48 | $typeName, 49 | Utils::suggestionList($typeName, array_keys($schema->getTypeMap())) 50 | ), 51 | [$node] 52 | )); 53 | }, 54 | ]; 55 | } 56 | 57 | /** 58 | * @param string $type 59 | * @param string[] $suggestedTypes 60 | */ 61 | public static function unknownTypeMessage($type, array $suggestedTypes) 62 | { 63 | $message = sprintf('Unknown type "%s".', $type); 64 | if (! empty($suggestedTypes)) { 65 | $suggestions = Utils::quotedOrList($suggestedTypes); 66 | 67 | $message .= sprintf(' Did you mean %s?', $suggestions); 68 | } 69 | 70 | return $message; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Executor/Promise/PromiseAdapter.php: -------------------------------------------------------------------------------- 1 | value; 85 | } 86 | 87 | // Intentionally without message, as all information already in wrapped Exception 88 | throw new Exception(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/01-blog/Blog/Type/CommentType.php: -------------------------------------------------------------------------------- 1 | 'Comment', 17 | 'fields' => function() { 18 | return [ 19 | 'id' => Types::id(), 20 | 'author' => Types::user(), 21 | 'parent' => Types::comment(), 22 | 'isAnonymous' => Types::boolean(), 23 | 'replies' => [ 24 | 'type' => Types::listOf(Types::comment()), 25 | 'args' => [ 26 | 'after' => Types::int(), 27 | 'limit' => [ 28 | 'type' => Types::int(), 29 | 'defaultValue' => 5 30 | ] 31 | ] 32 | ], 33 | 'totalReplyCount' => Types::int(), 34 | 35 | Types::htmlField('body') 36 | ]; 37 | }, 38 | 'resolveField' => function($value, $args, $context, ResolveInfo $info) { 39 | $method = 'resolve' . ucfirst($info->fieldName); 40 | if (method_exists($this, $method)) { 41 | return $this->{$method}($value, $args, $context, $info); 42 | } else { 43 | return $value->{$info->fieldName}; 44 | } 45 | } 46 | ]; 47 | parent::__construct($config); 48 | } 49 | 50 | public function resolveAuthor(Comment $comment) 51 | { 52 | if ($comment->isAnonymous) { 53 | return null; 54 | } 55 | return DataSource::findUser($comment->authorId); 56 | } 57 | 58 | public function resolveParent(Comment $comment) 59 | { 60 | if ($comment->parentId) { 61 | return DataSource::findComment($comment->parentId); 62 | } 63 | return null; 64 | } 65 | 66 | public function resolveReplies(Comment $comment, $args) 67 | { 68 | $args += ['after' => null]; 69 | return DataSource::findReplies($comment->id, $args['limit'], $args['after']); 70 | } 71 | 72 | public function resolveTotalReplyCount(Comment $comment) 73 | { 74 | return DataSource::countReplies($comment->id); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Language/Source.php: -------------------------------------------------------------------------------- 1 | body = $body; 48 | $this->length = mb_strlen($body, 'UTF-8'); 49 | $this->name = $name ?: 'GraphQL request'; 50 | $this->locationOffset = $location ?: new SourceLocation(1, 1); 51 | 52 | Utils::invariant( 53 | $this->locationOffset->line > 0, 54 | 'line in locationOffset is 1-indexed and must be positive' 55 | ); 56 | Utils::invariant( 57 | $this->locationOffset->column > 0, 58 | 'column in locationOffset is 1-indexed and must be positive' 59 | ); 60 | } 61 | 62 | /** 63 | * @param int $position 64 | * 65 | * @return SourceLocation 66 | */ 67 | public function getLocation($position) 68 | { 69 | $line = 1; 70 | $column = $position + 1; 71 | 72 | $utfChars = json_decode('"\u2028\u2029"'); 73 | $lineRegexp = '/\r\n|[\n\r' . $utfChars . ']/su'; 74 | $matches = []; 75 | preg_match_all($lineRegexp, mb_substr($this->body, 0, $position, 'UTF-8'), $matches, PREG_OFFSET_CAPTURE); 76 | 77 | foreach ($matches[0] as $index => $match) { 78 | $line += 1; 79 | 80 | $column = $position + 1 - ($match[1] + mb_strlen($match[0], 'UTF-8')); 81 | } 82 | 83 | return new SourceLocation($line, $column); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Type/Definition/StringType.php: -------------------------------------------------------------------------------- 1 | coerceString($value); 57 | } 58 | 59 | private function coerceString($value) 60 | { 61 | if (is_array($value)) { 62 | throw new Error( 63 | 'String cannot represent an array value: ' . 64 | Utils::printSafe($value) 65 | ); 66 | } 67 | 68 | return (string) $value; 69 | } 70 | 71 | /** 72 | * @param mixed $value 73 | * 74 | * @return string 75 | * 76 | * @throws Error 77 | */ 78 | public function parseValue($value) 79 | { 80 | return $this->coerceString($value); 81 | } 82 | 83 | /** 84 | * @param Node $valueNode 85 | * @param mixed[]|null $variables 86 | * 87 | * @return string|null 88 | * 89 | * @throws Exception 90 | */ 91 | public function parseLiteral($valueNode, ?array $variables = null) 92 | { 93 | if ($valueNode instanceof StringValueNode) { 94 | return $valueNode->value; 95 | } 96 | 97 | // Intentionally without message, as all information already in wrapped Exception 98 | throw new Exception(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Executor/Promise/Adapter/ReactPromiseAdapter.php: -------------------------------------------------------------------------------- 1 | adoptedPromise; 41 | 42 | return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this); 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function create(callable $resolver) 49 | { 50 | $promise = new ReactPromise($resolver); 51 | 52 | return new Promise($promise, $this); 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public function createFulfilled($value = null) 59 | { 60 | $promise = resolve($value); 61 | 62 | return new Promise($promise, $this); 63 | } 64 | 65 | /** 66 | * @inheritdoc 67 | */ 68 | public function createRejected($reason) 69 | { 70 | $promise = reject($reason); 71 | 72 | return new Promise($promise, $this); 73 | } 74 | 75 | /** 76 | * @inheritdoc 77 | */ 78 | public function all(array $promisesOrValues) 79 | { 80 | // TODO: rework with generators when PHP minimum required version is changed to 5.5+ 81 | $promisesOrValues = Utils::map( 82 | $promisesOrValues, 83 | static function ($item) { 84 | return $item instanceof Promise ? $item->adoptedPromise : $item; 85 | } 86 | ); 87 | 88 | $promise = all($promisesOrValues)->then(static function ($values) use ($promisesOrValues) { 89 | $orderedResults = []; 90 | 91 | foreach ($promisesOrValues as $key => $value) { 92 | $orderedResults[$key] = $values[$key]; 93 | } 94 | 95 | return $orderedResults; 96 | }); 97 | 98 | return new Promise($promise, $this); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /docs/type-system/type-language.md: -------------------------------------------------------------------------------- 1 | # Defining your schema 2 | Since 0.9.0 3 | 4 | [Type language](http://graphql.org/learn/schema/#type-language) is a convenient way to define your schema, 5 | especially with IDE autocompletion and syntax validation. 6 | 7 | Here is a simple schema defined in GraphQL type language (e.g. in a separate **schema.graphql** file): 8 | 9 | ```graphql 10 | schema { 11 | query: Query 12 | mutation: Mutation 13 | } 14 | 15 | type Query { 16 | greetings(input: HelloInput!): String! 17 | } 18 | 19 | input HelloInput { 20 | firstName: String! 21 | lastName: String 22 | } 23 | ``` 24 | 25 | In order to create schema instance out of this file, use 26 | [`GraphQL\Utils\BuildSchema`](../reference.md#graphqlutilsbuildschema): 27 | 28 | ```php 29 | $v) { 46 | switch ($k) { 47 | case 'defaultValue': 48 | $this->defaultValue = $v; 49 | $this->defaultValueExists = true; 50 | break; 51 | case 'defaultValueExists': 52 | break; 53 | default: 54 | $this->{$k} = $v; 55 | } 56 | } 57 | $this->config = $opts; 58 | } 59 | 60 | /** 61 | * @return mixed 62 | */ 63 | public function getType() 64 | { 65 | return $this->type; 66 | } 67 | 68 | /** 69 | * @return bool 70 | */ 71 | public function defaultValueExists() 72 | { 73 | return $this->defaultValueExists; 74 | } 75 | 76 | /** 77 | * @throws InvariantViolation 78 | */ 79 | public function assertValid(Type $parentType) 80 | { 81 | try { 82 | Utils::assertValidName($this->name); 83 | } catch (Error $e) { 84 | throw new InvariantViolation(sprintf('%s.%s: %s', $parentType->name, $this->name, $e->getMessage())); 85 | } 86 | $type = $this->type; 87 | if ($type instanceof WrappingType) { 88 | $type = $type->getWrappedType(true); 89 | } 90 | Utils::invariant( 91 | $type instanceof InputType, 92 | sprintf( 93 | '%s.%s field type must be Input Type but got: %s', 94 | $parentType->name, 95 | $this->name, 96 | Utils::printSafe($this->type) 97 | ) 98 | ); 99 | Utils::invariant( 100 | empty($this->config['resolve']), 101 | sprintf( 102 | '%s.%s field type has a resolve property, but Input Types cannot define resolvers.', 103 | $parentType->name, 104 | $this->name 105 | ) 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Language/AST/NodeList.php: -------------------------------------------------------------------------------- 1 | nodes = $nodes; 38 | } 39 | 40 | /** 41 | * @param mixed $offset 42 | * 43 | * @return bool 44 | */ 45 | public function offsetExists($offset) 46 | { 47 | return isset($this->nodes[$offset]); 48 | } 49 | 50 | /** 51 | * @param mixed $offset 52 | * 53 | * @return mixed 54 | */ 55 | public function offsetGet($offset) 56 | { 57 | $item = $this->nodes[$offset]; 58 | 59 | if (is_array($item) && isset($item['kind'])) { 60 | $this->nodes[$offset] = $item = AST::fromArray($item); 61 | } 62 | 63 | return $item; 64 | } 65 | 66 | /** 67 | * @param mixed $offset 68 | * @param mixed $value 69 | */ 70 | public function offsetSet($offset, $value) 71 | { 72 | if (is_array($value) && isset($value['kind'])) { 73 | $value = AST::fromArray($value); 74 | } 75 | $this->nodes[$offset] = $value; 76 | } 77 | 78 | /** 79 | * @param mixed $offset 80 | */ 81 | public function offsetUnset($offset) 82 | { 83 | unset($this->nodes[$offset]); 84 | } 85 | 86 | /** 87 | * @param int $offset 88 | * @param int $length 89 | * @param mixed $replacement 90 | * 91 | * @return NodeList 92 | */ 93 | public function splice($offset, $length, $replacement = null) 94 | { 95 | return new NodeList(array_splice($this->nodes, $offset, $length, $replacement)); 96 | } 97 | 98 | /** 99 | * @param NodeList|Node[] $list 100 | * 101 | * @return NodeList 102 | */ 103 | public function merge($list) 104 | { 105 | if ($list instanceof self) { 106 | $list = $list->nodes; 107 | } 108 | return new NodeList(array_merge($this->nodes, $list)); 109 | } 110 | 111 | /** 112 | * @return Generator 113 | */ 114 | public function getIterator() 115 | { 116 | $count = count($this->nodes); 117 | for ($i = 0; $i < $count; $i++) { 118 | yield $this->offsetGet($i); 119 | } 120 | } 121 | 122 | /** 123 | * @return int 124 | */ 125 | public function count() 126 | { 127 | return count($this->nodes); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Server/OperationParams.php: -------------------------------------------------------------------------------- 1 | originalInput = $params; 71 | 72 | $params += [ 73 | 'query' => null, 74 | 'queryid' => null, 75 | 'documentid' => null, // alias to queryid 76 | 'id' => null, // alias to queryid 77 | 'operationname' => null, 78 | 'variables' => null, 79 | ]; 80 | 81 | if ($params['variables'] === '') { 82 | $params['variables'] = null; 83 | } 84 | 85 | if (is_string($params['variables'])) { 86 | $tmp = json_decode($params['variables'], true); 87 | if (! json_last_error()) { 88 | $params['variables'] = $tmp; 89 | } 90 | } 91 | 92 | $instance->query = $params['query']; 93 | $instance->queryId = $params['queryid'] ?: $params['documentid'] ?: $params['id']; 94 | $instance->operation = $params['operationname']; 95 | $instance->variables = $params['variables']; 96 | $instance->readOnly = (bool) $readonly; 97 | 98 | return $instance; 99 | } 100 | 101 | /** 102 | * @param string $key 103 | * 104 | * @return mixed 105 | * 106 | * @api 107 | */ 108 | public function getOriginalInput($key) 109 | { 110 | return $this->originalInput[$key] ?? null; 111 | } 112 | 113 | /** 114 | * Indicates that operation is executed in read-only context 115 | * (e.g. via HTTP GET request) 116 | * 117 | * @return bool 118 | * 119 | * @api 120 | */ 121 | public function isReadOnly() 122 | { 123 | return $this->readOnly; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | [![GitHub stars](https://img.shields.io/github/stars/webonyx/graphql-php.svg?style=social&label=Star)](https://github.com/webonyx/graphql-php) 2 | [![Build Status](https://travis-ci.org/webonyx/graphql-php.svg?branch=master)](https://travis-ci.org/webonyx/graphql-php) 3 | [![Coverage Status](https://coveralls.io/repos/github/webonyx/graphql-php/badge.svg)](https://coveralls.io/github/webonyx/graphql-php) 4 | [![Latest Stable Version](https://poser.pugx.org/webonyx/graphql-php/version)](https://packagist.org/packages/webonyx/graphql-php) 5 | [![License](https://poser.pugx.org/webonyx/graphql-php/license)](https://packagist.org/packages/webonyx/graphql-php) 6 | 7 | # About GraphQL 8 | 9 | GraphQL is a modern way to build HTTP APIs consumed by the web and mobile clients. 10 | It is intended to be a replacement for REST and SOAP APIs (even for **existing applications**). 11 | 12 | GraphQL itself is a [specification](https://github.com/facebook/graphql) designed by Facebook 13 | engineers. Various implementations of this specification were written 14 | [in different languages and environments](http://graphql.org/code/). 15 | 16 | Great overview of GraphQL features and benefits is presented on [the official website](http://graphql.org/). 17 | All of them equally apply to this PHP implementation. 18 | 19 | 20 | # About graphql-php 21 | 22 | **graphql-php** is a feature-complete implementation of GraphQL specification in PHP (5.5+, 7.0+). 23 | It was originally inspired by [reference JavaScript implementation](https://github.com/graphql/graphql-js) 24 | published by Facebook. 25 | 26 | This library is a thin wrapper around your existing data layer and business logic. 27 | It doesn't dictate how these layers are implemented or which storage engines 28 | are used. Instead, it provides tools for creating rich API for your existing app. 29 | 30 | Library features include: 31 | 32 | - Primitives to express your app as a [Type System](type-system/index.md) 33 | - Validation and introspection of this Type System (for compatibility with tools like [GraphiQL](complementary-tools.md#tools)) 34 | - Parsing, validating and [executing GraphQL queries](executing-queries.md) against this Type System 35 | - Rich [error reporting](error-handling.md), including query validation and execution errors 36 | - Optional tools for [parsing GraphQL Type language](type-system/type-language.md) 37 | - Tools for [batching requests](data-fetching.md#solving-n1-problem) to backend storage 38 | - [Async PHP platforms support](data-fetching.md#async-php) via promises 39 | - [Standard HTTP server](executing-queries.md#using-server) 40 | 41 | Also, several [complementary tools](complementary-tools.md) are available which provide integrations with 42 | existing PHP frameworks, add support for Relay, etc. 43 | 44 | ## Current Status 45 | The first version of this library (v0.1) was released on August 10th 2015. 46 | 47 | The current version (v0.10) supports all features described by GraphQL specification 48 | (including April 2016 add-ons) as well as some experimental features like 49 | [Schema Language parser](type-system/type-language.md) and 50 | [Schema printer](reference.md#graphqlutilsschemaprinter). 51 | 52 | Ready for real-world usage. 53 | 54 | ## GitHub 55 | Project source code is [hosted on GitHub](https://github.com/webonyx/graphql-php). 56 | -------------------------------------------------------------------------------- /src/Language/Token.php: -------------------------------------------------------------------------------- 1 | '; 15 | const EOF = ''; 16 | const BANG = '!'; 17 | const DOLLAR = '$'; 18 | const AMP = '&'; 19 | const PAREN_L = '('; 20 | const PAREN_R = ')'; 21 | const SPREAD = '...'; 22 | const COLON = ':'; 23 | const EQUALS = '='; 24 | const AT = '@'; 25 | const BRACKET_L = '['; 26 | const BRACKET_R = ']'; 27 | const BRACE_L = '{'; 28 | const PIPE = '|'; 29 | const BRACE_R = '}'; 30 | const NAME = 'Name'; 31 | const INT = 'Int'; 32 | const FLOAT = 'Float'; 33 | const STRING = 'String'; 34 | const BLOCK_STRING = 'BlockString'; 35 | const COMMENT = 'Comment'; 36 | 37 | /** 38 | * The kind of Token (see one of constants above). 39 | * 40 | * @var string 41 | */ 42 | public $kind; 43 | 44 | /** 45 | * The character offset at which this Node begins. 46 | * 47 | * @var int 48 | */ 49 | public $start; 50 | 51 | /** 52 | * The character offset at which this Node ends. 53 | * 54 | * @var int 55 | */ 56 | public $end; 57 | 58 | /** 59 | * The 1-indexed line number on which this Token appears. 60 | * 61 | * @var int 62 | */ 63 | public $line; 64 | 65 | /** 66 | * The 1-indexed column number at which this Token begins. 67 | * 68 | * @var int 69 | */ 70 | public $column; 71 | 72 | /** @var string|null */ 73 | public $value; 74 | 75 | /** 76 | * Tokens exist as nodes in a double-linked-list amongst all tokens 77 | * including ignored tokens. is always the first node and 78 | * the last. 79 | * 80 | * @var Token 81 | */ 82 | public $prev; 83 | 84 | /** @var Token */ 85 | public $next; 86 | 87 | /** 88 | * @param string $kind 89 | * @param int $start 90 | * @param int $end 91 | * @param int $line 92 | * @param int $column 93 | * @param mixed|null $value 94 | */ 95 | public function __construct($kind, $start, $end, $line, $column, ?Token $previous = null, $value = null) 96 | { 97 | $this->kind = $kind; 98 | $this->start = $start; 99 | $this->end = $end; 100 | $this->line = $line; 101 | $this->column = $column; 102 | $this->prev = $previous; 103 | $this->next = null; 104 | $this->value = $value; 105 | } 106 | 107 | /** 108 | * @return string 109 | */ 110 | public function getDescription() 111 | { 112 | return $this->kind . ($this->value ? ' "' . $this->value . '"' : ''); 113 | } 114 | 115 | /** 116 | * @return (string|int|null)[] 117 | */ 118 | public function toArray() 119 | { 120 | return [ 121 | 'kind' => $this->kind, 122 | 'value' => $this->value, 123 | 'line' => $this->line, 124 | 'column' => $this->column, 125 | ]; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Type/Definition/IntType.php: -------------------------------------------------------------------------------- 1 | coerceInt($value); 48 | } 49 | 50 | /** 51 | * @param mixed $value 52 | * 53 | * @return int 54 | */ 55 | private function coerceInt($value) 56 | { 57 | if ($value === '') { 58 | throw new Error( 59 | 'Int cannot represent non 32-bit signed integer value: (empty string)' 60 | ); 61 | } 62 | 63 | $num = floatval($value); 64 | if ((! is_numeric($value) && ! is_bool($value)) || $num > self::MAX_INT || $num < self::MIN_INT) { 65 | throw new Error( 66 | 'Int cannot represent non 32-bit signed integer value: ' . 67 | Utils::printSafe($value) 68 | ); 69 | } 70 | $int = intval($num); 71 | // int cast with == used for performance reasons 72 | // phpcs:ignore 73 | if ($int != $num) { 74 | throw new Error( 75 | 'Int cannot represent non-integer value: ' . 76 | Utils::printSafe($value) 77 | ); 78 | } 79 | 80 | return $int; 81 | } 82 | 83 | /** 84 | * @param mixed $value 85 | * 86 | * @return int|null 87 | * 88 | * @throws Error 89 | */ 90 | public function parseValue($value) 91 | { 92 | return $this->coerceInt($value); 93 | } 94 | 95 | /** 96 | * @param Node $valueNode 97 | * @param mixed[]|null $variables 98 | * 99 | * @return int|null 100 | * 101 | * @throws Exception 102 | */ 103 | public function parseLiteral($valueNode, ?array $variables = null) 104 | { 105 | if ($valueNode instanceof IntValueNode) { 106 | $val = (int) $valueNode->value; 107 | if ($valueNode->value === (string) $val && self::MIN_INT <= $val && $val <= self::MAX_INT) { 108 | return $val; 109 | } 110 | } 111 | 112 | // Intentionally without message, as all information already in wrapped Exception 113 | throw new Exception(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Validator/Rules/KnownArgumentNamesOnDirectives.php: -------------------------------------------------------------------------------- 1 | getSchema(); 37 | $definedDirectives = $schema !== null ? $schema->getDirectives() : Directive::getInternalDirectives(); 38 | 39 | foreach ($definedDirectives as $directive) { 40 | $directiveArgs[$directive->name] = array_map( 41 | static function (FieldArgument $arg) : string { 42 | return $arg->name; 43 | }, 44 | $directive->args 45 | ); 46 | } 47 | 48 | $astDefinitions = $context->getDocument()->definitions; 49 | foreach ($astDefinitions as $def) { 50 | if (! ($def instanceof DirectiveDefinitionNode)) { 51 | continue; 52 | } 53 | 54 | $name = $def->name->value; 55 | if ($def->arguments !== null) { 56 | $arguments = $def->arguments; 57 | 58 | if ($arguments instanceof NodeList) { 59 | $arguments = iterator_to_array($arguments->getIterator()); 60 | } 61 | 62 | $directiveArgs[$name] = array_map(static function (InputValueDefinitionNode $arg) : string { 63 | return $arg->name->value; 64 | }, $arguments); 65 | } else { 66 | $directiveArgs[$name] = []; 67 | } 68 | } 69 | 70 | return [ 71 | NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($directiveArgs, $context) { 72 | $directiveName = $directiveNode->name->value; 73 | $knownArgs = $directiveArgs[$directiveName] ?? null; 74 | 75 | if ($directiveNode->arguments === null || ! $knownArgs) { 76 | return; 77 | } 78 | 79 | foreach ($directiveNode->arguments as $argNode) { 80 | $argName = $argNode->name->value; 81 | if (in_array($argName, $knownArgs, true)) { 82 | continue; 83 | } 84 | 85 | $context->reportError(new Error( 86 | self::unknownDirectiveArgMessage($argName, $directiveName), 87 | [$argNode] 88 | )); 89 | } 90 | }, 91 | ]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /examples/01-blog/README.md: -------------------------------------------------------------------------------- 1 | ## Blog Example 2 | Simple yet full-featured example of GraphQL API. Models blogging platform with Stories, Users 3 | and hierarchical comments. 4 | 5 | ### Run locally 6 | ``` 7 | php -S localhost:8080 ./graphql.php 8 | ``` 9 | 10 | ### Test if GraphQL is running 11 | If you open `http://localhost:8080` in browser you should see `json` response with 12 | following message: 13 | ``` 14 | { 15 | data: { 16 | hello: "Your GraphQL endpoint is ready! Install GraphiQL to browse API" 17 | } 18 | } 19 | ``` 20 | 21 | Note that some browsers may try to download JSON file instead of showing you the response. 22 | In this case try to install browser plugin that adds JSON support (like JSONView or similar) 23 | 24 | ### Debugging Mode 25 | By default GraphQL endpoint exposed at `http://localhost:8080` runs in production mode without 26 | additional debugging tools enabled. 27 | 28 | In order to enable debugging mode with additional validation, error handling and reporting - 29 | use `http://localhost:8080?debug=1` as endpoint 30 | 31 | ### Browsing API 32 | The most convenient way to browse GraphQL API is by using [GraphiQL](https://github.com/graphql/graphiql) 33 | But setting it up from scratch may be inconvenient. An easy alternative is to use one of 34 | the existing Google Chrome extensions: 35 | - [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij) 36 | - [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp) 37 | 38 | Set `http://localhost:8080?debug=1` as your GraphQL endpoint/server in one of these extensions 39 | and try clicking "Docs" button (usually in the top-right corner) to browse auto-generated 40 | documentation. 41 | 42 | ### Running GraphQL queries 43 | Copy following query to GraphiQL and execute (by clicking play button on top bar) 44 | 45 | ``` 46 | { 47 | viewer { 48 | id 49 | email 50 | } 51 | user(id: "2") { 52 | id 53 | email 54 | } 55 | stories(after: "1") { 56 | id 57 | body 58 | comments { 59 | ...CommentView 60 | } 61 | } 62 | lastStoryPosted { 63 | id 64 | hasViewerLiked 65 | 66 | author { 67 | id 68 | photo(size: ICON) { 69 | id 70 | url 71 | type 72 | size 73 | width 74 | height 75 | # Uncomment following line to see validation error: 76 | # nonExistingField 77 | 78 | # Uncomment to see error reporting for fields with exceptions thrown in resolvers 79 | # fieldWithError 80 | # nonNullFieldWithError 81 | } 82 | lastStoryPosted { 83 | id 84 | } 85 | } 86 | body(format: HTML, maxLength: 10) 87 | } 88 | } 89 | 90 | fragment CommentView on Comment { 91 | id 92 | body 93 | totalReplyCount 94 | replies { 95 | id 96 | body 97 | } 98 | } 99 | ``` 100 | 101 | ### Run your own query 102 | Use GraphiQL autocomplete (via CTRL+space) to easily create your own query. 103 | 104 | Note: GraphQL query requires at least one field per object type (to prevent accidental overfetching). 105 | For example following query is invalid in GraphQL: 106 | 107 | ``` 108 | { 109 | viewer 110 | } 111 | ``` 112 | 113 | Try copying this query and see what happens 114 | 115 | ### Run mutation query 116 | TODOC 117 | 118 | ### Dig into source code 119 | Now when you tried GraphQL API as a consumer, see how it is implemented by browsing 120 | source code. 121 | -------------------------------------------------------------------------------- /docs/security.md: -------------------------------------------------------------------------------- 1 | # Query Complexity Analysis 2 | 3 | This is a PHP port of [Query Complexity Analysis](http://sangria-graphql.org/learn/#query-complexity-analysis) in Sangria implementation. 4 | 5 | Complexity analysis is a separate validation rule which calculates query complexity score before execution. 6 | Every field in the query gets a default score 1 (including ObjectType nodes). Total complexity of the 7 | query is the sum of all field scores. For example, the complexity of introspection query is **109**. 8 | 9 | If this score exceeds a threshold, a query is not executed and an error is returned instead. 10 | 11 | Complexity analysis is disabled by default. To enabled it, add validation rule: 12 | 13 | ```php 14 | 'MyType', 34 | 'fields' => [ 35 | 'someList' => [ 36 | 'type' => Type::listOf(Type::string()), 37 | 'args' => [ 38 | 'limit' => [ 39 | 'type' => Type::int(), 40 | 'defaultValue' => 10 41 | ] 42 | ], 43 | 'complexity' => function($childrenComplexity, $args) { 44 | return $childrenComplexity * $args['limit']; 45 | } 46 | ] 47 | ] 48 | ]); 49 | ``` 50 | 51 | # Limiting Query Depth 52 | 53 | This is a PHP port of [Limiting Query Depth](http://sangria-graphql.org/learn/#limiting-query-depth) in Sangria implementation. 54 | For example, max depth of the introspection query is **7**. 55 | 56 | It is disabled by default. To enable it, add following validation rule: 57 | 58 | ```php 59 | 'Query', 17 | 'fields' => [ 18 | 'user' => [ 19 | 'type' => Types::user(), 20 | 'description' => 'Returns user by id (in range of 1-5)', 21 | 'args' => [ 22 | 'id' => Types::nonNull(Types::id()) 23 | ] 24 | ], 25 | 'viewer' => [ 26 | 'type' => Types::user(), 27 | 'description' => 'Represents currently logged-in user (for the sake of example - simply returns user with id == 1)' 28 | ], 29 | 'stories' => [ 30 | 'type' => Types::listOf(Types::story()), 31 | 'description' => 'Returns subset of stories posted for this blog', 32 | 'args' => [ 33 | 'after' => [ 34 | 'type' => Types::id(), 35 | 'description' => 'Fetch stories listed after the story with this ID' 36 | ], 37 | 'limit' => [ 38 | 'type' => Types::int(), 39 | 'description' => 'Number of stories to be returned', 40 | 'defaultValue' => 10 41 | ] 42 | ] 43 | ], 44 | 'lastStoryPosted' => [ 45 | 'type' => Types::story(), 46 | 'description' => 'Returns last story posted for this blog' 47 | ], 48 | 'deprecatedField' => [ 49 | 'type' => Types::string(), 50 | 'deprecationReason' => 'This field is deprecated!' 51 | ], 52 | 'fieldWithException' => [ 53 | 'type' => Types::string(), 54 | 'resolve' => function() { 55 | throw new \Exception("Exception message thrown in field resolver"); 56 | } 57 | ], 58 | 'hello' => Type::string() 59 | ], 60 | 'resolveField' => function($val, $args, $context, ResolveInfo $info) { 61 | return $this->{$info->fieldName}($val, $args, $context, $info); 62 | } 63 | ]; 64 | parent::__construct($config); 65 | } 66 | 67 | public function user($rootValue, $args) 68 | { 69 | return DataSource::findUser($args['id']); 70 | } 71 | 72 | public function viewer($rootValue, $args, AppContext $context) 73 | { 74 | return $context->viewer; 75 | } 76 | 77 | public function stories($rootValue, $args) 78 | { 79 | $args += ['after' => null]; 80 | return DataSource::findStories($args['limit'], $args['after']); 81 | } 82 | 83 | public function lastStoryPosted() 84 | { 85 | return DataSource::findLatestStory(); 86 | } 87 | 88 | public function hello() 89 | { 90 | return 'Your graphql-php endpoint is ready! Use GraphiQL to browse API'; 91 | } 92 | 93 | public function deprecatedField() 94 | { 95 | return 'You can request deprecated field, but it is not displayed in auto-generated documentation by default.'; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Error/Warning.php: -------------------------------------------------------------------------------- 1 | 0 && ! isset(self::$warned[$warningId])) { 101 | self::$warned[$warningId] = true; 102 | trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING); 103 | } 104 | } 105 | 106 | public static function warn($errorMessage, $warningId, $messageLevel = null) 107 | { 108 | if (self::$warningHandler) { 109 | $fn = self::$warningHandler; 110 | $fn($errorMessage, $warningId); 111 | } elseif ((self::$enableWarnings & $warningId) > 0) { 112 | trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /docs/type-system/index.md: -------------------------------------------------------------------------------- 1 | # Type System 2 | To start using GraphQL you are expected to implement a type hierarchy and expose it as [Schema](schema.md). 3 | 4 | In graphql-php **type** is an instance of internal class from 5 | `GraphQL\Type\Definition` namespace: [`ObjectType`](object-types.md), 6 | [`InterfaceType`](interfaces.md), [`UnionType`](unions.md), [`InputObjectType`](input-types.md), 7 | [`ScalarType`](scalar-types.md), [`EnumType`](enum-types.md) (or one of subclasses). 8 | 9 | But most of the types in your schema will be [object types](object-types.md). 10 | 11 | # Type Definition Styles 12 | Several styles of type definitions are supported depending on your preferences. 13 | 14 | Inline definitions: 15 | ```php 16 | 'MyType', 24 | 'fields' => [ 25 | 'id' => Type::id() 26 | ] 27 | ]); 28 | ``` 29 | 30 | Class per type: 31 | ```php 32 | [ 46 | 'id' => Type::id() 47 | ] 48 | ]; 49 | parent::__construct($config); 50 | } 51 | } 52 | ``` 53 | 54 | Using [GraphQL Type language](http://graphql.org/learn/schema/#type-language): 55 | 56 | ```graphql 57 | schema { 58 | query: Query 59 | mutation: Mutation 60 | } 61 | 62 | type Query { 63 | greetings(input: HelloInput!): String! 64 | } 65 | 66 | input HelloInput { 67 | firstName: String! 68 | lastName: String 69 | } 70 | ``` 71 | 72 | Read more about type language definitions in a [dedicated docs section](type-language.md). 73 | 74 | # Type Registry 75 | Every type must be presented in Schema by a single instance (**graphql-php** 76 | throws when it discovers several instances with the same **name** in the schema). 77 | 78 | Therefore if you define your type as separate PHP class you must ensure that only one 79 | instance of that class is added to the schema. 80 | 81 | The typical way to do this is to create a registry of your types: 82 | 83 | ```php 84 | myAType ?: ($this->myAType = new MyAType($this)); 95 | } 96 | 97 | public function myBType() 98 | { 99 | return $this->myBType ?: ($this->myBType = new MyBType($this)); 100 | } 101 | } 102 | ``` 103 | And use this registry in type definition: 104 | 105 | ```php 106 | [ 116 | 'b' => $types->myBType() 117 | ] 118 | ]); 119 | } 120 | } 121 | ``` 122 | Obviously, you can automate this registry as you wish to reduce boilerplate or even 123 | introduce Dependency Injection Container if your types have other dependencies. 124 | 125 | Alternatively, all methods of the registry could be static - then there is no need 126 | to pass it in constructor - instead just use use **TypeRegistry::myAType()** in your 127 | type definitions. 128 | --------------------------------------------------------------------------------