diff --git a/docs/best-practices.md b/docs/best-practices.md index 4e28593..a5f135e 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -12,7 +12,7 @@ but make sure to restrict it to debug/development mode only. **graphql-php** expects that each type in Schema is presented by single instance. Therefore if you define your types as separate PHP classes you need to ensure that each type is referenced only once. -Technically you can create several instances of your type (for example for tests), but `GraphQL\Schema` +Technically you can create several instances of your type (for example for tests), but `GraphQL\Type\Schema` will throw on attempt to add different instances with the same name. There are several ways to achieve this depending on your preferences. We provide reference diff --git a/docs/executing-queries.md b/docs/executing-queries.md index a99bade..c676c36 100644 --- a/docs/executing-queries.md +++ b/docs/executing-queries.md @@ -27,7 +27,7 @@ Description of method arguments: Argument | Type | Notes ------------ | -------- | ----- -schema | `GraphQL\Schema` | **Required.** Instance of your application [Schema](type-system/schema/) +schema | `GraphQL\Type\Schema` | **Required.** Instance of your application [Schema](type-system/schema/) queryString | `string` or `GraphQL\Language\AST\DocumentNode` | **Required.** Actual GraphQL query string to be parsed, validated and executed. If you parse query elsewhere before executing - pass corresponding ast document here to avoid new parsing. rootValue | `mixed` | Any value that represents a root of your data graph. It is passed as 1st argument to field resolvers of [Query type](type-system/schema/#query-and-mutation-types). Can be omitted or set to null if actual root values are fetched by Query type itself. contextValue | `mixed` | Any value that holds information shared between all field resolvers. Most often they use it to pass currently logged in user, locale details, etc.

It will be available as 3rd argument in all field resolvers. (see section on [Field Definitions](type-system/object-types/#field-configuration-options) for reference) **graphql-php** never modifies this value and passes it *as is* to all underlying resolvers. diff --git a/docs/getting-started.md b/docs/getting-started.md index 44f1940..af08080 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -75,7 +75,7 @@ Now when our type is ready, let's create GraphQL endpoint for it `graphql.php`: ```php $queryType diff --git a/docs/type-system/schema.md b/docs/type-system/schema.md index c795394..2991d53 100644 --- a/docs/type-system/schema.md +++ b/docs/type-system/schema.md @@ -2,7 +2,7 @@ Schema is a container of your type hierarchy, which accepts root types in constructor and provides methods for receiving information about your types to internal GrahpQL tools. -In **graphql-php** schema is an instance of `GraphQL\Schema` which accepts configuration array +In **graphql-php** schema is an instance of `GraphQL\Type\Schema` which accepts configuration array in constructor: ```php @@ -25,7 +25,7 @@ of your API: ```php use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; -use GraphQL\Schema; +use GraphQL\Type\Schema; $queryType = new ObjectType([ 'name' => 'Query', diff --git a/src/Executor/ExecutionContext.php b/src/Executor/ExecutionContext.php index 5a61e9e..225b811 100644 --- a/src/Executor/ExecutionContext.php +++ b/src/Executor/ExecutionContext.php @@ -4,7 +4,7 @@ namespace GraphQL\Executor; use GraphQL\Error\Error; use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode; -use GraphQL\Schema; +use GraphQL\Type\Schema; /** * Data that must be available at all points during query execution. diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 0283377..0a62d78 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -15,7 +15,7 @@ use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Executor\Promise\PromiseAdapter; -use GraphQL\Schema; +use GraphQL\Type\Schema; use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldDefinition; diff --git a/src/Executor/Values.php b/src/Executor/Values.php index f57d217..b485e7e 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -15,7 +15,7 @@ use GraphQL\Language\AST\NodeList; use GraphQL\Language\AST\VariableNode; use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\Printer; -use GraphQL\Schema; +use GraphQL\Type\Schema; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputObjectType; diff --git a/src/GraphQL.php b/src/GraphQL.php index 8004e02..bf56d65 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -48,7 +48,7 @@ class GraphQL * Empty array would allow to skip query validation (may be convenient for persisted * queries which are validated before persisting and assumed valid during execution) * - * @param Schema $schema + * @param \GraphQL\Type\Schema $schema * @param string|DocumentNode $source * @param mixed $rootValue * @param array $contextValue @@ -61,7 +61,7 @@ class GraphQL * @return Promise|array */ public static function execute( - Schema $schema, + \GraphQL\Type\Schema $schema, $source, $rootValue = null, $contextValue = null, @@ -99,7 +99,7 @@ class GraphQL * Same as `execute`, but returns instance of ExecutionResult instead of array, * which can be used for custom error formatting or adding extensions to response * - * @param Schema $schema + * @param \GraphQL\Type\Schema $schema * @param string|DocumentNode $source * @param mixed $rootValue * @param mixed $contextValue @@ -112,7 +112,7 @@ class GraphQL * @return ExecutionResult|Promise */ public static function executeAndReturnResult( - Schema $schema, + \GraphQL\Type\Schema $schema, $source, $rootValue = null, $contextValue = null, diff --git a/src/Schema.php b/src/Schema.php index 9ef4af8..8e7335b 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -1,16 +1,6 @@ 1 || $config instanceof Type) { - trigger_error( - 'GraphQL\Schema constructor expects config object now instead of types passed as arguments. '. - 'See https://github.com/webonyx/graphql-php/issues/36', - E_USER_DEPRECATED - ); - list($queryType, $mutationType, $subscriptionType) = func_get_args() + [null, null, null]; - - $config = [ - 'query' => $queryType, - 'mutation' => $mutationType, - 'subscription' => $subscriptionType - ]; - } - if (is_array($config)) { - $config = Config::create($config); - } - - Utils::invariant( - $config instanceof Config, - 'Schema constructor expects instance of GraphQL\Schema\Config or an array with keys: %s; but got: %s', - implode(', ', [ - 'query', - 'mutation', - 'subscription', - 'types', - 'directives', - 'typeLoader', - 'descriptor' - ]), - Utils::getVariableType($config) - ); - - Utils::invariant( - $config->query instanceof ObjectType, - "Schema query must be Object Type but got: " . Utils::getVariableType($config->query) - ); - - $this->config = $config; - } - - /** - * Returns schema query type - * - * @return ObjectType - */ - public function getQueryType() - { - return $this->config->query; - } - - /** - * Returns schema mutation type - * - * @return ObjectType|null - */ - public function getMutationType() - { - return $this->config->mutation; - } - - /** - * Returns schema subscription - * - * @return ObjectType|null - */ - public function getSubscriptionType() - { - return $this->config->subscription; - } - - /** - * @return Config - */ - public function getConfig() - { - return $this->config; - } - - /** - * Returns array of all types in this schema. Keys of this array represent type names, values are instances - * of corresponding type definitions - * - * @return Type[] - */ - public function getTypeMap() - { - if (!$this->fullyLoaded) { - if ($this->config->descriptor && $this->config->typeLoader) { - // Following is still faster than $this->collectAllTypes() because it won't init fields - $typesToResolve = array_diff_key($this->config->descriptor->typeMap, $this->resolvedTypes); - foreach ($typesToResolve as $typeName => $_) { - $this->resolvedTypes[$typeName] = $this->loadType($typeName); - } - } else { - $this->resolvedTypes = $this->collectAllTypes(); - } - $this->fullyLoaded = true; - } - return $this->resolvedTypes; - } - - /** - * Returns type by it's name - * - * @param string $name - * @return Type - */ - public function getType($name) - { - return $this->resolveType($name); - } - - /** - * Returns serializable schema descriptor which can be passed later - * to Schema config to enable a set of performance optimizations - * - * @return Descriptor - */ - public function describe() - { - if ($this->descriptor) { - return $this->descriptor; - } - - $this->resolvedTypes = $this->collectAllTypes(); - $this->fullyLoaded = true; - - $descriptor = new Descriptor(); - $descriptor->version = '1.0'; - $descriptor->created = time(); - - foreach ($this->resolvedTypes as $type) { - if ($type instanceof ObjectType) { - foreach ($type->getInterfaces() as $interface) { - $descriptor->possibleTypeMap[$interface->name][$type->name] = 1; - } - } else if ($type instanceof UnionType) { - foreach ($type->getTypes() as $innerType) { - $descriptor->possibleTypeMap[$type->name][$innerType->name] = 1; - } - } - $descriptor->typeMap[$type->name] = 1; - } - - return $this->descriptor = $descriptor; - } - - private function collectAllTypes() - { - $initialTypes = array_merge( - [ - $this->config->query, - $this->config->mutation, - $this->config->subscription, - Introspection::_schema() - ], - array_values($this->resolvedTypes) - ); - - $typeMap = []; - foreach ($initialTypes as $type) { - $typeMap = TypeInfo::extractTypes($type, $typeMap); - } - - $types = $this->config->types; - if (is_callable($types)) { - $types = $types(); - - Utils::invariant( - is_array($types) || $types instanceof \Traversable, - 'Schema types callable must return array or instance of Traversable but got: %s', - Utils::getVariableType($types) - ); - } - - if (!empty($types)) { - foreach ($types as $type) { - Utils::invariant( - $type instanceof Type, - 'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but got: %s', - Utils::getVariableType($types) - ); - $typeMap = TypeInfo::extractTypes($type, $typeMap); - } - } - - return $typeMap + Type::getInternalTypes(); - } - - /** - * Returns all possible concrete types for given abstract type - * (implementations for interfaces and members of union type for unions) - * - * @param AbstractType $abstractType - * @return ObjectType[] - */ - public function getPossibleTypes(AbstractType $abstractType) - { - if ($abstractType instanceof UnionType) { - return $abstractType->getTypes(); - } - - /** @var InterfaceType $abstractType */ - $descriptor = $this->config->descriptor ?: $this->describe(); - - $result = []; - if (isset($descriptor->possibleTypeMap[$abstractType->name])) { - foreach ($descriptor->possibleTypeMap[$abstractType->name] as $typeName => $_) { - $result[] = $this->resolveType($typeName); - } - } - return $result; - } - - /** - * Accepts name of type or type instance and returns type instance. If type with given name is not loaded yet - - * will load it first. - * - * @param $typeOrName - * @return Type - */ - public function resolveType($typeOrName) - { - if ($typeOrName instanceof Type) { - if ($typeOrName->name && !isset($this->resolvedTypes[$typeOrName->name])) { - $this->resolvedTypes[$typeOrName->name] = $typeOrName; - } - return $typeOrName; - } - if (!isset($this->resolvedTypes[$typeOrName])) { - $this->resolvedTypes[$typeOrName] = $this->loadType($typeOrName); - } - return $this->resolvedTypes[$typeOrName]; - } - - private function loadType($typeName) - { - $typeLoader = $this->config->typeLoader; - - if (!$typeLoader) { - return $this->defaultTypeLoader($typeName); - } - - $type = $typeLoader($typeName); - // TODO: validate returned value - return $type; - } - - /** - * Returns true if object type is concrete type of given abstract type - * (implementation for interfaces and members of union type for unions) - * - * @param AbstractType $abstractType - * @param ObjectType $possibleType - * @return bool - */ - public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) - { - if ($this->config->descriptor) { - return !empty($this->config->descriptor->possibleTypeMap[$abstractType->name][$possibleType->name]); - } - - if ($abstractType instanceof InterfaceType) { - return $possibleType->implementsInterface($abstractType); - } - - /** @var UnionType $abstractType */ - return $abstractType->isPossibleType($possibleType); - } - - /** - * Returns a list of directives supported by this schema - * - * @return Directive[] - */ - public function getDirectives() - { - return $this->config->directives ?: GraphQL::getInternalDirectives(); - } - - /** - * Returns instance of directive by name - * - * @param $name - * @return Directive - */ - public function getDirective($name) - { - foreach ($this->getDirectives() as $directive) { - if ($directive->name === $name) { - return $directive; - } - } - return null; - } - - /** - * @param $typeName - * @return Type - */ - private function defaultTypeLoader($typeName) - { - // Default type loader simply fallbacks to collecting all types - if (!$this->fullyLoaded) { - $this->resolvedTypes = $this->collectAllTypes(); - $this->fullyLoaded = true; - } - if (!isset($this->resolvedTypes[$typeName])) { - return null; - } - return $this->resolvedTypes[$typeName]; - } } diff --git a/src/Server/ServerConfig.php b/src/Server/ServerConfig.php index 6661061..4f65d52 100644 --- a/src/Server/ServerConfig.php +++ b/src/Server/ServerConfig.php @@ -1,10 +1,9 @@ $MyAppQueryRootType, + * 'mutation' => $MyAppMutationRootType, + * ]); + * + * Note: If an array of `directives` are provided to GraphQL\Schema, that will be + * the exact list of directives represented and allowed. If `directives` is not + * provided then a default set of the specified directives (e.g. @include and + * @skip) will be used. If you wish to provide *additional* directives to these + * specified directives, you must explicitly declare them. Example: + * + * $mySchema = new GraphQL\Schema([ + * ... + * 'directives' => array_merge(GraphQL::getInternalDirectives(), [ $myCustomDirective ]), + * ]) + * + * @package GraphQL + */ +class Schema +{ + /** + * @var SchemaConfig + */ + private $config; + + /** + * Contains actual descriptor for this schema + * + * @var Descriptor + */ + private $descriptor; + + /** + * Contains currently resolved schema types + * + * @var Type[] + */ + private $resolvedTypes = []; + + /** + * True when $resolvedTypes contain all possible schema types + * + * @var bool + */ + private $fullyLoaded = false; + + /** + * Schema constructor. + * + * @param array|SchemaConfig $config + */ + public function __construct($config = null) + { + if (func_num_args() > 1 || $config instanceof Type) { + trigger_error( + 'GraphQL\Schema constructor expects config object now instead of types passed as arguments. '. + 'See https://github.com/webonyx/graphql-php/issues/36', + E_USER_DEPRECATED + ); + list($queryType, $mutationType, $subscriptionType) = func_get_args() + [null, null, null]; + + $config = [ + 'query' => $queryType, + 'mutation' => $mutationType, + 'subscription' => $subscriptionType + ]; + } + if (is_array($config)) { + $config = SchemaConfig::create($config); + } + + Utils::invariant( + $config instanceof SchemaConfig, + 'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s', + implode(', ', [ + 'query', + 'mutation', + 'subscription', + 'types', + 'directives', + 'typeLoader', + 'descriptor' + ]), + Utils::getVariableType($config) + ); + + Utils::invariant( + $config->query instanceof ObjectType, + "Schema query must be Object Type but got: " . Utils::getVariableType($config->query) + ); + + $this->config = $config; + } + + /** + * Returns schema query type + * + * @return ObjectType + */ + public function getQueryType() + { + return $this->config->query; + } + + /** + * Returns schema mutation type + * + * @return ObjectType|null + */ + public function getMutationType() + { + return $this->config->mutation; + } + + /** + * Returns schema subscription + * + * @return ObjectType|null + */ + public function getSubscriptionType() + { + return $this->config->subscription; + } + + /** + * @return SchemaConfig + */ + public function getConfig() + { + return $this->config; + } + + /** + * Returns array of all types in this schema. Keys of this array represent type names, values are instances + * of corresponding type definitions + * + * @return Type[] + */ + public function getTypeMap() + { + if (!$this->fullyLoaded) { + if ($this->config->descriptor && $this->config->typeLoader) { + // Following is still faster than $this->collectAllTypes() because it won't init fields + $typesToResolve = array_diff_key($this->config->descriptor->typeMap, $this->resolvedTypes); + foreach ($typesToResolve as $typeName => $_) { + $this->resolvedTypes[$typeName] = $this->loadType($typeName); + } + } else { + $this->resolvedTypes = $this->collectAllTypes(); + } + $this->fullyLoaded = true; + } + return $this->resolvedTypes; + } + + /** + * Returns type by it's name + * + * @param string $name + * @return Type + */ + public function getType($name) + { + return $this->resolveType($name); + } + + /** + * Returns serializable schema descriptor which can be passed later + * to Schema config to enable a set of performance optimizations + * + * @return Descriptor + */ + public function describe() + { + if ($this->descriptor) { + return $this->descriptor; + } + + $this->resolvedTypes = $this->collectAllTypes(); + $this->fullyLoaded = true; + + $descriptor = new Descriptor(); + $descriptor->version = '1.0'; + $descriptor->created = time(); + + foreach ($this->resolvedTypes as $type) { + if ($type instanceof ObjectType) { + foreach ($type->getInterfaces() as $interface) { + $descriptor->possibleTypeMap[$interface->name][$type->name] = 1; + } + } else if ($type instanceof UnionType) { + foreach ($type->getTypes() as $innerType) { + $descriptor->possibleTypeMap[$type->name][$innerType->name] = 1; + } + } + $descriptor->typeMap[$type->name] = 1; + } + + return $this->descriptor = $descriptor; + } + + private function collectAllTypes() + { + $initialTypes = array_merge( + [ + $this->config->query, + $this->config->mutation, + $this->config->subscription, + Introspection::_schema() + ], + array_values($this->resolvedTypes) + ); + + $typeMap = []; + foreach ($initialTypes as $type) { + $typeMap = TypeInfo::extractTypes($type, $typeMap); + } + + $types = $this->config->types; + if (is_callable($types)) { + $types = $types(); + + Utils::invariant( + is_array($types) || $types instanceof \Traversable, + 'Schema types callable must return array or instance of Traversable but got: %s', + Utils::getVariableType($types) + ); + } + + if (!empty($types)) { + foreach ($types as $type) { + Utils::invariant( + $type instanceof Type, + 'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but got: %s', + Utils::getVariableType($types) + ); + $typeMap = TypeInfo::extractTypes($type, $typeMap); + } + } + + return $typeMap + Type::getInternalTypes(); + } + + /** + * Returns all possible concrete types for given abstract type + * (implementations for interfaces and members of union type for unions) + * + * @param AbstractType $abstractType + * @return ObjectType[] + */ + public function getPossibleTypes(AbstractType $abstractType) + { + if ($abstractType instanceof UnionType) { + return $abstractType->getTypes(); + } + + /** @var InterfaceType $abstractType */ + $descriptor = $this->config->descriptor ?: $this->describe(); + + $result = []; + if (isset($descriptor->possibleTypeMap[$abstractType->name])) { + foreach ($descriptor->possibleTypeMap[$abstractType->name] as $typeName => $_) { + $result[] = $this->resolveType($typeName); + } + } + return $result; + } + + /** + * Accepts name of type or type instance and returns type instance. If type with given name is not loaded yet - + * will load it first. + * + * @param $typeOrName + * @return Type + */ + public function resolveType($typeOrName) + { + if ($typeOrName instanceof Type) { + if ($typeOrName->name && !isset($this->resolvedTypes[$typeOrName->name])) { + $this->resolvedTypes[$typeOrName->name] = $typeOrName; + } + return $typeOrName; + } + if (!isset($this->resolvedTypes[$typeOrName])) { + $this->resolvedTypes[$typeOrName] = $this->loadType($typeOrName); + } + return $this->resolvedTypes[$typeOrName]; + } + + private function loadType($typeName) + { + $typeLoader = $this->config->typeLoader; + + if (!$typeLoader) { + return $this->defaultTypeLoader($typeName); + } + + $type = $typeLoader($typeName); + // TODO: validate returned value + return $type; + } + + /** + * Returns true if object type is concrete type of given abstract type + * (implementation for interfaces and members of union type for unions) + * + * @param AbstractType $abstractType + * @param ObjectType $possibleType + * @return bool + */ + public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) + { + if ($this->config->descriptor) { + return !empty($this->config->descriptor->possibleTypeMap[$abstractType->name][$possibleType->name]); + } + + if ($abstractType instanceof InterfaceType) { + return $possibleType->implementsInterface($abstractType); + } + + /** @var UnionType $abstractType */ + return $abstractType->isPossibleType($possibleType); + } + + /** + * Returns a list of directives supported by this schema + * + * @return Directive[] + */ + public function getDirectives() + { + return $this->config->directives ?: GraphQL::getInternalDirectives(); + } + + /** + * Returns instance of directive by name + * + * @param $name + * @return Directive + */ + public function getDirective($name) + { + foreach ($this->getDirectives() as $directive) { + if ($directive->name === $name) { + return $directive; + } + } + return null; + } + + /** + * @param $typeName + * @return Type + */ + private function defaultTypeLoader($typeName) + { + // Default type loader simply fallbacks to collecting all types + if (!$this->fullyLoaded) { + $this->resolvedTypes = $this->collectAllTypes(); + $this->fullyLoaded = true; + } + if (!isset($this->resolvedTypes[$typeName])) { + return null; + } + return $this->resolvedTypes[$typeName]; + } +} diff --git a/src/Config.php b/src/Type/SchemaConfig.php similarity index 95% rename from src/Config.php rename to src/Type/SchemaConfig.php index 58fe56f..a6b3688 100644 --- a/src/Config.php +++ b/src/Type/SchemaConfig.php @@ -1,5 +1,5 @@