A powerful search query parser that transforms freeform search queries into structured objects for querying SQL databases, Eloquent models, or other backends.
- π― Parse Google-style search queries with field names, ranges, negation, and more
- ποΈ Built-in transforms for SQL (PDO) and Laravel Eloquent
- π Extensible parser system for custom query types
- π Field filtering and mapping for security
- π Loose mode for fuzzy matching
- π PHP 8.2+ with modern type safety
use RebeccaTheDev\SearchParser\SearchParser;
$parser = new SearchParser();
$query = $parser->parse('from:foo@example.com "bar baz" !meef date:2018/01/01-2018/08/01');This tokenizes the search into a SearchQuery object with structured components:
RebeccaTheDev\SearchParser\SearchQuery Object
(
[position:RebeccaTheDev\SearchParser\SearchQuery:private] => 0
[data:protected] => Array
(
[0] => RebeccaTheDev\SearchParser\SearchQueryComponent Object
(
[type] => field
[field] => from
[value] => foo@example.com
[firstRangeValue] =>
[secondRangeValue] =>
[negate] =>
)
[1] => RebeccaTheDev\SearchParser\SearchQueryComponent Object
(
[type] => text
[field] =>
[value] => bar baz
[firstRangeValue] =>
[secondRangeValue] =>
[negate] =>
)
[2] => RebeccaTheDev\SearchParser\SearchQueryComponent Object
(
[type] => text
[field] =>
[value] => meef
[firstRangeValue] =>
[secondRangeValue] =>
[negate] => 1
)
[3] => RebeccaTheDev\SearchParser\SearchQueryComponent Object
(
[type] => range
[field] => date
[value] =>
[firstRangeValue] => 2018/01/01
[secondRangeValue] => 2018/08/01
[negate] =>
)
[4] => RebeccaTheDev\SearchParser\SearchQueryComponent Object
(
[type] => text
[field] =>
[value] => #hashtag
[firstRangeValue] =>
[secondRangeValue] =>
[negate] =>
)
)
)
composer require rebeccathedev/search-parserRequirements: PHP 8.2+
No external dependencies required for core functionality. Eloquent transform requires illuminate/database.
use RebeccaTheDev\SearchParser\SearchParser;
$parser = new SearchParser();
$query = $parser->parse('from:foo@example.com "exact phrase" !excluded');The SearchQuery object is iterable:
foreach ($query as $component) {
echo $component->type; // 'field', 'text', 'range'
echo $component->value;
}Extend the parser by implementing the Parser interface:
use RebeccaTheDev\SearchParser\Parsers\Parser;
use RebeccaTheDev\SearchParser\SearchQueryComponent;
class HashtagParser implements Parser {
public function parsePart(string $part): SearchQueryComponent {
$component = new SearchQueryComponent();
if (preg_match('!#(.*)!', $part, $match)) {
$component->type = 'hashtag';
$component->value = $match[1];
}
return $component;
}
}
// Use it
$parser = new SearchParser();
$parser->addParser(new HashtagParser());
$query = $parser->parse('search #trending');See src/Parsers/Hashtag.php for a working example. Note: parsers don't fall through - if your parser handles a part, processing moves to the next part.
Transform parsed queries into SQL WHERE clauses or Eloquent query builders.
use RebeccaTheDev\SearchParser\Transforms\SQL\SQL;
$pdo = new PDO("sqlite:/tmp/database.db");
$transform = new SQL('default_field', $pdo);
$query = $parser->parse('from:foo@example.com "bar baz" !meef date:2018/01/01-2018/08/01');
$where = $transform->transform($query);
// Result:
// `from` = 'foo@example.com' and `default_field` = 'bar baz' and
// `default_field` != 'meef' and (`date` between '2018/01/01' and '2018/08/01')use RebeccaTheDev\SearchParser\Transforms\Eloquent\Eloquent;
$users = User::query();
$transform = new Eloquent('name', $users);
$query = $parser->parse('status:active age:25-35');
$users = $transform->transform($query)->get();Enable fuzzy matching with LIKE queries:
$transform = new SQL('default_field', $pdo);
$transform->looseMode = true;
$where = $transform->transform($query);
// Result:
// `from` = 'foo@example.com' and `default_field` like '%bar baz%' and
// `default_field` not like '%meef%' and (`date` between '2018/01/01' and '2018/08/01')Add custom transforms for your custom parsers:
use RebeccaTheDev\SearchParser\Transforms\SQL\Hashtag;
$pdo = new PDO("sqlite:/tmp/database.db");
$transform = new SQL('default_field', $pdo);
$transform->addComponentTransform(new Hashtag('default_field', $pdo));
$query = $parser->parse('search #trending');
$where = $transform->transform($query);
// Result: `default_field` = 'search' and `hashtag` = 'trending'See src/Transforms/SQL/Hashtag.php for a working example.
Important: The SQL transform escapes values but not field names. Always allowlist allowed fields before passing queries to transforms. Never trust user input for field names.
Allowlist allowed fields for security:
use RebeccaTheDev\SearchParser\Filters\{Filter, FieldFilter};
$filter = new Filter();
$fieldFilter = new FieldFilter();
$fieldFilter->validFields = ['from', 'to', 'subject', 'date'];
$filter->addFilter($fieldFilter);
$query = $parser->parse('from:foo@example.com invalid:malicious subject:test');
$filtered = $filter->filter($query);
// Only 'from' and 'subject' fields are kept, 'invalid' is removedMap user-facing field names to database column names:
use RebeccaTheDev\SearchParser\Filters\{Filter, FieldNameMapper};
$filter = new Filter();
$mapper = new FieldNameMapper();
$mapper->mappingFields = [
'date' => 'created_at',
'author' => 'user_id'
];
$filter->addFilter($mapper);
$query = $parser->parse('date:2024-01-01-2024-12-31 author:123');
$filtered = $filter->filter($query);
// 'date' becomes 'created_at', 'author' becomes 'user_id'Implement the FiltersQueries interface:
use RebeccaTheDev\SearchParser\Filters\FiltersQueries;
use RebeccaTheDev\SearchParser\SearchQuery;
class MyCustomFilter implements FiltersQueries {
public function filter(SearchQuery $query): SearchQuery {
foreach ($query as $component) {
// Your custom filtering logic
}
return $query;
}
}
$filter = new Filter();
$filter->addFilter(new MyCustomFilter());Useful SearchQuery methods:
remove(SearchQueryComponent $item)- Remove a componentreplace(SearchQueryComponent $old, SearchQueryComponent $new)- Replace a componentmerge(SearchQuery $query)- Merge two queries
composer install
./vendor/bin/phpunitSome tests may be skipped if optional dependencies (like Eloquent) aren't installed.
MIT License - see LICENSE file for details.
Made with π©· by Rebecca Peck