This guide creates a complete page backed by the in-memory array provider. It assumes every step in the Installation Guide is complete.
Create src/Datatable/DemoUserDatatable.php:
<?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
->setOption(ArrayDataProvider::OPTION_ROWS, [
['id' => 1, 'email' => 'admin@example.test', 'role' => 'ROLE_ADMIN'],
['id' => 2, 'email' => 'user@example.test', 'role' => 'ROLE_USER'],
])
->addColumn('id', visible: false)
->addColumn('email', label: 'Email')
->addColumn(
name: 'role',
label: 'Role',
template: 'demo/cell/role.html.twig',
)
;
}
}The #[AsDatatable] attribute registers the service under the demo-users name and explicitly selects the array provider.
Create templates/demo/cell/role.html.twig:
<span class="badge text-bg-secondary">
{{ value }}
</span>
<small class="text-body-secondary">
#{{ row_identifier }} · {{ datatable.name }}
</small>The historical value variable remains available. Rich templates can also
use the normalized row, provider source, row_identifier, datatable
definition and explicit server-side context. See Cell Context and Computed
Values for the complete contract and named PHP resolvers.
Create src/Controller/DemoUserController.php:
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class DemoUserController extends AbstractController
{
#[Route('/demo/users', name: 'app_demo_users', methods: ['GET'])]
public function __invoke(): Response
{
return $this->render('demo/users.html.twig');
}
}Create templates/demo/users.html.twig:
{% extends 'base.html.twig' %}
{% block title %}Demo users{% endblock %}
{% block body %}
<main class="container py-4">
<h1>Demo users</h1>
{{ zhortein_datatable('demo-users', {
search: true
}) }}
</main>
{% endblock %}Run:
php bin/console debug:container --tag=zhortein_datatable.datatable
php bin/console debug:router app_demo_users
php bin/console debug:router zhortein_datatable_fragments
php bin/console debug:router zhortein_datatable_exportThe datatable service and all three routes must be present before opening the page.
If labels are translation keys, set the application catalog once on the
definition with setTranslationDomain('your_domain'). Columns, filters,
actions, confirmations, Search Builder labels and export headers will then use
the current request locale for the initial page, Ajax fragments and CSV/XLSX
files. See
declarative translations.
Start the application with your usual local server and open:
/demo/users
The initial response renders the table shell. The lazy Stimulus controller then requests:
/_zhortein/datatable/demo-users/fragments
The final table contains the two email addresses, search, sortable headers, pagination and CSV export. The hidden id column is not displayed or exported by default.
If the shell renders but the rows never appear, use the frontend checks in the installation troubleshooting section.
For an entity-backed table:
- declare
provider: 'doctrine'; - call
$definition->setEntityClass(YourEntity::class); - prefix fields from the root entity with
e..
Continue with the Doctrine Provider Guide for joins, filters, searching, sorting and performance guidance.