Skip to content

Laravel Integration

Configuration

config/graphql.php controls the endpoint, GraphiQL, schema sources, batching, error handling and subscriptions:

return [
    'route' => ['uri' => '/graphql', 'middleware' => ['api']],
    'graphiql' => ['enabled' => true, 'uri' => '/graphiql'],
    'schema' => [
        'types' => [/* code-first type/resolver classes */],
        'sdl_path' => [/* paths to .graphql files */],
    ],
    'debug' => env('GRAPHQL_DEBUG', env('APP_DEBUG', false)),
];

Resolver conventions

A root Query/Mutation field resolves in this order: an explicit resolver map entry, a directive (@all, @field, …), then convention — an invokable class <namespace>\<StudlyField>:

// config/graphql.php
'namespaces' => [
    'queries' => 'App\\GraphQL\\Queries',
    'mutations' => 'App\\GraphQL\\Mutations',
],
type Query { latestPosts: [Post!]! }   # -> App\GraphQL\Queries\LatestPosts::__invoke
namespace App\GraphQL\Queries;

final class LatestPosts
{
    public function __invoke(mixed $root, array $args, mixed $context, mixed $info): iterable
    {
        return \App\Models\Post::latest()->limit(10)->get();
    }
}

The class is resolved through the container (so dependencies are injected). If no such class exists the field falls back to the default resolver. make:graphql-query / make:graphql-mutation scaffold these.

Artisan commands

php artisan graphql:print                 # print the schema as SDL
php artisan graphql:print --write         # write SDL to base_path/schema.graphql
php artisan graphql:print --write=docs/schema.graphql
php artisan graphql:validate              # validate the configured schema (CI guard)
php artisan graphql:lint                  # report unsupported directives (migration aid)
php artisan graphql:cache                 # cache the parsed SDL (AST) for faster boots
php artisan graphql:clear                 # drop the schema cache + persisted-query (APQ) entries
php artisan graphql:subscriptions:serve   # run the graphql-ws WebSocket server (Swoole)

php artisan make:graphql-type UserType                 # code-first object type
php artisan make:graphql-directive UppercaseDirective  # build-time directive
php artisan make:graphql-scalar DateType               # custom scalar
php artisan make:graphql-query FetchUser               # single-field query resolver (@field)
php artisan make:graphql-mutation CreateUser           # single-field mutation resolver (@field)

Schema caching

For SDL schemas, cache the parsed AST so it is not re-parsed on every boot:

// config/graphql.php
'schema' => [
    'cache' => [
        'enabled' => env('GRAPHQL_SCHEMA_CACHE', false),   // true in production
        'path'    => storage_path('framework/cache/graphql-schema.cache'),
    ],
],

Run php artisan graphql:cache on deploy and php artisan graphql:clear to invalidate it. Caching applies to SDL schemas; factory/code-first schemas build directly.

vendor:publish exposes the config and views (and command stubs):

php artisan vendor:publish --tag=graphql-config
php artisan vendor:publish --tag=graphql-views
php artisan vendor:publish --tag=graphql-stubs   # customise make:graphql-* output

Published stubs land in stubs/graphql/ and take precedence over the package's built-in stubs.

Endpoint

POST /graphql accepts a single operation or a batched array:

{ "query": "{ hello }", "variables": {}, "operationName": null }

GraphQL errors are returned with HTTP 200 following the spec.

Authorization

Route-level middleware comes from config. Field-level authorization is exposed to resolvers through the execution context, delegating to Laravel Gates/policies.

Validation

Argument validation integrates with Laravel's validator; failures surface under extensions.validation in the error response.

Generating types from your app

Derive GraphQL types from existing Laravel artifacts instead of re-declaring them:

  • Generation\ModelTypeGenerator::fromModel(User::class) — object type from an Eloquent model's key, fillable attributes, casts and timestamps.
  • Generation\ValidationInputGenerator::fromRequest(StoreUserRequest::class) / ->fromRules([...]) — input type from validation rules (required → non-null).
  • Generation\ResponseTypeGenerator::fromArray($resource->toArray($request), 'UserResource') — object type inferred from a JSON resource / response shape (nested arrays → nested types).

array/json casts and array rules map to a bundled JSON scalar. See the README for a full end-to-end example composing generated types into a schema.

File uploads

Add the Upload scalar (Hmennen90\GraphQL\Support\UploadType::make()) to your schema and send a GraphQL multipart request (operations + map + files). Uploaded files arrive as Illuminate\Http\UploadedFile instances in your resolver arguments.

Persisted queries (APQ)

Enable Apollo Automatic Persisted Queries:

'persisted_queries' => ['enabled' => true],

Clients send extensions.persistedQuery.sha256Hash; the first request registers the query, subsequent requests may send the hash alone.

Authorization directive

@can(ability: "…") gates a field behind a Laravel Gate ability, checked before resolution. Register it as a schema directive:

SchemaBuilder::fromSdl($sdl, $resolvers, schemaDirectives: [
    'can' => new \Hmennen90\GraphQL\Directives\CanDirective(),
]);

Relay pagination

Hmennen90\GraphQL\Support\Relay\Relay builds connection/edge/pageInfo types and cursor-based connection payloads from an array of nodes.

HTTP caching (@cacheControl)

Annotate fields with @cacheControl(maxAge: Int, scope: String) and enable the header:

'cache_control' => ['enabled' => true],

For a query, the endpoint emits Cache-Control using the minimum maxAge of the selected fields and private if any field is private. A field without a hint makes the response no-store. Combined with Automatic Persisted Queries (GET + hash), this enables CDN/HTTP caching — the safe, idiomatic GraphQL approach (no per-user response store).

Subscriptions

Subscriptions use Laravel broadcasting. Enable them in config:

'subscriptions' => ['enabled' => true, 'broadcaster' => 'reverb'],

When a client sends a subscription operation, the endpoint registers a subscriber and returns the channel to listen on:

{ "data": null, "extensions": { "subscription": { "channel": "graphql.<id>" } } }

The client subscribes to that channel via Laravel Echo. When your application has new data, broadcast it — the stored operation is re-executed against the payload and pushed to every subscriber on that topic:

app(\Hmennen90\GraphQL\Subscriptions\SubscriptionManager::class)
    ->broadcast('postAdded', $post);

The topic defaults to the subscription's root field name.

graphql-ws (WebSocket) transport

The package ships a spec-compliant graphql-ws protocol handler (connection_init/ack, subscribe/next/complete, ping/pong), Hmennen90\GraphQL\Subscriptions\GraphqlWs\ProtocolHandler. It is transport-agnostic — drive it from any WebSocket server via the small Connection interface.

Because cboden/ratchet is incompatible with Laravel 12's Symfony 7, no WebSocket server is bundled (keeping the package dependency-light). Wire the handler to a server you control — for example OpenSwoole:

$handler = new ProtocolHandler(app(GraphQL::class), app(ResponseBuilder::class));

$server = new Swoole\WebSocket\Server('0.0.0.0', 9501);
$server->on('message', function ($server, $frame) use ($handler) {
    $handler->onMessage(new SwooleConnection($server, $frame->fd), json_decode($frame->data, true));
});
// Redis pub/sub bridge: app calls broadcast -> WS server calls $handler->publish($topic, $event)
$server->start();

A bundled Swoole/OpenSwoole driver is available. With the extension installed, run:

php artisan graphql:subscriptions:serve --host=0.0.0.0 --port=9501

Set graphql.subscriptions.driver to redis; application code that calls SubscriptionManager::broadcast($topic, $event) then publishes to Redis pub/sub, and the server fans events out to connected clients. Without the extension the command exits with an instruction to install it — the core package stays dependency-light.

Server-Sent Events (SSE) transport

For a transport that needs no WebSocket server or extension, enable the graphql-sse endpoint — it streams over plain HTTP:

// config/graphql.php
'subscriptions' => [
    'sse' => ['enabled' => true, 'uri' => '/graphql/sse', 'middleware' => ['api']],
],

A query or mutation POSTed to the endpoint streams one next frame then complete. For live subscriptions, bind Hmennen90\GraphQL\Subscriptions\Sse\EventStream to a Redis-backed implementation whose listen($topic) yields events as they are published; the controller writes each as a next frame until the client disconnects. The transport core (SseProtocolHandler) is transport-agnostic and unit-tested.