whatwedo/table-bundle
whatwedo/table-bundle is a Symfony bundle for building reusable data tables in PHP. It provides configurable columns, sorting, filtering, pagination, and Twig rendering to speed up admin listings and search results without repetitive template code.
This is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/table.php. composer require whatwedo/table-bundle only gets you anywhere inside a Symfony application (or a Symfony-based admin/skeleton). If you are working in a Laravel codebase, the realistic options are: (a) run it in a separate Symfony service behind HTTP, (b) borrow the pattern (one class per table, rendered via a view helper), or (c) skip it and use Filament/Livewire/yajra/laravel-datatables. There is no Laravel adapter and no dependents doing this work for you.
Assuming a Symfony app:
composer require whatwedo/table-bundle — check composer.json for the Symfony/PHP constraint band before committing; a 5-star package will not have wide compatibility matrices tested.config/bundles.php (Flex usually does this automatically; older/non-Flex apps must add it manually).README.md, src/Table/ (the abstract/base table class and column definitions), src/DependencyInjection/ (service tags and autoconfiguration), src/Resources/views/ and src/Twig/ (the render helper — this is where the actual API you'll type lives).First use case: the index/list page of an admin CRUD. Define one table class per entity exposing a handful of columns (id, name, created date), a sortable default, and pagination; render it from the controller with a single Twig call. Get that working on one screen before touching filters or custom column types.
// shape of the API — verify class/method names against the README
final class UserTable extends AbstractTable
{
public function buildTable(TableBuilder $builder): void
{
$builder
->addColumn('id', IntegerColumn::class, ['sortable' => true])
->addColumn('email', TextColumn::class, ['sortable' => true])
->addColumn('createdAt', DateTimeColumn::class, ['label' => 'Registered']);
}
}
One table class per listing. Treat table classes as self-contained view models registered as services (autowiring plus whatever tag the bundle's compiler pass expects — confirm in DependencyInjection/). Do not branch inside a controller between three different table configurations; subclass or parameterize instead. This is the single biggest predictor of whether the bundle stays maintainable at 20 tables.
Thin controllers. The only controller responsibility should be: obtain the table instance from the factory, hand it the request (for page/sort/filter state), pass it to the view. Any if ($request->query->get('sort') === ...) logic belongs in the table class.
Push pagination and sorting into the query, not into PHP. The bundle renders rows; it should not be fetching an entire table and slicing an array. Wire the table to a Doctrine QueryBuilder (or a repository method) so LIMIT/ORDER BY run in the database. If the bundle's data source abstraction is collection-only, that's your signal to wrap it — this is the difference between a page that works at 500 rows and one that works at 500k.
Reuse columns across tables. Define shared column types for your domain's recurring formatting (money, status badges, user avatar). If the bundle exposes a column-type extension point, use it rather than repeating closures in every table class.
Keep presentation and policy separate. Table classes are for display configuration. "Who may see this row" is a Doctrine query concern that belongs upstream, not a filter applied after the fact.
Testing. Table classes are cheap to unit test if you inject the data source: assert column configuration and that sorting maps to the expected ORDER BY. For the rendered result, do one functional test per table asserting a known row appears — that catches Twig helper breakage on bundle upgrades, which is where low-adoption bundles actually bite.
API consumption. Do not assume a JSON serializer or endpoint. If you need the same table as a data API, build the serialization yourself from the table's rows and column definitions, and keep the Twig rendering as a separate concern.
TableServiceProvider, no Blade directive, no artisan anything. Any snippet you find assumes Symfony's container, Twig, and often Doctrine ORM. Budget for translation cost, not just composer require.composer.json, commit the lockfile, and read src/ when behavior surprises you — the source is the documentation.composer.json before you start. A bundle this small is frequently a single-maintainer, single-Symfony-version project; composer require succeeding is not proof it runs on your stack.?page=2&sort=name will drive both. Look for a table-id/namespace option; if there isn't one, namespace the query params yourself in the controller before passing the request in.$user->getTeam()->getName() fires a query per row. Join and select the relation in the underlying query builder, or render a raw foreign key. This is the most common performance complaint against table generators generally.templates/bundles/<BundleName>/...). Use this rather than editing vendor/ — vendor edits vanish on composer update, and you will be updating manually since there's no active maintainer.bin/console debug:container <table service> confirms the table is wired; bin/console debug:twig confirms the render function/filter exists. "Function not found" almost always means the bundle isn't in config/bundles.php or the Twig extension isn't tagged — not a syntax problem in your template.How can I help you explore Laravel packages today?