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()` / renderingAuto-populates Ajax settings when resource metadata is available
Column auto-detectionBuilds columns from readable API Platform resource properties