- Overview
- Installation
- How to use
- The Mapper service
- The Mappable trait
- Polymorphic types with Subtype
- Flat rows with Layout
- JSON columns with JsonColumn
- Collections that map themselves
- Scalar codecs with Codec
- Self-describing scalars with ScalarCodec
- Transparent delegation for single-property wrappers
- Preserving object shape with Structured
- Factory construction with FactoryMethod
- Ignoring properties with Transient
- Configuration and naming
- Exceptions
- License
- Contributing
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.
composer require tiny-blocks/mapperMapper::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.
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.
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.
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.
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']);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.
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.
#[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.
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 property | Serialized 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.
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']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.
#[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 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());The library raises four public exceptions, all under TinyBlocks\Mapper\Exceptions.
| Exception | Raised when |
|---|---|
UnmappableSource | The source value cannot be mapped to the requested type (malformed JSON, type mismatch). |
UnknownSubtype | A Subtype value matches no case and no default factory is configured. |
UnexpectedKey | A source key matches no property and the mapper was built with rejectingUnknownKeys(). |
InvalidSubtypeCase | A Subtype maps a case outside the registered type, or derives one value for two types. |
Mapper is licensed under MIT.
Please follow the contributing guidelines to contribute to the project.