A Symfony bundle that integrates Tabulator (a JavaScript table/datagrid library) into Symfony applications via Stimulus. Tables are declared as PHP classes, rendered with a single Twig function, and — for server-side pagination/sorting/filtering — backed by an auto-registered controller that queries Doctrine ORM entities or native DBAL connections on your behalf.
Before you start, make sure you have StimulusBundle configured in your app.
Install the bundle using Composer and Symfony Flex:
composer require deviantlab/tabulator-bundle
If you're using WebpackEncore, install your assets and restart Encore (not needed if you're using AssetMapper):
npm install --force
npm run watch
# or use yarn
yarn install --force
yarn watch
A table is described by a class implementing TableInterface. The bundle ships two
abstract base classes depending on your data source:
AbstractOrmTableType — builds a Doctrine ORM QueryBuilder against an entity.AbstractNativeTableType — builds a Doctrine\DBAL\Query\QueryBuilder against a
raw DBAL connection (no entity mapping required).Any service tagged (auto-configured) with TableInterface is automatically registered
as a deviantlab.tabulator.table_type, so as long as autowiring/autoconfiguration is
enabled for your services, no extra wiring is needed.
namespace App\Table;
use App\Entity\Product;
use DeviantLab\TabulatorBundle\AbstractOrmTableType;
use DeviantLab\TabulatorBundle\Column;
use DeviantLab\TabulatorBundle\FilterMode;
use DeviantLab\TabulatorBundle\Pagination;
use DeviantLab\TabulatorBundle\PaginationMode;
use DeviantLab\TabulatorBundle\SortMode;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
final class ProductTable extends AbstractOrmTableType
{
public static function getName(): string
{
return 'product_table';
}
public function getEntityClass(): string
{
return Product::class;
}
public function getQueryBuilder(EntityRepository $repo, array $params): QueryBuilder
{
return $repo->createQueryBuilder('p');
}
public function getColumns(): iterable
{
yield new Column('Name', 'name');
yield new Column('Price', 'price', hozAlign: \DeviantLab\TabulatorBundle\HozAlign::RIGHT);
}
public function getPagination(): ?Pagination
{
return new Pagination(mode: PaginationMode::REMOTE, size: 20);
}
public function getSortMode(): SortMode
{
return SortMode::REMOTE;
}
public function getFilterMode(): FilterMode
{
return FilterMode::REMOTE;
}
}
The getName() value must be unique across all table types — it identifies the table
in the auto-generated data endpoint (/{_tableName}, route
deviantlab_tabulatorbundle_get_data) and is used to resolve the type from the
container.
For a native/DBAL-backed table, extend AbstractNativeTableType instead and implement
getConnectionName() and getQueryBuilder(Connection $connection, array $params).
Use TableFactory to turn a table type into a Table instance (this also wires up the
Ajax endpoint automatically), then render it with the render_table() Twig function:
use App\Table\ProductTable;
use DeviantLab\TabulatorBundle\TableFactory;
final class ProductController
{
public function list(TableFactory $tableFactory): Response
{
$table = $tableFactory->create(ProductTable::class, params: ['categoryId' => 5]);
return $this->render('product/list.html.twig', [
'table' => $table,
]);
}
}
{{ render_table(table) }}
This renders a single <div> with Stimulus controller attributes; the
deviantlab--tabulator-bundle--tabulator Stimulus controller (bundled in
assets/dist/tabulator_controller.js) reads the serialized options and boots the
Tabulator instance in the browser.
Column maps closely to Tabulator's own column definition options (title, field,
width, widthGrow, widthShrink, resizable, minWidth, maxWidth, frozen,
headerSort, headerHozAlign, hozAlign, vertAlign, print, etc.) plus a set of
strategy objects:
formatter / topCalcFormatter / bottomCalcFormatter — any FormatterInterface
(e.g. HtmlFormatter, LinkFormatter, MoneyFormatter, DateTimeFormatter,
TickCrossFormatter, RowNumFormatter, RowSelectionFormatter, TextAreaFormatter).editor — any EditorInterface (e.g. InputEditor, TextareaEditor,
NumberEditor, SelectEditor, CheckboxEditor, DateEditor, DateTimeEditor,
TimeEditor, RangeEditor, StarRatingEditor, ProgressBarEditor).mutator / accessor — any MutatorInterface / AccessorInterface.sorter — any SorterInterface (e.g. AlphanumericSorter, NumberSorter,
StringSorter, DateSorter, DateTimeSorter, TimeSorter, BooleanSorter,
ArraySorter, ExistsSorter).validator — a single ValidatorInterface or array of them (e.g. Required,
MinLength, MaxLength).topCalc / bottomCalc — a ColumnCalculationInterface (Sum, Average, Count,
Minimum, Maximum, Unique, Concatenate).headerFilter — a HeaderFilter combining an editor, a FilterFunction
(=, !=, starts, ends, >, <, >=, <=, in, keywords, like, regex)
and an optional initial value/placeholder.Table exposes fluent setters for the most common Tabulator options: layout,
height/minHeight/maxHeight, rowHeight, autoResize, resizableColumnFit,
selectableRows, locale, placeholder, printHeader, dataTree (and its
dataTreeStartExpanded/dataTreeFilter/dataTreeSort flags), columnCalcs /
groupClosedShowCalcs / dataTreeChildColumnCalcs, rowHeader (a RowHeader for row
selection/handle columns), editTriggerEvent, columnHeaderSortMulti,
headerSortClickElement / headerSortElement, popupContainer, initialSort /
initialFilter, and the dataLoader/dataLoaderLoading/dataLoaderError/
dataLoaderErrorTimeout options.
Ajax configures the request Tabulator makes for remote data: url, params,
method, contentType and arbitrary headers (addHeader()).
Pagination configures mode (PaginationMode::LOCAL/REMOTE), size,
initialPage, the query parameter names sent to the server (pageParamName,
sizeParamName), sizeSelector and counter. Remote pagination requires the table
to already have an Ajax configured, otherwise a LogicException is thrown.
When SortMode::REMOTE, FilterMode::REMOTE or PaginationMode::REMOTE are used,
Tabulator sends the current page/sort/filter state to the Ajax URL, and the bundle's
TableController (mounted on /{_tableName}) resolves the matching OrmTableHandler
or NativeTableHandler to build the query, apply pagination/sorting/filtering, and
serialize the result as JSON.
You can customize how a specific field is sorted or filtered by returning a
SortOverride / FilterOverride from your table type:
use DeviantLab\TabulatorBundle\FilterFunction;
use DeviantLab\TabulatorBundle\Server\Filter\FilterOverride;
use DeviantLab\TabulatorBundle\Server\Sorter\SortOverride;
public function getSortOverride(): ?SortOverride
{
return (new SortOverride())->add('fullName', function (QueryBuilder $qb, string $field, string $dir) {
$qb->addOrderBy('CONCAT(p.firstName, p.lastName)', $dir);
});
}
public function getFilterOverride(): ?FilterOverride
{
return (new FilterOverride())->add('fullName', FilterFunction::LIKE, function (QueryBuilder $qb, mixed $value) {
$qb->andWhere('CONCAT(p.firstName, p.lastName) LIKE :fullName')
->setParameter('fullName', "%{$value}%");
});
}
Use configureQuery(Query $query) (ORM tables) to tweak the built Doctrine Query
(e.g. hints, result cache), and doTransform(array $items, array $params): array to
post-process the fetched rows before they're serialized to the client.
composer test
How can I help you explore Laravel packages today?