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
queryBuilderConfigurator()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->getDataTable(),
]);
}
}
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
{
public function queryBuilderConfigurator(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:
public function queryBuilderConfigurator(QueryBuilder $qb, DataTableRequest $request): QueryBuilder
{
// Access custom parameters
if ($roleId = $request->getCustomParameter('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
->pageLength(25)
->searching(true)
->ordering(handler: true, indicators: true)
->serverSide(true)
->processing(true);
}
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 |
| `mapRow(mixed $item)` | Map a single entity to array |
| `createRowMapper()` | Protected helper returning the internal row-processing mapper |
| `queryBuilderConfigurator(QueryBuilder $qb, ...)` | Modify Doctrine query |
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 |
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;
}
}
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 |