ns/filtered-pagination-bundle
Filtered Pagination Bundle adds filter-aware pagination to PHP applications, preserving query strings and active filters across page links. Generate paginated results that keep search, sort, and filter state intact, with simple integration and configurable templates.
First, the reality check. This is a Symfony bundle, not a Laravel package. It has no Illuminate\Support\ServiceProvider, no Facade, no auto-discovery, and no config/filtered_pagination.php. If you composer require ns/filtered-pagination-bundle into a Laravel app you will get a package whose composer.json requires symfony/framework-bundle and whose service definitions are only loaded by Symfony's kernel. Laravel will happily install it and never boot it. Treat it as a reference implementation and a source of patterns, not a drop-in dependency.
Where to look first, in this order:
composer.json — confirm the Symfony version constraint and which paginator it depends on (KnpPaginator, Pagerfanta, or its own). This tells you how much of the package is glue vs. logic.src/ — find the handler class. It is almost certainly a service that takes a Doctrine QueryBuilder, reads filters off the request, applies andWhere clauses, and hands the builder to a paginator.DependencyInjection/ and Resources/config/services.* — the service id and constructor arguments. In a Symfony host you'd confirm the id with bin/console debug:container | grep -i filter.README / tests — the whitelist format for filters is the real API surface.Minimal Symfony usage (the only place this actually runs):
// bundles.php (Symfony 4+/Flex)
NobletSolutions\FilteredPaginationBundle\FilteredPaginationBundle::class => ['all' => true],
// Controller
public function index(Request $request, FilterHandler $handler): Response
{
$qb = $this->getDoctrine()->getRepository(Order::class)->createQueryBuilder('o');
$result = $handler->handle($qb, $request); // paginated result
return $this->render('orders/index.html.twig', ['result' => $result]);
}
Your first Laravel use case is an admin listing — /admin/orders?status=paid&min_total=100&sort=-created_at&page=2 — where you want filtering and pagination without hand-writing thirty if ($request->filled(...)) blocks. Read the bundle's filter-application loop once, then build the equivalent in Laravel (see below). That is the fastest path to value from this package.
Pattern 1 — Port the filter loop to Eloquent (recommended). The bundle's core is "whitelist of filter keys → closure that mutates a query builder." Express that as a Laravel pipeline:
class FilterPipeline
{
public function __construct(private array $filters) {}
public function apply(Builder $query, array $input): Builder
{
foreach ($this->filters as $key => $filter) {
if (array_key_exists($key, $input) && $input[$key] !== null && $input[$key] !== '') {
$filter($query, $input[$key]); // whitelisted closure
}
}
return $query;
}
}
Register per-model filter maps (OrderFilters, UserFilters) in a service provider and resolve them by model or route name. This preserves the bundle's separation of concerns without the Symfony baggage.
Pattern 2 — Symfony host with a Laravel-adjacent API layer. If your stack genuinely has a Symfony service (or you're migrating), use the handler in a controller and return JSON via a serializer. Keep the handler call in one thin controller action so the Symfony dependency stays at the edge and the rest of your domain code is portable.
Pattern 3 — Filter metadata endpoint. Expose the whitelist to your frontend so the UI can render the correct filter controls and validate client-side:
Route::get('/api/orders/filters', fn () => OrderFilters::schema());
The bundle's config-driven filter definitions map naturally onto this.
Pattern 4 — Sorting as a filter. Most naive implementations forget sorting. Model it as a separate whitelist (['created_at', 'total', 'status'] with a - prefix convention) rather than letting ?sort=anything reach orderBy(). The bundle typically handles pagination params only, so this is on you either way.
Workflow that works day to day:
filters() map and an allowedSorts() list.$request->all() — into the pipeline.->paginate($perPage)->withQueryString().simplePaginate() for large tables.app(FilterHandler::class) to work is the single most common dead end.Doctrine\ORM\QueryBuilder. Passing an Eloquent builder will fail type checks. Even the query semantics differ (DQL vs. SQL, different join syntax, different null handling).Symfony\Component\HttpFoundation\Request directly, you cannot reuse it from a queue job, console command, or API client. Extract parameters into an array/DTO first and treat request-parsing as a thin adapter.?filter[o.secret]=1 or a crafted sort value can leak columns or produce invalid queries. Validate against an explicit map and reject unknown keys.items, page, per_page, total) at the boundary.COUNT(*) on every request. On large, heavily-filtered tables this dominates latency. Profile it, cache it, or switch to simple pagination.bin/console debug:container <id> to confirm the service is registered, debug:event-dispatcher if filters are applied via listeners, and log the final DQL/SQL. In Laravel's port, ->dd() the builder after all filters are applied, not before.composer.json.How can I help you explore Laravel packages today?