SelectFields for graphql-laravel

June 18, 2026 ยท View on GitHub

Latest Stable Version License Tests Downloads Get on Slack

Optimizes GraphQL queries backed by Eloquent models. Analyzes the GraphQL request's field selection to generate minimal SELECT columns and eager-load only the requested relations - preventing N+1 queries and over-fetching.

This is an optional companion package for rebing/graphql-laravel.

Requirements

  • PHP ^8.2
  • Laravel 12+
  • rebing/graphql-laravel 10.0.0+

Installation

composer require rebing/graphql-laravel-select-fields

The package auto-discovers its service provider. No manual registration is needed. On boot it:

  1. Registers a ResolverParameterInjector so that Closure and SelectFields type-hints in resolver methods work automatically.
  2. Replaces the core pagination types with SelectFields-aware subclasses that implement WrapType and mark metadata fields as non-selectable.

Quick Start

1. Add model to your Type:

use App\Models\User;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Type as GraphQLType;

class UserType extends GraphQLType
{
    protected $attributes = [
        'name'  => 'User',
        'model' => User::class,
    ];

    public function fields(): array
    {
        return [
            'id'    => ['type' => Type::nonNull(Type::id())],
            'email' => ['type' => Type::nonNull(Type::string())],
            'posts' => [
                'type' => Type::listOf(GraphQL::type('Post')),
            ],
        ];
    }
}

2. Use $getSelectFields in your Query:

use Closure;
use App\Models\User;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;
use Rebing\GraphQL\Support\SelectFields;

class UsersQuery extends Query
{
    protected $attributes = [
        'name' => 'users',
    ];

    public function type(): Type
    {
        return Type::listOf(GraphQL::type('User'));
    }

    public function args(): array
    {
        return [
            'id'    => ['type' => Type::string()],
            'email' => ['type' => Type::string()],
        ];
    }

    public function resolve($root, array $args, $context, ResolveInfo $info, Closure $getSelectFields)
    {
        /** @var SelectFields $fields */
        $fields = $getSelectFields();
        $select = $fields->getSelect();
        $with = $fields->getRelations();

        return User::select($select)->with($with)->get();
    }
}

When a client queries { users { id email posts { title } } }, SelectFields generates:

SELECT "users"."id", "users"."email" FROM "users";
SELECT "posts"."title", "posts"."user_id" FROM "posts" WHERE "posts"."user_id" IN (?, ?);

Only the requested columns are fetched, and the posts relation is eager-loaded in a single query.

Usage

Resolver Injection Patterns

SelectFields can be injected into your resolve() method in two ways.

public function resolve($root, array $args, $context, ResolveInfo $info, Closure $getSelectFields)
{
    /** @var SelectFields $fields */
    $fields = $getSelectFields();
    $select = $fields->getSelect();
    $with = $fields->getRelations();

    return User::select($select)->with($with)->get();
}

The SelectFields instance is only constructed when you call $getSelectFields(). If your resolver has an early return path (cache hit, authorization check), you avoid the cost of walking the query plan.

Class (eager)

use Rebing\GraphQL\Support\SelectFields;

public function resolve($root, array $args, $context, ResolveInfo $info, SelectFields $fields)
{
    $select = $fields->getSelect();
    $with = $fields->getRelations();

    return User::select($select)->with($with)->get();
}

The SelectFields instance is constructed before your resolver runs.

Type Configuration

The model Attribute

The model attribute on your Type's $attributes array is required for SelectFields to work. It enables:

  • Table-qualified column names ("users"."id" instead of "id")
  • Automatic primary key inclusion in SELECT
  • Eloquent relation traversal for eager loading
protected $attributes = [
    'name'  => 'User',
    'model' => User::class,
];

Without model, SelectFields operates in a degraded mode - no table qualification, no primary key inclusion, no relation detection.

Field Configuration Keys

These keys can be placed in the arrays returned by your Type's fields() method:

KeyTypeDefaultPurpose
selectablebooltrueWhether to include the field in SQL SELECT. Set to false for computed/virtual fields that have no database column (e.g. accessors).
is_relationbooltrueWhether sub-fields represent an Eloquent relationship. Set to false for JSON columns or cast arrays.
alwaysstring|string[]-Additional columns always included in SELECT when this field is requested. Useful for computed properties that depend on other columns.
queryClosure-Custom query callback applied to the Eloquent eager-loading query for this relation.
aliasstring|Closure|Expressionfield nameMaps a GraphQL field name to a different database column or relation method name.

Eager Loading Relationships

The profile and posts relations must also exist on the User Eloquent model. If some fields are required for the relation to load or for validation, you can define an always attribute that will add the given attributes to select.

The attribute can be a comma separated string or an array of attributes to always include.

// Array form:
'always' => ['title', 'body'],
// String form (comma-separated):
'always' => 'title,body',
declare(strict_types = 1);
namespace App\GraphQL\Types;

use App\Models\User;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Type as GraphQLType;

class UserType extends GraphQLType
{
    protected $attributes = [
        'name'          => 'User',
        'description'   => 'A user',
        'model'         => User::class,
    ];

    public function fields(): array
    {
        return [
            'uuid' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The uuid of the user'
            ],
            'email' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The email of user'
            ],
            'profile' => [
                'type' => GraphQL::type('Profile'),
                'description' => 'The user profile',
            ],
            'posts' => [
                'type' => Type::listOf(GraphQL::type('Post')),
                'description' => 'The user posts',
                // Can also be defined as a string
                'always' => ['title', 'body'],
            ]
        ];
    }
}

At this point we have a profile and a post type as expected for any model:

class ProfileType extends GraphQLType
{
    protected $attributes = [
        'name'          => 'Profile',
        'description'   => 'A user profile',
        'model'         => UserProfileModel::class,
    ];

    public function fields(): array
    {
        return [
            'name' => [
                'type' => Type::string(),
                'description' => 'The name of user'
            ]
        ];
    }
}
class PostType extends GraphQLType
{
    protected $attributes = [
        'name'          => 'Post',
        'description'   => 'A post',
        'model'         => PostModel::class,
    ];

    public function fields(): array
    {
        return [
            'title' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The title of the post'
            ],
            'body' => [
                'type' => Type::string(),
                'description' => 'The body the post'
            ]
        ];
    }
}

Custom Relation Queries

You can specify a query callback that will be applied to the Eloquent eager-loading query for a relation:

class UserType extends GraphQLType
{

    // ...

    public function fields(): array
    {
        return [
            // ...

            // Relation
            'posts' => [
                'type'          => Type::listOf(GraphQL::type('Post')),
                'description'   => 'A list of posts written by the user',
                'args'          => [
                    'date_from' => [
                        'type' => Type::string(),
                    ],
                 ],
                // $args are the local arguments passed to the relation
                // $query is the relation builder object
                // $ctx is the GraphQL context (customizable via execution middleware)
                // The return value should be the query builder or void
                'query'         => function (array $args, $query, $ctx): void {
                    $query->addSelect('some_column')
                          ->where('posts.created_at', '>', $args['date_from']);
                }
            ]
        ];
    }
}

Pagination

Pagination will be used if a query or mutation returns a PaginationType.

Note that unless you use resolver middleware, you will have to manually supply both the limit and page values:

declare(strict_types = 1);
namespace App\GraphQL\Queries;

use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;

class PostsQuery extends Query
{
    public function type(): Type
    {
        return GraphQL::paginate('posts');
    }

    // ...

    public function resolve($root, array $args, $context, ResolveInfo $info, Closure $getSelectFields)
    {
        $fields = $getSelectFields();

        return Post::with($fields->getRelations())
            ->select($fields->getSelect())
            ->paginate($args['limit'], ['*'], 'page', $args['page']);
    }
}

Query posts(limit:10,page:1){data{id},total,per_page} might return:

{
    "data": {
        "posts": {
            "data": [
                {"id": 3},
                {"id": 5}
            ],
            "total": 21,
            "per_page": 10
        }
    }
}

Note that you need to add the extra data object when you request paginated resources, as the returned data gives you the paginated resources in a data object at the same level as the returned pagination metadata.

Simple Pagination

Simple Pagination will be used if a query or mutation returns a SimplePaginationType.

class PostsQuery extends Query
{
    public function type(): Type
    {
        return GraphQL::simplePaginate('posts');
    }

    // ...

    public function resolve($root, array $args, $context, ResolveInfo $info, Closure $getSelectFields)
    {
        $fields = $getSelectFields();

        return Post::with($fields->getRelations())
            ->select($fields->getSelect())
            ->simplePaginate($args['limit'], ['*'], 'page', $args['page']);
    }
}

SimplePaginationType exposes the following fields: data (the paginated items), per_page, current_page, from, to, and has_more_pages. Unlike full pagination, total and last_page are not available.

Cursor Pagination

Cursor Pagination will be used if a query or mutation returns a CursorPaginationType.

class PostsQuery extends Query
{
    public function type(): Type
    {
        return GraphQL::cursorPaginate('posts');
    }

    // ...

    public function resolve($root, array $args, $context, ResolveInfo $info, Closure $getSelectFields)
    {
        $fields = $getSelectFields();

        return Post::with($fields->getRelations())
            ->select($fields->getSelect())
            ->cursorPaginate($args['limit'], ['*'], 'cursorName', $args['cursor']);
    }
}

CursorPaginationType exposes the following fields: data (the paginated items), per_page, previous_cursor (String, nullable), and next_cursor (String, nullable).

Pagination type auto-replacement

This package automatically replaces the core pagination types with SelectFields-aware subclasses. The subclasses implement WrapType (so SelectFields can traverse into the paginated data) and mark metadata fields like total, per_page, etc. as selectable: false (so they are not included in SQL SELECT).

If you have set a custom pagination type in your config, the package will not override it.

JSON Columns

When using JSON columns in your database, the field won't be defined as a "relationship", but rather a simple column with nested data. Use the is_relation attribute to tell SelectFields not to treat it as an Eloquent relation:

class UserType extends GraphQLType
{
    // ...

    public function fields(): array
    {
        return [
            // ...

            // JSON column containing all posts made by this user
            'posts' => [
                'type'          => Type::listOf(GraphQL::type('Post')),
                'description'   => 'A list of posts written by the user',
                // Now this will simply request the "posts" column, and it won't
                // query for all the underlying columns in the "post" object
                // The value defaults to true
                'is_relation' => false
            ]
        ];
    }

    // ...
}

Wrap Types

If you use SelectFields in a query that returns a wrap type, your wrapper class must implement the WrapType marker interface. This tells SelectFields to look through the wrapper's data field to find the underlying model type and generate the correct SELECT/WITH clauses.

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Contracts\WrapType;
use Rebing\GraphQL\Support\Facades\GraphQL;

class PostWrappedType extends ObjectType implements WrapType
{
    public function __construct()
    {
        parent::__construct([
            'name' => 'PostWrapped',
            'fields' => fn () => [
                'data' => [
                    'type' => Type::listOf(GraphQL::type('Post')),
                    'is_relation' => false,
                ],
                'message' => [
                    'type' => Type::string(),
                    'selectable' => false,
                ],
            ],
        ]);
    }
}

The package's pagination types already implement this interface. Custom pagination classes configured via the pagination_type, simple_pagination_type, or cursor_pagination_type config keys must also implement it.

Abstract Types (Unions and Interfaces)

When using SelectFields with union or interface types, custom query callbacks on relation fields defined in member/concrete types are supported. SelectFields will match the concrete type at eager-load time and apply the callback automatically.

Note: When a query includes inline fragments on multiple member types that each request different relations, SelectFields will merge all requested relations into the eager-load set.

Note: For union types, SelectFields cannot determine the concrete type at query-build time, so it uses SELECT * instead of selecting specific columns.

API Reference

SelectFields

use Rebing\GraphQL\Support\SelectFields;

// Constructed automatically via DI - you rarely need the constructor directly
$fields = new SelectFields($parentType, $queryArgs, $ctx, $fieldsAndArguments);

// Get the columns to select
$fields->getSelect();   // array<int, string|Expression>

// Get the relations to eager-load (with constrained closures)
$fields->getRelations(); // array<string, Closure|mixed>

WrapType

use Rebing\GraphQL\Support\Contracts\WrapType;

// Marker interface - no methods to implement
class MyCustomWrapper extends ObjectType implements WrapType { ... }

SelectFieldsParameterInjector

Implements Rebing\GraphQL\Support\Contracts\ResolverParameterInjector. This is registered automatically by the service provider. You only need to interact with it if you're building a custom SelectFields subclass:

use Rebing\GraphQL\Support\Field;
use Rebing\GraphQL\Support\SelectFieldsParameterInjector;

// Registered automatically - shown here for reference
Field::registerParameterInjector(new SelectFieldsParameterInjector());

Known Limitations

  • Resolving fields via aliases will only resolve them once, even if the fields have different arguments (Issue).

License

MIT