yuriitatur / search
A simple framework/db agnostic api search package
Requires
- php: >=8.2
- yuriitatur/repository: dev-master
Requires (Dev)
- dg/bypass-finals: ^1.9
- jms/serializer: ^3.32
- kint-php/kint: ^6.0
- league/fractal: ^0.20.2
- phpunit/phpunit: ^12.0
- symfony/property-access: ^7.3
- symfony/serializer: ^7.3
- symfony/validator: ^7.0
Suggests
- jms/serializer: Allows you to use JmsSerializerCursorHydrator
- symfony/property-access: Allows you to use ProperyAccessCursorHydrator
- symfony/serializer: Allows you to use SymfonySerializerCursorHydrator
This package is auto-updated.
Last update: 2026-04-12 21:26:03 UTC
README
Search module
A framework-agnostic PHP 8.2+ library for building filterable, sortable, and paginated API queries. It validates incoming request options, composes a QueryBuilder, and executes the query — all driven by a set of composable filters you define.
Installation
composer require yuriitatur/search
Core concepts
Seeker
Seeker is the main entry point. It wires together a validator, a query composer, an executor, and a map of filters:
$seeker = new Seeker(
new RequestValidator(Validation::createValidator()),
new QueryComposer,
new PaginatorQueryExecutor($repository),
DefaultFilters::get()
);
$result = $seeker->seek($request->query->all());
seek() validates the options against the registered filters, builds a QueryBuilder, executes it, and returns an IteratorAggregate result.
Filters
Filters implement FilterInterface:
interface FilterInterface
{
public function applyFilter(QueryBuilder $builder, mixed $value): void;
/** @return Constraint[] */
public function getValidationRules(): array;
}
Each filter both mutates the query and declares its own Symfony Validator constraints. If any option fails validation a ValidationException is thrown before the query is built.
Built-in filters
DefaultFilters::get(array $mergeFilters = [], int $defaultLimit = 100) returns a ready-made filter map:
| Key | Filter | Behaviour |
|---|---|---|
limit | LimitFilter | Sets result limit (default 100) |
pagination | PaginationFilter | Offset-based page navigation, shares the same LimitFilter instance |
all | NoLimit | Removes the limit |
cursor | CursorFilter | Enables cursor-based pagination |
id | PositiveIntegerFilter | Filters by a single positive integer id |
ids | InConcatenatedFilter | Filters by a comma-separated list of ids |
sort | MultiValueFilter(OrderByFilter) | Accepts one or more column:asc\|desc values |
Additional filters available to use directly:
LikeFilter($field, $minLength)— full-text LIKE search on a fieldEnumFilter($field, $enum)— restricts a field to a backed enum's valuesLimitedStringFilter($field, $maxLength)— string equality with a length capCompositeFilter— groups multiple filters under a single keyConcatenatedMultiValueFilter— parses a comma-separated string into multiple filter values
Pass any overrides or additions as the first argument to DefaultFilters::get():
DefaultFilters::get([
'name' => new LikeFilter('name'),
'status' => new EnumFilter('status', StatusEnum::class),
])
Sorting
The sort key accepts column:asc or column:desc. Pass multiple values as an array for multi-column sorting:
$seeker->seek(['sort' => ['created_at:desc', 'name:asc']]);
QueryComposer defaults
QueryComposer applies a default limit (100), offset (0), and sort (id desc) when those are not set by any filter. Override via constructor:
new QueryComposer(defaultLimit: 50, defaultSortColumn: 'created_at', defaultSortDirection: 'asc')
Executors
Offset pagination — PaginatorQueryExecutor
Returns a PaginatorResult with data, totalCount, and helpers like getCurrentPage(), getLastPage(), getPerPage().
$executor = new PaginatorQueryExecutor($repository);
Cursor pagination — CursorQueryExecutor
Returns a CursorResult with data, currentCursor, nextCursor, and previousCursor. Cursor values are opaque strings. The previous cursor is computed lazily and cached.
$executor = new CursorQueryExecutor($repository, $cache, $cursorQueryBuilder, $extractor);
Auto-routing — CombinedExecutor
Routes to CursorQueryExecutor when a cursor option is present in the query, otherwise falls back to PaginatorQueryExecutor. This lets a single endpoint support both pagination styles.
$executor = new CombinedExecutor($paginatorExecutor, $cursorExecutor);
CursorResult and PaginatorResult both implement IteratorAggregate, so the result can always be iterated directly regardless of which executor was used.
Cursor hydrators
CursorQueryExecutor needs to extract the sort-column value from result entities. Three hydrator implementations are provided:
| Class | Requires |
|---|---|
PropertyAccessCursorHydrator | symfony/property-access |
SymfonySerializerCursorHydrator | symfony/serializer |
JmsSerializerCursorHydrator | jms/serializer |
Laravel integration
A Laravel service provider is auto-discovered from composer.json. It registers Symfony Validator, PropertyAccessCursorHydrator as the default hydrator, and a Symfony Cache adapter. No manual registration is needed.
Testing
composer test
License
This code is under MIT license, read more in the LICENSE file.