rtxlabs/datatransformation-bundle
A PHP data transformation bundle for converting arrays, objects, and API payloads into consistent output using reusable transformers. It offers a structured, extensible way to map and normalize data across your application.
Install with Composer and register the bundle if you are on Symfony:
composer require rtxlabs/datatransformation-bundle
Then enable it in config/bundles.php:
return [
// ...
RtxLabs\DataTransformationBundle\DataTransformationBundle::class => ['all' => true],
];
In a Laravel app you skip the bundle registration entirely and just resolve mapper classes through the container (or new them directly) — see the gotchas section for the caveats.
The first thing to read is the mapper base class and a mapper in the package's own fixtures/tests. The mental model is: one mapper class per entity, exposing a map($entity): array method, with field-level mapping definitions that can point at other mappers for relations.
Minimal first use case — expose a Product entity as JSON:
class ProductMapper extends AbstractMapper
{
public function getEntityClass(): string
{
return Product::class;
}
protected function getMapping(): array
{
return [
'id' => 'id',
'title' => 'name',
'price' => 'price',
'inStock' => 'stock.quantity', // dotted path into the entity
];
}
}
// In a controller:
$data = $this->get(ProductMapper::class)->map($product);
return new JsonResponse($data);
That's the whole loop: entity in, plain array out, json_encode/JsonResponse downstream. Nested mappers, collections and conditionals are layered on this same base.
Mapper-per-entity, composed via nested mappers. The package's value proposition is complex JSON. Define a CategoryMapper and reference it from ProductMapper rather than inlining category fields. This keeps each mapper small and lets you reuse the same JSON shape across every endpoint that emits a category.
Collections. Mappers generally handle a single entity. For lists, loop in the controller or add a thin service that maps each item:
$payload = array_map(fn ($p) => $productMapper->map($p), $products);
DTO / View-model mapping. Don't limit the mapper to Eloquent/Doctrine entities — point it at any object with getters. This is useful when you build a dedicated ProductView object and want the mapping declarative instead of a 40-line toArray().
Thin controller, mapper owns the shape. The pattern that pays off day-to-day: controllers never build arrays. They call $mapper->map($entity) and return JsonResponse. When the API contract changes you edit one mapper, not every endpoint.
Extension via subclassing. Override getMapping() in a child mapper to add/remove fields for a specific endpoint (ProductSummaryMapper extends ProductMapper). It's a cheap way to serve list vs. detail payloads without branching on a flag inside one mapper.
Integration with a serializer. If you already run Symfony Serializer or Laravel's JsonResource, pick one and don't mix. Two serialization systems in one codebase means two places to reason about field names, null handling and dates.
Testing. Because a mapper is a pure-ish transform, unit-test it with a real entity fixture and assert the exact array shape. These tests are fast and catch contract drift before clients do.
Ecosystem mismatch is the biggest risk. Despite the "Laravel developer" framing, this is a Symfony bundle. In Laravel you gain none of the bundle's wiring (DI tags, compiler passes, config) — you'd be using AbstractMapper and its mapping engine as a plain library. Budget for manual register() bindings or new-ing mappers and injecting their dependencies yourself.
Adoption and maintenance signals are weak. 13 stars and 0 dependents means: no community answers on Stack Overflow, no third-party tutorials, and a real chance the package is dormant or dropped from support for newer Symfony/PHP versions. Before adopting, pin the version, check the last commit date, and confirm it installs on your PHP version. Vendor-lock risk is high for the value delivered versus Symfony Serializer or Laravel API Resources.
N+1 on nested mappers. Any mapper that resolves a relation will lazily load it. Eager-load everything the mapper touches before calling map():
$product = Product::with(['category', 'tags', 'stock'])->find($id);
Doctrine proxy / lazy-loading surprises. Accessing uninitialized proxies from a mapper is fine functionally but will fire queries and, in edge cases, throw outside a persistence context. Map entities fetched within a live EntityManager, not detached ones.
Circular references. A Category that references Product that references Category will recurse until PHP runs out of memory. Decide early whether a nested mapper is one-directional (usually yes) and never map the parent back down.
Dotted paths fail silently. Most mapping configs tolerate a missing path by emitting null rather than throwing — which is exactly why a wrong field name escapes code review. Add an assertion-level test on the output shape so a typo shows up as a failing test, not a broken frontend field.
Date/number formatting is not free. Cast DateTime to your API string format and cast numerics ((float) $price) inside the mapper. Don't rely on json_encode to guess — it produces inconsistent types across DB drivers.
Debug in one place. When the output looks wrong, dump($mapper->map($entity)) before json_encode. Inspecting the array is faster than tracing JSON, and it tells you whether the bug is in mapping or serialization.
Metadata compilation quirks. Bundles of this vintage often compile mapping metadata (annotations → attributes cache). If a mapping change doesn't take effect after a deploy, clear the cache directory — a stale compiled mapper is a common and confusing failure mode.
Extension points to lean on: subclassing the abstract mapper for variant payloads, custom mapper classes for computed fields, and registering mappers as container services so nested mappers get injected rather than instantiated inline. Prefer these over forking the bundle.
Have an exit plan. Because the surface is small (map an entity to an array), the transform is easy to swap later. Keep mappers thin and avoid leaking bundle-specific types into controllers or resources, so replacing them with JsonResource, Fractal, or Symfony Serializer is a mechanical refactor rather than a rewrite.
How can I help you explore Laravel packages today?