elma/devexpressbundle
Symfony bundle that bridges DevExpress JS widgets with Doctrine, currently supporting dxDataGrid. It parses DevExpress load options and applies filtering, sorting, and pagination to Doctrine queries via simple column-to-field mapping. Unofficial and a work in progress; contributions welcome.
The bundle bridges DevExpress dxDataGrid client widgets to Doctrine query builders in Symfony. It is not an official DevExpress product and does not ship any DevExpress JS — you still install/serve the widgets yourself. It is explicitly a WIP that currently only supports dxDataGrid.
Install and register it (the README targets Symfony 2/3-era kernel style):
composer require elma/devexpressbundle "~1.0@dev"
// app/AppKernel.php
$bundles = [
new Bilendi\DevExpressBundle\BilendiDevExpressBundle,
];
For Symfony 4+/Flex you must instead add Bilendi\DevExpressBundle\BilendiDevExpressBundle::class to config/bundles.php and adapt any service registration yourself.
Look first at the README’s indexAction example — it shows the whole workflow in one place. The entry point is SearchQueryParser, which decodes the DevExpress loadOptions payload:
$parser = new SearchQueryParser();
$query = $parser->parse(json_decode($request->get('loadOptions')));
Then define the map between grid column dataField names and Doctrine fields:
$map = [
'Username' => 'u.username',
'FirstName' => 'u.firstName',
// ...
];
Your first real use case: a controller endpoint that backs a dxDataGrid configured with remoteOperations enabled, returning server-side paging, sorting, and filtering for a single Doctrine entity.
Standard grid controller. The canonical pattern is: parse loadOptions → build a QueryBuilder for the entity → apply the parsed query using the column map → serialize rows to JSON, including totalCount.
public function indexAction(Request $request)
{
$parser = new SearchQueryParser();
$query = $parser->parse(json_decode($request->get('loadOptions'), true));
$map = [
'Username' => 'u.username',
'FirstName' => 'u.firstName',
];
$qb = $this->getDoctrine()->getRepository(User::class)
->createQueryBuilder('u');
// apply $query against $qb using $map (sorting, filtering, paging)
// ...
return new JsonResponse([
'data' => $qb->getQuery()->getArrayResult(),
'totalCount' => $totalCount,
]);
}
Column map as an explicit whitelist. Keep the map defined per-endpoint. Never pass client-supplied column names straight into DQL — the map doubles as your allow-list and is your primary defense against injection through filter/sort selectors.
Separate the count query. dxDataGrid sends requireTotalCount; run a SELECT COUNT(...) on the filtered query builder before applying skip/take, otherwise paging metadata will be wrong.
Frontend wiring. In dxDataGrid, enable remoteOperations: { paging: true, sorting: true, filtering: true } and forward loadOptions as JSON to your endpoint. The parser expects the exact object shape DevExpress sends (filter, sort, skip, take, requireTotalCount).
Service-ize it. Rather than new SearchQueryParser() in every action, register it as a Symfony service and inject a reusable “apply query to QueryBuilder” helper. This keeps controllers thin and makes the map easy to unit test.
Repository/manager abstraction. For larger apps, wrap the controller logic in a GridDataProvider service that accepts a grid name, resolves the map from config, and returns the payload. Multiple grids then share one code path with different maps.
Integration tip. The bundle is a bridge, not a full stack — pair it with your normal serializer (e.g. Symfony Serializer or JMS) instead of getArrayResult() for richer entity shapes and relations.
and/or filter trees and some operators likely won’t parse. Test every operator your grid exposes (contains, startswith, >=, between, etc.) before shipping.AppKernel.php) are for old Symfony. On 4+/Flex, register in config/bundles.php and verify whether the bundle ships a services.xml/services.yaml auto-loaded under the right path; you may need manual service definitions.null payloads. json_decode(null) returns null; guard against a missing loadOptions request parameter to avoid fatal errors on initial grid load.dataField values in the grid and keys in the map must match exactly (case-sensitive). A typo silently yields no filter/sort rather than an error.COUNT, then apply sort and skip/take. Applying take before counting is the most common bug producing a wrong totalCount.json_decode + dump()/var_dump() the raw loadOptions and the parsed SearchQuery object side-by-side; nearly every issue is a shape mismatch between what the grid sends and what the parser expects.filter and sort. Keep the map as a hard whitelist and validate operators server-side; don’t concatenate raw selectors.SearchQueryParser if you hit unsupported operators, and wrap the QueryBuilder application step in your own service so you can swap out the bundle later without touching controllers.How can I help you explore Laravel packages today?