Installation Add the bundle via Composer:
composer require app-verk/datatable-bundle
Enable it in config/bundles.php:
AppVerk\DatatableBundle\AppVerkDatatableBundle::class => ['all' => true],
First Use Case Create a controller method to handle DataTable AJAX requests:
use AppVerk\DatatableBundle\Controller\DataTableControllerTrait;
class ProductController extends AbstractController
{
use DataTableControllerTrait;
public function dataTableAction(Request $request)
{
$queryBuilder = $this->createQueryBuilder(); // Your Doctrine QL
return $this->handleDataTableRequest($request, $queryBuilder);
}
Frontend Integration Use jQuery DataTables with server-side processing:
$('#products-table').DataTable({
processing: true,
serverSide: true,
ajax: {
url: '/products/datatable',
type: 'POST'
},
columns: [
{ data: 'id', name: 'p.id' },
{ data: 'name', name: 'p.name' }
]
});
Configuration
Check config/packages/app_verk_datatable.yaml for default settings (e.g., pagination, sorting).
Query Builder Integration
Extend DataTableControllerTrait to inject your custom query logic:
protected function createQueryBuilder()
{
return $this->getDoctrine()
->getRepository(Product::class)
->createQueryBuilder('p')
->where('p.active = :active')
->setParameter('active', true);
}
Dynamic Column Handling Map DataTable column names to Doctrine fields via annotations or YAML:
# config/packages/app_verk_datatable.yaml
app_verk_datatable:
columns:
Product:
name: 'p.name'
price: 'p.price'
actions: '@app_verk_datatable.action_link'
Custom Actions Register action buttons (e.g., edit/delete) in your controller:
public function getActions(Product $product)
{
return [
'edit' => [
'route' => 'product_edit',
'params' => ['id' => $product->getId()],
'title' => 'Edit'
],
'delete' => [
'route' => 'product_delete',
'params' => ['id' => $product->getId()],
'title' => 'Delete',
'class' => 'btn-danger'
]
];
}
Pagination & Sorting Leverage KnpPaginatorBundle integration for advanced features:
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$queryBuilder,
$request->query->getInt('start') / $request->query->getInt('length') + 1,
$request->query->getInt('length')
);
return $this->handleDataTableResponse($request, $pagination);
Conditional Logic: Use onDataTableRequest to modify the query dynamically:
protected function onDataTableRequest(Request $request, QueryBuilder $qb)
{
if ($request->query->has('search')) {
$qb->andWhere('p.name LIKE :search')
->setParameter('search', '%'.$request->query->get('search').'%');
}
}
Caching: Cache responses for static data:
$cache = $this->get('cache.app');
$cacheKey = 'datatable_products_'.md5($request->query->get('search'));
if ($cache->has($cacheKey)) {
return $cache->get($cacheKey);
}
Export Integration: Combine with KnpSnappyBundle for PDF/Excel exports:
public function exportAction(Request $request)
{
$queryBuilder = $this->createQueryBuilder();
$data = $this->handleDataTableRequest($request, $queryBuilder)->getData();
return $this->get('knp_snappy.pdf')->generateFromHtml($this->renderView('export/grid.html.twig', ['data' => $data]));
}
Deprecated Dependencies
jms/serializer-bundle is listed but may not be actively used. Remove if unused to avoid conflicts.Query Builder Assumptions
Query objects or repositories directly.createQueryBuilder() call.Column Mapping Issues
APP_VERBOSE_LOGGING=1 to see raw SQL and column mappings.CSRF Token Conflicts
config/packages/security.yaml:
security:
access_control:
- { path: ^/datatable, roles: PUBLIC }
Pagination Offsets
handleDataTableResponse() method instead of manual pagination.Log Raw Requests:
use Psr\Log\LoggerInterface;
public function dataTableAction(Request $request, LoggerInterface $logger)
{
$logger->debug('DataTable Request:', [
'query' => $request->query->all(),
'request' => $request->request->all()
]);
// ...
}
Validate JSON Response: Ensure the response matches DataTables’ expected format:
{
"draw": 1,
"recordsTotal": 100,
"recordsFiltered": 50,
"data": [...]
}
Use jsonlint.com to validate output.
Check for Deprecation Warnings: The bundle was last updated in 2020. Test with PHP 8.1+ and Symfony 5+ for compatibility issues.
Custom Response Transformers
Override AppVerk\DatatableBundle\Response\DataTableResponse to modify the JSON structure:
class CustomDataTableResponse extends DataTableResponse
{
protected function transformData($data)
{
return array_map(function ($item) {
$item['custom_field'] = 'value';
return $item;
}, $data);
}
}
Register the service in config/services.yaml:
AppVerk\DatatableBundle\Response\DataTableResponse:
class: App\CustomDataTableResponse
Event Listeners
Subscribe to datatable.build.query and datatable.response events:
use AppVerk\DatatableBundle\Event\DataTableEvents;
$dispatcher->addListener(DataTableEvents::BUILD_QUERY, function ($event) {
$event->getQueryBuilder()->andWhere('...');
});
Twig Extensions Add custom filters for frontend processing:
class DatatableExtension extends \Twig\Extension\AbstractExtension
{
public function getFilters()
{
return [
new \Twig\TwigFilter('format_datatable_date', [$this, 'formatDate']),
];
}
}
Use in templates:
{{ item.createdAt|format_datatable_date }}
Selective Field Loading:
Explicitly define fields in SELECT to reduce query overhead:
$qb->select('p.id, p.name, p.price'); // Instead of 'p'
Indexed Columns: Ensure frequently filtered/sorted columns are indexed in the database.
Lazy Loading:
For large datasets, use setFetchMode(\Doctrine\ORM\Query\ResultSetMapping::HYDRATE_ARRAY) to avoid hydration overhead.
How can I help you explore Laravel packages today?