This document introduces the main developer experience of zhortein/datatable-bundle.
The bundle is still under active development, but the current API already supports:
- PHP datatable declarations;
#[AsDatatable]service discovery;- Twig rendering through
zhortein_datatable(); - server-side Ajax fragments;
- array-backed demo datatables;
- Doctrine-backed datatables;
- row and global actions;
- typed and custom cell templates.
For the full architecture, see architecture.md.
A datatable is declared as a PHP class in the host application.
The class must:
- use the
#[AsDatatable]attribute; - implement
DatatableInterface; - configure a
DatatableDefinition.
Example:
<?php
declare(strict_types=1);
namespace App\Datatable;
use App\Entity\User;
use Zhortein\DatatableBundle\Attribute\AsDatatable;
use Zhortein\DatatableBundle\Contract\DatatableInterface;
use Zhortein\DatatableBundle\Definition\DatatableDefinition;
#[AsDatatable(name: 'users')]
final class UserDatatable implements DatatableInterface
{
public function buildDatatable(DatatableDefinition $definition): void
{
$definition
->setEntityClass(User::class)
->setTranslationDomain('user')
->addColumn('e.id', visible: false, sortable: false, searchable: false)
->addColumn('e.email', label: 'Email')
->addColumn('e.displayName', label: 'Display name')
->addColumn('e.createdAt', label: 'Created at', searchable: false)
;
}
}The datatable name is the public identifier used by Twig, Ajax routes and the frontend controller.
Use the zhortein_datatable Twig function:
{{ zhortein_datatable('users') }}Runtime options can be passed as the second argument:
{{ zhortein_datatable('users', {
search: true,
pageSize: 25
}) }}Current supported rendering options include:
search: displays the global search input;pageSize: defines the initial page size;fragmentsUrl: overrides the Ajax fragments URL.
Example with a custom fragments URL:
{{ zhortein_datatable('users', {
search: true,
pageSize: 50,
fragmentsUrl: path('custom_users_fragments')
}) }}The rendered datatable shell contains Stimulus values and targets.
The frontend controller calls the fragments endpoint and receives server-rendered HTML fragments.
Current request parameters:
page
pageSize
search
sortField
sortDirection
Example request:
/_zhortein/datatable/users/fragments?page=1&pageSize=25&search=alice
Current response shape:
{
"body": "<tr>...</tr>",
"pagination": "<div>...</div>",
"summary": "Showing 1 to 25 of 83 entries",
"page": 1,
"pageSize": 25,
"totalItems": 83,
"filteredItems": 83,
"totalPages": 4
}The Stimulus controller updates:
- table body;
- pagination;
- summary;
- loading state;
- error state.
The frontend does not render business cells manually.
The complete flow is documented in end-to-end-flow.md.
The array provider is useful for tests, demos and early integration.
It should not be considered the main production provider.
Example:
<?php
declare(strict_types=1);
namespace App\Datatable;
use Zhortein\DatatableBundle\Attribute\AsDatatable;
use Zhortein\DatatableBundle\Contract\DatatableInterface;
use Zhortein\DatatableBundle\Definition\DatatableDefinition;
use Zhortein\DatatableBundle\Provider\ArrayDataProvider;
#[AsDatatable(name: 'demo-users', provider: 'array')]
final class DemoUserDatatable implements DatatableInterface
{
public function buildDatatable(DatatableDefinition $definition): void
{
$definition
->addColumn('id', visible: false, sortable: false, searchable: false)
->addColumn('email', label: 'Email')
->addColumn('displayName', label: 'Display name')
->setOption(ArrayDataProvider::OPTION_PROVIDER, ArrayDataProvider::PROVIDER_NAME)
->setOption(ArrayDataProvider::OPTION_ROWS, [
[
'id' => 1,
'email' => 'alice@example.test',
'displayName' => 'Alice',
],
[
'id' => 2,
'email' => 'bob@example.test',
'displayName' => 'Bob',
],
])
;
}
}Render it from Twig:
{{ zhortein_datatable('demo-users', {
search: true,
pageSize: 25
}) }}The array provider currently supports:
- pagination;
- simple scalar search;
- single-column sorting.
Doctrine ORM is the first production-oriented provider.
Example:
<?php
declare(strict_types=1);
namespace App\Datatable;
use App\Entity\User;
use Zhortein\DatatableBundle\Attribute\AsDatatable;
use Zhortein\DatatableBundle\Contract\DatatableInterface;
use Zhortein\DatatableBundle\Definition\DatatableDefinition;
use Zhortein\DatatableBundle\Enum\FilterOperator;
#[AsDatatable(name: 'users', provider: 'doctrine')]
final class UserDatatable implements DatatableInterface
{
public function buildDatatable(DatatableDefinition $definition): void
{
$definition
->setEntityClass(User::class)
->setTranslationDomain('user')
->addColumn('e.id', visible: false, sortable: false, searchable: true)
->addColumn('e.email', label: 'Email')
->addColumn('e.displayName', label: 'Display name')
->addColumn('e.createdAt', label: 'Created at', searchable: false, type: 'datetime')
->addPermanentFilter('e.enabled', FilterOperator::Equals, true)
;
}
}Render it from Twig:
{{ zhortein_datatable('users', {
search: true,
pageSize: 25
}) }}The Doctrine provider currently supports:
- entity-class based datatables;
- main alias
e; - simple scalar columns;
- offset pagination;
- permanent filters;
- simple global search;
- single-column sorting;
- typed result output.
More details are available in doctrine-provider.md.
Permanent filters are backend-defined filters.
They are never controlled by the frontend.
Example:
use Zhortein\DatatableBundle\Enum\FilterOperator;
$definition->addPermanentFilter('e.deletedAt', FilterOperator::IsNull);Another example:
$definition->addPermanentFilter('e.enabled', FilterOperator::Equals, true);Permanent filters apply to:
- loaded rows;
- total visible item count;
- filtered item count.
This means totalItems represents the visible universe for the datatable context, not necessarily the full database table.
Row actions are rendered for each row.
Example:
$definition->addRowAction(
name: 'view',
route: 'app_user_show',
label: 'View',
routeParameters: [
'id' => 'e.id',
],
className: 'btn btn-sm btn-outline-primary',
);Route parameters are resolved from row values.
Supported row key styles include:
id
e_id
e.id
GET row actions are rendered as links.
Non-GET row actions are rendered as forms.
Example delete action:
$definition->addRowAction(
name: 'delete',
route: 'app_user_delete',
label: 'Delete',
httpMethod: 'DELETE',
routeParameters: [
'id' => 'e.id',
],
className: 'btn btn-sm btn-danger',
);When a CSRF token manager is available, non-GET action forms include a CSRF token.
Global actions are rendered in the datatable toolbar.
They are useful for operations such as create, import or future batch actions.
Example:
$definition->addGlobalAction(
name: 'create',
route: 'app_user_create',
label: 'Create',
className: 'btn btn-sm btn-primary',
);GET global actions are rendered as links.
Non-GET global actions are rendered as forms with CSRF token support when available.
More details are available in actions-and-cells.md.
Column rendering can use built-in cell types.
Supported initial types:
default
string
numeric
boolean
datetime
array
enum
Examples:
$definition->addColumn('e.createdAt', label: 'Created at', type: 'datetime');
$definition->addColumn('e.enabled', label: 'Enabled', type: 'boolean');
$definition->addColumn('e.amount', label: 'Amount', type: 'numeric', className: 'text-end');Doctrine-backed datatables can receive inferred cell types through Doctrine metadata.
Explicit column types are preserved.
A column can define its own Twig template:
$definition->addColumn(
name: 'e.status',
label: 'Status',
template: 'admin/datatable/cell/status.html.twig',
type: 'string',
);Custom templates take precedence over built-in type-specific templates.
A custom cell template receives:
{{ column.name }}
{{ column.label }}
{{ value }}Example:
<span class="badge text-bg-info">
{{ value }}
</span>More details are available in actions-and-cells.md.
The bundle currently supports several table controls:
- search input;
- page size selector;
- sortable headers;
- pagination controls;
- loading state;
- error state;
- summary updates.
Example:
{{ zhortein_datatable('users', {
search: true,
pageSize: 25,
allowedPageSizes: [10, 25, 50, 100],
pageSizeSelector: true
}) }}More details are available in table-controls.md.
Filters can be declared in the datatable definition:
use Zhortein\DatatableBundle\Enum\FilterType;
$definition
->addFilter(
name: 'email',
field: 'e.email',
label: 'Email',
type: FilterType::Text,
placeholder: 'Search an email',
)
->addFilter(
name: 'enabled',
field: 'e.enabled',
label: 'Enabled',
type: FilterType::Boolean,
)
;Filters are rendered in the toolbar and sent as request parameters:
filters[email]=alice
filters[enabled]=1
More details are available in filters.md.
Column visibility can be controlled at render time:
{{ zhortein_datatable('users', {
visibleColumns: ['e.email', 'e.displayName']
}) }}Columns can also be hidden at runtime:
{{ zhortein_datatable('users', {
hiddenColumns: ['e.createdAt']
}) }}Definition-level hidden columns remain hidden even if requested in visibleColumns.
Runtime column visibility can be controlled through Twig options:
{{ zhortein_datatable('users', {
visibleColumns: ['e.email', 'e.displayName'],
hiddenColumns: ['e.createdAt']
}) }}The toolbar can render a column visibility dropdown by default.
Disable it if needed:
{{ zhortein_datatable('users', {
columnVisibility: false
}) }}Applications can provide user-specific defaults by implementing DatatablePreferenceProviderInterface.
More details are available in preferences.md.
CSV exports are available server-side.
The default toolbar renders two CSV export links:
- current view;
- full dataset.
Disable export controls:
{{ zhortein_datatable('users', {
export: false
}) }}Use a custom export URL:
{{ zhortein_datatable('users', {
exportUrl: path('custom_users_export')
}) }}More details are available in exports.md.
The bundle uses Twig-first rendering and Bootstrap templates.
Host applications can override bundle templates with Symfony override paths:
templates/bundles/ZhorteinDatatableBundle/bootstrap/_toolbar.html.twig
For a single custom cell, prefer a column template:
$definition->addColumn(
name: 'e.status',
template: 'admin/datatable/cell/status.html.twig',
);More details are available in templates.md.
Table display variants can be configured at render time:
{{ zhortein_datatable('users', {
tableStriped: true,
tableHover: true,
tableBordered: true,
tableSmall: true
}) }}Disable the responsive wrapper:
{{ zhortein_datatable('users', {
tableResponsive: false
}) }}For a complete minimal example without Doctrine, see examples/array-datatable.md.
For a complete Doctrine-backed example, see examples/doctrine-datatable.md.
The bundle is still under active development.
Current limitations include:
- Doctrine provider supports only the main alias
e; - association traversal is not implemented yet;
- custom joins are not implemented yet;
- advanced filters are not implemented yet;
- multi-column sorting is not implemented yet;
- exports are not implemented yet;
- action visibility/security voters are not implemented yet;
- batch selected-row actions are not implemented yet;
- built-in labels are not fully translated yet;
- frontend test suite is not implemented yet.
See roadmap.md for planned work.