Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Search Laravel Package

rollerworks/search

Rollerworks Search is a PHP search component that turns user-friendly query strings into structured search conditions. It integrates with Doctrine ORM/DBAL and Symfony, supports custom fields, validation, and flexible parsing for building powerful, safe search interfaces.

View on GitHub
Deep Wiki
Context7

Getting Started

rollerworks/search is a search abstraction for PHP: you model a search as a typed, validated SearchCondition/SearchSpecification object, and then translate that model to a backend (usually SQL through the companion Doctrine packages). It deliberately does not query your database itself — it gives you a safe, structured representation of "what the user asked for", plus the machinery to render it into a query.

Install:

composer require rollerworks/search
# and, for the SQL side, the adapter you actually need:
composer require rollerworks/search-doctrine-dbal
# or
composer require rollerworks/search-doctrine-orm

Where to look first, in order:

  1. README.md — the conceptual overview (SearchFactory, FieldSet, SearchSpecification).
  2. docs/ — the "usage", "field types" and "input processors" pages; these are the ones that explain the mental model.
  3. tests/ — this package documents itself through its functional tests more reliably than through prose. When the docs are ambiguous about a method signature, copy the pattern from the nearest test.
  4. The Doctrine adapter's tests — that is where the "condition → SQL" behaviour actually lives.

Minimal first use case — build a field set, build a condition, hand it to the DBAL adapter:

use Rollerworks\Component\Search\Field\Field;
use Rollerworks\Component\Search\SearchFactoryBuilder;
use Rollerworks\Component\Search\Extension\Core\CoreExtension;
use Rollerworks\Component\Search\SearchCondition;
use Rollerworks\Component\Search\Value\SingleValue;
use Rollerworks\Component\Search\ValuesBag;

$factory = (new SearchFactoryBuilder())
    ->addExtension(new CoreExtension())
    ->getSearchFactory();

$fieldSet = $factory->createFieldSetBuilder()
    ->add(Field::create('title', 'string'))
    ->add(Field::create('created_at', 'date'))
    ->getFieldSet();

// What a user "typed":
$condition = new SearchCondition($fieldSet, (new ValuesBag())
    ->add('title', new SingleValue('invoice', ['pattern' => 'contains']))
    ->add('created_at', new SingleValue(new \DateTime('2026-01-01'), ['comparison' => '>=']))
);

From there the DBAL adapter turns the condition into a WHERE fragment with bound parameters. The important thing to internalise on day one: you never pass user strings into SQL — you pass them into typed field values, and the adapter emits parameter placeholders.

Implementation Patterns

One factory per application, built at container boot. SearchFactory is configured with extensions (core types, your custom types) and is intended to be shared. Register it as a service (or build it in a service provider) and inject it. Do not rebuild it per request in a loop.

Field set = your public search API. Treat the FieldSet like a form type: it is the whitelist of what a client may search on. Each Field has a name (the public identifier the client sends), a type, and options. Only fields registered here can ever reach the query builder, which is the single most valuable security property of this package.

Pipeline: input → condition → specification → query.

// 1. Parse untrusted input into a condition using an input processor
$processor = new JsonInput();                 // or XmlInput / your own
$condition = $processor->process($fieldSet, $request->getContent(), $factory);

// 2. Wrap it in a specification when you need ordering / pagination
$specification = $factory->createSearchSpecification($fieldSet)
    ->setCondition($condition)
    ->setMaxResults(25)
    ->setFirstResult(($page - 1) * 25)
    ->setOrder(['created_at' => 'DESC']);

// 3. Translate to SQL
$sqlConditionBuilder->apply($specification, $dbalQueryBuilder);

Keeping these three steps separate pays off: the condition is serialisable and cacheable, the specification is where you impose server-side limits (max page size, allowed orderings), and only the last step knows about your schema.

Map public field names to real columns explicitly. The adapters offer a mapping hook (a field-name/column resolver on the FieldSet, or the adapter's field config). Always set it — Field::create('title', 'string') should map to a.title, not be guessed. This also lets you expose author as a field while the SQL joins users and orders by users.name.

Custom field types for domain enumerations. A select type is fine for a handful of values, but for things like "status" or a tenant-scoped ID, implement a FieldType (or a FieldTypeExtension over string) so validation happens once, at parse time, instead of scattered through controllers:

final class StatusType extends AbstractFieldType
{
    public function buildType(FieldConfigInterface $config, array $options): void
    {
        $config->setValueComparator(new MyComparator());
    }
}

Register it on the factory builder via addTypeExtension() / the type registry, and reference it as Field::create('status', 'status').

Reuse a base specification. Ordering and pagination rules (max 100 per page, default sort by id, only created_at/title orderable) belong in one place — a SearchSpecificationFactory helper — not in each controller. Same for a default "no conditions ⇒ no WHERE" short-circuit.

Test against the generated SQL, not the object graph. The unit-testable seam is $sqlConditionBuilder->getSQL($specification) / the bound parameters. Assert on the SQL string and parameter array; that catches accidental LIKE '%…%' regressions and missing bindings.

Symfony / framework integration. If there is an official bundle for your framework version, use it for wiring only, and keep your field sets as plain services. If not, the two-service setup above (factory + field set) is ~30 lines of container config and is what you want anyway.

Gotchas and Tips

  • Values are typed, conditions are not free-form. SingleValue, array/multi-value variants and the per-value option bag (comparison, pattern) are enforced per field type. Passing a plain string where a SingleValue is expected throws at parse time — which is good, but the error messages are terse. Catch InvalidSearchConditionException (or its subclasses) at the boundary and turn it into a 400 with the violated field name.
  • Case sensitivity and LIKE are not abstracted away for you. contains on a string field produces a case-sensitive LIKE unless your DB collation saves you. Decide deliberately whether you need lower()-based matching or a ILIKE-style custom comparator; don't discover it in production.
  • Leading-wildcard searches do not use indexes. contains/ends_with are fine for small tables and terrible for large ones. Consider restricting those patterns to specific fields, or wiring a full-text/trigram comparator as a custom value comparator.
  • Ordering must be whitelisted by you. The condition layer can carry ordering, but whether setOrder(['password' => 'ASC']) is allowed is your policy. Validate against an allowlist before building the specification — the package will happily emit whatever column you give it.
  • Field name ≠ column name. Forgetting to configure the mapping is the most common "why is this SQL invalid / why does ordering do nothing" bug. It also silently makes ordering a no-op in the ORM adapter if the field can't be resolved.
  • null needs its own comparison semantics. Equality against NULL is not = NULL. Use the field type's is_null/empty comparison support rather than crafting a value and hoping.
  • Do not serialise conditions into a cache key without versioning. SearchCondition/SearchSpecification are serialisable and cacheable, but a FieldSet change (new field, changed type) can make old serialised payloads fail to unserialise. Include a schema version in the cache key.
  • Immutability assumptions bite. The field set and factory are expensive-ish to construct and are effectively configuration; the specification is the mutable per-request object. Don't mutate the field set at runtime.
  • Low-maintenance library. ~110 stars, quiet issue tracker, and the API surface has changed between minor versions historically. Pin with a caret constraint, keep the Doctrine adapter and core on matching versions, and prefer reading tests/ over blog posts — third-party tutorials are almost all for older releases. Add a handful of SQL-snapshot tests of your own so a dependency bump that changes generated SQL shows up in CI rather than in a customer's search results.
  • Extension points worth knowing: custom FieldType (validation + default comparator), FieldTypeExtension (tweak an existing type), input processors (accept your own request format), and the adapter's field/column resolvers. Between those four you can cover nearly every "but our search needs…" requirement without forking.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons