Skip to content

Schema Definition

The schema is a container of your type hierarchy, which accepts root types in a constructor and provides methods for receiving information about your types to internal GraphQL tools.

In graphql-php, the schema is an instance of GraphQL\Type\Schema:

use GraphQL\Type\Schema;

$schema = new Schema([
    'query' => $queryType,
    'mutation' => $mutationType,
]);

See possible constructor options below.

Query and Mutation types

The schema consists of two root types:

  • Query type is a surface of your read API
  • Mutation type (optional) exposes write API by declaring all possible mutations in your app.

Query and Mutation types are regular object types containing root-level fields of your API:

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

$queryType = new ObjectType([
    'name' => 'Query',
    'fields' => [
        'hello' => [
            'type' => Type::string(),
            'resolve' => fn () => 'Hello World!',
        ],
        'hero' => [
            'type' => $characterInterface,
            'args' => [
                'episode' => [
                    'type' => $episodeEnum,
                ],
            ],
            'resolve' => fn ($rootValue, array $args): Hero => StarWarsData::getHero($args['episode'] ?? null),
        ]
    ]
]);

$mutationType = new ObjectType([
    'name' => 'Mutation',
    'fields' => [
        'createReview' => [
            'type' => $createReviewOutput,
            'args' => [
                'episode' => Type::nonNull($episodeEnum),
                'review' => Type::nonNull($reviewInputObject),
            ],
            // TODO
            'resolve' => fn ($rootValue, array $args): Review => StarWarsData::createReview($args['episode'], $args['review']),
        ]
    ]
]);

Keep in mind that other than the special meaning of declaring a surface area of your API, those types are the same as any other object type, and their fields work exactly the same way.

Mutation type is also just a regular object type. The difference is in semantics. Field names of Mutation type are usually verbs and they almost always have arguments - quite often with complex input values (see Mutations and Input Types for details).

Configuration Options

The schema constructor expects an instance of GraphQL\Type\SchemaConfig or an array with the following options:

Option Type Notes
query ObjectType or callable(): ?ObjectType or null Required. Object type (usually named Query) containing root-level fields of your read API
mutation ObjectType or callable(): ?ObjectType or null Object type (usually named Mutation) containing root-level fields of your write API
subscription ObjectType or callable(): ?ObjectType or null Reserved for future subscriptions implementation. Currently presented for compatibility with introspection query of graphql-js, used by various clients (like Relay or GraphiQL)
directives array<Directive> A full list of directives supported by your schema. By default, contains built-in @skip and @include directives.

If you pass your own directives and still want to use built-in directives - add them explicitly. For example:

array_merge(GraphQL::getStandardDirectives(), [$myCustomDirective]);
types array<ObjectType> List of object types which cannot be detected by graphql-php during static schema analysis.

Most often this happens when the object type is never referenced in fields directly but is still a part of a schema because it implements an interface which resolves to this object type in its resolveType callable.

Note that you are not required to pass all of your types here - it is simply a workaround for a concrete use-case.
typeLoader callable(string $name): Type Expected to return a type instance given the name. Must always return the same instance if called multiple times, see lazy loading. See section below on lazy type loading.

Using config class

If you prefer a fluid interface for the config with auto-completion in IDE and static time validation, use GraphQL\Type\SchemaConfig instead of an array:

use GraphQL\Type\SchemaConfig;
use GraphQL\Type\Schema;

$config = SchemaConfig::create()
    ->setQuery($myQueryType)
    ->setTypeLoader($myTypeLoader);

$schema = new Schema($config);

Lazy loading of types

If your schema makes use of a large number of complex or dynamically-generated types, they can become a performance concern. There are a few best practices that can lessen their impact:

  1. Use a type registry. This will put you in a position to implement your own caching and lookup strategies, and GraphQL won't need to preload a map of all known types to do its work.

  2. Define each custom type as a callable that returns a type, rather than an object instance. Then, the work of instantiating them will only happen as they are needed by each query.

  3. Define all of your object fields as callbacks. If you're already doing #2 then this isn't needed, but it's a quick and easy precaution.

It is recommended to centralize this kind of functionality in a type registry. A typical example might look like the following:

// StoryType.php
use GraphQL\Type\Definition\ObjectType;

final class StoryType extends ObjectType
{
    public function __construct()
    {
        parent::__construct([
            'fields' => static fn (): array => [
                'author' => [
                    'type' => Types::author(),
                    'resolve' => static fn (Story $story): ?Author => DataSource::findUser($story->authorId),
                ],
            ],
        ]);
    }
}

// AuthorType.php
use GraphQL\Type\Definition\ObjectType;

final class AuthorType extends ObjectType
{
    public function __construct()
    {
        parent::__construct([
            'description' => 'Writer of books',
            'fields' => static fn (): array => [
                'firstName' => Types::string(),
            ],
        ]);
    }
}

// Types.php
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\NamedType;

final class Types
{
    /** @var array<string, Type&NamedType> */
    private static array $types = [];

    /** @return Type&NamedType */
    public static function load(string $typeName): Type
    {
        if (isset(self::$types[$typeName])) {
            return self::$types[$typeName];
        }

        // For every type, this class must define a method with the same name
        // but the first letter is in lower case.
        $methodName = match ($typeName) {
            'ID' => 'id',
            default => lcfirst($typeName),
        };
        if (! method_exists(self::class, $methodName)) {
            throw new \Exception("Unknown GraphQL type: {$typeName}.");
        }

        $type = self::{$methodName}(); // @phpstan-ignore-line variable static method call
        if (is_callable($type)) {
            $type = $type();
        }

        return self::$types[$typeName] = $type;
    }

    /** @return Type&NamedType */
    private static function byClassName(string $className): Type
    {
        $classNameParts = explode('\\', $className);
        $baseClassName = end($classNameParts);
        // All type classes must use the suffix Type.
        // This prevents name collisions between types and PHP keywords.
        $typeName = preg_replace('~Type$~', '', $baseClassName);

        // Type loading is very similar to PHP class loading, but keep in mind
        // that the **typeLoader** must always return the same instance of a type.
        // We can enforce that in our type registry by caching known types.
        return self::$types[$typeName] ??= new $className;
    }

    /** @return \Closure(): (Type&NamedType) */
    private static function lazyByClassName(string $className): \Closure
    {
        return static fn () => self::byClassName($className);
    }

    public static function boolean(): ScalarType { return Type::boolean(); }
    public static function float(): ScalarType { return Type::float(); }
    public static function id(): ScalarType { return Type::id(); }
    public static function int(): ScalarType { return Type::int(); }
    public static function string(): ScalarType { return Type::string(); }
    public static function author(): callable { return self::lazyByClassName(AuthorType::class); }
    public static function story(): callable { return self::lazyByClassName(StoryType::class); }
    ...
}

// api/index.php
use GraphQL\Type\Definition\ObjectType;

$schema = new Schema([
    'query' => new ObjectType([
        'name' => 'Query',
        'fields' => static fn() => [
            'story' => [
                'args'=>[
                  'id' => Types::int(),
                ],
                'type' => Types::story(),
                'description' => 'Returns my A',
                'resolve' => static fn ($rootValue, array $args): ?Story => DataSource::findStory($args['id']),
            ],
        ],
    ]),
    'typeLoader' => Types::load(...),
]);

A working demonstration of this kind of architecture can be found in the 01-blog sample.

Schema Validation

By default, the schema is created with only shallow validation of type and field definitions
(because validation requires a full schema scan and is very costly on bigger schemas).

There is a special method assertValid() on the schema instance which throws GraphQL\Error\InvariantViolation exception when it encounters any error, like:

  • Invalid types used for fields/arguments
  • Missing interface implementations
  • Invalid interface implementations
  • Other schema errors...

Schema validation is supposed to be used in CLI commands or during a build step of your app. Don't call it in web requests in production.

Usage example:

try {
    $schema = new GraphQL\Type\Schema([
        'query' => $myQueryType
    ]);
    $schema->assertValid();
} catch (GraphQL\Error\InvariantViolation $e) {
    echo $e->getMessage();
}