retailcrm/auto-mapper-bundle
Symfony bundle integrating RetailCRM AutoMapper to automatically map arrays and objects to typed DTOs. It simplifies request/response hydration with service auto-configuration and support for nested and collection mappings.
retailcrm/auto-mapper-bundle is a Symfony bundle wrapping RetailCRM's object-to-object mapper. It has no Laravel service provider, no package discovery, no publishable config — Composer will install it, and then nothing happens. Treat it as "a library plus a DI wiring layer you must redo yourself."
Minimal path:
composer.json first. Check the require block for symfony/framework-bundle, symfony/config, symfony/dependency-injection. If those are hard requirements you're pulling a chunk of Symfony into a Laravel app — acceptable in a large app, a smell in a small one. Also confirm the underlying mapper package it wraps (likely a retailcrm/auto-mapper-style library) — that library is what you actually want, and you can usually use it standalone without the bundle.DependencyInjection/*Extension.php. This is the whole bundle in one file: it reads the config tree and constructs the mapper service. Whatever constructor arguments it passes are what you must bind in Laravel.Resources/config/services.* for the service id and any tagged services (custom mappers/normalizers/transformers). Those tags become your manual bindings/tagged bindings.composer require retailcrm/auto-mapper (or whatever the underlying package is named), instantiate the mapper directly, and avoid the Symfony DI baggage.First use case that justifies the dependency: you're integrating with RetailCRM (retailcrm/api-client-php) and need to map API payloads (orders, customers, catalogs) into typed DTOs and back. That's the vendor's own problem domain, which is where the default conventions will fit best. If you're mapping unrelated internal objects, Laravel-native options (spatie/laravel-data, eventsauce/object-hydrator, or plain symfony/serializer with ObjectNormalizer) will likely be less friction.
Translate the Symfony service definition into a Laravel singleton, and keep the bundle's service id out of your application code behind an interface:
// app/Providers/AutoMapperServiceProvider.php
public function register(): void
{
// Constructor args copied from the bundle's Extension::load()
$this->app->singleton(MapperInterface::class, function ($app) {
return new Mapper(
cacheDir: storage_path('framework/mapper'),
// transformers/normalizers from config
);
});
}
Then alias: $this->app->alias(MapperInterface::class, 'auto-mapper');.
The bundle expects Symfony's config tree (auto_mapper: { ... } in config/packages/*.yaml). In Laravel, create config/auto-mapper.php, return the same keys, and adapt them at binding time. Two things break in translation:
%env(VAR)% parameters don't exist. Replace with env()/config() lookups in the provider.snake_case config keys are usually passed straight to a constructor as an options array — flatten nested mappings: / transformers: lists into plain PHP arrays.The high-value pattern: keep Eloquent models out of the mapping layer. Map into readonly DTOs, then fill()/create() explicitly.
$dto = $mapper->map($payload, OrderDto::class); // array -> typed object
$model = Order::create($dto->toArray()); // explicit persistence boundary
Mapping straight onto Eloquent models is tempting but fragile — mass-assignment guards, mutators, casts, and HasAttributes hydration don't line up with reflection-based assignment, and you lose the ability to distinguish "not provided" from "null". DTO-then-fill keeps that distinction.
For list endpoints, map element-wise rather than expecting a magic collection type:
$orders = collect($response['orders'])
->pipe(fn ($c) => $c->map(fn ($o) => $mapper->map($o, OrderDto::class)));
If the underlying mapper has a mapCollection()/iterable overload, use it — it typically reuses reflection metadata across elements and is measurably faster on large catalog syncs.
Vendor-specific normalization (RetailCRM date strings, money as string floats, phone normalization) belongs in dedicated transformer classes, registered once, so mapping rules live in one place rather than in foreach loops scattered across jobs.
class RetailCrmDateTransformer
{
public function __invoke($value): \DateTimeImmutable
{
return new \DateTimeImmutable($value, new \DateTimeZone('UTC'));
}
}
The realistic day-to-day flow in a Laravel app:
upsert() on external id) and emits domain events.Put the mapper only in steps 2 and 4. Controllers and Eloquent models should never see it; that isolates you from a 1-star dependency.
Snapshot-test mapping rather than asserting field-by-field:
it('maps a retailcrm order payload', function () {
$dto = app(MapperInterface::class)->map($this->payload(), OrderDto::class);
expect($dto)->toMatchSnapshot();
});
Snapshots catch silent drift when DTO properties change — important when the mapper is reflection-driven and won't error on a renamed property.
MapperInterface so you can swap in spatie/laravel-data later without touching call sites.storage_path('framework/cache/auto-mapper') and add a php artisan auto-mapper:clear-equivalent (or clear it in your deploy script), otherwise stale maps after DTO refactors will produce confusing errors in production only.new it per item."1" into 1 or throw depending on the setter's declared type. RetailCRM payloads are heavily string-typed, so declare DTO property types deliberately and add explicit transformers for money/quantities.null on a non-nullable property, or a TypeError deep in a job. Add an explicit "required fields" assertion at the boundary rather than relying on mapping failures."1.x.y", not "^1.0") and let Renovate/Dependabot surface updates for review. A surprise minor bump in a mapper can silently change coercion behaviour.How can I help you explore Laravel packages today?