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

Data Grid Bundle Laravel Package

kitpages/data-grid-bundle

Kitpages DataGridBundle is a Symfony bundle for building configurable data grids with pagination, sorting, filtering, and customizable columns. It helps turn Doctrine queries or arrays into reusable, template-friendly grid views for admin and listing pages.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not Laravel — everything below assumes a Symfony app (it was written for the Symfony 2.8–4.x era). If you're actually in Laravel, the equivalent is yajra/laravel-datatables or spatie/laravel-query-builder; read this for the shape of the abstraction (grid config + datasource + request state), not the API.

Install and register:

composer require kitpages/data-grid-bundle
// config/bundles.php
Kitpages\DataGridBundle\KitpagesDataGridBundle::class => ['all' => true],

There is almost nothing to configure. The only knob most projects touch is which template renders the table (the bundle ships a Bootstrap 3 template), e.g. in config/packages/kitpages_data_grid.yaml:

kitpages_data_grid:
    template: '@KitpagesDataGrid/Grid/bootstrap3.html.twig'

Where to look first, in this order:

  1. README.md — it contains the canonical 15-line Doctrine ORM example; copy it verbatim before reading anything else.
  2. Resources/views/Grid/*.html.twig — this is the documentation for what a column/toolbar/pager renders and which variables your custom cell templates receive.
  3. DependencyInjection/Configuration.php — the real list of config keys your installed version accepts (it differs between 1.x/2.x, so don't trust blog posts).
  4. Datasource/DoctrineOrmDatasource.php — read it once; it's ~200 lines and tells you exactly how sorting, filtering and counting are translated to DQL, which is where 90% of surprises come from.

First use case: an admin CRUD list. You want an entity table with sortable headers, per-column text filters, and pagination, with zero hand-written table markup. That is precisely the bundle's sweet spot — it is not a full admin generator (no forms, no actions, no row editing).

Mental model, three objects:

  • GridConfig — you declare columns, labels, sortable/filterable flags, datasource, page size.
  • Datasource — you hand it a Doctrine QueryBuilder (or an array, or your own implementation); it applies sort/filter/pagination.
  • GridRequest — the bundle reads page, sort, order, filter[...] out of the query string, namespaced by grid name, so state is bookmarkable and shareable.

Implementation Patterns

Put the grid config in a service, not in the controller. This is the single pattern that keeps the bundle pleasant to live with; inline configs rot fast.

namespace App\Grid;

use Doctrine\ORM\EntityManagerInterface;
use Kitpages\DataGridBundle\Datasource\DoctrineOrmDatasource;
use Kitpages\DataGridBundle\Grid\GridConfig;
use Kitpages\DataGridBundle\Grid\GridConfigInterface;

class UserGridConfig implements GridConfigInterface
{
    public function __construct(private EntityManagerInterface $em) {}

    public function getGridConfig(): GridConfig
    {
        $qb = $this->em->createQueryBuilder()
            ->select('u')
            ->from(\App\Entity\User::class, 'u')
            ->leftJoin('u.company', 'c')->addSelect('c'); // join, don't lazy-load per row

        $grid = new GridConfig();
        $grid->setName('users');                        // namespaces the query string
        $grid->setDatasource(new DoctrineOrmDatasource($qb));

        $grid->addColumn('id',        'u.id',        ['label' => 'ID',     'sortable' => true, 'filterable' => true]);
        $grid->addColumn('email',     'u.email',     ['label' => 'Email',  'sortable' => true, 'filterable' => true]);
        $grid->addColumn('company',   'c.name',      ['label' => 'Société','sortable' => true, 'filterable' => true]);
        $grid->addColumn('createdAt', 'u.createdAt', ['label' => 'Créé le', 'sortable' => true, 'filterable' => false, 'format' => 'd/m/Y']);

        $grid->setPaginator(true);
        $grid->setMaxPerPage(25);

        return $grid;
    }
}

Controller stays trivial:

#[Route('/users', name: 'user_index')]
public function index(GridManager $gridManager, UserGridConfig $config): Response
{
    return $this->render('user/index.html.twig', [
        'grid' => $gridManager->getGrid($config->getGridConfig()),
    ]);
}

In 2.x the manager pulls the current request from the request stack; older versions expect an explicitly built GridRequest. If sort/page never change, that's the line to check.

Twig, all or in pieces. kitpages_data_grid(grid) renders filter row + table + pager. When you want the toolbar/buttons in your own layout, render the pieces separately (kitpages_data_grid_filter(grid), kitpages_data_grid_pager(grid) — confirm names with bin/console debug:twig | grep kitpages):

{{ kitpages_data_grid_filter(grid) }}
<div class="table-responsive">{{ kitpages_data_grid(grid) }}</div>
{{ kitpages_data_grid_pager(grid) }}

Custom cells: use a twig-typed column rather than fighting the default formatter. Map the column to a field you can sort by, then render whatever you want:

$grid->addColumn('actions', 'u.id', [
    'label'      => '',
    'sortable'   => false,
    'filterable' => false,
    'type'       => 'twig',
    'twig'       => 'grid/column/actions.html.twig',
]);
{# templates/grid/column/actions.html.twig — mirror the variables used in the bundle's column template #}
<a href="{{ path('user_edit', {id: item.id}) }}" class="btn btn-sm btn-default">Éditer</a>

Non-Doctrine sources. ArrayDatasource for aggregated/report data and small API payloads (sorting/filtering then happens in PHP over the whole array — fine at 200 rows, a memory incident at 200k). For anything else implement DatasourceInterface: you provide getData($offset, $limit), getNbTotalRows(), and the filter setter/getter; the grid handles the rest. This is the cleanest way to put an Elasticsearch or REST endpoint behind the same UI.

Multiple grids on one page just need distinct setName() values — that's what prevents their page/sort/filter params from colliding.

Export is a second action, not a checkbox away: build the same QueryBuilder, skip the grid paginator, fputcsv to php://output. Twenty lines, and it reuses the filter values from the query string so "export what I'm looking at" works. (2.x also has an Export namespace/toolbar item; check it before writing your own.)

External search box: a normal GET form whose field name matches the bundle's namespaced filter param. Read the generated filter input's name attribute in the HTML — that's your contract, and it's stable across versions in a way the docs aren't.

Gotchas and Tips

  • Never share a QueryBuilder instance. DoctrineOrmDatasource mutates the QB (order by, and depending on version setMaxResults/setFirstResult). Build a fresh one inside getGridConfig() every call, or you'll get order-by leakage between grids and "row 26 shows up on page 1" bugs.
  • Sorting fields must be real DQL paths (u.email, c.name), not column aliases. Passing email where the query has no alias produces a DQL error deep inside the datasource, not a friendly validation message.
  • Filtering is LIKE %value% against the mapped field. On Postgres that blows up for integer/date columns (operator does not exist: integer ~~ unknown) — mark those filterable => false or map to a casted expression. On MySQL it silently defeats indexes; on a large table, per-column free-text filters are the first performance complaint you'll get.
  • The count query is issued on every render. With GROUP BY, DISTINCT, or HAVING in your QB the total can disagree with the rows, and pagination goes wrong at the last page. Test a grid with a join that duplicates rows before shipping it.
  • N+1 is hidden but real: each row is a full entity, so any column touching an unjoined relation triggers a query per row. Always leftJoin(...)->addSelect(...) for displayed relations.
  • Query-string state is the feature and the trap. Filters are in the URL, so results are bookmarkable and cacheable — which means an aggressive HTTP cache or CDN can serve one user's filtered list to another. Keep these routes out of shared caches.
  • Styling is Bootstrap 3. On Bootstrap 4/5 markup the table renders but looks wrong, and the pager's <ul class="pagination"> classes are dated. Override the template in templates/bundles/KitpagesDataGridBundle/Grid/… rather than editing vendor/; copying the whole template and adjusting the classes is a 10-minute job and the intended extension point.
  • Debugging checklist: dump($grid->getGridRequest()) for parsed state, dump($qb->getDQL())/getParameters() (log it inside a local fork of the datasource if needed) for the actual query, bin/console debug:container kitpages_data_grid for the service ids, bin/console debug:twig | grep kitpages for the real function names in your version.
  • Injection is handled for you — filter values go through bound parameters — but sorting uses the field mapping you defined, so never build a column's field path from request data.
  • visible => false keeps a column configured (useful for hidden sort/filter targets) while omitting it from markup; it's the cleanest way to sort by a column you don't want to display.
  • Vendor risk is the real assessment. 77 stars, zero dependents, last release March 2019: this is effectively unmaintained, and Symfony 5/6 will surface abandoned code paths (old service aliases, removed templating helpers). Pin the version in composer.json, budget a day for Symfony-upgrade breakage, and be prepared to fork — it's MIT, the surface area is small, and the parts you'd have to maintain are one datasource class and a handful of Twig templates. That's acceptable for an internal admin screen; it is not acceptable as the foundation of a customer-facing feature with a multi-year life.
  • If you outgrow it, the migration is easier than it looks: keep the GridConfig service, throw away the bundle, and render a plain Twig table from the same QueryBuilder plus a hand-rolled pager. The bundle's value is the first 200 lines of boilerplate, not the lock-in.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource