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