league/object-mapper
A lightweight object-to-object mapper for PHP that converts one class into another using reflection and PHP attributes. Define property mappings, handle nested objects, arrays, and collections, and reduce boilerplate when transforming DTOs, entities, and API payloads.
What the package is. league/object-mapper is a reflection-based hydrator: it takes structured data (decoded JSON, request arrays, queue payloads, config arrays) and constructs strict, typed PHP objects — DTOs, command objects, value objects — resolving constructor arguments, nested objects, and collections, with an extensible cast layer for enums, dates, and scalar coercion. It is deliberately not an ORM, not a validator, and not a serializer with format awareness. It occupies the same slot as cuyz/valinor, spatie/laravel-data, and Symfony Serializer's object-constructor path.
Architecture fit. Good fit for a hexagonal/ports-and-adapters Laravel codebase where controllers and jobs currently hand-roll new SomeDto($request->input(...)) calls. It gives you a single, testable anti-corruption boundary between transport shapes and domain types. Poor fit if the team's mental model is "Eloquent models + API Resources" — the package adds nothing there and would be a second, competing hydration mechanism. It complements, and does not replace, Laravel's validation layer or Eloquent's attribute casting.
Integration feasibility. High on mechanics, low on conveniences. It is framework-agnostic PSR-4 Composer code: no service provider, no facade, no config file. Binding it into the container is a one-liner, and wrapping it behind an internal interface is trivial. The absence of Laravel-native affordances is the real cost: no FormRequest integration, no automatic ValidationException, no resource/response transformation, no queued-job DTO resolution. Every one of those is glue we write and own.
Technical risk. The score signal is unambiguous and worth stating plainly: 36 stars, zero dependents, and a composite score near 1.0 means effectively no production adoption footprint to inherit. Principal risks:
readonly properties, promoted constructor params, default values, nullable-vs-missing-key semantics, enum backing types, and DNF types. Behavior on each must be verified, not assumed.422 responses. Without a translation layer, malformed client input becomes a 500.Key questions to close before committing.
readonly, nested collections, and circular references — verified by spike, not by README.null; extra/unknown keys; partial coercion. Is there a strict mode?Money, UserId, Email) without forking?Stack fit. Clean. It sits at the inbound boundary of the application layer and has no opinion about HTTP, queues, or persistence, so it composes with Laravel rather than fighting it. The intended wiring is FormRequest validation → validated array → mapper → typed Command/Query DTO → handler. That keeps validation (Laravel's strength) and construction (the package's job) separate and individually testable.
Containment strategy — the non-negotiable. Do not let League\ObjectMapper\* types appear in application or domain code. Define a narrow internal contract, e.g. App\Support\Mapping\DataMapper with map(array $data, string $class): object, and ship a LeagueObjectMapperAdapter implementing it. This costs a day and converts an unquantified vendor risk into a one-class swap to Valinor or spatie/laravel-data. Given 0 dependents and a single maintainer, this is the difference between a reversible and an irreversible decision.
Migration path. Do not retrofit. Introduce it only at new boundaries and at boundaries already being touched. Concretely: pick one bounded context, replace its manual new Dto(...) blocks, and run the old and new hydration paths side by side behind a feature flag with contract tests asserting deep equality of the resulting objects. Retire the flag after one release cycle.
Compatibility checks. Run composer why-not league/object-mapper against the current lockfile before anything else; confirm the PHP constraint against our CI matrix and production runtime; confirm attribute usage (likely the configuration surface for casts) does not collide with any static analysis or code-style tooling; confirm Rector/PHPStan behavior on mapped classes, since reflection-heavy code sometimes needs baseline entries.
Sequencing.
ValidationException/422 with field-path context. Without this, do not ship.Maintenance. Low surface area, low code volume, but concentration risk. Budget: pin to a minor version, review the changelog on every bump, and pre-agree a fork trigger (e.g. no release or issue response for 6 months, or a blocking incompatibility with a PHP minor we must adopt). Because the integration is contained behind the adapter, a fork is a repositories entry and a namespace change, not a refactor.
Support. No first-party Laravel support and effectively no community corpus to search — Stack Overflow will not save an engineer here. Required investment: a one-page internal pattern guide ("how we map inbound data"), the adapter contract documented as the only sanctioned entry point, and the swap procedure written down while the context is fresh. This is the main hidden cost and it is small but real.
Scaling. Reflection-based hydration is comfortable at request-scale volumes; the overhead is per-object construction, not per-byte. Two things to watch: (a) hot paths with large collections — mapping 10k rows item-by-item will show up in profiles, and reflection metadata caching (if the package offers it — verify) or bulk handling becomes necessary; (b) queue workers mapping high-throughput payloads, where a 0.2 ms per-object cost multiplies. Measure in the pilot rather than pre-optimizing; the failure mode is not catastrophic, just wasteful.
Failure modes.
500 on user input. Highest-severity risk, mitigated by step 3 above."1" silently becoming an int and masking an upstream contract violation. Mitigate with strict mode if available, plus contract tests on representative payloads.null. Document the chosen semantics; test both.Ramp-up. Low. A mid-level Laravel developer is productive in half a day; the concept (typed objects instead of arrays) is already idiomatic in the codebase or it is not, and that — not the package — determines adoption speed. The realistic risk is pattern divergence: three teams, three ways of mapping. Mitigate by making the internal DataMapper interface the only approved path and enforcing it in review, which also happens to be the mitigation for the maintenance risk. One decision, two problems.
Recommendation. Conditional pilot, not standards adoption. The package is technically well-shaped for the problem and cheap to integrate, but the adoption and maintenance signals (36 stars, 0 dependents, single maintainer) do not support making it a team-wide standard on the strength of its design alone. Contain it behind an internal interface, prove it on one boundary with real payloads and a real error-translation layer, then make an explicit adopt/hold/replace call at the end of the pilot. The opportunity score is the reason to run that pilot; it is not evidence that the pilot will pass.
How can I help you explore Laravel packages today?