0Pricing
PHP Academy · Lesson

Building a Schema with graphql-php

Define types and a schema with webonyx/graphql-php.

Building a Schema with graphql-php is a free PHP Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

graphql-php: The Reference Implementation

webonyx/graphql-php is the de-facto PHP port of the GraphQL reference implementation. It gives you the type system, parser, validator and executor. You define your schema either programmatically (PHP objects) or from SDL (schema-first). In this lesson we build a schema by hand so you understand exactly what each piece does.

composer require webonyx/graphql-php

Scalars and the Type Registry

Every GraphQL value bottoms out in a scalar: Int, Float, String, Boolean, ID. In graphql-php these live on the Type facade. Because object types reference each other (and themselves), a common pattern is a static TypeRegistry that memoizes each type so you build it only once.

<?php
use GraphQL\Type\Definition\Type;

// Built-in scalars, returned as singletons:
var_dump(Type::int()->name);     // "Int"
var_dump(Type::string()->name);  // "String"
var_dump(Type::id()->name);      // "ID"
var_dump(Type::nonNull(Type::string())->toString()); // "String!"

Defining an ObjectType

An ObjectType has a name and a fields map. Each field declares its type and optionally a resolve callback. If you omit resolve, graphql-php uses the default resolver, which reads the matching array key or property/getter off the parent value.

<?php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

$postType = new ObjectType([
    'name' => 'Post',
    'fields' => [
        'id'    => Type::nonNull(Type::id()),
        'title' => Type::nonNull(Type::string()),
        'body'  => Type::string(),
    ],
]);

Non-Null and List Wrapping

Wrapping types express nullability and cardinality:

  • Type::nonNull(T)T! (never null).
  • Type::listOf(T)[T] (a list, possibly null, possibly with null members).
  • [Post!]! = a non-null list of non-null posts → nonNull(listOf(nonNull($postType))).

Get this right: it is your schema's null contract with clients.

<?php
use GraphQL\Type\Definition\Type;

// [Post!]!  -- a required list whose elements are never null
$wrapped = Type::nonNull(Type::listOf(Type::nonNull(Type::string())));
echo $wrapped->toString(), "\n"; // [String!]!

Lazy Fields Break Circular References

A User has posts, and a Post has an author (a User). To build mutually-referencing types, pass fields as a closure instead of an array. The closure runs lazily after both types exist, dodging the chicken-and-egg problem.

<?php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

class Types {
    private static array $cache = [];

    public static function user(): ObjectType {
        return self::$cache['User'] ??= new ObjectType([
            'name' => 'User',
            'fields' => fn() => [           // lazy!
                'id'    => Type::nonNull(Type::id()),
                'name'  => Type::nonNull(Type::string()),
                'posts' => Type::listOf(self::post()),
            ],
        ]);
    }

    public static function post(): ObjectType {
        return self::$cache['Post'] ??= new ObjectType([
            'name' => 'Post',
            'fields' => fn() => [
                'id'     => Type::nonNull(Type::id()),
                'title'  => Type::nonNull(Type::string()),
                'author' => self::user(),    // back-reference
            ],
        ]);
    }
}

Field Arguments

Fields can take arguments. Declare them under the args key; they arrive as the second parameter ($args) of the resolver. Arguments are themselves typed, can be non-null, and can carry defaultValue.

<?php
use GraphQL\Type\Definition\Type;

$userField = [
    'type' => Type::string(),
    'args' => [
        'id'     => Type::nonNull(Type::id()),
        'locale' => ['type' => Type::string(), 'defaultValue' => 'en'],
    ],
    'resolve' => fn($root, array $args) => "user {$args['id']} ({$args['locale']})",
];

The Root Query Type

Every schema needs a root Query type — the entry points clients can start from. Here we expose a single hello field so we can execute end-to-end. The root resolver's first argument is the schema's rootValue (often null).

<?php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

$queryType = new ObjectType([
    'name' => 'Query',
    'fields' => [
        'hello' => [
            'type' => Type::string(),
            'args' => ['name' => Type::nonNull(Type::string())],
            'resolve' => fn($root, array $args) => 'Hello, ' . $args['name'],
        ],
    ],
]);

Assembling and Executing the Schema

Wrap the query type in a Schema and run a query string through GraphQL::executeQuery(). The result object converts to the canonical { data, errors } array via toArray().

<?php
require 'vendor/autoload.php';

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

$queryType = new ObjectType([
    'name' => 'Query',
    'fields' => [
        'hello' => [
            'type' => Type::string(),
            'args' => ['name' => Type::nonNull(Type::string())],
            'resolve' => fn($root, $args) => 'Hello, ' . $args['name'],
        ],
    ],
]);

$schema = new Schema(['query' => $queryType]);
$result = GraphQL::executeQuery($schema, '{ hello(name: "Ada") }');
echo json_encode($result->toArray());
// {"data":{"hello":"Hello, Ada"}}

Schema-First Alternative with BuildSchema

If you prefer SDL, BuildSchema::build() parses a schema string into an executable schema. You then attach resolvers separately (e.g. a field-resolver callback), keeping the type definitions declarative while logic stays in PHP.

<?php
use GraphQL\Utils\BuildSchema;

$sdl = <<<'GQL'
type Query {
  hello(name: String!): String
}
GQL;

$schema = BuildSchema::build($sdl);
// Provide resolvers via the executeQuery $fieldResolver argument
// or with a type config decorator.

Validate Before You Ship

graphql-php validates incoming queries against the schema automatically before execution. You can also assert the schema itself is internally consistent at build/CI time with $schema->assertValid() — catch typos and broken references before deploy, not at request time.

<?php
use GraphQL\Type\Schema;

/** @var Schema $schema */
$schema->assertValid(); // throws InvariantViolation on a broken schema
echo "schema OK\n";

Enums and Custom Scalars

Beyond objects, two type kinds round out most schemas. EnumType constrains a field to a fixed set of named values. CustomScalarType lets you define domain scalars (DateTime, Email) with your own serialize/parseValue/parseLiteral logic so values are validated and normalized at the boundary.

<?php
use GraphQL\Type\Definition\EnumType;

$statusEnum = new EnumType([
    'name' => 'PostStatus',
    'values' => [
        'DRAFT'     => ['value' => 0],
        'PUBLISHED' => ['value' => 1],
        'ARCHIVED'  => ['value' => 2],
    ],
]);
// A field typed as $statusEnum only accepts DRAFT/PUBLISHED/ARCHIVED.

Quick Check

Why pass fields as a closure?

Recap

You built a schema from the ground up:

  • Scalars and wrapping types (nonNull, listOf) express the null/cardinality contract.
  • ObjectType with a fields map (or lazy closure) defines your shapes.
  • A memoizing type registry handles circular references.
  • Fields take typed args; the root Query type is the entry point.
  • GraphQL::executeQuery() runs it; assertValid() guards the schema in CI.

Next: resolvers, mutations, and subscriptions.

Frequently asked questions

Is the “Building a Schema with graphql-php” lesson free?

Yes — the full text of “Building a Schema with graphql-php” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.

What will I learn in “Building a Schema with graphql-php”?

Define types and a schema with webonyx/graphql-php. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start PHP Academy?

No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Building a Schema with graphql-php” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this PHP Academy lesson?

Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. GraphQL vs REST
  2. Building a Schema with graphql-php
  3. Resolvers, Mutations and Subscriptions
  4. Performance: N+1 and DataLoader
← Back to PHP Academy