Parse GitHub-style search queries into structured filters for Laravel Eloquent. Supports field:value, comparisons, exclusions, and relation filters.
composer require philiprehberger/laravel-search-query-parserParse GitHub-style search queries into structured filters for Laravel Eloquent. Supports field:value, comparisons, exclusions, and relation filters.
composer require philiprehberger/laravel-search-query-parser
The service provider is registered automatically via Laravel's package auto-discovery.
use PhilipRehberger\SearchQueryParser\QueryParser;
$parser = new QueryParser();
$parsed = $parser->parse('design status:active amount:>1000 -archived');
$parsed->textSearch; // "design"
$parsed->filters; // [['field' => 'status', 'operator' => 'equals', 'value' => 'active'], ...]
$parsed->excludeTerms; // ["archived"]
$parsed->hasTextSearch(); // true
$parsed->hasFilters(); // true
$parsed->hasExcludeTerms(); // true
$parsed->isEmpty(); // false
You can also resolve QueryParser from the container or use the facade:
// Via facade
use PhilipRehberger\SearchQueryParser\Facades\SearchQueryParser;
$parsed = SearchQueryParser::parse('status:active');
// Via dependency injection
public function __construct(private QueryParser $parser) {}
| Syntax | Example | Description |
|---|---|---|
keyword | design | Plain keyword — goes into textSearch |
"phrase" | "web design" | Quoted phrase — treated as a single text search term |
field:value | status:active | Exact match filter (equals operator) |
field:>value | amount:>1000 | Greater than comparison |
field:<value | date:<2026-01-01 | Less than comparison |
field:>=value | total:>=500 | Greater than or equal |
field:<=value | hours:<=40 | Less than or equal |
field:!=value | status:!=archived | Not equal comparison |
field:v1,v2,v3 | status:active,pending | Match any of (comma-separated → in operator) |
-keyword | -archived | Exclude keyword — goes into excludeTerms |
has:relation | has:invoices | Has related records |
no:relation | no:projects | Has no related records |
>=, <=, !=) are checked before shorter ones (>, <, =) to prevent partial matches.https://..., http://...) are never treated as field filters./^[a-zA-Z_][a-zA-Z0-9_]*$/ — digits-first tokens fall through to text search.has: and no: prefix matching is case-insensitive.Security: Text operators (
ContainsOperator,StartsWithOperator,EndsWithOperator,NotContainsOperator) automatically escape LIKE wildcards (%and_) in user input to prevent wildcard injection.
| Class | Label | Input Type | Requires Value |
|---|---|---|---|
ContainsOperator | contains | text | yes |
NotContainsOperator | does not contain | text | yes |
EqualsOperator | equals | text | yes |
NotEqualsOperator | does not equal | text | yes |
StartsWithOperator | starts with | text | yes |
EndsWithOperator | ends with | text | yes |
IsEmptyOperator | is empty | text | no |
IsNotEmptyOperator | is not empty | text | no |
| Class | Label | Input Type |
|---|---|---|
GreaterThanOperator | greater than | number |
GreaterOrEqualOperator | greater than or equal | number |
LessThanOperator | less than | number |
LessOrEqualOperator | less than or equal | number |
BetweenOperator | between | range |
NotBetweenOperator | not between | range |
BetweenOperator and NotBetweenOperator accept a value of ['min' => x, 'max' => y] or [x, y].
| Class | Label | Input Type | Requires Value |
|---|---|---|---|
DateEqualsOperator | on date | date | yes |
DateBeforeOperator | before | date | yes |
DateAfterOperator | after | date | yes |
DateBetweenOperator | between dates | daterange | yes |
DateInLastOperator | in the last | duration | yes |
DateInNextOperator | in the next | duration | yes |
IsTodayOperator | is today | text | no |
IsThisWeekOperator | is this week | text | no |
IsThisMonthOperator | is this month | text | no |
DateInLastOperator and DateInNextOperator accept ['amount' => int, 'unit' => 'days|weeks|months|years'].
DateBetweenOperator accepts ['start' => date, 'end' => date] or [date, date].
| Class | Label | Input Type |
|---|---|---|
InOperator | is any of | multiselect |
NotInOperator | is not any of | multiselect |
HasAnyOperator | has any of | multiselect |
HasAllOperator | has all of | multiselect |
HasAnyOperator and HasAllOperator use whereJsonContains for JSON array columns.
| Class | Label | Requires Value |
|---|---|---|
HasRelationOperator | has | no |
HasNotRelationOperator | does not have | no |
HasCountOperator | has count | yes |
HasCountOperator accepts ['operator' => '>=', 'count' => 1].
The ParsedQuery DTO gives you structured data you can apply to your queries however you like. Here is a typical pattern:
use PhilipRehberger\SearchQueryParser\QueryParser;
use PhilipRehberger\SearchQueryParser\Operators\Text\ContainsOperator;
use PhilipRehberger\SearchQueryParser\Operators\Text\EqualsOperator;
use PhilipRehberger\SearchQueryParser\Operators\Numeric\GreaterThanOperator;
use PhilipRehberger\SearchQueryParser\Operators\Array\InOperator;
use PhilipRehberger\SearchQueryParser\Operators\Relation\HasRelationOperator;
use PhilipRehberger\SearchQueryParser\Operators\Relation\HasNotRelationOperator;
$parser = new QueryParser();
$parsed = $parser->parse($request->input('q', ''));
$query = Project::query();
// Apply free-text search
if ($parsed->hasTextSearch()) {
$term = $parsed->textSearch;
$query->where(function ($q) use ($term) {
$q->where('name', 'like', "%{$term}%")
->orWhere('description', 'like', "%{$term}%");
});
}
// Apply field filters
$operatorMap = [
'equals' => new EqualsOperator(),
'in' => new InOperator(),
'greater_than' => new GreaterThanOperator(),
'has' => new HasRelationOperator(),
'has_not' => new HasNotRelationOperator(),
];
foreach ($parsed->filters as $filter) {
$operator = $operatorMap[$filter['operator']] ?? null;
if ($operator) {
$operator->apply($query, $filter['field'], $filter['value']);
}
}
// Apply exclusion terms
foreach ($parsed->excludeTerms as $term) {
$query->where('name', 'not like', "%{$term}%");
}
$projects = $query->get();
QueryParser::build() serializes a ParsedQuery back into a query string. This is useful for storing canonical search state or passing queries between requests.
$parsed = $parser->parse('design status:active -archived');
// Modify the parsed query...
$built = $parser->build($parsed);
// "design status:active -archived"
getSyntaxHelp() returns all supported syntax patterns, suitable for rendering a help tooltip or autocomplete:
$help = $parser->getSyntaxHelp();
// [
// ['syntax' => 'keyword', 'example' => 'design', 'description' => 'Search for keyword in all fields'],
// ['syntax' => '"phrase"', 'example' => '"web design"', 'description' => 'Search for exact phrase'],
// ['syntax' => 'field:value', 'example' => 'status:active', 'description' => 'Filter by specific field'],
// ...
// ]
readonly class ParsedQuery
{
public string $textSearch;
public array $filters; // array<{field: string, operator: string, value: mixed}>
public array $excludeTerms; // array<string>
public function hasTextSearch(): bool;
public function hasFilters(): bool;
public function hasExcludeTerms(): bool;
public function isEmpty(): bool;
public function toArray(): array;
}
QueryParser| Method | Description |
|---|---|
parse(string $query): ParsedQuery | Parse a query string into a structured ParsedQuery DTO |
build(ParsedQuery $parsed): string | Serialize a ParsedQuery back into a query string |
getSyntaxHelp(): array | Return all supported syntax patterns for tooltips or autocomplete |
ParsedQuery DTO| Property / Method | Type | Description |
|---|---|---|
$textSearch | string | Free-text portion of the query |
$filters | array | Structured filters: [{field, operator, value}] |
$excludeTerms | array<string> | Terms prefixed with - |
hasTextSearch(): bool | — | Whether a free-text term is present |
hasFilters(): bool | — | Whether any field filters are present |
hasExcludeTerms(): bool | — | Whether any exclusion terms are present |
isEmpty(): bool | — | Whether the query produced no results |
toArray(): array | — | Serialize to array |
composer install
vendor/bin/phpunit
vendor/bin/pint --test
vendor/bin/phpstan analyse
If you find this project useful: