rebing/graphql-laravel
Code-first GraphQL integration for Laravel based on webonyx/graphql-php. Define schemas, types, queries and mutations in PHP. Supports multiple schemas, per-schema middleware, resolver middleware, and n+1 prevention via dataloaders or SelectFields eager loading.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require rebing/graphql-laravel
php artisan vendor:publish --provider="Rebing\GraphQL\GraphQLServiceProvider"
Configure config/graphql.php (default schema, routes, middleware).
First Query:
php artisan make:graphql:query GetUser
php artisan make:graphql:type UserType
Register in config/graphql.php:
'schemas' => [
'default' => [
'query' => [App\GraphQL\Queries\GetUser::class],
'types' => [App\GraphQL\Types\UserType::class],
],
],
Test Endpoint:
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "{ user(id: 1) { id name email } }"}' \
http://localhost:8000/graphql
// app/GraphQL/Queries/GetUser.php
public function resolve($root, array $args) {
return User::findOrFail($args['id']);
}
Key: Use SelectFields for eager loading:
use Rebing\GraphQL\Support\Facades\SelectFields;
public function resolve($root, array $args) {
return SelectFields::resolve(
User::findOrFail($args['id']),
$this->getSelectFields(),
UserType::class
);
}
'schemas' => [
'admin' => [
'query' => [AdminQuery::class],
'middleware' => [AdminAuthMiddleware::class],
],
'public' => [...],
],
#[GraphQLSchema('admin')] on controllers.| Strategy | Use Case | Example |
|---|---|---|
| Dataloaders | Batch DB queries (n+1 problem) | UserLoader::batchLoadByIds() |
| SelectFields | Eloquent eager loading | SelectFields::resolve($model) |
Example: Dataloader Integration
// app/GraphQL/Types/UserType.php
public function fields() {
return [
'posts' => [
'type' => Type::listOf(PostType::class),
'resolve' => fn($user) => $this->loaders->get(UserLoader::class)->load($user->id),
],
];
}
resolve{FieldName}Field():
public function resolveAuthorField() {
return $this->author; // Eloquent relationship
}
resolve():
public function resolve($root, array $args) {
return User::with(['posts' => fn($q) => $q->where('published', true)])
->find($args['id']);
}
public function args() {
return [
'email' => [
'type' => Type::nonNull(Type::string()),
'rules' => ['required', 'email'],
],
];
}
rules() method:
public function rules() {
return [
'email' => ['required', 'email'],
'password' => ['required', 'min:8'],
];
}
// app/GraphQL/Middleware/LogResolver.php
public function handle($root, $args, $context, $info, Closure $next) {
Log::info("Resolving {$info->parentType->name}.{$info->fieldName}");
return $next($root, $args, $context, $info);
}
Register in config/graphql.php:
'resolver_middleware_append' => [App\GraphQL\Middleware\LogResolver::class],
// app/GraphQL/Middleware/Execution/ValidateSchema.php
public function handle($schemaName, Schema $schema, OperationParams $params, $root, $context, Closure $next) {
if (config('graphql.disable_introspection') && $params->getQuery() === introspectionQuery()) {
throw new \RuntimeException('Introspection disabled');
}
return $next($schemaName, $schema, $params, $root, $context);
}
$response = $this->httpGraphql('{ user(id: 1) { name } }');
$response->assertJson(['data' => ['user' => ['name' => 'John']]]);
$this->assertSchemaHasType(UserType::class);
$this->assertSchemaHasQuery(GetUser::class);
N+1 Queries:
SelectFields or Dataloaders for nested relationships.SelectFields::resolve() for Eloquent models or Dataloader for custom queries.// ❌ Bad (n+1)
public function resolvePosts() { return $this->posts; }
// ✅ Good (eager load)
public function resolvePosts() {
return SelectFields::resolve($this->posts, $this->getSelectFields(), PostType::class);
}
Circular Dependencies:
User → Post → User).lazy() for circular references:
'posts' => [
'type' => Type::listOf(PostType::class),
'resolve' => fn($user) => $user->posts->load('user'), // Avoid circular load
],
Introspection:
GRAPHQL_DISABLE_INTROSPECTION=true).GRAPHQL_DISABLE_INTROSPECTION=false
Query Depth:
user.posts.comments.user.posts...).config/graphql.php:
'query_depth_limit' => 10,
Type Registration:
config/graphql.php.GraphQL::type('User') in resolvers to auto-register.Enable Tracing:
'tracing' => [
'driver' => 'opentelemetry',
'enabled' => env('GRAPHQL_TRACING_ENABLED', false),
],
Log Resolvers: Add middleware to log resolver calls:
// app/GraphQL/Middleware/LogResolver.php
public function handle($root, $args, $context, $info, Closure $next) {
Log::debug("Resolving {$info->parentType->name}.{$info->fieldName}");
return $next($root, $args, $context, $info);
}
Query Variables:
variables in the request body:
{
"query": "{ user(id: $id) { name } }",
"variables": { "id": 1 }
}
Error Handling:
'error_formatter' => App\GraphQL\ErrorFormatter::class,
// app/GraphQL/Scalars/JsonScalar.php
class JsonScalar extends ScalarType {
public function serialize($value) {
return json_encode($value);
}
public function parseValue($value) {
return json_decode($value, true);
}
public function parseLiteral(ast\Node $valueNode) {
return json_decode($valueNode->value, true
How can I help you explore Laravel packages today?