northernco/datagrid-bundle
A Laravel package for building configurable data grids with sortable columns, filtering, and pagination. Northernco/datagrid-bundle helps turn Eloquent queries into reusable tables for admin panels and dashboards.
Know what you're installing first. Despite the "datagrid" name, northernco/datagrid-bundle is a Symfony bundle, not a Laravel package. It will not autoload into a Laravel app via composer require and "just work" — there is no service provider, no facade, no Blade integration. If you're coming from Laravel, treat this as a reference implementation you may port concepts from, or a package you only use inside a Symfony service. Do not expect it to slot into a Laravel project.
Assuming a Symfony host app:
composer require northernco/datagrid-bundle
Register it in config/bundles.php:
return [
// ...
NorthernCo\DatagridBundle\NorthernDatagridBundle::class => ['all' => true],
];
Where to look first, in order:
src/ — find the grid builder/factory service. This package is small (4 stars, 0 dependents), so the entire public API is probably 3–6 classes.Resources/config/ (or config/) — the service definitions tell you what's autowirable. Look for a GridFactory, GridBuilder, DatagridFactory, or similarly named service.README / any docs/ — likely thin, so the tests directory (tests/) is usually the most honest documentation of intended usage.DependencyInjection/Configuration.php — this is where any configurable defaults (page size, template, css classes) live.First use case — a sortable, paginated table in a controller:
The shape is almost always "build a grid from a query, feed it request params, hand it to Twig." Confirm the exact class names from src/, but expect something like:
#[Route('/admin/users', name: 'admin_users')]
public function index(Request $request, GridFactory $gridFactory): Response
{
$qb = $this->userRepository->createQueryBuilder('u')
->leftJoin('u.roles', 'r')->addSelect('r');
$grid = $gridFactory->create($qb)
->addColumn('id', ['sortable' => true])
->addColumn('email', ['sortable' => true])
->addColumn('roles', ['renderer' => 'roles_badge'])
->setPageSize(25);
$grid->handleRequest($request);
return $this->render('admin/users/index.html.twig', ['grid' => $grid]);
}
{{ datagrid(grid) }}
If the bundle does not ship a Twig function/extension, it likely exposes a render() method or a template you include manually — check Resources/views/.
Minimum viable integration checklist: bundle registered → service autowired in a controller → grid built from a QueryBuilder (not an array; most such bundles assume a query) → request handled → rendered. Get that working before adding filters or custom columns.
1. Grid definitions as reusable service classes, not controller code. The single biggest maintainability win with any datagrid bundle is to stop building grids inline. Define a grid per resource:
final class UserGrid implements GridInterface
{
public function build(GridBuilder $builder): void
{
$builder
->addColumn('id', Column::TYPE_INTEGER, ['sortable' => true])
->addColumn('email', Column::TYPE_TEXT, ['sortable' => true, 'filterable' => true])
->addColumn('createdAt', Column::TYPE_DATETIME, ['sortable' => true, 'label' => 'Joined'])
->addFilter('role', RoleFilter::class)
->setDefaultSort('createdAt', 'DESC');
}
}
Then in the controller you only pass the query source and the grid class. This is also how you make the grid testable without booting a kernel.
2. Query source should be a QueryBuilder, populated with joins up front.
Bundles like this generally paginate with the Doctrine Paginator under the hood. Sorting by a joined column or filtering on an association only works if the join is already on the builder. Pre-join anything you expose as a column or filter, and addSelect() it to avoid lazy-load storms in the template.
3. Sort/filter whitelisting is your job.
Do not assume the bundle validates ?sort= against declared columns. In practice, small bundles map the request parameter straight onto the query. Always:
$grid->setAllowedSorts(['id', 'email', 'createdAt']);
$grid->setAllowedFilters(['role', 'status']);
If the bundle has no such API, guard in the controller by sanitising $request->query->get('sort') against $grid->getColumns() before handleRequest().
4. Form-based filters integrate cleanly with Symfony forms.
For anything beyond a single LIKE box, define a FilterType (a Symfony form with GET method and no CSRF), hydrate it from the request, and have it apply criteria to the builder before the grid paginates. This keeps the bundle out of the business of complex filtering and gives you validation for free.
5. Export pattern.
A common day-to-day need is "same grid, CSV output." Because the grid is built on a query, reuse the grid definition and iterate the unpaginated query in a separate export action. Structure your grid class so build() is pure and the pagination is applied only in the HTML controller. Don't try to make the bundle export for you.
6. Twig presentation overrides.
Most such bundles expose a base template you can override by convention (templates/bundles/NorthernDatagridBundle/...). Prefer overriding the template over subclassing columns — it survives upgrades and is the intended extension seam.
7. Cache grid metadata, not results.
If your grid does per-request reflection over entity metadata to infer column types, that's cheap but repeated. Column definitions are stable per class; paginated results are user-specific. Cache the former (e.g. in the DI container via a compiler pass or a cache.app pool keyed by grid class) only if you measure a problem — at this package's scale it's probably unnecessary.
8. Testing.
For grid classes, test build() against a mocked/real GridBuilder to assert columns, sorts and filters — no HTTP. For controller actions, use a KernelBrowser test with query strings (?sort=email&page=2) and assert the response contains expected rows. This catches the sorting/pagination edge cases that are the usual source of bugs.
It is not a Laravel package. This is the headline gotcha. Despite your background, there is no config/datagrid.php, no publishable assets, no Datagrid facade. If the goal is a Laravel admin table, look at spatie/laravel-query-builder + filament/tables or livewire instead. Treat this bundle as inspiration or as something you fork.
Maintenance risk is real. 4 stars, 0 dependents, last release October 2023. Assume:
composer.json constraints before adopting.doctrine/orm or symfony/http-kernel are unlikely to be patched.Expect thin documentation. With this adoption level, the README is probably a single example. Read tests/ and the service container config — they are the spec. If there are no tests, budget time to build a smoke test before you build features on top.
Pagination + DISTINCT. If you join a to-many association (roles, tags) to show it as a column, you will get duplicate rows and a wrong COUNT unless the paginator applies DISTINCT or a GROUP BY. Most simple datagrid implementations get this wrong. Symptom: page 1 shows 18 rows instead of 25 and total count is inflated. Fix by selecting from a subquery, or by using doctrine/orm's Paginator with a properly distinct root query.
Sort parameter injection. If the bundle maps ?sort= directly to addOrderBy, an attacker can pass ?sort=(SELECT ...) in some Doctrine versions, or at minimum cause 500s. Always whitelist against declared column IDs. Do not rely on the bundle to do it.
Lazy loading will bite you in Twig. The grid render iterates every row; every association column not previously addSelect()ed triggers a query. Enable the Doctrine profiler and check query count on a 25-row page. If it's 26, that's your join.
Route/query-string preservation. Filtering and sorting must preserve each other's query params, and pagination must preserve both. If the bundle's pagination links hardcode ?page=, you'll silently lose filters. Check the rendered pagination HTML early; override the pagination template if needed.
Config quirks to check in Configuration.php:
templates/ dir, causing a confusing Template not found instead of a useful error.Debugging tips:
bin/console debug:container | grep -i grid to confirm the service ID and whether it's public/autowirable.bin/console debug:config northern_datagrid to see the resolved config tree (substitute the actual alias from Configuration.php).dump($grid->getQuery()->getDQL()) and dump($grid->getQuery()->getParameters()) right before rendering — most "wrong results" issues are visible here.getEmail vs email), and whether the column type is registered.Extension points worth knowing before you fork:
services.yaml lets you inject global defaults (page size, allowed sorts) across every grid without touching call sites.If none of these seams exist in the source, that's the signal to vendor the code and evolve it yourself rather than adopting it as a dependency.
How can I help you explore Laravel packages today?