*/ private $introspection; /** @var array */ private $options; /** @var array */ private $typeMap; /** * @param array $introspectionQuery * @param array $options */ public function __construct(array $introspectionQuery, array $options = []) { $this->introspection = $introspectionQuery; $this->options = $options; } /** * Build a schema for use by client tools. * * Given the result of a client running the introspection query, creates and * returns a \GraphQL\Type\Schema instance which can be then used with all graphql-php * tools, but cannot be used to execute a query, as introspection does not * represent the "resolver", "parse" or "serialize" functions or any other * server-internal mechanisms. * * This function expects a complete introspection result. Don't forget to check * the "errors" field of a server response before calling this function. * * Accepts options as a third argument: * * - assumeValid: * When building a schema from a GraphQL service's introspection result, it * might be safe to assume the schema is valid. Set to true to assume the * produced schema is valid. * * Default: false * * @param array $introspectionQuery * @param array $options * * @api */ public static function build(array $introspectionQuery, array $options = []) : Schema { $builder = new self($introspectionQuery, $options); return $builder->buildSchema(); } public function buildSchema() : Schema { if (! array_key_exists('__schema', $this->introspection)) { throw new InvariantViolation('Invalid or incomplete introspection result. Ensure that you are passing "data" property of introspection response and no "errors" was returned alongside: ' . json_encode($this->introspection) . '.'); } $schemaIntrospection = $this->introspection['__schema']; $this->typeMap = Utils::keyValMap( $schemaIntrospection['types'], static function (array $typeIntrospection) { return $typeIntrospection['name']; }, function (array $typeIntrospection) : NamedType { return $this->buildType($typeIntrospection); } ); $builtInTypes = array_merge( Type::getStandardTypes(), Introspection::getTypes() ); foreach ($builtInTypes as $name => $type) { if (! isset($this->typeMap[$name])) { continue; } $this->typeMap[$name] = $type; } $queryType = isset($schemaIntrospection['queryType']) ? $this->getObjectType($schemaIntrospection['queryType']) : null; $mutationType = isset($schemaIntrospection['mutationType']) ? $this->getObjectType($schemaIntrospection['mutationType']) : null; $subscriptionType = isset($schemaIntrospection['subscriptionType']) ? $this->getObjectType($schemaIntrospection['subscriptionType']) : null; $directives = isset($schemaIntrospection['directives']) ? array_map( [$this, 'buildDirective'], $schemaIntrospection['directives'] ) : []; $schemaConfig = new SchemaConfig(); $schemaConfig->setQuery($queryType) ->setMutation($mutationType) ->setSubscription($subscriptionType) ->setTypes($this->typeMap) ->setDirectives($directives) ->setAssumeValid( isset($this->options) && isset($this->options['assumeValid']) && $this->options['assumeValid'] ); return new Schema($schemaConfig); } /** * @param array $typeRef */ private function getType(array $typeRef) : Type { if (isset($typeRef['kind'])) { if ($typeRef['kind'] === TypeKind::LIST) { if (! isset($typeRef['ofType'])) { throw new InvariantViolation('Decorated type deeper than introspection query.'); } return new ListOfType($this->getType($typeRef['ofType'])); } if ($typeRef['kind'] === TypeKind::NON_NULL) { if (! isset($typeRef['ofType'])) { throw new InvariantViolation('Decorated type deeper than introspection query.'); } /** @var NullableType $nullableType */ $nullableType = $this->getType($typeRef['ofType']); return new NonNull($nullableType); } } if (! isset($typeRef['name'])) { throw new InvariantViolation('Unknown type reference: ' . json_encode($typeRef) . '.'); } return $this->getNamedType($typeRef['name']); } /** * @return NamedType&Type */ private function getNamedType(string $typeName) : NamedType { if (! isset($this->typeMap[$typeName])) { throw new InvariantViolation( "Invalid or incomplete schema, unknown type: ${typeName}. Ensure that a full introspection query is used in order to build a client schema." ); } return $this->typeMap[$typeName]; } /** * @param array $typeRef */ private function getInputType(array $typeRef) : InputType { $type = $this->getType($typeRef); if ($type instanceof InputType) { return $type; } throw new InvariantViolation('Introspection must provide input type for arguments, but received: ' . json_encode($type) . '.'); } /** * @param array $typeRef */ private function getOutputType(array $typeRef) : OutputType { $type = $this->getType($typeRef); if ($type instanceof OutputType) { return $type; } throw new InvariantViolation('Introspection must provide output type for fields, but received: ' . json_encode($type) . '.'); } /** * @param array $typeRef */ private function getObjectType(array $typeRef) : ObjectType { $type = $this->getType($typeRef); return ObjectType::assertObjectType($type); } /** * @param array $typeRef */ public function getInterfaceType(array $typeRef) : InterfaceType { $type = $this->getType($typeRef); return InterfaceType::assertInterfaceType($type); } /** * @param array $type */ private function buildType(array $type) : NamedType { if (array_key_exists('name', $type) && array_key_exists('kind', $type)) { switch ($type['kind']) { case TypeKind::SCALAR: return $this->buildScalarDef($type); case TypeKind::OBJECT: return $this->buildObjectDef($type); case TypeKind::INTERFACE: return $this->buildInterfaceDef($type); case TypeKind::UNION: return $this->buildUnionDef($type); case TypeKind::ENUM: return $this->buildEnumDef($type); case TypeKind::INPUT_OBJECT: return $this->buildInputObjectDef($type); } } throw new InvariantViolation( 'Invalid or incomplete introspection result. Ensure that a full introspection query is used in order to build a client schema: ' . json_encode($type) . '.' ); } /** * @param array $scalar */ private function buildScalarDef(array $scalar) : ScalarType { return new CustomScalarType([ 'name' => $scalar['name'], 'description' => $scalar['description'], 'serialize' => static function ($value) : string { return (string) $value; }, ]); } /** * @param array $implementingIntrospection * * @return array */ private function buildImplementationsList(array $implementingIntrospection) : array { // TODO: Temporary workaround until GraphQL ecosystem will fully support 'interfaces' on interface types. if (array_key_exists('interfaces', $implementingIntrospection) && $implementingIntrospection['interfaces'] === null && $implementingIntrospection['kind'] === TypeKind::INTERFACE) { return []; } if (! array_key_exists('interfaces', $implementingIntrospection)) { throw new InvariantViolation('Introspection result missing interfaces: ' . json_encode($implementingIntrospection) . '.'); } return array_map([$this, 'getInterfaceType'], $implementingIntrospection['interfaces']); } /** * @param array $object */ private function buildObjectDef(array $object) : ObjectType { return new ObjectType([ 'name' => $object['name'], 'description' => $object['description'], 'interfaces' => function () use ($object) : array { return $this->buildImplementationsList($object); }, 'fields' => function () use ($object) { return $this->buildFieldDefMap($object); }, ]); } /** * @param array $interface */ private function buildInterfaceDef(array $interface) : InterfaceType { return new InterfaceType([ 'name' => $interface['name'], 'description' => $interface['description'], 'fields' => function () use ($interface) { return $this->buildFieldDefMap($interface); }, 'interfaces' => function () use ($interface) : array { return $this->buildImplementationsList($interface); }, ]); } /** * @param array> $union */ private function buildUnionDef(array $union) : UnionType { if (! array_key_exists('possibleTypes', $union)) { throw new InvariantViolation('Introspection result missing possibleTypes: ' . json_encode($union) . '.'); } return new UnionType([ 'name' => $union['name'], 'description' => $union['description'], 'types' => function () use ($union) : array { return array_map( [$this, 'getObjectType'], $union['possibleTypes'] ); }, ]); } /** * @param array> $enum */ private function buildEnumDef(array $enum) : EnumType { if (! array_key_exists('enumValues', $enum)) { throw new InvariantViolation('Introspection result missing enumValues: ' . json_encode($enum) . '.'); } return new EnumType([ 'name' => $enum['name'], 'description' => $enum['description'], 'values' => Utils::keyValMap( $enum['enumValues'], static function (array $enumValue) : string { return $enumValue['name']; }, static function (array $enumValue) : array { return [ 'description' => $enumValue['description'], 'deprecationReason' => $enumValue['deprecationReason'], ]; } ), ]); } /** * @param array $inputObject */ private function buildInputObjectDef(array $inputObject) : InputObjectType { if (! array_key_exists('inputFields', $inputObject)) { throw new InvariantViolation('Introspection result missing inputFields: ' . json_encode($inputObject) . '.'); } return new InputObjectType([ 'name' => $inputObject['name'], 'description' => $inputObject['description'], 'fields' => function () use ($inputObject) : array { return $this->buildInputValueDefMap($inputObject['inputFields']); }, ]); } /** * @param array $typeIntrospection */ private function buildFieldDefMap(array $typeIntrospection) { if (! array_key_exists('fields', $typeIntrospection)) { throw new InvariantViolation('Introspection result missing fields: ' . json_encode($typeIntrospection) . '.'); } return Utils::keyValMap( $typeIntrospection['fields'], static function (array $fieldIntrospection) : string { return $fieldIntrospection['name']; }, function (array $fieldIntrospection) : array { if (! array_key_exists('args', $fieldIntrospection)) { throw new InvariantViolation('Introspection result missing field args: ' . json_encode($fieldIntrospection) . '.'); } return [ 'description' => $fieldIntrospection['description'], 'deprecationReason' => $fieldIntrospection['deprecationReason'], 'type' => $this->getOutputType($fieldIntrospection['type']), 'args' => $this->buildInputValueDefMap($fieldIntrospection['args']), ]; } ); } /** * @param array> $inputValueIntrospections * * @return array> */ private function buildInputValueDefMap(array $inputValueIntrospections) : array { return Utils::keyValMap( $inputValueIntrospections, static function (array $inputValue) : string { return $inputValue['name']; }, [$this, 'buildInputValue'] ); } /** * @param array $inputValueIntrospection * * @return array */ public function buildInputValue(array $inputValueIntrospection) : array { $type = $this->getInputType($inputValueIntrospection['type']); $inputValue = [ 'description' => $inputValueIntrospection['description'], 'type' => $type, ]; if (isset($inputValueIntrospection['defaultValue'])) { $inputValue['defaultValue'] = AST::valueFromAST( Parser::parseValue($inputValueIntrospection['defaultValue']), $type ); } return $inputValue; } /** * @param array $directive */ public function buildDirective(array $directive) : Directive { if (! array_key_exists('args', $directive)) { throw new InvariantViolation('Introspection result missing directive args: ' . json_encode($directive) . '.'); } if (! array_key_exists('locations', $directive)) { throw new InvariantViolation('Introspection result missing directive locations: ' . json_encode($directive) . '.'); } return new Directive([ 'name' => $directive['name'], 'description' => $directive['description'], 'args' => $this->buildInputValueDefMap($directive['args']), 'isRepeatable' => $directive['isRepeatable'] ?? false, 'locations' => $directive['locations'], ]); } }