2015-07-15 20:05:46 +03:00
|
|
|
<?php
|
|
|
|
namespace GraphQL;
|
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
use GraphQL\Type\Definition\AbstractType;
|
2015-07-15 20:05:46 +03:00
|
|
|
use GraphQL\Type\Definition\Directive;
|
2015-08-17 17:01:55 +03:00
|
|
|
use GraphQL\Type\Definition\FieldArgument;
|
|
|
|
use GraphQL\Type\Definition\FieldDefinition;
|
|
|
|
use GraphQL\Type\Definition\InputObjectType;
|
2015-07-15 20:05:46 +03:00
|
|
|
use GraphQL\Type\Definition\InterfaceType;
|
2015-08-17 17:01:55 +03:00
|
|
|
use GraphQL\Type\Definition\ListOfType;
|
|
|
|
use GraphQL\Type\Definition\NonNull;
|
2015-07-15 20:05:46 +03:00
|
|
|
use GraphQL\Type\Definition\ObjectType;
|
|
|
|
use GraphQL\Type\Definition\Type;
|
|
|
|
use GraphQL\Type\Definition\UnionType;
|
|
|
|
use GraphQL\Type\Definition\WrappingType;
|
|
|
|
use GraphQL\Type\Introspection;
|
|
|
|
|
|
|
|
class Schema
|
|
|
|
{
|
2016-04-25 00:57:09 +03:00
|
|
|
/**
|
|
|
|
* @var ObjectType
|
|
|
|
*/
|
|
|
|
protected $_queryType;
|
2015-07-15 20:05:46 +03:00
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
/**
|
|
|
|
* @var ObjectType
|
|
|
|
*/
|
|
|
|
protected $_mutationType;
|
2015-07-15 20:05:46 +03:00
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
/**
|
|
|
|
* @var ObjectType
|
|
|
|
*/
|
|
|
|
protected $_subscriptionType;
|
2015-12-21 02:29:29 +03:00
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
/**
|
|
|
|
* @var Directive[]
|
|
|
|
*/
|
|
|
|
protected $_directives;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array<string, Type>
|
|
|
|
*/
|
2015-07-15 20:05:46 +03:00
|
|
|
protected $_typeMap;
|
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
/**
|
|
|
|
* @var array<string, ObjectType[]>
|
|
|
|
*/
|
|
|
|
protected $_implementations;
|
2015-07-15 20:05:46 +03:00
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
/**
|
|
|
|
* @var array<string, array<string, boolean>>
|
|
|
|
*/
|
|
|
|
protected $_possibleTypeMap;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Schema constructor.
|
|
|
|
* @param array $config
|
|
|
|
*/
|
|
|
|
public function __construct($config = null)
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
2016-04-25 00:57:09 +03:00
|
|
|
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();
|
|
|
|
|
|
|
|
$config = [
|
|
|
|
'query' => $queryType,
|
|
|
|
'mutation' => $mutationType,
|
|
|
|
'subscription' => $subscriptionType
|
|
|
|
];
|
|
|
|
}
|
2015-08-17 17:01:55 +03:00
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
$this->_init($config);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function _init(array $config)
|
|
|
|
{
|
|
|
|
Utils::invariant(isset($config['query']) || isset($config['mutation']), "Either query or mutation type must be set");
|
|
|
|
|
|
|
|
$config += [
|
|
|
|
'query' => null,
|
|
|
|
'mutation' => null,
|
|
|
|
'subscription' => null,
|
|
|
|
'directives' => [],
|
|
|
|
'validate' => true
|
|
|
|
];
|
|
|
|
|
|
|
|
$this->_queryType = $config['query'];
|
|
|
|
$this->_mutationType = $config['mutation'];
|
|
|
|
$this->_subscriptionType = $config['subscription'];
|
|
|
|
|
|
|
|
$this->_directives = array_merge($config['directives'], [
|
|
|
|
Directive::includeDirective(),
|
|
|
|
Directive::skipDirective()
|
|
|
|
]);
|
2016-04-19 22:16:09 +03:00
|
|
|
|
2015-08-17 17:01:55 +03:00
|
|
|
// Build type map now to detect any errors within this schema.
|
2016-04-25 00:57:09 +03:00
|
|
|
$initialTypes = [
|
|
|
|
$config['query'],
|
|
|
|
$config['mutation'],
|
|
|
|
$config['subscription'],
|
|
|
|
Introspection::_schema()
|
|
|
|
];
|
|
|
|
if (!empty($config['types'])) {
|
|
|
|
$initialTypes = array_merge($initialTypes, $config['types']);
|
|
|
|
}
|
|
|
|
|
2015-08-17 17:01:55 +03:00
|
|
|
$map = [];
|
2016-04-25 00:57:09 +03:00
|
|
|
foreach ($initialTypes as $type) {
|
2015-08-17 17:01:55 +03:00
|
|
|
$this->_extractTypes($type, $map);
|
|
|
|
}
|
|
|
|
$this->_typeMap = $map + Type::getInternalTypes();
|
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
// Keep track of all implementations by interface name.
|
|
|
|
$this->_implementations = [];
|
|
|
|
foreach ($this->_typeMap as $typeName => $type) {
|
|
|
|
if ($type instanceof ObjectType) {
|
|
|
|
foreach ($type->getInterfaces() as $iface) {
|
|
|
|
$this->_implementations[$iface->name][] = $type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($config['validate']) {
|
|
|
|
$this->validate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Additionaly validate schema for integrity
|
|
|
|
*/
|
|
|
|
public function validate()
|
|
|
|
{
|
2015-08-17 17:01:55 +03:00
|
|
|
// Enforce correct interface implementations
|
|
|
|
foreach ($this->_typeMap as $typeName => $type) {
|
|
|
|
if ($type instanceof ObjectType) {
|
|
|
|
foreach ($type->getInterfaces() as $iface) {
|
2016-04-25 00:57:09 +03:00
|
|
|
$this->_assertObjectImplementsInterface($type, $iface);
|
2015-08-17 17:01:55 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ObjectType $object
|
|
|
|
* @param InterfaceType $iface
|
|
|
|
* @throws \Exception
|
|
|
|
*/
|
2016-04-25 00:57:09 +03:00
|
|
|
protected function _assertObjectImplementsInterface(ObjectType $object, InterfaceType $iface)
|
2015-08-17 17:01:55 +03:00
|
|
|
{
|
|
|
|
$objectFieldMap = $object->getFields();
|
|
|
|
$ifaceFieldMap = $iface->getFields();
|
|
|
|
|
|
|
|
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
|
|
|
|
Utils::invariant(
|
|
|
|
isset($objectFieldMap[$fieldName]),
|
|
|
|
"\"$iface\" expects field \"$fieldName\" but \"$object\" does not provide it"
|
|
|
|
);
|
|
|
|
|
|
|
|
/** @var $ifaceField FieldDefinition */
|
|
|
|
/** @var $objectField FieldDefinition */
|
|
|
|
$objectField = $objectFieldMap[$fieldName];
|
|
|
|
|
|
|
|
Utils::invariant(
|
2016-04-25 00:57:09 +03:00
|
|
|
$this->_isEqualType($ifaceField->getType(), $objectField->getType()),
|
2015-08-17 17:01:55 +03:00
|
|
|
"$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " .
|
|
|
|
"$object.$fieldName provides type \"{$objectField->getType()}"
|
|
|
|
);
|
|
|
|
|
|
|
|
foreach ($ifaceField->args as $ifaceArg) {
|
|
|
|
/** @var $ifaceArg FieldArgument */
|
|
|
|
/** @var $objectArg FieldArgument */
|
|
|
|
$argName = $ifaceArg->name;
|
|
|
|
$objectArg = $objectField->getArg($argName);
|
|
|
|
|
|
|
|
// Assert interface field arg exists on object field.
|
|
|
|
Utils::invariant(
|
|
|
|
$objectArg,
|
|
|
|
"$iface.$fieldName expects argument \"$argName\" but $object.$fieldName does not provide it."
|
|
|
|
);
|
|
|
|
|
|
|
|
// Assert interface field arg type matches object field arg type.
|
|
|
|
// (invariant)
|
|
|
|
Utils::invariant(
|
2016-04-25 00:57:09 +03:00
|
|
|
$this->_isEqualType($ifaceArg->getType(), $objectArg->getType()),
|
2015-08-17 17:01:55 +03:00
|
|
|
"$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " .
|
|
|
|
"but $object.$fieldName($argName:) provides " .
|
|
|
|
"type \"{$objectArg->getType()}\""
|
|
|
|
);
|
|
|
|
|
|
|
|
// Assert argument set invariance.
|
|
|
|
foreach ($objectField->args as $objectArg) {
|
|
|
|
$argName = $objectArg->name;
|
|
|
|
$ifaceArg = $ifaceField->getArg($argName);
|
|
|
|
Utils::invariant(
|
|
|
|
$ifaceArg,
|
|
|
|
"$iface.$fieldName does not define argument \"$argName\" but " .
|
|
|
|
"$object.$fieldName provides it."
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param $typeA
|
|
|
|
* @param $typeB
|
|
|
|
* @return bool
|
|
|
|
*/
|
2016-04-25 00:57:09 +03:00
|
|
|
protected function _isEqualType($typeA, $typeB)
|
2015-08-17 17:01:55 +03:00
|
|
|
{
|
|
|
|
if ($typeA instanceof NonNull && $typeB instanceof NonNull) {
|
2016-04-25 00:57:09 +03:00
|
|
|
return $this->_isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
|
2015-08-17 17:01:55 +03:00
|
|
|
}
|
|
|
|
if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) {
|
2016-04-25 00:57:09 +03:00
|
|
|
return $this->_isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
|
2015-08-17 17:01:55 +03:00
|
|
|
}
|
|
|
|
return $typeA === $typeB;
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
/**
|
|
|
|
* @return ObjectType
|
|
|
|
*/
|
2015-07-15 20:05:46 +03:00
|
|
|
public function getQueryType()
|
|
|
|
{
|
2016-04-25 00:57:09 +03:00
|
|
|
return $this->_queryType;
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
/**
|
|
|
|
* @return ObjectType
|
|
|
|
*/
|
2015-07-15 20:05:46 +03:00
|
|
|
public function getMutationType()
|
|
|
|
{
|
2016-04-25 00:57:09 +03:00
|
|
|
return $this->_mutationType;
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
/**
|
|
|
|
* @return ObjectType
|
|
|
|
*/
|
2015-12-21 02:29:29 +03:00
|
|
|
public function getSubscriptionType()
|
|
|
|
{
|
2016-04-25 00:57:09 +03:00
|
|
|
return $this->_subscriptionType;
|
2015-12-21 02:29:29 +03:00
|
|
|
}
|
|
|
|
|
2015-07-15 20:05:46 +03:00
|
|
|
/**
|
2016-04-25 00:57:09 +03:00
|
|
|
* @return array
|
2015-07-15 20:05:46 +03:00
|
|
|
*/
|
2016-04-25 00:57:09 +03:00
|
|
|
public function getTypeMap()
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
2016-04-25 00:57:09 +03:00
|
|
|
return $this->_typeMap;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $name
|
|
|
|
* @return Type
|
|
|
|
*/
|
|
|
|
public function getType($name)
|
|
|
|
{
|
|
|
|
$map = $this->getTypeMap();
|
|
|
|
return isset($map[$name]) ? $map[$name] : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param AbstractType $abstractType
|
|
|
|
* @return ObjectType[]
|
|
|
|
*/
|
|
|
|
public function getPossibleTypes(AbstractType $abstractType)
|
|
|
|
{
|
|
|
|
if ($abstractType instanceof UnionType) {
|
|
|
|
return $abstractType->getTypes();
|
|
|
|
}
|
|
|
|
Utils::invariant($abstractType instanceof InterfaceType);
|
|
|
|
return $this->_implementations[$abstractType->name];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param AbstractType $abstractType
|
|
|
|
* @param ObjectType $possibleType
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType)
|
|
|
|
{
|
|
|
|
if (null === $this->_possibleTypeMap) {
|
|
|
|
$this->_possibleTypeMap = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isset($this->_possibleTypeMap[$abstractType->name])) {
|
|
|
|
$tmp = [];
|
|
|
|
foreach ($this->getPossibleTypes($abstractType) as $type) {
|
|
|
|
$tmp[$type->name] = true;
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
2016-04-25 00:57:09 +03:00
|
|
|
$this->_possibleTypeMap[$abstractType->name] = $tmp;
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
2016-04-25 00:57:09 +03:00
|
|
|
|
|
|
|
return !empty($this->_possibleTypeMap[$abstractType->name][$possibleType->name]);
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-04-25 00:57:09 +03:00
|
|
|
* @return Directive[]
|
2015-07-15 20:05:46 +03:00
|
|
|
*/
|
|
|
|
public function getDirectives()
|
|
|
|
{
|
|
|
|
return $this->_directives;
|
|
|
|
}
|
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
/**
|
|
|
|
* @param $name
|
|
|
|
* @return Directive
|
|
|
|
*/
|
|
|
|
public function getDirective($name)
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
2016-04-25 00:57:09 +03:00
|
|
|
foreach ($this->getDirectives() as $directive) {
|
|
|
|
if ($directive->name === $name) {
|
|
|
|
return $directive;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
protected function _extractTypes($type, &$map)
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
2015-08-17 17:01:55 +03:00
|
|
|
if (!$type) {
|
|
|
|
return $map;
|
|
|
|
}
|
|
|
|
|
2015-07-15 20:05:46 +03:00
|
|
|
if ($type instanceof WrappingType) {
|
|
|
|
return $this->_extractTypes($type->getWrappedType(), $map);
|
|
|
|
}
|
|
|
|
|
2015-08-17 17:01:55 +03:00
|
|
|
if (!empty($map[$type->name])) {
|
|
|
|
Utils::invariant(
|
|
|
|
$map[$type->name] === $type,
|
|
|
|
"Schema must contain unique named types but contains multiple types named \"$type\"."
|
|
|
|
);
|
2015-07-15 20:05:46 +03:00
|
|
|
return $map;
|
|
|
|
}
|
|
|
|
$map[$type->name] = $type;
|
|
|
|
|
|
|
|
$nestedTypes = [];
|
|
|
|
|
2016-04-25 00:57:09 +03:00
|
|
|
if ($type instanceof UnionType) {
|
|
|
|
$nestedTypes = $type->getTypes();
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
|
|
|
if ($type instanceof ObjectType) {
|
|
|
|
$nestedTypes = array_merge($nestedTypes, $type->getInterfaces());
|
|
|
|
}
|
2015-08-17 17:01:55 +03:00
|
|
|
if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) {
|
2015-07-15 20:05:46 +03:00
|
|
|
foreach ((array) $type->getFields() as $fieldName => $field) {
|
2015-08-17 17:01:55 +03:00
|
|
|
if (isset($field->args)) {
|
|
|
|
$fieldArgTypes = array_map(function($arg) { return $arg->getType(); }, $field->args);
|
|
|
|
$nestedTypes = array_merge($nestedTypes, $fieldArgTypes);
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
|
|
|
$nestedTypes[] = $field->getType();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
foreach ($nestedTypes as $type) {
|
|
|
|
$this->_extractTypes($type, $map);
|
|
|
|
}
|
|
|
|
return $map;
|
|
|
|
}
|
|
|
|
}
|