2017-02-19 22:26:56 +03:00
|
|
|
<?php
|
|
|
|
namespace GraphQL\Utils;
|
|
|
|
|
|
|
|
use GraphQL\Error\Error;
|
|
|
|
use GraphQL\Language\AST\DocumentNode;
|
|
|
|
use GraphQL\Language\AST\NodeKind;
|
2018-02-09 15:49:10 +03:00
|
|
|
use GraphQL\Language\AST\SchemaDefinitionNode;
|
2017-02-19 22:26:56 +03:00
|
|
|
use GraphQL\Language\Parser;
|
2017-03-04 23:10:52 +03:00
|
|
|
use GraphQL\Language\Source;
|
2017-08-12 17:32:07 +03:00
|
|
|
use GraphQL\Type\Schema;
|
2017-02-19 22:26:56 +03:00
|
|
|
use GraphQL\Type\Definition\Directive;
|
|
|
|
|
|
|
|
/**
|
2017-08-20 18:10:13 +03:00
|
|
|
* Build instance of `GraphQL\Type\Schema` out of type language definition (string or parsed AST)
|
|
|
|
* See [section in docs](type-system/type-language.md) for details.
|
2017-02-19 22:26:56 +03:00
|
|
|
*/
|
|
|
|
class BuildSchema
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* This takes the ast of a schema document produced by the parse function in
|
|
|
|
* GraphQL\Language\Parser.
|
|
|
|
*
|
|
|
|
* If no schema definition is provided, then it will look for types named Query
|
|
|
|
* and Mutation.
|
|
|
|
*
|
2017-08-20 18:10:13 +03:00
|
|
|
* Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
|
2017-02-19 22:26:56 +03:00
|
|
|
* has no resolve methods, so execution will use default resolvers.
|
|
|
|
*
|
2018-02-09 18:14:04 +03:00
|
|
|
* Accepts options as a second argument:
|
2018-02-08 21:33:54 +03:00
|
|
|
*
|
|
|
|
* - commentDescriptions:
|
|
|
|
* Provide true to use preceding comments as the description.
|
|
|
|
*
|
|
|
|
*
|
2017-08-20 18:10:13 +03:00
|
|
|
* @api
|
2017-02-19 22:26:56 +03:00
|
|
|
* @param DocumentNode $ast
|
2018-02-09 18:14:04 +03:00
|
|
|
* @param array $options
|
2017-02-19 22:26:56 +03:00
|
|
|
* @return Schema
|
|
|
|
* @throws Error
|
|
|
|
*/
|
2018-02-09 18:14:04 +03:00
|
|
|
public static function buildAST(DocumentNode $ast, array $options = [])
|
2017-02-19 22:26:56 +03:00
|
|
|
{
|
2018-02-09 18:14:04 +03:00
|
|
|
$builder = new self($ast, $options);
|
2017-02-19 22:26:56 +03:00
|
|
|
return $builder->buildSchema();
|
|
|
|
}
|
|
|
|
|
|
|
|
private $ast;
|
|
|
|
private $nodeMap;
|
2017-07-28 13:55:25 +03:00
|
|
|
private $loadedTypeDefs;
|
2018-02-08 21:33:54 +03:00
|
|
|
private $options;
|
2017-02-19 22:26:56 +03:00
|
|
|
|
2018-02-09 18:14:04 +03:00
|
|
|
public function __construct(DocumentNode $ast, array $options = [])
|
2017-02-19 22:26:56 +03:00
|
|
|
{
|
|
|
|
$this->ast = $ast;
|
2017-07-28 13:55:25 +03:00
|
|
|
$this->loadedTypeDefs = [];
|
2018-02-08 21:33:54 +03:00
|
|
|
$this->options = $options;
|
2017-02-19 22:26:56 +03:00
|
|
|
}
|
2018-02-08 21:33:54 +03:00
|
|
|
|
2017-02-19 22:26:56 +03:00
|
|
|
public function buildSchema()
|
|
|
|
{
|
2018-02-09 15:49:10 +03:00
|
|
|
/** @var SchemaDefinitionNode $schemaDef */
|
2017-02-19 22:26:56 +03:00
|
|
|
$schemaDef = null;
|
|
|
|
$typeDefs = [];
|
|
|
|
$this->nodeMap = [];
|
|
|
|
$directiveDefs = [];
|
|
|
|
foreach ($this->ast->definitions as $d) {
|
|
|
|
switch ($d->kind) {
|
|
|
|
case NodeKind::SCHEMA_DEFINITION:
|
|
|
|
if ($schemaDef) {
|
|
|
|
throw new Error('Must provide only one schema definition.');
|
|
|
|
}
|
|
|
|
$schemaDef = $d;
|
|
|
|
break;
|
|
|
|
case NodeKind::SCALAR_TYPE_DEFINITION:
|
|
|
|
case NodeKind::OBJECT_TYPE_DEFINITION:
|
|
|
|
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
|
|
|
case NodeKind::ENUM_TYPE_DEFINITION:
|
|
|
|
case NodeKind::UNION_TYPE_DEFINITION:
|
|
|
|
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
2017-07-04 09:59:46 +03:00
|
|
|
$typeName = $d->name->value;
|
|
|
|
if (!empty($this->nodeMap[$typeName])) {
|
|
|
|
throw new Error("Type \"$typeName\" was defined more than once.");
|
|
|
|
}
|
2017-02-19 22:26:56 +03:00
|
|
|
$typeDefs[] = $d;
|
2017-07-04 09:59:46 +03:00
|
|
|
$this->nodeMap[$typeName] = $d;
|
2017-02-19 22:26:56 +03:00
|
|
|
break;
|
|
|
|
case NodeKind::DIRECTIVE_DEFINITION:
|
|
|
|
$directiveDefs[] = $d;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-09 15:49:10 +03:00
|
|
|
$operationTypes = $schemaDef
|
|
|
|
? $this->getOperationTypes($schemaDef)
|
|
|
|
: [
|
|
|
|
'query' => isset($this->nodeMap['Query']) ? 'Query' : null,
|
|
|
|
'mutation' => isset($this->nodeMap['Mutation']) ? 'Mutation' : null,
|
|
|
|
'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null,
|
|
|
|
];
|
2017-02-19 22:26:56 +03:00
|
|
|
|
2018-02-09 18:14:04 +03:00
|
|
|
$defintionBuilder = new ASTDefinitionBuilder(
|
|
|
|
$this->nodeMap,
|
|
|
|
$this->options,
|
|
|
|
function($typeName) { throw new Error('Type "'. $typeName . '" not found in document.'); }
|
|
|
|
);
|
2017-02-19 22:26:56 +03:00
|
|
|
|
2018-02-09 18:14:04 +03:00
|
|
|
$directives = array_map(function($def) use ($defintionBuilder) {
|
|
|
|
return $defintionBuilder->buildDirective($def);
|
|
|
|
}, $directiveDefs);
|
2017-02-19 22:26:56 +03:00
|
|
|
|
|
|
|
// If specified directives were not explicitly declared, add them.
|
2018-02-09 14:54:34 +03:00
|
|
|
$skip = array_reduce($directives, function ($hasSkip, $directive) {
|
2017-02-19 22:26:56 +03:00
|
|
|
return $hasSkip || $directive->name == 'skip';
|
|
|
|
});
|
|
|
|
if (!$skip) {
|
|
|
|
$directives[] = Directive::skipDirective();
|
|
|
|
}
|
|
|
|
|
2018-02-09 14:54:34 +03:00
|
|
|
$include = array_reduce($directives, function ($hasInclude, $directive) {
|
2017-02-19 22:26:56 +03:00
|
|
|
return $hasInclude || $directive->name == 'include';
|
|
|
|
});
|
|
|
|
if (!$include) {
|
|
|
|
$directives[] = Directive::includeDirective();
|
|
|
|
}
|
|
|
|
|
2018-02-09 14:54:34 +03:00
|
|
|
$deprecated = array_reduce($directives, function ($hasDeprecated, $directive) {
|
2017-02-19 22:26:56 +03:00
|
|
|
return $hasDeprecated || $directive->name == 'deprecated';
|
|
|
|
});
|
|
|
|
if (!$deprecated) {
|
|
|
|
$directives[] = Directive::deprecatedDirective();
|
|
|
|
}
|
|
|
|
|
2018-02-13 00:41:52 +03:00
|
|
|
// Note: While this could make early assertions to get the correctly
|
|
|
|
// typed values below, that would throw immediately while type system
|
|
|
|
// validation with validateSchema() will produce more actionable results.
|
2018-02-09 15:49:10 +03:00
|
|
|
|
2017-03-04 23:10:52 +03:00
|
|
|
$schema = new Schema([
|
2018-02-13 00:41:52 +03:00
|
|
|
'query' => isset($operationTypes['query'])
|
|
|
|
? $defintionBuilder->buildType($operationTypes['query'])
|
|
|
|
: null,
|
|
|
|
'mutation' => isset($operationTypes['mutation'])
|
|
|
|
? $defintionBuilder->buildType($operationTypes['mutation'])
|
|
|
|
: null,
|
|
|
|
'subscription' => isset($operationTypes['subscription'])
|
|
|
|
? $defintionBuilder->buildType($operationTypes['subscription'])
|
|
|
|
: null,
|
2018-02-09 18:14:04 +03:00
|
|
|
'typeLoader' => function ($name) use ($defintionBuilder) {
|
|
|
|
return $defintionBuilder->buildType($name);
|
2017-07-28 13:55:25 +03:00
|
|
|
},
|
2017-02-19 22:26:56 +03:00
|
|
|
'directives' => $directives,
|
2017-09-20 13:43:06 +03:00
|
|
|
'astNode' => $schemaDef,
|
2018-02-09 18:14:04 +03:00
|
|
|
'types' => function () use ($defintionBuilder) {
|
2017-07-28 13:55:25 +03:00
|
|
|
$types = [];
|
|
|
|
foreach ($this->nodeMap as $name => $def) {
|
|
|
|
if (!isset($this->loadedTypeDefs[$name])) {
|
2018-02-09 18:14:04 +03:00
|
|
|
$types[] = $defintionBuilder->buildType($def->name->value);
|
2017-07-28 13:55:25 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return $types;
|
|
|
|
}
|
2017-02-19 22:26:56 +03:00
|
|
|
]);
|
2017-03-04 23:10:52 +03:00
|
|
|
|
|
|
|
return $schema;
|
2017-02-19 22:26:56 +03:00
|
|
|
}
|
|
|
|
|
2018-02-09 15:49:10 +03:00
|
|
|
/**
|
|
|
|
* @param SchemaDefinitionNode $schemaDef
|
|
|
|
* @return array
|
|
|
|
* @throws Error
|
|
|
|
*/
|
|
|
|
private function getOperationTypes($schemaDef)
|
|
|
|
{
|
|
|
|
$opTypes = [];
|
|
|
|
|
|
|
|
foreach ($schemaDef->operationTypes as $operationType) {
|
|
|
|
$typeName = $operationType->type->name->value;
|
|
|
|
$operation = $operationType->operation;
|
|
|
|
|
|
|
|
if (isset($opTypes[$operation])) {
|
|
|
|
throw new Error("Must provide only one $operation type in schema.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isset($this->nodeMap[$typeName])) {
|
|
|
|
throw new Error("Specified $operation type \"$typeName\" not found in document.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$opTypes[$operation] = $typeName;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $opTypes;
|
|
|
|
}
|
|
|
|
|
2017-02-19 22:26:56 +03:00
|
|
|
/**
|
|
|
|
* A helper function to build a GraphQLSchema directly from a source
|
|
|
|
* document.
|
2018-02-08 21:33:54 +03:00
|
|
|
*
|
2017-08-20 18:10:13 +03:00
|
|
|
* @api
|
2017-08-17 16:33:36 +03:00
|
|
|
* @param DocumentNode|Source|string $source
|
2018-02-09 18:14:04 +03:00
|
|
|
* @param array $options
|
2017-03-04 23:10:52 +03:00
|
|
|
* @return Schema
|
2017-02-19 22:26:56 +03:00
|
|
|
*/
|
2018-02-09 18:14:04 +03:00
|
|
|
public static function build($source, array $options = [])
|
2017-02-19 22:26:56 +03:00
|
|
|
{
|
2017-08-17 16:33:36 +03:00
|
|
|
$doc = $source instanceof DocumentNode ? $source : Parser::parse($source);
|
2018-02-09 18:14:04 +03:00
|
|
|
return self::buildAST($doc, $options);
|
2017-02-19 22:26:56 +03:00
|
|
|
}
|
2018-02-09 14:54:34 +03:00
|
|
|
}
|