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

Jsonapi Rql Finder Bundle Laravel Package

krilo89/jsonapi-rql-finder-bundle

A Symfony bundle that plugs RQL (Resource Query Language) filtering into JSON:API finder workflows, translating query strings into Doctrine queries so clients can filter, sort, and paginate resources through clean, standardized URLs.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: despite the "Laravel developer" framing, krilo89/jsonapi-rql-finder-bundle is a Symfony bundle, not a Laravel package. It plugs into paknahad/jsonapi-bundle (also Symfony). If your project is Laravel, you are signing up to port a small amount of glue code; if it's Symfony, this is drop-in-ish.

Minimal setup:

composer require krilo89/jsonapi-rql-finder-bundle paknahad/jsonapi-bundle

Register the bundle in config/bundles.php:

return [
    // ...
    Paknahad\JsonApiBundle\PaknahadJsonApiBundle::class => ['all' => true],
    Krilo89\JsonApiRqlFinderBundle\JsonApiRqlFinderBundle::class => ['all' => true],
];

Then expose a resource the way paknahad/jsonapi-bundle expects and point it at the RQL finder for that entity. Typical first call:

GET /api/articles?filter=eq(status,published)&sort=-createdAt&limit=20&offset=0

Where to look first (in this order):

  1. README.md on the repo — the only docs that exist; there are no tests/docs site.
  2. The bundle's DependencyInjection/ and Resources/config/services.yaml — reveals which service IDs you can override.
  3. The finder class itself — that's the thing you'll subclass or decorate.
  4. paknahad/jsonapi-bundle's finder/repository contract — you need to satisfy its interface, not this bundle's.

First use case worth proving out: a single read-only list endpoint with filter, sort, and limit/offset. Don't start with relationship filters or compound operators; those are where the sharp edges are.

Implementation Patterns

One finder per resource, explicit field whitelist

The day-to-day pattern is: a small finder class per Doctrine entity that receives the parsed RQL, validates fields against an allowlist, and compiles a QueryBuilder.

final class ArticleRqlFinder implements FinderInterface
{
    private const FILTERABLE = ['status', 'authorId', 'createdAt', 'title'];
    private const SORTABLE   = ['createdAt', 'title'];

    public function __construct(private EntityManagerInterface $em) {}

    public function find(array $rql): iterable
    {
        $qb = $this->em->createQueryBuilder()
            ->select('a')->from(Article::class, 'a');

        foreach ($rql['filter'] ?? [] as $node) {
            [$field, $op, $value] = $node;
            if (!in_array($field, self::FILTERABLE, true)) {
                throw new BadRequestHttpException("Unfilterable field: $field");
            }
            $qb->andWhere($this->expr($qb, $field, $op, $value));
        }

        // sort, then limit/offset
        return $qb->getQuery()->getResult();
    }
}

Keep the whitelist as a const on the finder, not in config — it's the security boundary and it should be visible in code review.

Map RQL operators explicitly, don't pass them through

RQL operators (eq, ne, lt, le, gt, ge, in, out, like) map cleanly to Expr\Comparison / Expr\In / Expr\Like. Build the mapping table yourself:

private function expr($qb, string $field, string $op, mixed $value): string
{
    $e = $qb->expr();
    return match ($op) {
        'eq'  => $e->eq("a.$field", ':p'.($this->i++)),
        'ne'  => $e->neq("a.$field", ':p'.($this->i++)),
        'gt'  => $e->gt("a.$field", ':p'.($this->i++)),
        'like'=> $e->like("a.$field", ':p'.($this->i++)),
        'in'  => $e->in("a.$field", ':p'.($this->i++)),
        default => throw new BadRequestHttpException("Unsupported operator: $op"),
    };
}

Never interpolate the operator string into DQL directly — that's the one place where a typo becomes an injection vector.

Wire it through the Symfony container, then override per resource

Register the default finder as a service and let paknahad/jsonapi-bundle resolve it by service ID or by an autoconfigured tag. When you need a bespoke finder for one entity, define a new service with the higher-priority ID and let the bundle's config point at it. This keeps the "boring" 80% of resources on the default finder and only special-cases the ones with real query needs.

Pagination + JSON:API envelope

RQL's limit/offset are the natural fit, but JSON:API clients expect page[number] / page[size] and a links block with first/last/next/prev. Either:

  • translate page[*] into limit/offset before the finder runs, and rebuild links after, or
  • accept RQL's limit/offset and document that your API is JSON:API-shaped but RQL-addressed.

Pick one and be consistent — mixing the two conventions is the single most common source of client confusion with this stack.

Laravel adaptation (if you must)

You'd be reimplementing roughly: (a) an RQL parser, (b) a query builder translator, (c) the route/controller binding. Two of those are cheap with spatie/laravel-query-builder-style allowlisting; the RQL grammar is the only non-trivial bit. Realistically, unless you're already committed to paknahad/jsonapi-bundle, a Laravel project is better served by spatie/laravel-query-builder or andersao/l5-repository than by porting a two-year-stale Symfony bundle.

Gotchas and Tips

  • Staleness is the headline risk. 0 stars, 0 dependents, last release May 2022. There is no ecosystem pressure to keep it working against current Symfony/Doctrine versions. Pin it in composer.json and be prepared to composer patch or vendor-fork it if a minor Doctrine bump breaks the query builder API (it has in the past for Expr signatures).

  • No tests, no docs. The README is the whole spec. Before you build on it, write your own integration test that hits filter=eq(x,y)&sort=-z end-to-end — that test becomes your upgrade canary.

  • Unmapped fields fail hard. A client sending filter=eq(nonExistentField,foo) will typically surface as a DBAL/Doctrine exception → 500, not a 400. Wrap the whole finder call and translate unknown-field and bad-operator errors into BadRequestHttpException (or your framework's 400 equivalent) before it leaks a stack trace.

  • Operator/type mismatches. gt on a datetime string, in on an integer column, like on a UUID — Doctrine will either error at the driver or silently do something surprising. Coerce per field type; don't trust RQL's loosely-typed values.

  • Relationship filters are where performance dies. Anything that joins a related entity multiplies rows unless you ->addSelect() deliberately or use explicit join() + distinct(). Watch for N+1 in the serializer after the finder returns — the finder's job is the query, not the hydration strategy.

  • Debugging: dump the generated DQL ($qb->getDQL()) behind a %kernel.debug% guard, or log it with a Monolog channel named for the finder. Symfony's profiler will not show you the DQL unless you give it a query; adding a one-line ->getQuery()->getSQL() log during development saves hours. For Laravel, DB::listen / ->toSql() equivalents.

  • Extension points: the practical ones are (1) subclass the default finder, (2) decorate it with a service that normalizes params before delegation, (3) override the service definition in your app's services.yaml. There is no event system and no plugin registry — don't look for one.

  • Version pinning: because dependents are zero, you're the only integration under test. Lock to a specific version ("krilo89/jsonapi-rql-finder-bundle": "1.0.*" or a commit hash) and pin paknahad/jsonapi-bundle and doctrine/orm alongside it. Treat upgrades as deliberate changes, not composer update side effects.

  • Honest recommendation: if you're on Symfony and already using paknahad/jsonapi-bundle, this fills a real gap (RQL → Doctrine) and is worth evaluating in a spike. If you're on Laravel, or on a modern Symfony with api-platform or a custom QueryBuilder allowlist, the maintenance cost of adopting a stale, zero-dependency bundle almost certainly outweighs the saved lines of code.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console