ayimdomnic / graph-ql-l5.3
A modern, feature-rich GraphQL package for Laravel — types, queries, mutations, subscriptions, pagination, and more.
Requires
- php: ^8.2
- ext-json: *
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/routing: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- illuminate/validation: ^10.0|^11.0|^12.0|^13.0
- overblog/dataloader-php: ^1.0
- webonyx/graphql-php: ^15.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- orchestra/workbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-19 09:42:31 UTC
README
A modern, feature-rich GraphQL package for Laravel.
Laragraph gives Laravel developers a clean, expressive, code-first API for building GraphQL services — powered by webonyx/graphql-php.
Features
| Capability | Status |
|---|---|
| Queries & Mutations | ✅ |
| Real-time Subscriptions (Laravel Broadcasting) | ✅ |
| Object / Input / Enum / Interface / Union types | ✅ |
| Custom scalars (DateTime, Date, JSON, Upload) | ✅ |
| Built-in argument validation (Laravel rules) | ✅ |
| Per-field authorization | ✅ |
| N+1-safe Eloquent relation batching | ✅ |
| Relay cursor pagination + simple paginator | ✅ |
| Batched queries | ✅ |
| File uploads (multipart spec) | ✅ |
| Multiple named schemas | ✅ |
| Query complexity & depth limiting | ✅ |
| Introspection toggle | ✅ |
| Per-field tracing (Apollo Tracing format) | ✅ |
| GraphiQL browser IDE | ✅ |
| Artisan generators | ✅ |
| Auto-discovery (no manual registration) | ✅ |
| Static analysis (PHPStan / Larastan) | ✅ |
Requirements
- PHP 8.2+
- Laravel 10 / 11 / 12
Installation
composer require ayimdomnic/laragraph
Laravel auto-discovers the package. Publish the config:
php artisan vendor:publish --tag=laragraph-config
Quick Start
1. Create a Type
php artisan laragraph:make:type UserType
// app/GraphQL/Types/UserType.php use Ayimdomnic\Laragraph\Support\Type; use GraphQL\Type\Definition\Type as GType; class UserType extends Type { protected array $attributes = [ 'name' => 'User', 'description' => 'A registered user.', ]; public function fields(): array { return [ 'id' => ['type' => GType::nonNull(GType::id())], 'name' => ['type' => GType::string()], 'email' => ['type' => GType::string()], ]; } }
2. Create a Query
php artisan laragraph:make:query UsersQuery
// app/GraphQL/Queries/UsersQuery.php use Ayimdomnic\Laragraph\Support\Query; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; class UsersQuery extends Query { public function type(): Type { return Type::listOf(app('laragraph')->type('User')); } public function args(): array { return [ 'limit' => ['type' => Type::int(), 'defaultValue' => 10], ]; } public function resolve(mixed $root, array $args, mixed $context, ResolveInfo $info): mixed { return \App\Models\User::limit($args['limit'])->get(); } }
3. Create a Mutation
php artisan laragraph:make:mutation CreateUserMutation
// app/GraphQL/Mutations/CreateUserMutation.php use Ayimdomnic\Laragraph\Support\Mutation; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; class CreateUserMutation extends Mutation { public function type(): Type { return app('laragraph')->type('User'); } public function args(): array { return [ 'name' => ['type' => Type::nonNull(Type::string())], 'email' => ['type' => Type::nonNull(Type::string())], ]; } public function rules(array $args = []): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'unique:users,email'], ]; } public function resolve(mixed $root, array $args, mixed $context, ResolveInfo $info): mixed { return \App\Models\User::create($args); } }
4. Register in config/laragraph.php
'types' => [ 'User' => \App\GraphQL\Types\UserType::class, ], 'schemas' => [ 'default' => [ 'query' => ['users' => \App\GraphQL\Queries\UsersQuery::class], 'mutation' => ['createUser' => \App\GraphQL\Mutations\CreateUserMutation::class], ], ],
5. Make requests
POST /graphql
Content-Type: application/json
{ "query": "{ users(limit: 5) { id name email } }" }
GraphiQL
Built-in browser IDE at /graphql/graphiql (enabled by default).
'graphiql' => ['enabled' => false], // disable
Artisan Generators
| Command | Creates |
|---|---|
laragraph:make:type UserType |
app/GraphQL/Types/UserType.php |
laragraph:make:query UsersQuery |
app/GraphQL/Queries/UsersQuery.php |
laragraph:make:mutation CreateUserMutation |
app/GraphQL/Mutations/CreateUserMutation.php |
laragraph:make:subscription UserCreatedSubscription |
app/GraphQL/Subscriptions/UserCreatedSubscription.php |
laragraph:make:input CreateUserInput |
app/GraphQL/Inputs/CreateUserInput.php |
Pagination
Relay Cursor Pagination
use Ayimdomnic\Laragraph\Pagination\ConnectionType; class UsersQuery extends Query { public function type(): Type { return new ConnectionType('UserConnection', app('laragraph')->type('User')); } public function args(): array { return ConnectionType::args(); // first, after, last, before } public function resolve(mixed $root, array $args, mixed $context, ResolveInfo $info): mixed { return ConnectionType::paginate(\App\Models\User::query(), $args); } }
{
users(first: 10) {
edges { cursor node { id name } }
pageInfo { hasNextPage endCursor total }
}
}
Simple Offset Pagination
return ConnectionType::simplePaginate(\App\Models\User::query(), $args); // → { data, total, per_page, current_page, last_page }
N+1-Safe Eloquent Relations
Every GraphQL request gets a fresh DataLoaderRegistry attached to $context. For hand-written batch loaders, extend BatchResolver:
use Ayimdomnic\Laragraph\DataLoader\BatchResolver; class UserLoader extends BatchResolver { public function batch(array $keys): array { return User::whereIn('id', $keys)->get()->keyBy('id')->toArray(); } } // In a resolver: return $context->dataLoaders->get(UserLoader::class)->load($root->user_id);
For a plain Eloquent relation, skip the hand-written loader entirely — Type::batchRelation() batches it through the relation's own eager-loading machinery (the same code path Model::with() uses), so it works for belongsTo, hasOne, hasMany, belongsToMany, and morph relations alike:
class PostType extends Type { public function fields(): array { return [ 'id' => GType::nonNull(GType::id()), 'comments' => GType::listOf(app('laragraph')->type('Comment')), ]; } protected function resolveCommentsField(mixed $root, array $args, mixed $context): mixed { return $this->batchRelation(Post::class, 'comments', $root, $context); } }
Regardless of how many Post parents are in the result set, comments resolves in a fixed, small number of queries per request instead of one query per post.
Authorization
public function authorize(mixed $root, array $args, mixed $context, ResolveInfo $info): bool { return $context->user()?->isAdmin() ?? false; }
false → AuthorizationException → extensions.category = 'authorization'.
Validation
public function rules(array $args = []): array { return ['email' => ['required', 'email']]; }
Errors appear in extensions.validation:
{
"errors": [{
"message": "Validation failed.",
"extensions": {
"category": "validation",
"validation": { "email": ["The email field is required."] }
}
}]
}
Built-in Scalars
'types' => [ 'DateTime' => \Ayimdomnic\Laragraph\Scalars\DateTimeType::class, 'Date' => \Ayimdomnic\Laragraph\Scalars\DateType::class, 'JSON' => \Ayimdomnic\Laragraph\Scalars\JsonType::class, 'Upload' => \Ayimdomnic\Laragraph\Scalars\UploadType::class, ],
Multiple Schemas
'schemas' => [ 'default' => ['query' => [...], 'mutation' => [...]], 'admin' => ['query' => [...], 'mutation' => [...], 'middleware' => ['auth:api', 'admin']], ],
Endpoints: POST /graphql and POST /graphql/admin.
Security
'security' => [ 'query_max_complexity' => 200, 'query_max_depth' => 10, 'disable_introspection' => true, // recommended in production ],
Batched Queries
[
{ "query": "{ users { id } }" },
{ "query": "mutation { createUser(name: \"Alice\", email: \"a@b.com\") { id } }" }
]
File Uploads
Follows the GraphQL multipart request spec.
'types' => ['Upload' => \Ayimdomnic\Laragraph\Scalars\UploadType::class], // In mutation args: 'avatar' => ['type' => app('laragraph')->type('Upload')] // In resolver — $args['avatar'] is \Illuminate\Http\UploadedFile $path = $args['avatar']->store('avatars', 'public');
Facade
use Ayimdomnic\Laragraph\Facades\Laragraph; $result = Laragraph::execute('{ users { id name } }'); $schema = Laragraph::schema('admin'); $type = Laragraph::type('User');
Subscriptions
webonyx/graphql-php has no subscription transport of its own, so Laragraph provides one on top of Laravel Broadcasting: the initial subscription request registers a subscriber and returns a channel; your app code later calls Laragraph::broadcast() to push a live update to every subscriber on that channel.
'subscriptions' => ['enabled' => true],
// app/GraphQL/Subscriptions/UserCreatedSubscription.php use Ayimdomnic\Laragraph\Support\Subscription; class UserCreatedSubscription extends Subscription { public function type(): Type { return app('laragraph')->type('User'); } public function subscribe(mixed $root, array $args, mixed $context, ResolveInfo $info): mixed { return 'users'; // the channel clients subscribe to } public function resolve(mixed $root, array $args, mixed $context, ResolveInfo $info): mixed { return $root; // $root is the payload passed to Laragraph::broadcast() } }
Trigger an update from anywhere — typically at the end of a mutation:
class CreateUserMutation extends Mutation { public function resolve(mixed $root, array $args, mixed $context, ResolveInfo $info): mixed { $user = \App\Models\User::create($args); Laragraph::broadcast('users', $user); return $user; } }
Client flow:
- POST the subscription operation like any other query:
{ "query": "subscription { userCreated { id name } }" }The response carries no data yet — instead:{ "data": { "userCreated": null }, "extensions": { "subscription": { "channel": "users", "subscriberId": "…" } } } - Listen for updates on that subscriber's private channel with Laravel Echo:
Echo.private(`graphql-subscriber.${subscriberId}`) .listen('.GraphQLSubscriptionUpdate', (payload) => { console.log(payload.data.userCreated); });
Delivery uses whichever broadcast driver your app has configured (Reverb, Pusher, …) — Laragraph only decides the channel and payload shape. Set 'subscriptions' => ['driver' => 'log'] to write updates to the log instead, useful for local development without a broadcast server.
Tracing
Enable per-field resolver timing in the Apollo Tracing format, understood out of the box by existing GraphQL tooling:
'tracing' => ['enabled' => true],
{
"extensions": {
"tracing": {
"version": 1,
"startTime": "2026-08-06T12:00:00.000Z",
"endTime": "2026-08-06T12:00:00.004Z",
"duration": 4200000,
"execution": {
"resolvers": [
{ "path": ["users", 0, "posts"], "parentType": "User", "fieldName": "posts", "returnType": "[Post]", "startOffset": 120000, "duration": 80000 }
]
}
}
}
}
Every resolved field is recorded — root Query/Mutation/Subscription fields and nested Type fields alike. Leave this off in production unless you're actively debugging performance; it adds a small wrapping cost to every resolver call.
Static Analysis
Larastan/PHPStan ships configured out of the box:
composer phpstan
Testing
composer test
Contributing
Contributions, issues, and feature requests are welcome!
License
MIT © Odhiambo Dormnic