Skip to content

technically-php/search-query

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

28 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Technically Search Query

πŸ” Parse plaintext search queries into easy-to-use filter structures.

This library takes a human-typed search query string and parses it into a structured Query object containing typed filters (KeywordFilter, FieldFilter). It supports quoted strings, negation, comparison operators, and field-based filtering.

Test


Installation

composer require technically/search-query

Requirements:

  • PHP 8.4+

Quick Start

use Technically\SearchQuery\QueryParser;

$parser = new QueryParser();
$query  = $parser->parse('tag:php -legacy "best practices"');

foreach ($query->filters as $filter) {
    // Filter instances...
}

Supported Query Syntax

Syntax Parsed As
hello KeywordFilter('hello')
"hello world" KeywordFilter('hello world', quoted: true)
-hello KeywordFilter('hello', exclude: true)
tag:php FieldFilter('tag', ':', 'php')
-tag:php FieldFilter('tag', ':', 'php', exclude: true)
year>2020 FieldFilter('year', '>', '2020')
year>=2020 FieldFilter('year', '>=', '2020')
year<2020 FieldFilter('year', '<', '2020')
year<=2020 FieldFilter('year', '<=', '2020')
hello\ world KeywordFilter('hello world') (escaped whitespace)
"custom field":value FieldFilter('custom field', ':', 'value')

Negation

A leading - (minus) before a keyword or field filter negates it. Multiple minuses are gracefully collapsed into a single negation.

-apple                    -> KeywordFilter('apple', exclude: true)
-tag:legacy               -> FieldFilter('tag', ':', 'legacy', exclude: true)

Quoting

Double quotes group multiple words into a single token. Quotes can be escaped with \.

"hello world"            -> KeywordFilter('hello world', quoted: true)
field:"hello world"      -> FieldFilter('field', ':', 'hello world', quoted: true)

Escaping

The backslash \ escape character works both inside and outside quoted strings:

apples\ fruits            -> KeywordFilter('apples fruits')
55\"                      -> KeywordFilter('55"')
"hello \"world\""         -> KeywordFilter('hello "world"', quoted: true)

The Tolerant Reader

The parser is built using the Tolerant Reader design pattern β€” to be forgiving with malformed input. It never throws.


API Reference

QueryParser

The main entry point for parsing query strings.

use Technically\SearchQuery\QueryParser;

$parser = new QueryParser();
$query  = $parser->parse('your search query');

The parser accepts an optional Tokenizer instance in its constructor. By default, it uses QueryTokenizer.

Methods

  • parse(string $query): Query β€” Parses a query string into a Query object.

Query

An immutable value object representing the parsed search query.

use Technically\SearchQuery\Query;

$query = new Query([
    new KeywordFilter('php'),
    new FieldFilter('tag', ':', 'tutorial'),
]);

Properties

  • public readonly array $filters β€” Array of Filter instances.

Methods

  • static empty(): self β€” Create a new empty query.
  • isEmpty(): bool β€” Check if the query is empty (has no filters).
  • toString(): string β€” Serializes the query back to the search query syntax string.

Filters

All filters implement the Technically\SearchQuery\Filters\Filter marker interface.

KeywordFilter

Represents a free-text keyword search term.

use Technically\SearchQuery\Filters\KeywordFilter;

new KeywordFilter('php');
new KeywordFilter('hello world', quoted: true);
new KeywordFilter('legacy', exclude: true);

Properties:

  • public readonly string $keyword β€” The keyword value.
  • public readonly bool $quoted β€” Whether the keyword was originally quoted.
  • public readonly bool $exclude β€” Whether the keyword is negated.

Methods:

  • unquote(): self β€” Returns a new instance with quoted set to false.
  • toString(): string β€” Serializes the filter back to query syntax.

FieldFilter

Represents a field-based filter (field:operator:value).

use Technically\SearchQuery\Filters\FieldFilter;

new FieldFilter('year', '>', '2020');
new FieldFilter('status', ':', 'active', quoted: true);
new FieldFilter('tag', ':', 'legacy', exclude: true);

Properties:

  • public readonly string $field β€” The field name.
  • public readonly FilterOperator $operator β€” The comparison operator.
  • public readonly string $value β€” The filter value.
  • public readonly bool $quoted β€” Whether the value was originally quoted.
  • public readonly bool $exclude β€” Whether the filter is negated.

Methods:

  • matches(...): bool β€” Check whether the filter matches the given properties.
  • unquote(): self β€” Returns a new instance with quoted set to false.
  • toString(): string β€” Serializes the filter back to query syntax.

Examples

Parse a complex query

use Technically\SearchQuery\QueryParser;
use Technically\SearchQuery\Filters\KeywordFilter;
use Technically\SearchQuery\Filters\FieldFilter;

$parser = new QueryParser();
$query  = $parser->parse('php -legacy "best practices" year>=2020');

foreach ($query->filters as $filter) {
    if ($filter instanceof KeywordFilter) {
        echo "Keyword: {$filter->keyword}"
           . ($filter->exclude ? ' (excluded)' : '')
           . ($filter->quoted ? ' (quoted)' : '')
           . "\n";
    } elseif ($filter instanceof FieldFilter) {
        echo "Field: {$filter->field} {$filter->operator->value} {$filter->value}"
           . ($filter->exclude ? ' (excluded)' : '')
           . ($filter->quoted ? ' (quoted)' : '')
           . "\n";
    }
}
// Output:
// Keyword: php
// Keyword: legacy (excluded)
// Keyword: best practices (quoted)
// Field: year >= 2020

Serialize filters back to strings

$filter = new FieldFilter('tag', ':', 'hello world', quoted: true, exclude: true);
echo $filter->toString(); // -tag:"hello world"

// Or serialize an entire Query back to string:
$query = new Query([
    new KeywordFilter('php'),
    new FieldFilter('year', '>', '2020', exclude: true),
]);
echo $query->toString(); // php -year>2020

Custom tokenization

use Technically\SearchQuery\QueryParser;
use Technically\SearchQuery\Contracts\Tokenizer;

class MyCustomTokenizer implements Tokenizer
{
    public function tokenize(string $query): iterable
    {
        // Custom tokenization logic...
    }
}

$parser = new QueryParser(new MyCustomTokenizer());

Running Tests

composer tests

Tests are written with Pest PHP.


License

MIT

Credits

Implemented by πŸ‘Ύ Ivan Voskoboinyk.

About

πŸ” Parse plaintext search queries into easy-to-use filter structures.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages