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 Table Bundle Laravel Package

kreyu/data-table-bundle

Kreyu DataTableBundle is a Symfony bundle for building feature-rich data tables with sorting, filtering, pagination, bulk actions, AJAX loading, and exports. Configure columns and actions declaratively, customize templates, and integrate quickly with a flexible, developer-friendly API.

View on GitHub
Deep Wiki
Context7

Getting Started

kreyu/data-table-bundle is a Symfony bundle (not Laravel). Its design will feel familiar if you've used Symfony Forms: you declare a table as a class, the bundle renders it, and the request drives filters/sorting/pagination. As a Laravel dev, treat it as the equivalent of a "resource table" abstraction you'd otherwise hand-roll with Blade + query scopes — except the declarative layer is the product.

Minimal path:

composer require kreyu/data-table-bundle
# Flex enables it; otherwise register Kreyu\Bundle\DataTableBundle\KreyuDataTableBundle

Version note: the package is still on 0.x. v0.33.0 is the first release allowing Symfony 8 (on top of the existing 6/7 range), and it also fixes a Twig deprecation in DataTableExtension — so if you're on Symfony 8 / PHP 8.4, that's your floor. On Symfony 6/7 there's no urgency to move.

First place to look: the DataTableType docs and the examples in docs/ — not the source. Then copy the "basic usage" into one real controller.

// src/DataTable/ProductDataTableType.php
final class ProductDataTableType extends AbstractDataTableType
{
    public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
    {
        $builder
            ->addColumn('name', TextColumnType::class, ['sortable' => true])
            ->addColumn('price', MoneyColumnType::class, ['currency' => 'USD'])
            ->addColumn('createdAt', DateTimeColumnType::class, ['format' => 'Y-m-d'])
            ->addFilter('name', TextFilterType::class)
            ->addFilter('createdAt', DateFilterType::class);
    }
}
#[Route('/products', name: 'app_product_index')]
public function index(Request $request, DataTableFactoryInterface $factory): Response
{
    $qb = $productRepository->createQueryBuilder('p');

    $dataTable = $factory->create(ProductDataTableType::class, $qb);
    $dataTable->handleRequest($request);

    return $this->render('product/index.html.twig', [
        'dataTable' => $dataTable->createView(),
    ]);
}
{{ data_table(dataTable) }}

First real use case: replace an existing "index" action that concatenates Request params into a QueryBuilder, has a hand-written paginator, and a sortable <th> macro. That's exactly the code this bundle deletes.

Implementation Patterns

One type per table, composed via parents. Keep ProductDataTableType thin and factor shared behaviour into an AbstractEntityDataTableType with getParent(). Options go through configureOptions(OptionsResolver $resolver) — same mental model as FormType, including setDefaults, setAllowedTypes, setRequired.

Pass the data source, let the bundle pick the adapter. A QueryBuilder gets the Doctrine ORM adapter; arrays/Traversable get the array adapter. That means the same DataTableType can serve a Doctrine-backed admin screen and an in-memory stub in tests. For anything non-trivial (joins, GROUP BY, DQL functions), construct the QueryBuilder in the controller or a dedicated factory and keep it out of the type class.

Reusable column/filter sets via builder composition. A common pattern is a TimestampableDataTableTrait-style helper that does $builder->addColumn('createdAt', ...) in a loop, so audit columns are consistent across every table.

Custom cells = Twig column type. Don't fight TextColumnType with getter closures for rich markup; use the Twig column with a small template and pass the row entity. Keep templates in one templates/data_table/ directory and register them as a theme so per-column overrides stay in one place.

Actions live on the builder, not in the template. Row and batch actions are declared with route name/params (lazy params can be closures receiving the row entity), with options for confirmation and method/CSRF. This is the biggest win over hand-rolled tables: the batch-action form, CSRF token and confirmation dialog are wired for you. Keep the actual handling in a separate controller action that reads the selected identifiers.

Filters/sorting/pagination all flow through handleRequest(). Call it exactly once, before createView(). For AJAX or Turbo-driven partial refreshes, re-render the same template and let the view's state (page, sorting, active filters) drive the markup — the shipped themes are already structured so the table region can be swapped.

Personalization for per-user state. Enable persistence so a user's filters, sorting and page survive navigation. Default storage is session-backed; swap in a custom PersonalizationStorageInterface if you want DB- or preference-backed state per user. Disable it for stateless/API endpoints.

Testing. Write a kernel/bundle test that builds the table, feeds a Request with query params, and asserts on createView() plus the adapter's item count. That catches filter regressions faster than browser tests, and it's the closest analogue to Laravel feature tests you'll get here.

Gotchas and Tips

  • handleRequest() is stateful and mutating. Calling it twice, or after createView(), produces confusing results. Do it in the controller, once, immediately after create().
  • The ORM adapter mutates the QueryBuilder you give it. Don't pass a shared/cached builder from a service — pass a fresh one (or a closure/factory). Otherwise filters from request A leak into request B in long-running workers.
  • getTotalItemCount() breaks with to-many joins. Classic Doctrine paginator problem: add ->distinct() or restrict the select, otherwise page counts inflate and rows duplicate across pages.
  • Sorting a computed/derived property silently fails unless you map it explicitly. Mark such columns non-sortable rather than debugging an ignored ORDER BY.
  • Batch and form actions need CSRF and the right HTTP method. If the batch action "does nothing", check that the generated form method matches the route's requirements and that the CSRF token id isn't being regenerated between the render and the submit (common behind aggressive caching or with multiple tabs).
  • Bootstrap theme + modal, post-v0.33.0. That release removed a "useless" Bootstrap modal JS import from the bundle's bootstrap theme template. If confirmation dialogs silently stop opening after upgrading, the modal JS was being provided transitively — pull Bootstrap's modal into your own entrypoint or keep a template override that includes it. Non-Bootstrap themes are unaffected.
  • Translation domain defaults are easy to miss. Labels fall back to the humanized column name; if your app uses a custom domain, set it once at the table level rather than per column.
  • Themes and template blocks are cached by Twig. After overriding a block, bin/console cache:clear before concluding the override "isn't working".
  • Pagination/filter query parameter names are configurable — if you have two tables on one page, they will collide unless you set distinct parameter names.
  • Debugging toolbox: bin/console debug:container --tag=data_table.type (and the filter/column/action tags) to confirm autoconfiguration; debug:config kreyu_data_table for bundle config; dump $dataTable->getSorting() / filter data to see what the request actually produced. Most "the filter isn't applied" reports are a missing handleRequest() or a mismatched parameter name.
  • Pin the version, but read the constraint. The package ships frequently and is still pre-1.0, so minor bumps can carry template and behavioural changes (the v0.33.0 Bootstrap modal import removal is a good example). Lock to ^0.33 after testing, and check the upgrade notes before a composer update on a large app — especially if you have themed template overrides that reach into the shipped blocks.
  • Twig deprecation noise is fixed as of v0.33.0. If DataTableExtension is spamming deprecations in your logs, that's the upgrade that silences it — useful even if you're staying on Symfony 7.
  • Framework boundary: if your day job is Laravel, this only helps on Symfony projects — but the type-class + adapter + request-handling split is a good blueprint for building an equivalent internal package with Laravel's query builder.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource