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

Query Parser Laravel Package

gdbots/query-parser

A PHP library for parsing query strings into a structured AST, with support for boolean operators, fielded terms, grouping, ranges, and wildcards. Useful for validating and translating user search input into database queries.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer (no service provider, no config file — this is a plain library):

composer require gdbots/query-parser

Then verify what you actually got before writing code against it, because the project was dormant from late 2021 until this release and the maintenance gap means the API around nodes/enums changed:

composer show gdbots/query-parser          # version, PHP constraint, deps
php -r "var_dump((new Gdbots\QueryParser\QueryParser())->parse('urgent #billing @alice \"past due\"'));"

v3.0.0 requires PHP 8.1. If you are still on 8.0 or 7.4 you must upgrade before adopting. The second breaking change is the migration from the package's home-grown "versions" to native PHP enums — this is the one that will bite your existing code. Anywhere you were receiving a scalar or a getValue()-style object from the parsed nodes, confirm whether it is now a \BackedEnum case.

Read these files first, in order:

  1. README.md — the canonical example and supported syntax. (Double-check it against src/; it may lag the v3.0.0 refactor.)
  2. src/QueryParser.php — the single public entry point (parse()).
  3. src/ParsedQuery.php — the value object you will actually live in (getTerms(), getPhrases(), getHashtags(), getMentions(), has*() helpers).
  4. src/Node/ — the node types (Term, Phrase, Hashtag, Mention) if you need a visitor. Check whether each node's value accessor now returns an enum case.

First use case: a global search box on an admin listing that accepts social-style syntax — status:open #vip @alice "renewal failed" — and maps each token class onto a different column/filter instead of dumping the whole string into one LIKE.

Start by binding it once in a service provider so you are not new-ing it in controllers:

// app/Providers/AppServiceProvider.php
$this->app->singleton(\Gdbots\QueryParser\QueryParser::class);

Implementation Patterns

Parse at the edge, pass the parsed object inward. Do the parse in a FormRequest or a small middleware, throw a ValidationException on bad syntax, and hand the ParsedQuery (or your own DTO) to the controller. Downstream code should never see the raw string.

final class SearchRequest extends FormRequest
{
    public function rules(): array
    {
        return ['q' => ['nullable', 'string', 'max:512']];
    }

    public function parsed(): ParsedQuery
    {
        try {
            return app(QueryParser::class)->parse(trim($this->input('q', '')));
        } catch (\Throwable $e) {
            throw ValidationException::withMessages(['q' => 'Invalid search syntax.']);
        }
    }
}

Split structured tokens from free text. The whole value of this package is that #tag and @person are not full-text terms. Treat them as filters and everything else as scored relevance.

Normalise node values to scalars at the boundary — this is essential in v3.0.0. With the enum migration, the same logical token can come back as a string, a backed-enum case, or a legacy object depending on the node type. Do the unwrapping in one place and never let an enum case leak into Eloquent or Elasticsearch:

$parsed = $request->parsed();

$hashtags = array_map($this->value(...), $parsed->getHashtags());
$mentions = array_map($this->value(...), $parsed->getMentions());
$phrases  = array_map($this->value(...), $parsed->getPhrases());
$terms    = array_map($this->value(...), $parsed->getTerms());

private function value(mixed $n): string
{
    return match (true) {
        is_string($n)          => $n,
        $n instanceof \BackedEnum => (string) $n->value,
        default                => (string) $n->getValue(),
    };
}

If you were on the pre-3.0 defensive is_string($n) ? $n : $n->getValue() form, it will fatal on the new enum cases (BackedEnum has no getValue()). Update it and add a test that asserts the enum path actually runs.

Wrap translation in your own builder. Implement a small class that turns the parsed collections into Eloquent constraints or a Scout/Elasticsearch body. This is your extension point if you later swap the library out.

$query = Invoice::query()
    ->when($terms, fn ($q) => $q->where(function ($q) use ($terms, $phrases) {
        foreach ($terms as $t)   { $q->orWhere('reference', 'like', "%{$t}%"); }
        foreach ($phrases as $p) { $q->orWhere('notes', 'like', "%{$p}%"); }
    }))
    ->when($hashtags, fn ($q) => $q->whereHas('tags', fn ($t) => $t->whereIn('name', $hashtags)))
    ->when($mentions, fn ($q) => $q->whereIn('assignee_handle', $mentions));

For Scout/Elasticsearch, use term/match_phrase for phrases and terms filters for hashtags/mentions — do not concatenate parsed output back into a query string.

Memoize per request. Parsing is cheap but not free, and retries/partials can call it repeatedly. A request-scoped array<string, ParsedQuery> keyed by md5($input) inside the singleton is enough. Do not cache the raw ParsedQuery across requests — the node graph now contains enum cases that don't round-trip cleanly through serialize()/json_encode() without extra work.

Keep a no-syntax fallback. If getTerms() and friends are all empty (blank input, or input the parser silently drops), fall back to a plain substring search so the user gets something rather than an empty page.

Gotchas and Tips

  • PHP 8.1 is now a hard floor. v3.0.0 raises the requirement; your CI matrix, Docker base image, and any shared library that also depends on this package must all move together. Confirm no transitive dependency pins the package to 2.x.
  • Native enums are the big v3.0.0 migration. Anywhere you compared against a scalar, did ->getValue(), or stored nodes directly, audit. Comparisons should be $node === SomeEnum::Foo (identity works for enum cases) or against ->value. Serialising a ParsedQuery to JSON now requires casting enum cases to their ->value first — json_encode will otherwise produce {} or throw.
  • Unbalanced quotes and parentheses are the common failure. Wrap parse() in try/catch and return a 422; do not let a stray " 500 the page. Fuzz this with "", "a, ((, #, @, emoji, and 10k-character tokens.
  • # and @ collide with legitimate input. Searching for C#, an email address, or a Slack handle produces surprising tokens. If your domain allows those, sanitize or require whitespace-delimited tags before parsing.
  • Operators are usually case-sensitive (AND/OR/NOT uppercase). Document this for users; consider uppercasing operators client-side.
  • Do not assume boolean semantics. +/-/OR handling varies between ports and versions — confirm against the source rather than trusting the README example. This is doubly true after a major refactor where the README may be stale.
  • No Unicode guarantees. Expect whitespace-only tokenization; CJK and combining characters may not split as you want. Test with your actual locale data.
  • The AST is immutable. Never mutate nodes; build your own arrays/DTOs instead. If you cache parsed results, serialise your DTO — with enum values already flattened to scalars — rather than the node graph.
  • Zero framework integration. No facade, no config, no published migrations. Everything is manual DI — that is fine, just don't expect config/query-parser.php.
  • Maintenance risk is now different, not gone. v3.0.0 is the first release since 2021-12-05 and is a breaking major, so there is little downstream battle-testing. Pin the exact version (3.0.0, not ^3.0), run your test suite on 8.1+ and 8.2+, and keep it behind your own SearchQueryParser interface so a fork or replacement (custom tokenizer, spatie/laravel-searchable) is a one-file change.
  • Parsing is not sanitization. The output still contains attacker-controlled text. Always bind parameters / use array query DSL; never string-interpolate terms into SQL or ES.
  • Extension point: if you need custom node types or a different translation, implement a visitor over Node subclasses rather than patching QueryParser. With enum cases in the tree, keep your visitor tolerant of both enum and scalar values until you have confirmed the shape across all node types.
  • Test with data providers covering: empty string, only hashtags, only a phrase, mixed, malformed, and a very long single token — then assert on your DTO, not the library's node objects or enum cases, so the suite survives an upgrade.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect