Resolvers, Mutations and Subscriptions
Fetch and change data through resolvers.
Resolvers, Mutations and Subscriptions is a free PHP Academy lesson on CoddyKit — lesson 3 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.
Resolvers Are Where Logic Lives
The schema describes what exists; resolvers decide how each field's value is produced. A resolver is just a callable. Mutations are resolvers that change state. Subscriptions stream values over time. This lesson covers all three and the execution model that ties them together.
The Resolver Signature
Every resolver receives four arguments: ($objectValue, $args, $context, ResolveInfo $info).
- $objectValue — the parent's resolved value (the
rootValueat the top). - $args — the field's arguments.
- $context — per-request shared state (DB handle, current user).
- $info — AST/field metadata (field name, selection set, path).
<?php
use GraphQL\Type\Definition\ResolveInfo;
$resolve = function ($objectValue, array $args, $context, ResolveInfo $info) {
// $context['db'], $context['user'] set up per request
return $context['db']->find($args['id']);
};
The Default Resolver
If you do not supply resolve, graphql-php's default resolver reads the field name from the parent value: an array key, a public property, or a get<Field>() method. This means you can often resolve whole object types with zero boilerplate by returning plain arrays or DTOs from the parent.
<?php
// Parent returns this array; child fields resolve by key automatically:
$user = [
'id' => 1,
'name' => 'Ada',
'email' => 'ada@example.com',
];
// 'name' field -> $user['name'] with no explicit resolver needed
var_dump($user['name']);
Resolvers Cascade Parent-to-Child
Execution is top-down: the Query.user resolver returns a user, which becomes the $objectValue for User.posts, whose result becomes the parent for each Post.title. Understanding this cascade is essential — it is exactly where the N+1 problem appears (covered next lesson).
<?php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
$userType = new ObjectType([
'name' => 'User',
'fields' => fn() => [
'id' => Type::id(),
'name' => Type::string(),
'posts' => [
'type' => Type::listOf(Type::string()),
// $user is the parent value resolved by Query.user
'resolve' => fn($user) => Posts::titlesForUser($user['id']),
],
],
]);
Returning Promises (Async)
Resolvers may return a value or a promise. graphql-php ships a sync promise adapter; with ReactPHP/Amp adapters resolution can be deferred and batched. Even synchronously, returning Deferred objects lets the executor collect work and run it after the current resolution level — the mechanism DataLoader is built on.
<?php
use GraphQL\Deferred;
$resolve = function ($post) use ($authorBuffer) {
$authorBuffer->add($post['author_id']); // queue the id
return new Deferred(function () use ($authorBuffer, $post) {
$authorBuffer->loadOnce(); // one batched query
return $authorBuffer->get($post['author_id']);
});
};
Mutations Change State
A Mutation is just a root type named Mutation. By convention its top-level fields run sequentially (not in parallel) so side effects are ordered. Inputs are typically grouped into an InputObjectType for a clean signature.
<?php
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\Type;
$createPostInput = new InputObjectType([
'name' => 'CreatePostInput',
'fields' => [
'title' => Type::nonNull(Type::string()),
'body' => Type::string(),
],
]);
Wiring the Mutation Type
The mutation field takes the input object as an argument and returns the created entity (so clients can read back fields in the same round trip). Do validation and authorization inside the resolver, throwing on failure.
<?php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
$mutationType = new ObjectType([
'name' => 'Mutation',
'fields' => [
'createPost' => [
'type' => $postType,
'args' => ['input' => Type::nonNull($createPostInput)],
'resolve' => function ($root, array $args, $context) {
if (!$context['user']) {
throw new \RuntimeException('Unauthenticated');
}
return PostRepo::create($args['input'], $context['user']);
},
],
],
]);
Errors: Client-Safe vs Internal
By default graphql-php hides exception messages, showing Internal server error to avoid leaking internals. To surface a message to clients, implement GraphQL\Error\ClientAware and return true from isClientSafe(). Add machine-readable codes via extensions.
<?php
use GraphQL\Error\ClientAware;
class ValidationError extends \RuntimeException implements ClientAware {
public function isClientSafe(): bool { return true; }
// older versions also used getCategory(): string
}
Subscriptions: The Concept
A Subscription root type lets clients receive a stream of results when events occur (new message, price tick). The GraphQL spec defines subscription semantics, but graphql-php executes a single operation per call — it does not run a long-lived socket server itself. You provide the transport.
- graphql-php resolves the subscription payload for each event you push.
- A transport (WebSocket via Ratchet/Mercure/Pusher) delivers events to clients.
A Subscription Resolver Shape
In practice you split a subscription into a subscribe step (register interest, returns an event source) and a resolve step (map each event to the field's payload). Many PHP stacks pair graphql-php with Mercure or a pub/sub broker; the resolver below shows the per-event mapping graphql-php is responsible for.
<?php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
$subscriptionType = new ObjectType([
'name' => 'Subscription',
'fields' => [
'messageAdded' => [
'type' => $messageType,
'args' => ['channelId' => Type::nonNull(Type::id())],
// graphql-php resolves each pushed event into the payload;
// a WebSocket/Mercure transport drives when this runs.
'resolve' => fn($event) => $event['message'],
],
],
]);
Context Is Your Auth and DI Channel
The third resolver argument, $context, is built once per request and threaded into every resolver. It is the right place for the authenticated user, a database connection, and your DataLoaders. Centralizing auth here keeps resolvers thin — they ask the context who the user is rather than re-deriving it.
<?php
require 'vendor/autoload.php';
// Built once per HTTP request, passed to executeQuery():
$context = [
'user' => authenticate($_SERVER['HTTP_AUTHORIZATION'] ?? ''),
'db' => $pdo,
];
$resolve = function ($root, array $args, array $context) {
if ($context['user'] === null) {
throw new \RuntimeException('Unauthenticated');
}
return $context['db']->find($args['id']);
};
Quick Check
How do you make an exception message visible to GraphQL clients?
Recap
You learned the execution heart of GraphQL:
- Resolvers take
($value, $args, $context, $info); the default resolver reads keys/getters off the parent. - Resolution cascades parent-to-child — the source of N+1.
- Returning
Deferred/promises enables batching. - Mutations are sequential root fields using
InputObjectType; do auth/validation in the resolver. - Subscriptions define payload resolution while you supply the transport.
ClientAwarecontrols which error messages clients can see.
Next: killing the N+1 problem with DataLoader.
Frequently asked questions
Is the “Resolvers, Mutations and Subscriptions” lesson free?
Yes — the full text of “Resolvers, Mutations and Subscriptions” 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 “Resolvers, Mutations and Subscriptions”?
Fetch and change data through resolvers. 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 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Resolvers, Mutations and Subscriptions” 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