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

v0.3 2025-12-13 15:22 UTC

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 component
  • replace(SearchQueryComponent $old, SearchQueryComponent $new) - Replace a component
  • merge(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