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-phpScalars 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. ObjectTypewith a fields map (or lazy closure) defines your shapes.- A memoizing type registry handles circular references.
- Fields take typed
args; the rootQuerytype 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
- GraphQL vs REST
- Building a Schema with graphql-php
- Resolvers, Mutations and Subscriptions
- Performance: N+1 and DataLoader