Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Restql Laravel Package

gregorip02/restql

RESTQL is a Laravel package for quickly exposing Eloquent models as flexible REST APIs. It provides a query language for selecting fields, filtering, sorting, paginating, and including relationships, helping you build JSON endpoints with less boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin.

composer require gregorip02/restql

Before you commit, pin the constraint ("gregorip02/restql": "^1.0") and verify it resolves against your Laravel version — the last release was April 2021, so on Laravel 10/11 you may need --ignore-platform-reqs, a fork, or a replace entry. Check composer.json inside the package for the illuminate/* constraints before you build anything on top of it.

Where to look first, in this order:

  1. The README on GitHub — it carries the query-string grammar (fields, include, sorting, pagination).
  2. config/restql.php after php artisan vendor:publish — this is where the global defaults live (default field sets, max depth, parameter names).
  3. src/Queries/Query.php — the base query class. This is the real API surface; every feature is a protected property or an overridable method on this class.
  4. src/RestqlServiceProvider.php — shows what gets bound into the container and whether the trait is auto-registered.

First use case — kill over-fetching and N+1 on an index endpoint. Define one query class per resource and let the client narrow it:

namespace App\Queries;

use Gregori\Restql\Queries\Query;

class UserQuery extends Query
{
    protected $fields = ['id', 'name', 'email', 'created_at'];
    protected $relations = ['posts', 'posts.comments'];
}

Wire it into a controller, then hit GET /api/users?fields=id,name&include=posts. You get a whitelisted field set, eager-loaded relations, and a stable contract without hand-writing a transformer for each endpoint.

Implementation Patterns

One query class per resource, living next to the model. Treat App\Queries\UserQuery like a FormRequest: it is the authorization + shape boundary for one resource. Namespacing by API version (App\Queries\V2\UserQuery) makes versioning cheap later.

Two-tier whitelists. Keep $fields as the allowed set, and use a separate $defaults (or $required) list for what is returned when the client sends nothing. Never let an empty fields= fall through to SELECT *; make "no fields specified" mean "the default list", not "everything".

Relationship chains as the main lever. The package's value is letting clients ask for ?include=posts.comments.author in one request instead of three. Model this explicitly:

protected $relations = [
    'posts',
    'posts.comments',
    'posts.comments.author' => ['id', 'name'], // constrain nested selects
];

If your version supports per-relation field maps, use them — they are the cheapest defence against accidentally exposing a password_hash two levels deep.

Pair it with API Resources for the response envelope. Let RestQL decide what is loaded, and let JsonResource decide how it is serialized. This keeps pagination meta, links and included arrays consistent across endpoints, and means the query class never has to know about your envelope format.

Compose in a base controller.

abstract class ApiController extends Controller
{
    protected function resolve(string $queryClass, string $modelClass)
    {
        $this->authorize('viewAny', $modelClass);

        return app($queryClass)->collection($modelClass);
    }
}

This is where you put cross-cutting concerns: policy checks, tenant scoping (->where('tenant_id', ...)), and a hard cap on include depth.

Use it for read-heavy, filterable admin/table UIs first. SPA tables benefit most: sortable columns, per-column filtering, and relation loading all come from the query string, so you stop writing bespoke endpoints for each table. Public APIs are a harder sell — you lose response-shape stability the moment a client can choose fields.

Test the grammar, not the package. Assert on assertJsonStructure and a handful of golden query strings (?fields=id&include=posts, invalid field, unauthorized include). Snapshot the generated SQL with DB::listen if you need to prove eager loading is happening.

Gotchas and Tips

GPL-3.0 is the biggest one. Unlike most Laravel packages (MIT), this is a copyleft license. Internal or SaaS use is generally fine, but distributing a product that includes it can obligate you to release source. Get a legal read before shipping it in anything you hand to a customer; otherwise plan to fork it or reimplement the ~500 lines of query parsing yourself.

It resolves data; it does not authorize it. The package will happily load posts.comments for a user who shouldn't see them. Every relation in $relations needs a corresponding policy check — ideally a can() filter applied when building the eager-load array, or a viewComments gate checked in the query class before the relation is added.

select on a relation must include the foreign key. ?fields[posts]=title,body produces empty relations because user_id is gone. Always append the model's key and the relation's foreign key to any per-relation field list — this is the single most common "the package is broken" bug report.

Depth and breadth are a DoS vector. ?include=posts.comments.author.posts.comments... and ?include=a,b,c,... with unlimited values will generate enormous queries. Set a max depth and a max include count in config, and reject requests that exceed them with a 422 rather than letting the ORM decide.

Anything mapped to SQL must be whitelisted, not interpolated. Custom filters/sorts (if your version exposes them) that accept a column name from the query string are an injection route. Validate against the $fields array with in_array(..., true).

Unmaintained means you own the fork. Expect breakage on new Laravel majors (getQualifiedKeyName changes, Model::resolveRelationUsing internals) and on PHP 8.2+ dynamic-property deprecations in the package's own classes. Keep a patches/ directory and a composer.json repositories VCS entry so a fork is a one-line swap.

Debug with SQL, not with the response. \DB::listen(fn ($q) => logger($q->sql)) or Telescope will tell you instantly whether include produced a whereIn (good) or a loop of select * from posts where user_id = ? (N+1 — usually a mis-shaped relation name that silently fell back to lazy loading).

Cache carefully. Responses are a function of the full query string, so cache keys must include a normalized, sorted parameter string. Two clients sending ?fields=id,name and ?fields=name,id should hit the same key — normalize before hashing, or you'll get a fragmented cache with near-zero hit rate.

Extension point: override the base Query class in your own App\Queries\Query and swap it via the container binding. That is the cleanest place to add tenant scoping, default orderBy, or a ->withCount() pass — far better than subclassing every resource query individually.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core