AbstractDataTable
AbstractDataTable is the recommended way to create reusable, configurable tables with built-in support for Doctrine entities and server-side processing.
It now acts primarily as a configuration class. Request handling, provider resolution, and row processing run through internal collaborators behind the existing public façade.
Overview
The class wires together:
- DataTable: ID, columns, and extensions
- DataProvider resolution: Chooses a manual provider or an auto-configured Doctrine provider
- Row processing pipeline: Maps entities into frontend-compatible arrays, then applies template rendering and row actions
- Actions: Optional row-level actions appended automatically when configured
Quick Start with #[AsDataTable]
For Doctrine-backed tables, use the #[AsDataTable] attribute:
use App\Entity\User;
use Pentiminax\UX\DataTables\Attribute\AsDataTable;
use Pentiminax\UX\DataTables\Model\AbstractDataTable;
use Pentiminax\UX\DataTables\Column\TextColumn;
use Pentiminax\UX\DataTables\Column\NumberColumn;
#[AsDataTable(User::class)]
final class UsersDataTable extends AbstractDataTable
{
public function configureColumns(): iterable
{
yield NumberColumn::new('id', 'ID');
yield TextColumn::new('lastName', 'Last name');
yield TextColumn::new('firstName', 'First name');
yield TextColumn::new('email', 'Email');
}
protected function mapRow(mixed $item): array
{
/** @var User $item */
return [
'id' => $item->getId(),
'lastName' => $item->getLastName(),
'firstName' => $item->getFirstName(),
'email' => $item->getEmail(),
];
}
}
The attribute automatically creates a DoctrineDataProvider configured with:
- Your entity class
- The internal row processing pipeline built from
mapRow() - The
customizeQueryBuilder()method
Defining Columns with Attributes
When your Doctrine entity already uses #[ORM\Column(...)], alias UX DataTables attributes to avoid naming conflicts:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Pentiminax\UX\DataTables\Attribute as DataTable;
#[ORM\Entity]
final class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[DataTable\Column(title: 'ID')]
private ?int $id = null;
#[ORM\Column(length: 180)]
#[DataTable\Column(title: 'Email', searchable: true, orderable: true)]
private string $email = '';
#[ORM\Column]
#[DataTable\Column(title: 'Active')]
private bool $active = true;
}
Then your table can rely on these attributes:
use App\Entity\User;
use Pentiminax\UX\DataTables\Attribute\AsDataTable;
use Pentiminax\UX\DataTables\Model\AbstractDataTable;
#[AsDataTable(User::class)]
final class UsersDataTable extends AbstractDataTable
{
// No configureColumns() needed when using #[DataTable\Column] on the entity.
}
Using in a Controller
use App\DataTables\UsersDataTable;
use Pentiminax\UX\DataTables\DataTableRequest\DataTableRequest;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class UsersController extends AbstractController
{
#[Route('/users', name: 'app_users')]
public function index(UsersDataTable $table, Request $request): Response
{
// Handle Ajax requests for server-side processing
$table->handleRequest($request);
if ($table->isRequestHandled()) {
return $table->getResponse();
}
// For client-side tables, fetch data before rendering
if (!$table->getDataTable()->isServerSide()) {
$table->fetchData(DataTableRequest::fromRequest($request));
}
return $this->render('users/index.html.twig', [
'table' => $table,
]);
}
}
Customizing the Query
Query Builder Configurator
Filter or modify the Doctrine query:
use Doctrine\ORM\QueryBuilder;
use Pentiminax\UX\DataTables\DataTableRequest\DataTableRequest;
#[AsDataTable(User::class)]
final class ActiveUsersDataTable extends AbstractDataTable
{
protected function customizeQueryBuilder(QueryBuilder $qb, DataTableRequest $request): QueryBuilder
{
return $qb
->andWhere('e.active = :active')
->setParameter('active', true)
->andWhere('e.deletedAt IS NULL');
}
public function configureColumns(): iterable
{
// ...
}
}
Dynamic Filtering
Use request parameters for dynamic filters:
protected function customizeQueryBuilder(QueryBuilder $qb, DataTableRequest $request): QueryBuilder
{
// Access the current Symfony HTTP request for custom parameters
if ($roleId = $this->getHttpRequest()?->query->get('role')) {
$qb->andWhere('e.role = :role')
->setParameter('role', $roleId);
}
return $qb;
}
Configuring the DataTable
Table Options
use Pentiminax\UX\DataTables\Model\DataTable;
#[AsDataTable(User::class)]
final class UsersDataTable extends AbstractDataTable
{
public function configureDataTable(DataTable $table): DataTable
{
return $table
->ajax(url: '/users')
->pageLength(25)
->ordering(handler: true, indicators: true)
->searching()
->serverSide()
->processing();
}
public function configureColumns(): iterable
{
// ...
}
}
Extensions
use Pentiminax\UX\DataTables\Enum\ButtonType;
use Pentiminax\UX\DataTables\Enum\SelectStyle;
use Pentiminax\UX\DataTables\Model\DataTableExtensions;
use Pentiminax\UX\DataTables\Model\Extensions\ButtonsExtension;
use Pentiminax\UX\DataTables\Model\Extensions\ColumnControlExtension;
use Pentiminax\UX\DataTables\Model\Extensions\SelectExtension;
public function configureExtensions(DataTableExtensions $extensions): DataTableExtensions
{
return $extensions
->addExtension(new ButtonsExtension([ButtonType::CSV, ButtonType::EXCEL]))
->addExtension(new SelectExtension(SelectStyle::MULTI))
->addExtension(new ColumnControlExtension());
}
Row Actions
Override configureActions() to append a built-in action column without manually creating an
ActionColumn:
use Pentiminax\UX\DataTables\Model\Action;
use Pentiminax\UX\DataTables\Model\Actions;
public function configureActions(Actions $actions): Actions
{
return $actions
->add(
Action::edit()
->setIcon('bi bi-pencil')
)
->add(
Action::delete()
->askConfirmation('Delete this user?')
->displayIf('isDeletable', true)
);
}
When the table uses #[AsDataTable(Entity::class)], the entity class is injected automatically
into each action. See the Action Columns reference for the full API.
Available Methods
Configuration Methods
| Method | Description |
|---|---|
| `configureActions(Actions $actions)` | Configure built-in row actions |
| `configureDataTable(DataTable $table)` | Configure table options |
| `configureColumns()` | Define columns (required) |
| `configureExtensions(DataTableExtensions $ext)` | Configure all extensions |
Data Methods
| Method | Description |
|---|---|
| `createDataProvider()` | Protected hook for manual DataProvider instances |
| `getDataProvider()` | Public façade returning the resolved provider |
| `fetchData(DataTableRequest $request)` | Fetch data using the provider |
| `getRequest()` | Get the parsed DataTables request |
| `getHttpRequest()` | Get the current Symfony HTTP request after `handleRequest()` |
| `mapRow(mixed $item)` | Map a single entity to array |
| `setData(array $data)` | Set inline data through the same row-processing pipeline used by providers |
| `createRowMapper()` | Protected helper returning the internal row-processing mapper |
| `customizeQueryBuilder(QueryBuilder $qb, ...)` | Modify Doctrine query before DataTables filters are applied |
Request Handling
| Method | Description |
|---|---|
| `handleRequest(Request $request)` | Handle Ajax requests |
| `isRequestHandled()` | Check if request was handled |
| `getResponse()` | Get JsonResponse for Ajax; returns an empty dataset when no provider is available |
| `getDataTable()` | Get the configured DataTable |
| `getConfiguredDataTable()` | Read the configured DataTable without rendering preparation or data hydration |
Manual Data Provider
Override createDataProvider() for custom data sources:
use Pentiminax\UX\DataTables\Contracts\DataProviderInterface;
use Pentiminax\UX\DataTables\DataProvider\ArrayDataProvider;
#[AsDataTable(User::class)] // Auto Doctrine wiring is skipped when createDataProvider() returns a provider
final class CustomUsersDataTable extends AbstractDataTable
{
protected function createDataProvider(): ?DataProviderInterface
{
// Static data
$rows = [
['id' => 1, 'name' => 'John Doe'],
['id' => 2, 'name' => 'Jane Smith'],
];
return new ArrayDataProvider($rows, $this->createRowMapper());
}
protected function mapRow(mixed $item): array
{
// For arrays, just return as-is
return is_array($item) ? $item : (array) $item;
}
}
createRowMapper() and setData() both run the built-in row-processing pipeline. That means
template columns and action URLs are resolved before the rows are stored for inline rendering.
Inline Data with setData()
When you already have rows in memory, setData() is the simplest way to populate an
AbstractDataTable without building a custom provider:
use App\Entity\User;
#[AsDataTable(User::class)]
final class UsersDataTable extends AbstractDataTable
{
public function configureColumns(): iterable
{
yield NumberColumn::new('id', 'ID');
yield TextColumn::new('email', 'Email');
}
public function configureActions(Actions $actions): Actions
{
return $actions->add(
Action::detail()->linkToUrl(static fn (User $user): string => '/users/'.$user->getId())
);
}
}
$table->setData($users);
Use setData() when your rows are domain objects or preloaded arrays and you still want
mapRow(), template columns, and typed action callables to work consistently.
If an Ajax request is handled and the resolved provider is null, getResponse() returns an
empty DataTables payload using the current draw value:
{
"draw": 3,
"recordsTotal": 0,
"recordsFiltered": 0,
"data": []
}
This avoids runtime errors, but in practice a server-side table should still expose a provider.
Core Interfaces
DataProviderInterface
interface DataProviderInterface
{
public function fetchData(DataTableRequest $request): DataTableResult;
}
RowMapperInterface
interface RowMapperInterface
{
public function map(mixed $row): array;
}
DataTableRequest
Contains pagination, search, ordering, and column metadata from the client request.
DataTableResult
Wraps data payload with total and filtered counts:
new DataTableResult(
recordsTotal: 1000,
recordsFiltered: 150,
data: $rows
);
API Platform Integration
API Platform specific behavior is documented in:
This includes:
| Capability | Description |
|---|---|
| Ajax adapter mode (`apiPlatform(true)`) | Maps DataTables requests/responses to API Platform and Hydra shapes |
| Auto Ajax wiring during `getDataTable()` / rendering | Auto-populates Ajax settings when resource metadata is available |
| Column auto-detection | Builds columns from readable API Platform resource properties |