-
Notifications
You must be signed in to change notification settings - Fork 8
Home
- Scenery
- Step 1. Implement request handler
- Step 2. Register the handler
- Step 3. Invoke the handler
- Using context
- Handlers service autoloading
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).
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.
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.
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());
}
}
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);
}
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.