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

Table Laravel Package

kilik/table

A Laravel package for building dynamic, customizable data tables with fluent column definitions, sorting, filtering, search, pagination, row actions, and Blade rendering. Generate responsive tables from Eloquent models or arrays with minimal code.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install

    composer require kilik/table
    

    Register the bundle — config/bundles.php on Symfony 4+:

    Kilik\TableBundle\KilikTableBundle::class => ['all' => true],
    

    Then publish its public assets (the bundle ships jQuery-based JS/CSS):

    php bin/console assets:install --symlink public/
    
  2. First place to look — Kilik\TableBundle\Components\Table, Column, Filter and FilterType. These are the only classes you'll touch on a normal day; everything else (the TableFactory, the response service, the Twig extension) exists to serialize a Table object into JSON and back.

  3. Wire up a table as a service. Don't new Table() in a controller — the bundle discovers tables by service tag:

    app.table.user:
        class: App\Table\UserTable
        tags:
            - { name: kilik_table.table, alias: user }
    
    // src/Table/UserTable.php
    class UserTable extends Table
    {
        public function __construct(EntityManagerInterface $em)
        {
            $qb = $em->createQueryBuilder()
                ->select('u')
                ->from(User::class, 'u');
    
            $this->setId('user')
                ->setPath('user_table_data')   // route that returns JSON
                ->setTitle('Users')
                ->setQueryBuilder($qb)
                ->setRowsPerPage(25)
                ->addColumn(
                    (new Column())->setName('u.id')->setLabel('ID')
                        ->setSortable(true)
                        ->setFilter((new Filter())->setType(FilterType::TYPE_NUMBER))
                )
                ->addColumn(
                    (new Column())->setName('u.email')->setLabel('Email')
                        ->setSortable(true)
                        ->setFilter((new Filter())->setType(FilterType::TYPE_TEXT))
                );
        }
    }
    
  4. Expose the Ajax endpoint — one trivial controller action per table:

    /**
     * @Route("/user/table-data", name="user_table_data")
     */
    public function userTableAction(Request $request, TableFactory $factory)
    {
        $table = $factory->getTable('user');
        return $this->get('kilik_table.service')->getResponse($table, $request);
    }
    
  5. Render it in Twig and let the bundle's JS do the Ajax:

    {{ kilik_table_render('user') }}
    

    Add the bundle's assets to your layout (or your Encore/Webpack config) and the table becomes sortable, filterable and paginated with zero extra code.

First real use case: the "list screen" of any CRUD — a paginated, sortable, per-column-filterable user/order/product list backed by a Doctrine query, where only the filter/sort/page params round-trip to the server.

Implementation Patterns

Table-as-service is the unit of composition. One Table subclass per grid, one tagged service, one route, one Twig call. Keep the query builder construction in the constructor so the definition is stable; if you need request-aware query tweaks (e.g. tenant scoping), do it in the controller before calling getResponse(), not inside the table.

Column naming follows DQL aliases. setName('u.email') must match an alias/field that exists in the select() of your query builder. If you select('u'), use u.email; if you select('u.id, u.email'), use those scalar names. Sorting and filtered WHERE/HAVING clauses are generated from these names, so a mismatch surfaces as a Doctrine Semantical Error.

Filters per type. FilterType::TYPE_TEXT, TYPE_NUMBER, TYPE_SELECT, TYPE_DATE behave differently on the server (LIKE vs =, date ranges vs single value). For TYPE_SELECT supply the choices eagerly:

(new Filter())
    ->setType(FilterType::TYPE_SELECT)
    ->setOptions(['active' => 'Active', 'locked' => 'Locked'])

or a callback that hits a repository when the option set is large.

Renderers for presentation-only transforms. Column::setRenderer(fn($row) => ...) receives the raw row (aliased keys like $row['u.email']) and must return the HTML/string to display. Use this for badges, formatted dates, links, and status pills instead of mutating the entity in the query. Keep renderers pure — they're called once per row and cannot see the request.

Joins and relations. Add joins explicitly in the query builder (->leftJoin('u.group', 'g')->addSelect('g')) and then expose g.name as a column. Avoid selecting whole relation graphs for large tables; select scalars for the columns you display.

Multiple tables on one page. Each table gets its own id, path, and Twig render call; the JS keys off the id, so there is no collision as long as the ids are unique. This is the normal pattern for dashboards.

Integration into existing pages. The endpoint is just a controller returning JSON — you can put it behind your normal firewall, reuse route prefixes, and wrap it with a @Security attribute. Because it's GET, the browser caches are the main concern (see gotchas).

Export/bulk actions. The bundle is display-only. If you need CSV export, build a sibling route that re-uses the same Table object's query builder and filters rather than duplicating query logic — pass the current filter params through.

Gotchas and Tips

  • Alias mismatch is the #1 error. Every Column::setName() must resolve in the DQL. If you use addSelect('COUNT(x) AS cnt'), expose it as cnt, not u.cnt.
  • Sorting on aggregates breaks. ORDER BY COUNT(x) on a non-grouped query throws. Either setSortable(false) on computed columns or add the aggregate to GROUP BY/HAVING yourself.
  • Autoescaping of renderers. Depending on your Twig config, renderer output may be escaped. If you return <span class="badge">...</span> and it shows as literal text, wrap it (new HtmlString(...) / Twig |raw) or configure the bundle's template — don't html_entity_decode in the renderer.
  • Bundle ships its own jQuery-based JS. It predates modern Symfony UX tooling; if your app uses Stimulus/Turbo, load the bundle's JS outside the Turbo frame or the table won't re-bind after navigation. Assets must be installed (assets:install) or wired into Encore — a 404 on the bundle JS manifests as a table that renders but never filters.
  • GET responses and caching. The JSON endpoint is a plain GET; behind a proxy or with aggressive HTTP caching, stale rows appear after edits. Set Cache-Control: no-store on the response, or add a cache-busting param on the JS side.
  • The count query. Pagination requires a total count; on complex queries with joins this is the slow part. Profile with the Symfony profiler — a slow list page is almost always the count, not the page fetch. Consider narrowing the query (select only displayed fields) before reaching for custom count logic.
  • Empty filter values. A TYPE_NUMBER filter with an empty input should be skipped, not coerced to 0. Verify the generated DQL via the profiler's query list when filters "silently" return wrong rows.
  • Debugging the JSON. Hit the endpoint directly with ?page=1&sort=u.email&order=asc (the exact parameter names the bundle's JS sends — inspect the network tab once and note them; do not rename the route or its params). The response is the fastest way to tell whether the problem is server-side query generation or client-side JS.
  • Container compile-time construction. Table constructors run when the service is instantiated; any exception there (bad DQL, missing entity) surfaces as a 500 on the endpoint, not on page load. Keep constructors side-effect free and validate the query builder early in dev.
  • Extension points: custom filter types via a Filter subclass, custom column renderers for anything visual, and Twig template overrides under templates/bundles/KilikTableBundle/ for markup changes (.twig override paths win automatically).
  • Version/compat check before committing. With only 33 stars and zero dependents, treat this as a small, lightly-maintained bundle: pin the version, check the release notes against your Symfony version, and keep the table definitions isolated behind your own service layer so swapping it for a maintained grid later is a contained change.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package