ayimdomnic/graph-ql-l5.3
| Install | |
|---|---|
composer require ayimdomnic/graph-ql-l5.3 |
|
| Latest Version: | v3.1.1 |
| PHP: | ^8.2 | ^8.3 | ^8.4 | ^8.5 |
| License: | MIT |
| Last Updated: | Aug 6, 2026 |
| Links: | GitHub · Packagist |
Laragraph
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:
The response carries no data yet — instead:{ "query": "subscription { userCreated { id name } }" }{ "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
Related Packages
Laravel Package for TMDB ( The Movie Database ) API. Provides easy access to the...
Cloud native, API first Laravel eCommerce package with integrated AI for ultra-f...