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 Bundle Laravel Package

rollerworks/search-bundle

Symfony bundle integrating Rollerworks Search: build user-friendly search forms, parse complex query strings, and translate them into Doctrine ORM/DBAL queries with reusable field definitions, validation, and extensible conditions.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the honest framing: rollerworks/search-bundle is a Symfony bundle, not a Laravel package. It ships no service provider, no facade, no config file that Laravel would read. For a Laravel codebase the bundle itself is dead weight — what you actually want is the framework-agnostic core it wraps, rollerworks/search, plus (optionally) one of its backend bridges (rollerworks/search-doctrine-orm, search-doctrine-dbal, search-elasticsearch). Everything below is written for that reality: how to pull the useful parts in and where the bundle's conventions still inform the design.

What the package gives you (and why it's worth the detour): it separates describing a search from executing it. You define a FieldSet (the searchable schema), parse user input into an immutable SearchCondition, and only at the edge translate that condition into SQL/DQL/ES/Eloquent. That seam is the whole value proposition — it makes search testable, serializable, and backend-agnostic. Most Laravel codebases hand-roll $request->filled('name') ? $q->where(...) chains and then struggle to make them bookmarkable, validatable, or reusable across an export job.

Minimal steps (Laravel side):

composer require rollerworks/search:^1.0
# only if you want a reference implementation to read:
composer require --dev rollerworks/search-bundle

Bind the factory as a singleton in a service provider — building and compiling a SearchFactory is not cheap and must not happen per-request-per-row:

// app/Providers/SearchServiceProvider.php
$this->app->singleton(SearchFactory::class, function () {
    return (new SearchFactoryBuilder())
        ->addExtension(new CoreExtension())
        ->addExtension(new \App\Search\Extension\AppExtension()) // your custom types
        ->getSearchFactory();
});

Define your first field set — start with one entity and three or four fields, not the whole domain:

$fieldSet = $factory->createFieldSetBuilder('users')
    ->add('id', IntegerType::class)
    ->add('name', TextType::class, ['comparison_type' => TextType::PATTERN_CONTAINS])
    ->add('registered', DateType::class)
    ->getFieldSet();

Where to look first: the core's FieldSet / FieldSetBuilder and the Extension\Core\Type directory (that's your vocabulary of types), then one input processor (StringQueryInput for querystring-style, JsonQueryInput for API payloads), then the bundle's param converter and form type — read them as reference wiring, not as code you'll run.

First use case: a filterable index page where the same condition object drives the paginated list, the CSV export, and the "save this search" link. That's the moment the abstraction pays for itself; anything smaller is overhead.

Implementation Patterns

1. FieldSet as a cached, named artifact. Treat a field set like a schema, not a request object. Register one per search domain (users, orders), build it once, and cache the compiled factory in the container. If you let controllers construct field sets inline you lose the ability to reuse them in exports, queued jobs, and API endpoints — which is the only reason to adopt this library.

2. Input → condition → backend, three explicit steps. Never let the controller do all three inline.

// 1. parse
$condition = $inputProcessor->process(
    new StringQueryInput(),
    $fieldSet,
    $request->only(['id', 'name', 'registered'])   // or $request->query() wholesale
);

// 2. persist/share (optional but cheap)
$payload = $serializer->serialize($condition);      // store in session or ?search=
$condition = $serializer->unserialize($request->input('search'));

// 3. apply
$query = (new EloquentConditionBuilder($condition))->apply(User::query());

Keeping step 1 free of step 3 is what lets you unit-test parsing without a database.

3. One condition, many consumers. Because SearchCondition is a plain value object, the same instance feeds a paginated view, a toSql()-checked export query, and an index sync job. Write your builder against an interface so the backend is swappable:

interface ConditionBuilder { public function apply($query, SearchCondition $c); }

Laravel devs will find the Doctrine ORM bridge in the ecosystem instructive but unusable directly (doctrine/dbal ≠ Eloquent). Budget half a day to write an Eloquent builder: iterate $condition->getValues(), switch on field name, map comparison type (=, !=, >, <, between, starts_with, contains) to where / whereBetween / whereLike, and orWhere for OR groups.

foreach ($condition->getValues() as $fieldName => $values) {
    if ($values === null) { continue; }
    match ($fieldName) {
        'name'       => $q->where('name', 'like', '%'.$values->first().'%'),
        'registered' => $q->whereBetween('created_at', [$values->getLower(), $values->getUpper()]),
        default      => null,
    };
}

4. Use the string-query input for human-facing filters, JSON for APIs. StringQueryInput gives you foo bar, 1..10, ! (not-empty), >10 range syntax for free — that's an enormous amount of UX for zero front-end work. The same user intent arriving as a JSON filter payload goes through JsonQueryInput into the same condition, so both surfaces stay consistent.

5. Keep the condition in the URL for pagination. Serialize the condition into a compact query parameter and let Laravel's paginator append it automatically (->withQueryString()). In Symfony the bundle ships a param converter that resolves $searchCondition from the request — replicate that with a small FormRequest or a route-middleware that hydrates the condition onto the request. Users can then bookmark and share filtered views, which stakeholders notice immediately.

6. Surface validation errors, don't swallow them. Input processing throws an input-processor exception carrying per-field errors (wrong type, malformed range, unknown field). Map those onto Laravel's ValidationException / $errors bag rather than 500-ing. A malformed registered=2024-13-45 should render on the form field, exactly as a validation rule would.

7. Extend with custom field types only when core types fall short. A custom type extends AbstractFieldType and usually pairs with a view transformer (e.g., a StatusType that maps string labels to enum values). Register through your own extension class added to the SearchFactoryBuilder — mirroring how the bundle registers CoreExtension. Do this for one or two domain types and stop; it's an escape hatch, not a design system.

8. Test at the parsing boundary. The cheapest high-value tests are golden-file style: given fieldset + raw input, assert the resulting condition's normalized values. No DB, no HTTP, milliseconds each. Then a handful of integration tests asserting generated SQL via ->toSql().

Gotchas and Tips

The package is dormant — plan accordingly. Last release was October 2017, 10 stars, zero dependents. It targets the PHP 5.5–7.x / Symfony 2.8–3.x era. Expect friction on PHP 8+ (dynamic properties, removed each(), old container XML/YAML) and on Symfony 4+ Flex. The core rollerworks/search is the same age. Treat this as a reference architecture you can vendor or port, not a dependency you upgrade casually. Fork-and-patch is the realistic posture if you commit to it.

The bundle won't install usefully in a modern app. No Laravel provider, and its Symfony service definitions assume the old bundle/container conventions. Install it --dev if at all, purely to read Resources/config and the form type.

Field sets are immutable but not free. createFieldSetBuilder()->getFieldSet() compiles and caches; don't call it inside a loop or a Blade partial. One per domain, singleton.

Empty string ≠ null ≠ "not empty". Input processors generally coerce '' to null (field ignored), which surprises people who expect an empty filter to match empty rows. Use the explicit !value (not-empty) and value (empty) operators, and be deliberate about whether a blank form submission means "no filter" or "clear filters".

Ranges are inclusive by default and the syntax is strict. 1..10 works; 1...10 or spaces around the dots may not. Date ranges require the format the DateType was configured with — a mismatch produces a validation error that's easy to misread as a server fault.

Text comparison is opt-in. A bare TextType is exact-match; you need comparison_type (equals, starts_with, contains, pattern) to get LIKE behaviour. Users will expect "contains" immediately. Related: decide explicitly whether % and _ in user input are wildcards or literals, and escape them if not — otherwise name=50% behaves strangely.

Serialized conditions are fragile across schema changes. The serializer is version-tagged for a reason. If you add/remove a field or change a field's type, previously stored payloads can fail to deserialize. Version your stored searches and handle deserialization failure by falling back to an empty condition rather than a 500.

SearchCondition values are nested, not flat. Iterating naively gives you arrays of value objects per field; range fields carry lower/upper bounds rather than a list. Write your Eloquent builder defensively — default: null in the match, skip null fields — because a new field added to the field set but not the builder produces silently-broader queries, not errors.

The library does not sort — only filters. Ordering, pagination, and full-text relevance are your problem (or your search engine's). Don't expect SearchCondition to carry an ORDER BY; wire ?sort= separately.

The Elasticsearch bridge is effectively unusable. It targets ES 1.x/2.x query DSL. If you need ES, use the condition only as a validated filter representation and translate it yourself.

Delegation of translation labels. Comparison operators and form labels resolve through the Symfony translation domain. In Laravel you'll be writing your own labels anyway — just be aware the bundle's Twig helpers and translations don't follow you.

Debugging tip: dump the condition via the serializer (json_encode($serializer->serialize($condition))) rather than var_dump on the object graph — it's the fastest way to see whether a bug is in parsing or in query building. Then check ->toSql() / ->getBindings() on the resulting builder: almost every "search returns everything" report is a field-name mismatch between field set and builder.

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.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings