everlutionsk/pagination-bundle
Everlution Pagination Bundle adds simple table pagination and sortable headers to Symfony apps, built on Doctrine Paginator. Configure page sizes, sort query strings, and Twig templates, then render pages and paginate links with ease.
Read this first: the package is a Symfony bundle, not a Laravel package. EverlutionPaginationBundle registers itself in app/AppKernel.php, reads YAML under everlution_pagination, and renders Twig with Doctrine's Paginator. It has no service provider, no facade, no config publishing, and no Laravel-compatible dependency constraints. Dropping it into a Laravel app will not "just work" — you would be writing a bridge for a 2017-era, zero-star bundle.
Given that, treat this as a reference implementation / pattern source, not a dependency to composer require.
Minimal evaluation steps:
EverlutionPaginationBundle.php — shows the DI extension and config tree.sortable_header() — this is the only genuinely reusable idea.getPage() on uc.client.list) — shows how page state is parsed from the request and handed to Doctrine.default_page_size / max_page_size → config/pagination.php or a dedicated config file.default_sort_query_string → the query-string key (sort by default).sortable_header(label, key) → a Blade component or a helper.Paginator → LengthAwarePaginator.If you still want to try it: it targets Symfony 2/3 and PHP ~7.0, depends on Doctrine ORM and Twig, and its last release was July 2017. Check composer.json constraints against your PHP version first; a modern Laravel (PHP 8.1+, Symfony 6/7 components) will almost certainly produce dependency conflicts. Do not make it a hard requirement of an application you intend to maintain.
Because this is not installable in Laravel, the useful work is reimplementing the bundle's contract in Laravel idioms. The bundle's actual contract is small:
1. Config surface
// config/pagination.php
return [
'default_page_size' => 20,
'max_page_size' => 100,
'default_sort_query_string' => 'sort',
'sortable_header_view' => 'components.sortable-header',
];
Mirror the bundle's defaults exactly (20 / 100 / sort) so any documentation you inherit still maps one-to-one.
2. Page state parsed once, in a request object
The bundle's "Page" concept separates what the client asked for from what the query will do. Reproduce that with a small value object rather than sprinkling request()->query() calls through controllers:
final class PageRequest
{
public function __construct(
public readonly int $page,
public readonly int $perPage,
public readonly ?string $sort,
public readonly string $direction = 'asc',
) {}
public static function fromRequest(Request $request): self
{
$key = config('pagination.default_sort_query_string');
$sort = $request->query($key);
return new self(
page: max(1, (int) $request->query('page', 1)),
perPage: min(
(int) $request->query('per_page', config('pagination.default_page_size')),
config('pagination.max_page_size')
),
sort: $sort,
direction: $request->query('direction') === 'desc' ? 'desc' : 'asc',
);
}
}
The min(..., max_page_size) clamp is the single most valuable behaviour in the bundle. Without it, ?per_page=100000 is a trivial DoS on any table endpoint.
3. Whitelisted sorting
The bundle only sorts on keys you explicitly pass to sortable_header(...) in the template, which implicitly whitelists columns. Preserve that property — never pass a raw request value to orderBy:
$sortable = ['name', 'job_title', 'created_at'];
$query = User::query()
->when(
$page->sort && in_array($page->sort, $sortable, true),
fn ($q) => $q->orderBy($page->sort, $page->direction),
fn ($q) => $q->orderBy('name')
);
$users = $query->paginate($page->perPage)->withQueryString();
withQueryString() is the Laravel equivalent of the bundle carrying sort state across pagination links — omit it and sorting silently resets on page 2.
4. Sortable header component
The bundle's sortable_header(label, key) auto-translates the label and renders a link that toggles direction. A Blade component gets you the same ergonomics:
{{-- resources/views/components/sortable-header.blade.php --}}
@props(['label', 'column'])
@php
$key = config('pagination.default_sort_query_string');
$active = request()->query($key) === $column;
$dir = $active && request()->query('direction') === 'desc' ? 'asc' : 'desc';
$url = request()->fullUrlWithQuery([$key => $column, 'direction' => $dir, 'page' => 1]);
@endphp
<th>
<a href="{{ $url }}" @class(['sorted' => $active, 'sorted-desc' => $active && $dir === 'asc'])>
{{ __($label) }}
@if ($active) <span aria-hidden="true">{{ $dir === 'desc' ? '↓' : '↑' }}</span> @endif
</a>
</th>
<x-sortable-header label="Job title" column="job_title" />
<x-sortable-header label="Name" column="name" />
5. Controller shape
Keep the controller as thin as the bundle's listAction() example — build the page, hand it to the view:
public function index(Request $request)
{
$page = PageRequest::fromRequest($request);
return view('users.index', [
'users' => User::filtered($page)->paginate($page->perPage)->withQueryString(),
]);
}
6. Integration tips
per_page + page with ->paginate(); the same PageRequest object serves both Blade and JSON.The big one: framework mismatch. The installation instructions (AppKernel::registerBundles, YAML everlution_pagination) are Symfony 2/3 only. There is no Laravel service provider. Any tutorial or answer you find referencing everlution_pagination: config or sortable_header() in a Laravel app is either outdated or expects a Symfony bridge you will have to write yourself.
Abandoned-since-2017 risk. Last release 2017-07-04, zero stars, zero dependents. That means: no security patches, no PHP 8 compatibility guarantees, and no Doctrine 3 support. Pin nothing to it that you can't rip out in an afternoon. Its dependency on Doctrine also means it's irrelevant if your Laravel app uses Eloquent.
max_page_size is not optional. The bundle clamps requested page size to max_page_size. If you reimplement and forget the clamp, you've regressed on something the bundle got right. Also coerce to a positive integer — (int) 'abc' is 0, and per_page=0 can mean "unlimited" in some query builders.
Sort key must be underscore notation. The README explicitly says "please do use underscore notation" for the second sortable_header argument. That's because the bundle maps the query-string key back to a column/property name. In Laravel you control this mapping, so prefer an explicit ['job_title' => 'job_title'] map over string concatenation — it prevents sort=users.password style column-probing.
Sorting and stable pagination. ORDER BY name alone is not deterministic when duplicates exist; rows can repeat or vanish across pages. Always append a tiebreaker (->orderBy('name')->orderBy('id')) — this is a Laravel/Eloquent reality the bundle's Doctrine paginator shares.
Reset page when sort or filters change. The component above sets page => 1 in the sort link. If you forget, users land on page 7 of a re-sorted list and conclude pagination is broken.
Translation is implicit in the bundle. sortable_header('Name', 'name') translates the label automatically. If you replicate it with __(), remember your component receives an already-translated string if callers pass __('Name') — pick one convention and document it, or labels get double-translated.
Extension points, if adapting the original: the bundle exposes sortable_header_template so you can swap the header markup without forking. That is the cleanest hook it offers — if you're writing a bridge, surface the same config key and let the app override the view rather than subclassing.
Debugging checklist for "sorting doesn't stick":
jobTitle vs job_title is the classic.)withQueryString() present on the paginator?orderBy in the model overriding the controller's sort?Don't over-invest. With a score of ~2.5, no dependents, and no releases since 2017, the correct day-to-day decision is: read the README, steal the three good ideas (page-size clamp, whitelisted sortable headers, sortable-header component), implement them in ~80 lines of Laravel, and move on. Reserve actual adoption for a legacy Symfony codebase that already depends on it.
How can I help you explore Laravel packages today?