omerkoseoglu/devextreme-data-symfony
A Symfony package for integrating DevExtreme DataSource with Symfony applications. It helps serve data to DevExtreme grids and components, simplifying backend wiring for common data operations like filtering, sorting, and paging so front-end widgets work with Symfony-powered APIs.
What you're adopting. omerkoseoglu/devextreme-data-symfony is the server-side half of a DevExtreme Data Grid / Data Source integration: the browser sends a loadOptions object (filter, sort, skip, take, requireTotalCount, group, summary) and the package turns it into a query plus the { data, totalCount, summary, groupCount } envelope DevExtreme expects back. Note the name: it is a Symfony package. Read the first 20 lines of composer.json before anything else and confirm the symfony/* constraints — in a Laravel app you will be using it as a standalone library with a thin bridge, not as an installed bundle.
Install and pin. v0.1.0 is a first release; do not float the constraint.
composer require omerkoseoglu/devextreme-data-symfony:0.1.*
Where to look first, in order:
README.md — the canonical example. It will show the request-to-response call and the exact return shape; everything else is inference until you've matched it.CHANGELOG.md — currently a single "first release" entry. That absence of history is itself information: treat the public API as unproven.composer.json — PHP version floor, PSR-4 namespace, whether Doctrine ORM/DBAL or a generic query builder is the dependency. This single file tells you whether the package can plug into Eloquent at all.src/ — find the entry-point class that accepts an array of load options and the class that builds the response. Those two are your entire integration surface.tests/ — the fastest, most accurate spec of the filter grammar the package actually supports. Read the fixtures; they are real loadOptions payloads.First use case. One endpoint, one grid, no grouping, no summaries: a "Customers" list with a text filter, one sortable column, and server-side paging. Get totalCount correct before adding anything else — it is the single most common place these integrations break in production.
// Shape only — verify names against the README.
Route::post('/api/customers/load', function (Request $request) {
$options = $request->input('loadOptions', []);
$provider = new DataProvider(Customer::query()); // adapter per README
$result = $provider->load($options);
return response()->json([
'data' => $result['data'],
'totalCount' => $result['totalCount'],
]);
});
Isolate the package behind your own interface. With a 0.x dependency from a single maintainer, the correct architectural posture is a one-file seam:
interface GridDataProvider {
public function load(array $loadOptions): array;
}
Everything in your app talks to GridDataProvider; only the implementation class mentions the package namespace. When v0.2.0 renames things, you edit one file.
Separate the two concerns the package conflates. DevExtreme integration is really (a) parsing untrusted client options and (b) applying them to a query. Pattern: parse into your own value objects (Filter, Sort, Page) first, validate the field whitelist, then hand off. This keeps the third-party types out of your domain and gives you a place to log/replay bad payloads.
Field allowlisting as a first-class config. Maintain a per-endpoint map of client-visible field names to real columns and casts, and reject unknown names with a 422 rather than letting a filter reach a where on an arbitrary column:
protected array $sortable = [
'name' => 'customers.last_name',
'createdAt' => 'customers.created_at',
];
Every DevExtreme backend integration in the wild needs this map; the package will not supply it for you.
Eloquent vs Doctrine. If the package ships a Doctrine query adapter only, the pragmatic patterns are, in order of preference:
Illuminate\Database\Query\Builder. Do this if the interface is narrow (it usually is: apply filter, apply sort, apply limit, count).Wrap the endpoint in a feature flag / route toggle. Because the package is new, ship the grid behind a flag and keep the previous table working until the endpoint has survived real traffic.
Test with recorded payloads. Capture the actual JSON DevExtreme posts (browser devtools → network → request body) and commit those as fixtures. Assert on both data and totalCount. This is far more valuable than testing your own hand-written options arrays, because DevExtreme's filter nesting (["and", [...], ["or", ...]]) is idiosyncratic and only real payloads catch regressions.
Cache the count query. requireTotalCount means every page request runs a second, unpageable COUNT. For any non-trivial table, cache the count keyed on the filter subtree (not the sort/skip/take) with a short TTL.
This is Symfony, not Laravel — budget for the bridge. Expect friction at three specific points: the HTTP request type (Symfony Request/ParameterBag vs Illuminate\Http\Request), the DI story (Symfony bundle config vs a Laravel service provider you must write yourself), and configuration loading (YAML vs config/*.php). None are hard, all are undocumented for your target framework. Test the request-handling path first — if the entry point type-hints Symfony's Request, you'll be adapting at the boundary.
v0.1.0 means the public API is a draft. Pin 0.1.* at most, read the diff of every patch before bumping, and do not expose package classes in your own public signatures. There is no deprecation runway to rely on.
Trust nothing from the client. Field names, operators, and take values all arrive as untrusted input. Clamp take to a maximum (DevExtreme happily sends huge values), reject unknown operators, and never construct a column name by string concatenation.
DevExtreme contract quirks to verify against the package's tests:
skip/take may be absent or zero — the grid often pages groups client-side. Don't assume paging is always server-side.requireTotalCount and requireGroupCount are separate flags; returning totalCount unconditionally is wasteful, omitting it when required breaks the pager.groupSummary definitions are an array of aggregator/field pairs — a package at v0.1.0 may implement only count, sum, min, max, avg. Confirm before promising totals in the UI.sort can be multi-column; ensure stable ordering by appending a deterministic tiebreaker (usually the primary key) or paging will duplicate and skip rows.Debugging. When the grid renders wrong data, the bug is almost always in the payload, not the package. First step: log the raw loadOptions server-side and compare to what DevExtreme docs say that grid config should produce. Second step: log the generated query/SQL. Third: verify the JSON encoding — DateTime objects and decimals serialized as strings/objects instead of ISO/numeric will silently break sorting and formatting on the client.
Extension points to locate early. Look for (a) a pluggable query adapter or interface, (b) a filter-operator registry you can add to for custom/database-specific operators (full-text MATCH, JSON path contains), and (c) a response formatter or an event hook before serialization. Even at v0.1.0, one of these three usually exists, and each saves you a fork.
Bus factor. Single maintainer, first release, Symfony naming under a Laravel review question — evaluate it as a reference implementation of the DevExtreme load-options contract as much as a dependency. If the code is small, vendoring it (copy into your app, keep the license header) is a defensible call and removes the version-drift risk entirely.
How can I help you explore Laravel packages today?