Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Object Mapper Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and orient yourself in 10 minutes.

composer require league/object-mapper

The package does exactly one thing: take a plain, already-parsed structure (array — from json_decode(..., true), a DB row, $request->validated(), a message payload) and hydrate it into a strict, typed object. It is not a serializer, not a validator, and not an ORM. Keep that mental model and the API makes sense immediately.

Where to look first, in order:

  1. README.md — the first code block is the whole contract; copy the exact namespace/class name from it (this is a small package, the surface is deliberately tiny).
  2. src/ — the mapper entry point plus any Exception classes. You want to know the exception type thrown on a mapping failure, because you'll catch it at your input boundary.
  3. tests/ — the fixtures are the real documentation for nested objects, collections, and nullable handling.

Define DTOs with promoted, typed constructors — that's the shape the mapper expects:

namespace App\Data;

final class Address
{
    public function __construct(
        public readonly string $street,
        public readonly string $city,
        public readonly string $postalCode,
    ) {}
}

final class User
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly Address $address,
        public readonly ?string $nickname = null,
    ) {}
}

First use case — kill the manual hydration boilerplate:

use League\ObjectMapper\ObjectMapper; // confirm namespace from README

$mapper = new ObjectMapper();

$user = $mapper->map(User::class, [
    'id' => 7,
    'name' => 'Ada',
    'address' => [
        'street' => '1 Analytical Way',
        'city' => 'London',
        'postalCode' => 'N1',
    ],
    'nickname' => null,
]);

// $user is a User; $user->address is an Address, not an array.

That's the whole first win: no new User($data['id'], $data['name'], new Address(...)) hand-wiring, and no mixed leaking into your domain layer.

Implementation Patterns

1. Map at the boundary, never deeper. Resolve the mapper once (container singleton / static factory) and call it only where untrusted structures enter: controllers, console commands, queue consumers, webhook handlers. Everything downstream takes typed objects.

public function store(StoreUserRequest $request): JsonResponse
{
    $user = $this->mapper->map(CreateUser::class, $request->validated());

    $this->commandBus->dispatch(new RegisterUser($user));

    return response()->json(['id' => $user->id], 201);
}

2. Validation first, mapping second. This package enforces types, not business rules. Run Laravel's FormRequest (or Symfony's validator) first so you fail with 422s; let the mapper throw a 500 only on genuine programmer error.

3. Nested objects and collections. Nest a DTO-typed property and the mapper recurses. For lists, declare the element type and map a array<Item>-style property — expect the package to hydrate each element into the target class:

final class Order
{
    /** @param list<LineItem> $items */
    public function __construct(
        public readonly string $reference,
        public readonly Address $shipping,
        public readonly array $items,
    ) {}
}

Verify in tests/ whether collections need an attribute/generic notation or whether the mapper infers from constructor docblocks — this is the one area worth confirming before you build on it.

4. Enums and scalars. PHP 8.1 backed enums hydrate cleanly from their backing value; use them instead of string constants so invalid values fail at the edge:

enum Status: string { case Draft = 'draft'; case Paid = 'paid'; }

final class Invoice
{
    public function __construct(
        public readonly Status $status,
        public readonly int $amountCents,
    ) {}
}

5. Keep a toArray() on your DTOs. The package is one-directional. For responses, events, and logs you still need the reverse trip — implement a small explicit toArray() per DTO rather than reaching for a full serializer. Explicit is cheaper to reason about than a second library.

6. Wrap it behind your own interface. Given the maturity level, do not let League\ObjectMapper\* leak into 40 files:

interface Hydrator
{
    /** @template T of object @param class-string<T> $class @return T */
    public function hydrate(string $class, array $data): object;
}

Bind the League implementation in a service provider. Swapping to cuyz/valinor or Symfony Serializer later becomes a one-file change.

7. Queue/consumer pipeline. A single, consistent decode step dramatically simplifies jobs:

public function handle(): void
{
    $event = $this->hydrator->hydrate(OrderPlaced::class, json_decode($this->payload, true));
    // typed from here on; static analysis works
}

Gotchas and Tips

  • Numeric strings vs ints. JSON gives you real ints, but HTML form input and some DB drivers hand you "123". If the mapper is strict, a string into an int parameter throws. Cast at the boundary ($request->validated() after Laravel numeric validation usually gives you the cast you expect) rather than loosening your DTO types.
  • Missing vs nullable. ?string $nickname = null tolerates an absent key; a required string does not. Prefer explicit defaults for genuinely optional fields so partial payloads from third parties don't blow up.
  • A bare array property is not hydrated. Declaring public readonly array $items gives you the raw arrays — it does not guess LineItem. Element typing is your job. This is the #1 source of "why is my nested object still an array".
  • Extra keys are typically ignored, not rejected. Don't rely on the mapper for "strict schema" enforcement (typo'd field names pass silently). That's a validator's job — or use #[...] configuration if the package exposes it.
  • No setters, no second pass. Constructor-based hydration means you cannot post-process an immutably-typed DTO after the fact. Do transformation before mapping (in a form request / input DTO) or accept a mutable intermediate.
  • Circular references recurse. A self-referencing DTO (parent: ?Node) will blow the stack if the payload is cyclic. Guard depth yourself; there is no built-in cycle detector you should assume.
  • Instantiate the mapper once. Hydration is reflection-driven. Building a mapper per row inside a 100k-row import is a measurable slowdown — inject a singleton and reuse it. If you're importing, also chunk the source.
  • Know your exception. Find the mapping exception class in src/Exception/ and catch that at your boundary, not \TypeError or \Throwable. Log the offending payload (redact secrets) — the exception message often won't carry the full path through nested objects, so isolate by mapping the sub-object directly when debugging.
  • Debugging deep failures: map the nested child class on its own first. If Address hydrates fine but User doesn't, the mismatch is at the parent level (key name or nullability), not in the child.
  • Low bus factor — pin it. 36 stars and zero dependents means limited community debugging and slow upstream fixes. Use "league/object-mapper": "^x.y" with a committed composer.lock, keep the wrapper interface from Pattern 6, and don't extend it via monkey-patching. If the package lacks a feature you need (custom datetime casting, key renaming), implement it around the mapper — pre-normalize the array — rather than forking.
  • Extension points to check in src/: any Mapper interface you can decorate (to add logging/metrics), any attribute classes for key renaming or custom casting, and whether custom type handlers can be registered. If those hooks exist, use them; if not, don't invent them.
  • readonly + ?T vs T|null should behave identically, but confirm in tests before relying on one spelling in 200 DTOs.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource