rebeccathedev / search-parser
A parser that converts a freeform query into an intermediate object, that can be converted to query many backends (SQL, ElasticSearch, etc).
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 2
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/rebeccathedev/search-parser
Requires
- php: >=8.2
Requires (Dev)
- phpunit/phpunit: ^11.5
Suggests
- illuminate/database: Required to use the Eloquent query builder.
This package is auto-updated.
Last update: 2025-12-13 15:36:02 UTC
README
A powerful search query parser that transforms freeform search queries into structured objects for querying SQL databases, Eloquent models, or other backends.
β¨ Features
- π― 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
π Quick Example
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] =>
)
)
)
π¦ Installation
composer require rebeccathedev/search-parser
Requirements: PHP 8.2+
No external dependencies required for core functionality. Eloquent transform requires illuminate/database.
π Usage
Basic Parsing
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; }
π§ Custom Parsers
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.
π Transforms
Transform parsed queries into SQL WHERE clauses or Eloquent query builders.
πΎ SQL Transform
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')
β¨ Eloquent Transform
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();
π Loose Mode
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')
π¨ Custom Component Transforms
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.
π‘οΈ Filters
β οΈ Security Note
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.
π― FieldFilter
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 removed
πΊοΈ FieldNameMapper
Map 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'
βοΈ Custom Filters
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
π§ͺ Testing
composer install ./vendor/bin/phpunit
Some tests may be skipped if optional dependencies (like Eloquent) aren't installed.
π License
MIT License - see LICENSE file for details.
π©βπ» Author
Made with π©· by Rebecca Peck