rebing/graphql-laravel-select-fields
Laravel package for rebing/graphql-laravel that adds select-field support to GraphQL queries, letting resolvers fetch only the Eloquent columns and relationships requested by clients. Reduce over-fetching, simplify field selection, and improve query performance.
rebing/graphql-laravel wraps webonyx/graphql-php in Laravel's container, routing, config, and validation. You write schema classes; the package resolves them and serves a single (or multi) endpoint.
Install and publish the config:
composer require rebing/graphql-laravel
php artisan vendor:publish --provider="Rebing\GraphQL\GraphQLServiceProvider" --tag=config
Package discovery registers the provider and the GraphQL facade (Rebing\GraphQL\Support\Facades\GraphQL).
Open config/graphql.php first — it is the real entry point. Confirm three keys before writing any code:
route.prefix (default graphql) and route.middlewaredefault_schema (default)graphiql (dev-only GUI, default /graphiql)Generate a starter schema with the package's generators:
php artisan make:graphql:type UserType
php artisan make:graphql:query UserQuery
php artisan make:graphql:mutation CreateUserMutation
php artisan make:graphql:scalar DateTime
Register the generated classes in the schemas.default block (query, mutation, types). Nothing is auto-discovered — an unregistered class is invisible to the schema.
First use case: a read-only type + query. A Type declares $attributes['name'] and a fields() array; a Query declares args(), type(), and resolve(). POST to /graphql from /graphiql and you have a working endpoint.
This 1.0.0 stable release is aligned with the 10.x line of the package. If you are coming from 9.x, diff your published config with php artisan vendor:publish --provider="Rebing\GraphQL\GraphQLServiceProvider" --tag=config --force before assuming defaults still apply, and check composer.json constraints — a major bump drops older Laravel/PHP combinations.
First verification: curl -X POST /graphql -d '{"query":"{ __typename }"}' should return JSON, not a 404 — if it 404s, the route prefix or middleware is the culprit, not your schema.
Types as the unit of reuse. Types are registered by name and looked up lazily via the facade. Use GraphQL::type('User') rather than importing the class, because it respects the registry and avoids instantiating types that a given query never touches.
public function fields(): array
{
return [
'id' => ['type' => Type::nonNull(Type::id())],
'name' => ['type' => Type::string()],
'posts' => [
'type' => Type::listOf(GraphQL::type('Post')),
'resolve' => fn ($user) => $user->posts()->published()->get(),
],
];
}
Resolvers on fields are how you keep the query layer thin — push relation logic into the type, not into every query that returns a user.
Query + args + validation in one class. Arguments carry rules, which the package turns into a Laravel validator. On failure it throws Rebing\GraphQL\Error\ValidationError, which the error formatter serialises into the errors array — not a 422.
public function args(): array
{
return [
'id' => ['name' => 'id', 'type' => Type::nonNull(Type::int()),
'rules' => ['required', 'integer', 'exists:users,id']],
];
}
public function resolve($root, array $args, $context, ResolveInfo $info)
{
return User::findOrFail($args['id']);
}
Mutations return a payload type, not a scalar. Model the mutation result as its own Type (CreateUserPayload with user and errors) so clients can branch on domain errors without inspecting top-level GraphQL errors. Throw for exceptional cases, return structured errors for expected ones.
Multiple schemas. Declare schemas.public and schemas.admin in config; each gets its own query/mutation roots, types, and middleware. Select via the schema segment on the route (or GraphQL::schema('admin') in tests). This is the cleanest way to keep __schema introspection of internal mutations off the public endpoint.
Pagination. Use the bundled Rebing\GraphQL\Support\PaginationType (or SimplePaginationType) and return a LengthAwarePaginator from the resolver — the type maps data, total, per_page, current_page, last_page, next_page_url, from, to for you. Override via pagination_type if you need cursor-style pages instead.
Authorization in two layers. Coarse: route.middleware / schemas.*.middleware (auth:sanctum, can:viewAdmin). Fine: per-query/mutation authorize() or per-field 'authorize' => fn ($root, $args, $ctx) => ... in fields(). Do both — middleware protects the endpoint, field-level checks protect against a user crafting a query that traverses into someone else's data.
Error formatting. Point error_formatter at your own class to control what leaks to clients, and errors_handler at a reporter to forward exceptions to Sentry/Flare. Keep the GraphQL spec shape (errors[].message, .locations, .path, .extensions) and add machine-readable codes in extensions rather than stuffing them into message.
Testing. Skip HTTP for schema tests: GraphQL::query($query, $variables) returns the full result array and lets you assert on errors precisely. For integration, $this->postJson('/graphql', [...])->assertJsonPath('data.user.id', 1) — remember the status is 200 even on failure.
Custom scalars. Extend Scalar, implement serialize/parseValue/parseLiteral, register in types. Date handling is the classic case: serialise to ISO-8601 and parse back to Carbon in one place rather than in every resolver.
File uploads go through the bundled UploadType scalar with multipart/form-data; the resolver receives an UploadedFile-like object, so validation rules behave as usual.
config:cache and closures. Any closure left in config/graphql.php (especially types or middleware) breaks php artisan config:cache. Use class-string arrays and let the container build them.GraphQL::type('Post') throws at schema-build time if Post is not in schemas.*.types or types. Circular references (User → Post → User) work fine because lookup is by name; class imports do not solve this.200 with an errors array. Middleware that assumes 401/422 semantics will misbehave, and API clients that only check status codes will silently treat errors as success.posts field resolving $user->posts per row will issue one query per user. Either eager-load in the parent resolver (User::with('posts')) or wire a DataLoader (overblog/dataloader-php) into the GraphQL context. This is the most common performance regression in this package.route.middleware, may be publicly reachable. Restrict it to local env or wrap it in auth, and consider setting security.disable_introspection on production schemas so clients cannot enumerate internal types.security.query_max_depth and security.query_max_complexity are your only defence against a hostile nested query. Set them; there is no automatic limit.context passed to resolvers is whatever the controller builds (typically the request). If you authenticate via Sanctum in middleware but resolve ownership via auth()->user(), confirm the guard matches — auth:sanctum plus auth()->user() on the default session guard is a frequent source of "user is null in resolver".APP_DEBUG=true the default formatter exposes exception messages and traces. Verify production output before shipping; the difference between "Internal error" and a full stack trace is one env var.config/graphql.php in a cached environment appears to do nothing. Run php artisan config:clear when a newly registered type "does not exist".path. A partial failure still returns 200.execution_method, schema-level middleware, the GraphQL::addType() runtime registry, and a custom defaultFieldResolver in config — the last is the lever for Eloquent attribute resolution if you want $user->name to work without declaring a resolver on every field.ErrorFormatter/ErrorHandler signatures — those are the classes most likely to shift across a major.How can I help you explore Laravel packages today?