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

Pagination Bundle Laravel Package

mazarini/pagination-bundle

mazarini/pagination-bundle provides straightforward pagination for PHP/Symfony applications. Configure page sizes, paginate result sets, and render consistent navigation with bundle services and template helpers for cleaner list and search pages.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check. mazarini/pagination-bundle is a Symfony bundle for Doctrine ORM, not a Laravel package. There is no service provider, no facade, no config/pagination.php. composer require mazarini/pagination-bundle in a Laravel app will install code that never boots — the bundle expects Symfony\Component\HttpKernel\Bundle\Bundle, Config\Definition\ConfigurationInterface, and Doctrine's QueryBuilder/Paginator. Two valid reasons to keep reading: you maintain a Symfony service alongside your Laravel app, or you want to lift the pattern into Laravel's LengthAwarePaginator. If you strictly need Laravel pagination, use the framework's built-in paginator and skip this package.

Minimal steps (Symfony host app):

composer require mazarini/pagination-bundle

Register it in config/bundles.php:

return [
    // ...
    Mazarini\PaginationBundle\MazariniPaginationBundle::class => ['all' => true],
];

Check composer.json in the package for its symfony/* and doctrine/orm constraints before installing — with 1 star and 0 dependents there is no ecosystem pressure keeping it compatible with current Symfony versions, so resolve conflicts early rather than at runtime.

Where to look first, in this order:

  1. README.md — the only documentation you will get; expect a short usage snippet, not a manual.
  2. src/ — identify the service class (likely a paginator wrapping Doctrine\ORM\Tools\Pagination\Paginator) and any Twig extension. The constructor signature tells you what you can inject and override.
  3. tests/ — if present, this is the de-facto API documentation. Copy the test setup verbatim.
  4. Resources/views/ (or templates/) — the pagination markup you will inevitably want to restyle.

First use case — paginate a Doctrine query in a controller and hand the page to Twig:

#[Route('/articles', name: 'article_index')]
public function index(Request $request, ArticleRepository $repo, PaginatorInterface $paginator): Response
{
    $qb = $repo->createQueryBuilder('a')
        ->where('a.published = :p')
        ->setParameter('p', true)
        ->orderBy('a.createdAt', 'DESC');

    $page = max(1, $request->query->getInt('page', 1));

    return $this->render('article/index.html.twig', [
        'articles' => $paginator->paginate($qb, $page, 20),
    ]);
}

Then render the links in the template with whatever Twig function or include the bundle ships. If the service id is not autowired by convention, run bin/console debug:container | grep -i pagin to find the actual id — do this before guessing.

Laravel equivalent, for comparison: Article::where('published', true)->paginate(20) already does the count query, page clamping, and ?page= handling. Anything this bundle gives you beyond that is presentation.

Implementation Patterns

Keep the service out of controllers. Inject the paginator into a thin controller; let a query object or repository method own the QueryBuilder. The bundle paginates queries, not entities, so your repository should expose createIndexQueryBuilder(): QueryBuilder and the controller should only supply page and perPage. This keeps pagination testable without HTTP.

Always pass a deterministic ORDER BY. Doctrine's paginator applies LIMIT/OFFSET; without an explicit sort, MySQL and Postgres will hand you different rows across pages and you will see duplicates or skipped records. Sort by id last if the natural sort key isn't unique.

Clamp inputs at the edge, not in the bundle.

$page    = max(1, (int) $request->query->get('page', 1));
$perPage = min(100, max(5, (int) $request->query->get('per_page', 20)));

An unclamped per_page from the query string is a trivial DoS — a client requests per_page=1000000 and you materialise the table.

Preserve query string state in links. Pagination almost always coexists with filters. Whatever template or helper the bundle provides, verify it forwards existing query params:

{# if the bundle's macro doesn't accept extra params, build the URL yourself #}
<a href="{{ path(app.request.attributes.get('_route'), app.request.query.all|merge({'page': page - 1})) }}">Prev</a>

If the bundle's Twig helper is too rigid, don't fight it — write a 15-line Twig macro. You will own it, but it will be correct.

Reusable pagination concern. A small trait or base controller method that takes a QueryBuilder and a Request and returns the paginated result removes the page/perPage clamping boilerplate from every action:

trait Paginates
{
    protected function paginate(QueryBuilder $qb, Request $request, int $default = 20): iterable
    {
        $page = max(1, $request->query->getInt('page', 1));
        $size = min(100, max(5, $request->query->getInt('per_page', $default)));

        return $this->paginator->paginate($qb, $page, $size);
    }
}

JSON/API responses. Symfony's serializer does not know how to flatten a paginator into a HAL-style envelope. Expose the pieces explicitly:

return $this->json([
    'data'       => $paginator->getResult(),   // or ->getIterator()->getArrayCopy()
    'meta'       => [
        'page'        => $paginator->getCurrentPage(),
        'per_page'    => $paginator->getPageSize(),
        'total'       => $paginator->getTotalItems(),
        'total_pages' => $paginator->getTotalPages(),
    ],
]);

Match the accessor names to the actual class — check src/ rather than trusting this snippet.

Porting the pattern to Laravel. If the goal is consistency between a Symfony service and a Laravel app, implement a Paginates trait over Eloquent instead of trying to shim the bundle:

protected function paginateQuery(Builder $query, Request $request): LengthAwarePaginator
{
    return $query->paginate(
        perPage: min(100, max(5, $request->integer('per_page', 20))),
        page: max(1, $request->integer('page', 1)),
    )->withQueryString();
}

withQueryString() is the Laravel analogue of the query-string preservation above, and it is the single most commonly forgotten call.

Gotchas and Tips

GPL-3.0 is the biggest practical blocker. This is a copyleft license, not MIT. Linking it into a proprietary Laravel or Symfony application is a legal question, not a technical one. Many teams blanket-ban GPL dependencies. Get sign-off before it lands in composer.json, or budget time to reimplement — pagination is ~100 lines, and the clean-room version will be cheaper than the legal review.

One star, zero dependents, no release history to speak of. Treat this as vendored source, not a dependency. Pin an exact version ("mazarini/pagination-bundle": "1.0.0"), commit composer.lock, and be prepared to fork into app/Support/ when the author stops responding. Do not build a public API on top of it.

It only knows Doctrine. No Eloquent, no query builder abstraction, no array/Collection adapter documented. If you need to paginate an array or an API response, this bundle is the wrong tool.

COUNT(*) on joined queries is the classic footgun. Doctrine's paginator issues a separate count query; with fetchJoinCollection or JOINs it can over-count or blow up on DISTINCT/GROUP BY. If totals look wrong, log the count SQL:

$qb->getQuery()->getSQL(); // inspect both the page query and the count query

Set ->setUseOutputWalkers(false) in the underlying Doctrine paginator when the count query fails with "Cannot count query which selects two FROM components" — but only after verifying the results are still correct.

Large OFFSET degrades linearly. Page 5000 of a 1M-row table is a full scan in disguise. If any endpoint is deep-paginated, switch to keyset pagination (WHERE id < :lastId ORDER BY id DESC LIMIT n) and accept losing "jump to page N". No bundle will save you here.

Page-out-of-range behaviour is undefined until you test it. Request ?page=99999 and ?page=-1 and ?page=abc against a real dataset. You want an empty result plus sane metadata, not a 500 or an unbounded query.

Debugging workflow: enable the Doctrine SQL logger or use the Symfony profiler's Doctrine panel and confirm exactly two queries fire per page render (page + count). If you see N+1 queries, the bundle is lazily hydrating a collection per row — add ->addSelect()/fetch joins upstream.

Extension points, in order of preference:

  1. Override the Twig template via templates/bundles/MazariniPaginationBundle/ — survives version bumps.
  2. Pass your own page size and page number rather than relying on bundle-side config.
  3. Decorate the paginator service in config/services.yaml if you need to normalise accessor names for a shared API envelope.
  4. Fork. With a package this small, forking is usually step 2, not step 4.

When to walk away: if the README doesn't get you to a working page in 20 minutes, stop. Symfony's own Doctrine\ORM\Tools\Pagination\Paginator plus a Twig macro covers the same ground with a maintenance guarantee and no license question.

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
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core