Skip to content
Artem Rodygin edited this page Dec 22, 2022 · 12 revisions

Contents

Scenery

Let's assume you have AppBundle\Entity\User entity with id, login, and fullname fields, and you would like to show existing users in a DataTable using server-side processing mode.

Your HTML might look like following:

<table id="users">
    <thead>
        <tr>
            <th>ID</th>
            <th>Login</th>
            <th>Full name</th>
        </tr>
    </thead>
</table>

and your JavaScript could be as following:

$('#users').DataTable({
    serverSide: true,
    ajax: '{{ path('users') }}'
});

where 'users' - named route in your Symfony application. The corresponding action must be able to process a request from DataTables plugin and respond with data in expected format as JSON (see here for details).

Step 1. Implement request handler

This part is biggest and most sophisticated, but it's not because of this bundle - it's always like that to handle requests from DataTables, and the bundle only makes it easier to extract request parameters and validate them.

You have to create a handler for each DataTable instance. A handler must implements the DataTableHandlerInterface interface. The only function this interface contains is handle:

/**
 * Handles specified DataTable request.
 *
 * @param DataTableQuery $request
 * @param array          $context
 *
 * @throws DataTableException
 * @return DataTableResults
 */
public function handle(DataTableQuery $request, array $context = []): DataTableResults;

The request from DataTable instance is mapped into DataTableQuery object automatically, so you don't have to extract sent parameters from the request. Moreover, the request is also validated, so your handler can rely on this object as containing valid parameters. In case of invalid parameters the DataTableException will be automatically thrown before the handler invocation.

Let's implement the handler:

use DataTables\DataTableHandlerInterface;
use DataTables\DataTableQuery;
use DataTables\DataTableResults;
use Symfony\Bridge\Doctrine\RegistryInterface;

class UsersDataTable implements DataTableHandlerInterface
{
    protected $doctrine;

    /**
     * Dependency Injection constructor.
     *
     * @param RegistryInterface $doctrine
     */
    public function __construct(RegistryInterface $doctrine)
    {
        $this->doctrine = $doctrine;
    }

    /**
     * {@inheritdoc}
     */
    public function handle(DataTableQuery $request, array $context = []): DataTableResults
    {
        /** @var \Doctrine\ORM\EntityRepository $repository */
        $repository = $this->doctrine->getRepository('AppBundle:User');

        $results = new DataTableResults();

        // Total number of users.
        $query = $repository->createQueryBuilder('u')->select('COUNT(u.id)');
        $results->recordsTotal = $query->getQuery()->getSingleScalarResult();

        // Query to get requested entities.
        $query = $repository->createQueryBuilder('u');

        // Search.
        if ($request->search->value) {
            $query->where('(LOWER(u.login) LIKE :search OR' .
                          ' LOWER(u.fullname) LIKE :search)');
            $query->setParameter('search', strtolower("%{$request->search->value}%"));
        }

        // Filter by columns.
        foreach ($request->columns as $column) {
            if ($column->search->value) {
                $value = strtolower($column->search->value);

                // "ID" column
                if ($column->data == 0) {
                    $query->andWhere('u.id = :id');
                    $query->setParameter('id', intval($value));
                }
                // "Login" column
                elseif ($column->data == 1) {
                    $query->andWhere('LOWER(u.login) LIKE :login');
                    $query->setParameter('login', "%{$value}%");
                }
                // "Full name" column
                elseif ($column->data == 2) {
                    $query->andWhere('LOWER(u.fullname) LIKE :fullname');
                    $query->setParameter('fullname', "%{$value}%");
                }
            }
        }

        // Get filtered count.
        $queryCount = clone $query;
        $queryCount->select('COUNT(u.id)');
        $results->recordsFiltered = $queryCount->getQuery()->getSingleScalarResult();

        // Order.
        foreach ($request->order as $order) {

            // "ID" column
            if ($order->column == 0) {
                $query->addOrderBy('u.id', $order->dir);
            }
            // "Login" column
            elseif ($order->column == 1) {
                $query->addOrderBy('u.login', $order->dir);
            }
            // "Full name" column
            elseif ($order->column == 2) {
                $query->addOrderBy('u.fullname', $order->dir);
            }
        }

        // Restrict results.
        $query->setMaxResults($request->length);
        $query->setFirstResult($request->start);

        /** @var \AppBundle\Entity\User[] $users */
        $users = $query->getQuery()->getResult();

        foreach ($users as $user) {
            $results->data[] = [
                $user->getId(),
                $user->getLogin(),
                $user->getFullname(),
            ];
        }

        return $results;
    }
}

As you can see, the handler is injected with Doctrine to get access to the database, and the only responsibility of the handler is to prepare and return a DataTableResults object.

Step 2. Register the handler

The rest of the job is very short and easy. We have to register our handler as a service:

services:
    datatable.users:
        class: AppBundle\DataTables\UsersDataTable
        tags: [{ name: datatable, id: users }]
        arguments: [ '@doctrine' ]

You may have as many handlers as needed. The important parts here are following:

  • each handler must be tagged with name=datatable tag,
  • each handler must have unique ID, specified via id tag.

Step 3. Invoke the handler

The bundle appends a special service - datatables. This service implements the DataTablesInterface which contains the handle function as following:

/**
 * @param Request $request Original request.
 * @param string  $id      DataTable ID.
 * @param array   $context Optional context of the request.
 *
 * @throws DataTableException
 * @return DataTableResults Object with data to return in JSON response.
 */
public function handle(Request $request, string $id, array $context = []): DataTableResults;

All we have to do is to call this function, providing it with original request and the handler ID. The resulted object implements JsonSerializable interface, so it's ready to be returned as JSON data:

use DataTables\DataTablesInterface;

/**
 * @Route("/users", name="users")
 *
 * @param Request $request
 * @param DataTablesInterface $datatables
 * @return JsonResponse
 */
public function usersAction(Request $request, DataTablesInterface $datatables): JsonResponse
{
    try {
        // Tell the DataTables service to process the request,
        // specifying ID of the required handler.
        $results = $datatables->handle($request, 'users');

        return $this->json($results);
    }
    catch (HttpException $e) {
        // In fact the line below returns 400 HTTP status code.
        // The message contains the error description.
        return $this->json($e->getMessage(), $e->getStatusCode());
    }
}

Using context

If you need to pass some extra data to the handler, you can use the $context parameter as in the example below.

public function usersAction(Request $request, DataTablesInterface $datatables): JsonResponse
{
    $context = [
        'ignoreBlank'  => true,
        'customFilter' => $request->get('custom'),
    ];

    $results = $datatables->handle($request, 'users', $context);

    return $this->json($results);
}

Handlers service autoloading

Services can be loaded automatically as of Symfony 3.3. This lets you to specify your service configuration just once, so it won't needed to update each time you introduce a new DataTable handler.

To use this feature just inherit your handlers from AbstractDataTableHandler and override the ID constant there. The class from our example above could look as following:

class UsersDataTable extends AbstractDataTableHandler
{
    const ID = 'users';

    ...
}

Now, when we moved the tag ID to the class implementation, the service configuration could be simplified to the following:

services:
    AppBundle\DataTables\:
        resource: '../../src/AppBundle/DataTables'
        autowire: true
        public: false
        tags: [ datatable ]

Such configuration will work for all your handlers, present and future.