{"id":50933113,"url":"https://github.com/technically-php/search-query","last_synced_at":"2026-06-22T11:00:53.421Z","repository":{"id":365043394,"uuid":"1270293277","full_name":"technically-php/search-query","owner":"technically-php","description":"🔍 Parse plaintext search queries into easy-to-use filter structures.","archived":false,"fork":false,"pushed_at":"2026-06-16T12:48:01.000Z","size":38,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-17T06:28:02.493Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/technically-php.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-15T15:13:03.000Z","updated_at":"2026-06-16T12:48:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/technically-php/search-query","commit_stats":null,"previous_names":["technically-php/search-query"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/technically-php/search-query","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technically-php%2Fsearch-query","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technically-php%2Fsearch-query/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technically-php%2Fsearch-query/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technically-php%2Fsearch-query/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/technically-php","download_url":"https://codeload.github.com/technically-php/search-query/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technically-php%2Fsearch-query/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34479555,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-18T02:00:06.871Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-17T06:04:25.293Z","updated_at":"2026-06-18T07:01:07.444Z","avatar_url":"https://github.com/technically-php.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Technically Search Query\n\n_🔍 Parse plaintext search queries into easy-to-use filter structures._\n\nThis 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.\n\n[![Test](https://github.com/technically-php/search-query/actions/workflows/test.yml/badge.svg)](https://github.com/technically-php/search-query/actions/workflows/test.yml)\n\n---\n\n## Installation\n\n```bash\ncomposer require technically/search-query\n```\n\nRequirements:\n- PHP 8.4+\n\n---\n\n## Quick Start\n\n```php\nuse Technically\\SearchQuery\\QueryParser;\n\n$parser = new QueryParser();\n$query  = $parser-\u003eparse('tag:php -legacy \"best practices\"');\n\nforeach ($query-\u003efilters as $filter) {\n    // Filter instances...\n}\n```\n\n---\n\n## Supported Query Syntax\n\n| Syntax                 | Parsed As                                           |\n|------------------------|-----------------------------------------------------|\n| `hello`                | `KeywordFilter('hello')`                            |\n| `\"hello world\"`        | `KeywordFilter('hello world', quoted: true)`        |\n| `-hello`               | `KeywordFilter('hello', exclude: true)`             |\n| `tag:php`              | `FieldFilter('tag', ':', 'php')`                    |\n| `-tag:php`             | `FieldFilter('tag', ':', 'php', exclude: true)`     |\n| `year\u003e2020`            | `FieldFilter('year', '\u003e', '2020')`                  |\n| `year\u003e=2020`           | `FieldFilter('year', '\u003e=', '2020')`                 |\n| `year\u003c2020`            | `FieldFilter('year', '\u003c', '2020')`                  |\n| `year\u003c=2020`           | `FieldFilter('year', '\u003c=', '2020')`                 |\n| `hello\\ world`         | `KeywordFilter('hello world')` (escaped whitespace) |\n| `\"custom field\":value` | `FieldFilter('custom field', ':', 'value')`         |\n\n### Negation\n\nA leading `-` (minus) before a keyword or field filter negates it. Multiple minuses are gracefully collapsed into a single negation.\n\n```\n-apple                    -\u003e KeywordFilter('apple', exclude: true)\n-tag:legacy               -\u003e FieldFilter('tag', ':', 'legacy', exclude: true)\n```\n\n### Quoting\n\nDouble quotes group multiple words into a single token. Quotes can be escaped with `\\`.\n\n```\n\"hello world\"            -\u003e KeywordFilter('hello world', quoted: true)\nfield:\"hello world\"      -\u003e FieldFilter('field', ':', 'hello world', quoted: true)\n```\n\n### Escaping\n\nThe backslash `\\` escape character works both inside and outside quoted strings:\n\n```\napples\\ fruits            -\u003e KeywordFilter('apples fruits')\n55\\\"                      -\u003e KeywordFilter('55\"')\n\"hello \\\"world\\\"\"         -\u003e KeywordFilter('hello \"world\"', quoted: true)\n```\n\n## The Tolerant Reader\n\nThe parser is built using the [Tolerant Reader](https://martinfowler.com/bliki/TolerantReader.html) \ndesign pattern — to be forgiving with malformed input. It never throws.\n\n---\n\n## API Reference\n\n### `QueryParser`\n\nThe main entry point for parsing query strings.\n\n```php\nuse Technically\\SearchQuery\\QueryParser;\n\n$parser = new QueryParser();\n$query  = $parser-\u003eparse('your search query');\n```\n\nThe parser accepts an optional `Tokenizer` instance in its constructor. By default, it uses `QueryTokenizer`.\n\n#### Methods\n\n- `parse(string $query): Query` — Parses a query string into a `Query` object.\n\n---\n\n### `Query`\n\nAn immutable value object representing the parsed search query.\n\n```php\nuse Technically\\SearchQuery\\Query;\n\n$query = new Query([\n    new KeywordFilter('php'),\n    new FieldFilter('tag', ':', 'tutorial'),\n]);\n```\n\n#### Properties\n\n- `public readonly array $filters` — Array of `Filter` instances.\n\n#### Methods\n\n- `static empty(): self` — Create a new empty query.\n- `isEmpty(): bool` — Check if the query is empty (has no filters).\n- `toString(): string` — Serializes the query back to the search query syntax string.\n\n---\n\n### Filters\n\nAll filters implement the `Technically\\SearchQuery\\Filters\\Filter` marker interface.\n\n#### `KeywordFilter`\n\nRepresents a free-text keyword search term.\n\n```php\nuse Technically\\SearchQuery\\Filters\\KeywordFilter;\n\nnew KeywordFilter('php');\nnew KeywordFilter('hello world', quoted: true);\nnew KeywordFilter('legacy', exclude: true);\n```\n\n**Properties:**\n- `public readonly string $keyword` — The keyword value.\n- `public readonly bool $quoted` — Whether the keyword was originally quoted.\n- `public readonly bool $exclude` — Whether the keyword is negated.\n\n**Methods:**\n- `unquote(): self` — Returns a new instance with `quoted` set to `false`.\n- `toString(): string` — Serializes the filter back to query syntax.\n\n#### `FieldFilter`\n\nRepresents a field-based filter (`field:operator:value`).\n\n```php\nuse Technically\\SearchQuery\\Filters\\FieldFilter;\n\nnew FieldFilter('year', '\u003e', '2020');\nnew FieldFilter('status', ':', 'active', quoted: true);\nnew FieldFilter('tag', ':', 'legacy', exclude: true);\n```\n\n**Properties:**\n- `public readonly string $field` — The field name.\n- `public readonly FilterOperator $operator` — The comparison operator.\n- `public readonly string $value` — The filter value.\n- `public readonly bool $quoted` — Whether the value was originally quoted.\n- `public readonly bool $exclude` — Whether the filter is negated.\n\n**Methods:**\n- `matches(...): bool` — Check whether the filter matches the given properties.\n- `unquote(): self` — Returns a new instance with `quoted` set to `false`.\n- `toString(): string` — Serializes the filter back to query syntax.\n\n---\n\n## Examples\n\n### Parse a complex query\n\n```php\nuse Technically\\SearchQuery\\QueryParser;\nuse Technically\\SearchQuery\\Filters\\KeywordFilter;\nuse Technically\\SearchQuery\\Filters\\FieldFilter;\n\n$parser = new QueryParser();\n$query  = $parser-\u003eparse('php -legacy \"best practices\" year\u003e=2020');\n\nforeach ($query-\u003efilters as $filter) {\n    if ($filter instanceof KeywordFilter) {\n        echo \"Keyword: {$filter-\u003ekeyword}\"\n           . ($filter-\u003eexclude ? ' (excluded)' : '')\n           . ($filter-\u003equoted ? ' (quoted)' : '')\n           . \"\\n\";\n    } elseif ($filter instanceof FieldFilter) {\n        echo \"Field: {$filter-\u003efield} {$filter-\u003eoperator-\u003evalue} {$filter-\u003evalue}\"\n           . ($filter-\u003eexclude ? ' (excluded)' : '')\n           . ($filter-\u003equoted ? ' (quoted)' : '')\n           . \"\\n\";\n    }\n}\n// Output:\n// Keyword: php\n// Keyword: legacy (excluded)\n// Keyword: best practices (quoted)\n// Field: year \u003e= 2020\n```\n\n### Serialize filters back to strings\n\n```php\n$filter = new FieldFilter('tag', ':', 'hello world', quoted: true, exclude: true);\necho $filter-\u003etoString(); // -tag:\"hello world\"\n\n// Or serialize an entire Query back to string:\n$query = new Query([\n    new KeywordFilter('php'),\n    new FieldFilter('year', '\u003e', '2020', exclude: true),\n]);\necho $query-\u003etoString(); // php -year\u003e2020\n```\n\n### Custom tokenization\n\n```php\nuse Technically\\SearchQuery\\QueryParser;\nuse Technically\\SearchQuery\\Contracts\\Tokenizer;\n\nclass MyCustomTokenizer implements Tokenizer\n{\n    public function tokenize(string $query): iterable\n    {\n        // Custom tokenization logic...\n    }\n}\n\n$parser = new QueryParser(new MyCustomTokenizer());\n```\n\n---\n\n## Running Tests\n\n```bash\ncomposer tests\n```\n\nTests are written with [Pest PHP](https://pestphp.com/).\n\n---\n\n## License\n\nMIT\n\n\n## Credits\n\nImplemented by :space_invader: [Ivan Voskoboinyk](https://voskoboinyk.com/).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechnically-php%2Fsearch-query","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftechnically-php%2Fsearch-query","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechnically-php%2Fsearch-query/lists"}