Skip to content

Repository files navigation

Mapper

License

Overview

Maps PHP objects to and from arrays, JSON, and iterables through reflection and pluggable strategies. Handles backed and pure enums, value objects, nested objects, date-time types, and collections out of the box. Designed for DTO hydration, serialization at the HTTP boundary, flat-row decoding at the persistence boundary, and data transfer between bounded contexts.

The library exposes two complementary ways to use it. The primary one is the Mapper service, an immutable builder that keeps the mapped class fully decoupled from the library. No interface to implement, no trait to use, nothing on the domain side. It fulfills two narrow service contracts, Serializer (object to array and JSON) and Deserializer (array, JSON, or iterable to object), so a consumer can depend on the capability it needs rather than on the concrete service.

The second is the Mappable interface plus the MappableBehavior trait, an opt-in hook for application DTOs that prefer to map themselves. Mappable combines Serializable (render to array and JSON) and Deserializable (build from a source). The mapping logic lives in the mapper, not on the type: objects are reflected by the engine and collections are built internally, so a Mappable type exposes no engine-facing method. When a configured Mapper maps a Mappable value, the engine reflects it through the active naming, so nested children resolve through any registered mappings.

Installation

composer require tiny-blocks/mapper

How to use

The Mapper service

Mapper::create() returns an empty mapper with identity naming and lenient unknown keys. The same instance hydrates from arrays, JSON strings, and iterables, and serializes back to arrays or JSON.

A currency-bearing amount is the value object the mapper hydrates.

<?phpdeclare(strict_types=1);
finalreadonlyclass Amount
{
publicfunction__construct(publicint$amount, publicCurrency$currency)
{
}
}

The amount references a backed enum that names the currency.

<?phpdeclare(strict_types=1);
enum Currency: string
{
caseBRL = 'BRL';
caseUSD = 'USD';
}

With those in place, the mapper reads and writes them through a single service. toObject hydrates from an array, a JSON string, or any iterable.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Mapper;
$mapper = Mapper::create();
$amount = $mapper->toObject(type: Amount::class, source: ['amount' => 50, 'currency' => 'BRL']);
$amount = $mapper->toObject(type: Amount::class, source: '{"amount":50,"currency":"BRL"}');

toObjectOrNull returns null when the source is null, and behaves like toObject otherwise.

$missing = $mapper->toObjectOrNull(type: Amount::class, source: null);

The same service serializes back to an array or a JSON string.

$array = $mapper->toArray(source: $amount);
$json = $mapper->toJson(source: $amount);

withNaming, withMapping, and rejectingUnknownKeys return a new mapper each time. The original instance keeps its previous configuration.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Mapper;
useTinyBlocks\Mapper\SnakeCase;
$strict = Mapper::create()
->withNaming(namingStrategy: SnakeCase::create())
->rejectingUnknownKeys();

The mapper fulfills two service contracts: Serializer (toArray, toJson, and their *OrNull variants) and Deserializer (toObject, toProperties, toObjectOrNull). A consumer can type-hint the narrow contract it depends on instead of the concrete Mapper.

The Mappable trait

When an application DTO prefers to map itself, implement Mappable and use MappableBehavior. Mappable combines Serializable (the toArray and toJson output methods) and Deserializable (the buildFrom factory). The trait implements all three on top of the same engine that backs the Mapper service, so the type needs no explicit Mapper instance at the call site and exposes no engine-facing method.

A two-field address wires itself into the mapper through the trait.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Mappable;
useTinyBlocks\Mapper\MappableBehavior;
finalreadonlyclass Address implements Mappable
{
use MappableBehavior;
publicfunction__construct(publicstring$city, publicstring$street)
{
}
}

The DTO drives its own hydration and serialization.

<?phpdeclare(strict_types=1);
$address = Address::buildFrom(source: ['street' => 'Av. Paulista', 'city' => 'São Paulo']);
$array = $address->toArray();
$json = $address->toJson();

buildFrom, toArray, and toJson map the instance on its own, with identity naming and no registered mappings. When a configured Mapper serializes or hydrates a Mappable subject, the engine reflects it through the active naming, so any mapping registered for a nested type applies. A mapping registered for the Mappable type itself takes precedence over the reflection-based default.

Polymorphic types with Subtype

Subtype::by builds a mapping that selects a concrete class by the value of a discriminator field. It lists the concrete types only and derives each discriminator value from the type's short name. The naming strategy applied to the short name produces the value and defaults to snake_case. On read, the field value picks the class. On write, the class is reverse-looked-up to its derived value, which is written back exactly once.

The abstract parent the discriminator resolves to.

<?phpdeclare(strict_types=1);
abstractreadonlyclass PaymentMethod
{
}

A Pix concrete type carries a payer identifier and a static factory used as the Subtype default.

<?phpdeclare(strict_types=1);
finalreadonlyclass Pix extends PaymentMethod
{
publicfunction__construct(publicstring$payerId)
{
}
publicstaticfunctionpending(): Pix
{
returnnewPix(payerId: 'pending');
}
}

A debit-card concrete type the same discriminator can resolve to.

<?phpdeclare(strict_types=1);
finalreadonlyclass DebitCard extends PaymentMethod
{
publicfunction__construct(publicstring$cardNumber)
{
}
}

The mapping wires both concrete types under a shared discriminator field. The field names the discriminator, types are the concrete classes whose short names derive the values, naming is the optional convention (snake_case by default), and default is the optional factory used when no case matches.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Mapper;
useTinyBlocks\Mapper\Subtype;
$mapper = Mapper::create()->withMapping(
type: PaymentMethod::class,
mapping: Subtype::by(
field: 'type',
types: [Pix::class, DebitCard::class],
default: staticfn(): Pix => Pix::pending()
)
);
$method = $mapper->toObject(type: PaymentMethod::class, source: ['type' => 'pix', 'payerId' => 'Alice']);

With the snake_case default, DebitCard derives debit_card and Pix derives pix, so the listed types alone define the discriminator values. The optional default factory is invoked when no case matches and when the discriminator field is absent. With no default, an unmatched case raises UnknownSubtype. A misconfigured mapping raises InvalidSubtypeCase: when two types derive the same value, or when a registered case is not a subtype of the mapped type.

Flat rows with Layout

Layout::from builds a mapping for a flat relational row whose columns map onto a nested object graph. Columns that follow the prefix-derivation convention ({field}{separator}{subfield} under the active naming strategy) are derived and need no entry. Only renamed leaves, columns outside their expected prefix, and JSON-encoded columns are declared.

A camera with a serial number and a shot counter.

<?phpdeclare(strict_types=1);
finalreadonlyclass Camera
{
publicfunction__construct(publicstring$serialNumber, publicint$shotCount)
{
}
}

A studio composed of a main camera and a tag.

<?phpdeclare(strict_types=1);
finalreadonlyclass Studio
{
publicfunction__construct(publicCamera$mainCamera, publicstring$tag)
{
}
}

The flat row maps onto the nested studio through prefix derivation.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Layout;
useTinyBlocks\Mapper\Mapper;
useTinyBlocks\Mapper\SnakeCase;
$mapper = Mapper::create()
->withNaming(namingStrategy: SnakeCase::create())
->withMapping(type: Studio::class, mapping: Layout::from(paths: []));
$studio = $mapper->toObject(
type: Studio::class,
source: [
'main_camera_serial_number' => 'sn-1',
'main_camera_shot_count' => 7,
'tag' => 'studio-a'
]
);

The empty paths array means every column is derived from the property prefix. A non-empty array overrides specific leaves with a different column name.

JSON columns with JsonColumn

A JsonColumn marks a column as holding a JSON document. On read, the column is decoded and mapped onto the graph path. On write, the corresponding subgraph is encoded back into the column. Other declared paths combine freely with JSON-marked ones in the same Layout::from(paths: [...]) call.

A member identifier wraps a single scalar with a value accessor.

<?phpdeclare(strict_types=1);
finalreadonlyclass MemberId
{
publicfunction__construct(privatestring$value)
{
}
publicfunctionvalue(): string
{
return$this->value;
}
}

An owner composed of a member identifier and a display name.

<?phpdeclare(strict_types=1);
finalreadonlyclass Owner
{
publicfunction__construct(publicMemberId$memberId, publicstring$name)
{
}
}

The mapping decodes the member column as JSON onto the memberId path.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\JsonColumn;
useTinyBlocks\Mapper\Layout;
useTinyBlocks\Mapper\Mapper;
$mapper = Mapper::create()->withMapping(
type: Owner::class,
mapping: Layout::from(paths: ['memberId' => newJsonColumn(column: 'member')])
);
$owner = $mapper->toObject(type: Owner::class, source: ['member' => '{"value":"m-1"}', 'name' => 'Alice']);

Collections that map themselves

PHP carries no runtime element type for a typed collection, so a collection implements IterableMappable and declares its element type with the #[ElementType] attribute. On read, the mapper maps each source element to that type and hands the built elements to createFrom. Absence of the attribute means passthrough: elements are kept as-is. On write, the collection is iterated and each element is serialized through the engine, so a mapping registered for the element type is honored on every element.

A refund row bound to a single amount.

<?phpdeclare(strict_types=1);
finalreadonlyclass Refund
{
publicfunction__construct(publicstring$reference, publicAmount$amount)
{
}
}

A refund collection that maps itself element by element.

<?phpdeclare(strict_types=1);
useGenerator;
useIteratorAggregate;
useTinyBlocks\Mapper\ElementType;
useTinyBlocks\Mapper\IterableMappable;
useTinyBlocks\Mapper\Mapper;
#[ElementType(Refund::class)]
class Refunds implements IteratorAggregate, IterableMappable
{
privatefunction__construct(publicreadonlyiterable$elements)
{
}
publicstaticfunctioncreateFrom(iterable$elements): static
{
returnnewstatic(elements: $elements);
}
publicfunctiontoJson(): string
{
return Mapper::create()->toJson(source: $this);
}
publicfunctiontoArray(): array
{
return Mapper::create()->toArray(source: $this);
}
publicfunctiongetIterator(): Generator
{
foreach ($this->elementsas$key => $element) {
yield$key => $element;
}
}
}

Mapping a list of refund rows into the collection needs no registration. The mapper builds each element from the declared #[ElementType] and hands the result to createFrom.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Mapper;
$mapper = Mapper::create();
$refunds = $mapper->toObject(
type: Refunds::class,
source: [
['reference' => 'r-1', 'amount' => ['amount' => 100, 'currency' => 'BRL']],
['reference' => 'r-2', 'amount' => ['amount' => 200, 'currency' => 'BRL']]
]
);

The tiny-blocks/collection library ships this behavior built in. Its Collection base class already implements IterableMappable, so a typed collection only declares its element with the #[ElementType] attribute.

Scalar codecs with Codec

Codec::from builds a mapping for a value object that has a single canonical scalar form, when the default reflection would not reproduce it. A typical case is a value object that wraps an inner representation whose direct serialization drifts from the canonical form, for example a date-only value that would otherwise widen to a full datetime. The consumer owns both conversions, so the library stays decoupled from the wrapped type.

A calendar date that wraps an inner value but presents a canonical date-only string.

<?phpdeclare(strict_types=1);
useDateTimeImmutable;
finalreadonlyclass CalendarDate
{
privatefunction__construct(privateDateTimeImmutable$value)
{
}
publicstaticfunctionfromIso(string$iso): CalendarDate
{
returnnewCalendarDate(value: DateTimeImmutable::createFromFormat('!Y-m-d', $iso));
}
publicfunctiontoIso(): string
{
return$this->value->format('Y-m-d');
}
}

A reservation composed of a single calendar date.

<?phpdeclare(strict_types=1);
finalreadonlyclass Reservation
{
publicfunction__construct(publicCalendarDate$checkIn)
{
}
}

The codec pins the canonical scalar form on both directions.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Codec;
useTinyBlocks\Mapper\Mapper;
$mapper = Mapper::create()->withMapping(
type: CalendarDate::class,
mapping: Codec::from(
decode: staticfn(string$iso): CalendarDate => CalendarDate::fromIso(iso: $iso),
encode: staticfn(CalendarDate$date): string => $date->toIso()
)
);
# The encode closure drives every write, so the nested date stays a canonical YYYY-MM-DD string.$row = $mapper->toArray(source: newReservation(checkIn: CalendarDate::fromIso(iso: '2026-05-23')));
# A bare scalar is decoded directly into the value object.$checkIn = $mapper->toObject(type: CalendarDate::class, source: '2026-05-23');

The encode closure is consulted for every write of the type, nested or top-level. The decode closure is consulted whenever the type is resolved, top-level or as a nested scalar property: a registered mapping takes precedence over the built-in single-property unwrap. Only unregistered single-value wrappers fall back to that unwrap.

Self-describing scalars with ScalarCodec

#[ScalarCodec] is the self-describing twin of Codec: rather than registering closures, a value object names the methods that convert it to and from a scalar, and the mapper calls them with no configuration. Each attribute is one decode and encode pair, and the attribute is repeatable. On read, the pair whose decode parameter type accepts the source scalar is selected, so a type can be built from more than one scalar form. On write, the first declared pair's encode is used. The attribute adds no public methods: it names methods the type already has. A mapping registered through withMapping for the same type takes precedence over the attribute.

A version that builds from a label string or a release number, and renders back to its label.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\ScalarCodec;
#[ScalarCodec(decode: 'fromLabel', encode: 'toLabel')]
#[ScalarCodec(decode: 'fromNumber', encode: 'toLabel')]
finalreadonlyclass Version
{
privatefunction__construct(privatestring$label)
{
}
publicstaticfunctionfromLabel(string$label): Version
{
returnnewVersion(label: $label);
}
publicstaticfunctionfromNumber(int$number): Version
{
returnnewVersion(label: (string) $number);
}
publicfunctiontoLabel(): string
{
return$this->label;
}
}

A release composed of a single version.

<?phpdeclare(strict_types=1);
finalreadonlyclass Release
{
publicfunction__construct(publicVersion$version)
{
}
}

No registration is needed: the attribute carries the conversion in both directions.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Mapper;
$mapper = Mapper::create();
# The string source selects the decode whose parameter is typed string.$fromLabel = $mapper->toObject(type: Version::class, source: 'v7');
# The integer source, nested in a release, selects the decode whose parameter is typed int.$release = $mapper->toObject(type: Release::class, source: ['version' => 7]);
# The first declared pair's encode renders the scalar form.$label = $mapper->toArray(source: Version::fromLabel(label: 'v7'));

A mapping is specified in one of two ways. Registered mappings are attached through withMapping: Codec, FactoryMethod, Layout, and Subtype each build a Mapping the mapper consults first. Self-describing types carry the rule themselves: the Mappable and IterableMappable interfaces, and the #[ElementType] and #[ScalarCodec] attributes. A registered mapping always wins over a self-describing one for the same type.

Transparent delegation for single-property wrappers

A type with a single property needs no mapping of its own to share the scalar form of the value it wraps. When that single property reduces to a scalar, the mapper unwraps the type on write and rebuilds it on read, delegating to the inner type's own mapping. The wrapper declares nothing, and the delegation recurses through nested wrappers until it reaches the scalar.

A property reduces to a scalar when its type is a native scalar, a backed enum, a DateTimeInterface, a type annotated with #[ScalarCodec], or, recursively, another single-property type whose own property reduces to a scalar. A pure enum reduces to its case name. A Traversable never reduces, and neither does an object with two nor more properties.

A priority label that wraps a backed enum and declares no mapping.

<?phpdeclare(strict_types=1);
enum Priority: string
{
caseLOW = 'low';
caseHIGH = 'high';
}
finalreadonlyclass Label
{
publicfunction__construct(publicPriority$priority)
{
}
}

A task composed of a name and a single label.

<?phpdeclare(strict_types=1);
finalreadonlyclass Task
{
publicfunction__construct(publicstring$name, publicLabel$label)
{
}
}

The label collapses to the backing value on write and rebuilds from the same scalar on read.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Mapper;
$mapper = Mapper::create();
# The label adopts the backed enum's scalar form, so the column holds a bare value.$row = $mapper->toArray(source: newTask(name: 'deploy', label: newLabel(priority: Priority::HIGH)));
# The same scalar reconstructs the nested label.$task = $mapper->toObject(type: Task::class, source: ['name' => 'deploy', 'label' => 'high']);

The serialized form of the single property follows the inner type.

Inner type of the single propertySerialized form
A native scalar (int, float, string, bool).The scalar itself.
A backed enum.The backing value.
A pure enum.The case name.
A DateTimeInterface.The ISO 8601 string.
A type annotated with #[ScalarCodec].The encoded scalar.
A single-property type that reduces to a scalar.The funneled inner scalar.
A Traversable collection.A nested array under the property key.
An object with two or more properties.A nested object under the property key.

Delegation is the fallback for a single-property type that neither registers a mapping nor self-describes. To take over the scalar form, make the wrapper itself carry the rule: register a mapping through withMapping, or annotate the wrapper with #[ScalarCodec]. The order of precedence is a registered mapping first, then a #[ScalarCodec] on the wrapper, then delegation, then plain reflection. A wrapper that owns a Codec or a #[ScalarCodec] always wins over the delegation to its inner type.

Preserving object shape with Structured

Structured::create() builds a mapping that keeps a single-property type as an object instead of letting it collapse to the scalar it wraps. It is the counterpart to the delegation above: an unmapped single-property wrapper unwraps to its inner scalar, while a wrapper registered with Structured emits the property as an object on write and rebuilds it by reflection on read, so the shape survives the round trip. It is the Subtype mapping without the discriminator field, and it honors the active naming strategy and omittingNulls.

An organization identified by a single registration id.

<?phpdeclare(strict_types=1);
finalreadonlyclass Organization
{
publicfunction__construct(publicstring$registrationId)
{
}
}

Without a mapping the wrapper collapses to its scalar; registered with Structured it keeps its object shape, follows the naming strategy, and rebuilds on read.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Mapper;
useTinyBlocks\Mapper\SnakeCase;
useTinyBlocks\Mapper\Structured;
# Default delegation collapses the wrapper to its inner scalar.
Mapper::create()->toArray(source: newOrganization(registrationId: 'org-1'));
# ['org-1']# Registered with Structured, the wrapper keeps its object shape and rebuilds on read.$mapper = Mapper::create()->withMapping(type: Organization::class, mapping: Structured::create());
$mapper->toArray(source: newOrganization(registrationId: 'org-1'));
# ['registrationId' => 'org-1']$mapper->toObject(type: Organization::class, source: ['registrationId' => 'org-1']);
# Organization(registrationId: 'org-1')# It composes with the naming strategy and with omittingNulls.
Mapper::create()
->withNaming(namingStrategy: SnakeCase::create())
->withMapping(type: Organization::class, mapping: Structured::create())
->toArray(source: newOrganization(registrationId: 'org-1'));
# ['registration_id' => 'org-1']

Factory construction with FactoryMethod

FactoryMethod::using builds a mapping that constructs the target through one of its own public static factory methods, reflecting the factory parameters from the source. It complements Codec: where a codec converts a scalar through closures, a factory mapping is reflection-based and works for any arity. The mapped type imports nothing from the library, and the factory drives the real construction path (invariants, lookups, parsing) that plain reflection injection would skip.

A money value object reconstructed through a named factory that normalizes the currency code.

<?phpdeclare(strict_types=1);
finalreadonlyclass Money
{
privatefunction__construct(publicint$cents, publicstring$currency)
{
}
publicstaticfunctionof(int$cents, string$currency): Money
{
returnnewMoney(cents: $cents, currency: strtoupper($currency));
}
}

The mapping is registered like any other, and the factory drives every read.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\FactoryMethod;
useTinyBlocks\Mapper\Mapper;
$mapper = Mapper::create()->withMapping(
type: Money::class,
mapping: FactoryMethod::using(method: 'of')
);
# Each parameter is resolved from the array by its name, then passed to the factory.$money = $mapper->toObject(type: Money::class, source: ['cents' => 500, 'currency' => 'brl']);
# Reflection over the declared properties writes the array back, symmetric with the read input.$row = $mapper->toArray(source: $money);

The factory parameter names must match the target's property names. Each parameter is resolved by its name under the active NamingStrategy, with scalar coercion and recursive mapping, honoring any registered mapping for nested types. A single-parameter factory is fed the scalar source directly. A multi-parameter factory is fed an array, so a top-level multi-parameter source must be an array, not a JSON string.

Writing is reflection over the instance's declared properties, not the inverse of the factory. A single-property object writes back to a scalar and a compound one to an array, so the round-trip is lossless only when the persisted form is the canonical form the factory consumes.

Layout::from(paths: [...], factory: 'of') composes the two: the flat row is reshaped onto the nested graph and the final object is built through the factory instead of reflection injection. The nested values inside resolve through the registry, just as a top-level factory mapping does.

Ignoring properties with Transient

#[Transient] marks a property the mapper skips in both directions. A transient property never appears in the serialized output and is never hydrated from a source document, so infrastructure state carried on the object (an event buffer, a version counter, a cached computation) stays out of the portable form while remaining an ordinary property of the class. Unlike Configuration::omitting, which drops a field for a single call, the attribute is a permanent trait of the type, honored by every mapper with no configuration.

An account that exposes its number and balance, and keeps a checksum it derives on its own.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Transient;
finalreadonlyclass Account
{
#[Transient]
privatestring$checksum;
publicfunction__construct(publicstring$number, publicint$balance)
{
$this->checksum = sprintf('%s:%d', $number, $balance);
}
publicfunctionchecksum(): string
{
return$this->checksum;
}
}

The mapper serializes the state and leaves the transient property out, and on read the type derives it again instead of taking it from the source.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Mapper;
$mapper = Mapper::create();
# The transient checksum is absent from the serialized state.$row = $mapper->toArray(source: newAccount(number: 'ac-1', balance: 500));
# ['number' => 'ac-1', 'balance' => 500]# On read the checksum is not taken from the source, the constructor recomputes it.$account = $mapper->toObject(type: Account::class, source: ['number' => 'ac-1', 'balance' => 500]);
# $account->checksum() === 'ac-1:500'

The attribute targets any property, including one declared on a trait. Because a transient property is dropped from the type descriptor, the type must construct itself without it: it belongs on infrastructure state the object derives or assigns on its own, not on a value the source is expected to provide.

Configuration and naming

Configuration carries per-call output options. The default preserves keys, omits no fields, and keeps null-valued properties. omitting excludes properties from the output. omittingNulls drops every object property whose value is null, recursing into nested objects. discardingKeys reindexes iterable content with numeric keys, switching the underlying KeyPreservation from PRESERVE to DISCARD.

A profile with a name, an optional title, a creation timestamp, and a severity.

<?phpdeclare(strict_types=1);
useDateTimeImmutable;
finalreadonlyclass Profile
{
publicfunction__construct(
publicstring$name,
public ?string$title,
publicDateTimeImmutable$createdAt,
publicSeverity$severity
) {
}
}

A pure enum that classifies the profile severity.

<?phpdeclare(strict_types=1);
enum Severity
{
caseLOW;
caseHIGH;
}

The profile is hydrated through the mapper from a source array, so omitting operates on a visible typed property. The $refunds collection from the previous section is reindex with numeric keys.

<?phpdeclare(strict_types=1);
useTinyBlocks\Mapper\Configuration;
useTinyBlocks\Mapper\Mapper;
$mapper = Mapper::create();
$profile = $mapper->toObject(
type: Profile::class,
source: [
'name' => 'Alice',
'title' => 'Owner',
'createdAt' => '2026-01-01T00:00:00+00:00',
'severity' => 'HIGH'
]
);
$array = $mapper->toArray(source: $profile, configuration: Configuration::default()->omitting('title'));
$reindex = $mapper->toArray(source: $refunds, configuration: Configuration::default()->discardingKeys());
$draft = $mapper->toObject(
type: Profile::class,
source: [
'name' => 'Alice',
'title' => null,
'createdAt' => '2026-01-01T00:00:00+00:00',
'severity' => 'HIGH'
]
);
# The explicit null title is dropped, so the output is:# ['name' => 'Alice', 'createdAt' => '2026-01-01T00:00:00+00:00', 'severity' => 'HIGH']$withoutNulls = $mapper->toArray(source: $draft, configuration: Configuration::default()->omittingNulls());

NamingStrategy is the interface controlling how source keys translate to property names. Identity (the default) expects keys that already match property names. SnakeCase translates between snake_case source keys and camelCase properties, and it also drives the prefix-derivation column names used by Layout. A custom convention implements NamingStrategy directly, defining toSourceKey (a property name to its source key) and derivedColumn (the ordered property-path segments to a flat column name).

Mapper::create()->withNaming(namingStrategy: SnakeCase::create());

Exceptions

The library raises four public exceptions, all under TinyBlocks\Mapper\Exceptions.

ExceptionRaised when
UnmappableSourceThe source value cannot be mapped to the requested type (malformed JSON, type mismatch).
UnknownSubtypeA Subtype value matches no case and no default factory is configured.
UnexpectedKeyA source key matches no property and the mapper was built with rejectingUnknownKeys().
InvalidSubtypeCaseA Subtype maps a case outside the registered type, or derives one value for two types.

License

Mapper is licensed under MIT.

Contributing

Please follow the contributing guidelines to contribute to the project.

About

Maps PHP objects to and from arrays, JSON, and iterables through reflection and pluggable strategies.

Topics

Resources

Security policy

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages