Download the PHP package zahran/mapper without Composer
On this page you can find all versions of the php package zahran/mapper. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download zahran/mapper
More information about zahran/mapper
Files in zahran/mapper
Package mapper
Short Description Compile a JSON mapping template once, map many payloads with it.
License MIT
Informations about the package mapper
zahran/mapper
Compile a JSON mapping template once, map many payloads with it.
A mapping template is data, not code: it says where each output field comes from, how to narrow and order lists, and how to transform values on the way out. Templates are validated when they compile, so a broken template fails once, up front, with the path to the problem — never silently, one payload at a time.
- Installation
- A first mapping
- The template
- Gathering several paths
- Narrowing a list
- Casts
- The shape of the result
- Errors
- Extending the mapper
- Reusing a compiled mapping
Installation
Requires PHP 8.0 or newer and ext-json. Tested on 8.0 through 8.5.
A first mapping
Payloads and templates may be given as JSON strings or as already-decoded PHP arrays.
The template
A template is an attribute. Most templates declare attributes and yield an object, but
a template that declares a path or a type of its own yields whatever that
describes.
Each attribute takes:
| Key | Meaning |
|---|---|
name |
the key it writes in the output — required, non-empty |
path |
where to read it from, as a list of segments |
paths |
several sources gathered into one value, instead of path |
default |
what to use when the payload holds nothing for it |
required |
reject the payload when it holds nothing for it — see validating |
conditions |
rewrite the value when it matches |
mutators |
transform the value |
cast |
convert the value |
type |
"array" — see lists |
attributes |
the shape of each item, on an "array" attribute |
where, sort, distinct, offset, limit |
narrow a list |
Anything else is rejected when the template compiles, naming the key and where it sat:
Paths
A path is a list of segments walked one at a time.
Keys and indexes
A negative index counts from the end of a list, but only after a literal lookup misses —
a payload that really does hold the key -1 still reads the way it always did.
* — every value one level down
Wildcards nest: ["orders", "*", "lines", "*", "sku"] reads every sku of every line of
every order. An element that lacks the key contributes nothing rather than a null.
** — every descendant
** matches the scope it stands on and everything nested anywhere beneath it, so
["kept", "**", "id"] searches only under kept.
{"where": …} — every child that matches
A clause reads path (optional — without it the element itself is tested),
condition_type (any registered condition) and value. Give where a
list of clauses and every one must hold:
A key the element lacks tests as null, so notnull says what you would expect about a
key that is not there at all.
@ — the element itself
Inside a list of scalars there is no key to reach the element by:
Collection-shaped paths
A path holding *, ** or a filter resolves to the list of every match — and to an
empty list when nothing matched, rather than to null:
Picking positions
A trailing list of indexes reads fixed positions out of whatever the path resolved to.
$-prefixed entries are hard-coded values rather than positions:
A position the payload has nothing at reads as null, so the result always has as many
entries as the template asked for. To take the first three items of a list and have the
result shorten when there are fewer, use limit instead.
When the payload holds nothing
An explicit null is a value; only a key that is not there at all falls back on the
default. An attribute with no path at all is a hard-coded value:
Gathering several paths
paths reads several sources into one list, so a mutator can combine them:
Without a mutator the gathered list is the value: {"name": "Both", "paths": [["a"], ["b"]]}
yields [1, 2]. A $-prefixed entry is a hard-coded value sitting between the paths.
A path the payload has no value for contributes null and keeps the others in place
(["Ada", null]). Only when every path is missing does the attribute fall back on its
default.
Values gathered this way are piped whole rather than one element at a time, which is what
lets implode and array_sum see the lot.
Lists
An attribute of "type": "array" maps each element of the array its path resolves to,
using its own attributes as the shape of each item:
Lists nest to any depth, and each level's paths are relative to its own element:
A list whose source is absent, or is not an array, yields [] — unless the attribute is
required, or the mapper is strict. A list attribute may omit
its path to map the scope it is already standing on. default, cast, conditions
and mutators belong on the nested attributes, not on the list itself.
Narrowing a list
where, sort, distinct, offset and limit decide which elements are mapped and in
what order. They run in that order, and they run before anything is mapped, so a limit
caps the work as well as the output.
Given {"items": [{"sku": "A", "price": 30, "active": true}, {"sku": "B", "price": 10, "active": false}, {"sku": "C", "price": 20, "active": true}]}:
All five together read as you would expect — keep the active ones, dearest first, take one:
where takes the same clauses as a filter segment:
one clause, or a list of clauses that must all hold.
sort takes one sort key or a list of them, weighed in turn so the second only
speaks where the first ties. direction is "asc" (the default) or "desc", and path
is optional — without it the element itself is compared. Elements the payload has no
value for gather at one end rather than scattering, and elements that tie on every key
keep the order the payload gave them.
distinct keeps the first of each group. Give it the path — or paths — that identify
an element, or true to compare whole elements. Identity is exact, so 1 and "1" stay
apart.
Because distinct runs after sort, sorting first decides which duplicate survives.
offset and limit take a window of what is left. offset may be negative to
count from the end; limit must be zero or more.
Narrowing works on any list, including one reached by a wildcard:
The pipeline
conditions, mutators and cast transform a value on its way out. They always run in
that order — conditions, then mutators, then the cast — however the template is written,
and each step feeds the next.
When the value is an array, each step applies to every element of it:
Values gathered with paths are the exception: they are piped whole, so that
array_sum and implode can combine them.
The pipeline is skipped entirely when the payload holds nothing for the path — the
default stands as written, rather than being cast or mutated.
Conditions
A condition rewrites the value to then when it matches, and to otherwise when it does
not. Without otherwise a value that does not match is left alone.
Conditions run in declaration order and feed each other, so a later one sees what an earlier one wrote:
condition_type |
Matches when |
|---|---|
eq |
the value loosely equals value ("1" equals 1) |
neq |
the value is not identical to value |
gt, gte, lt, lte |
the value compares that way against value |
in |
the value is in value — a list, or a comma-separated string |
not_in |
it is not |
contains |
every needle in value appears in the value, case-insensitively |
missing |
the negation of contains |
null, notnull |
the value is, or is not, null |
is_numeric, is_string, is_boolean, is_float (is_double) |
the value is of that type |
Mutators
A mutator transforms the value. arguments is optional; when it is omitted the value is
passed as the sole argument, and when it is given the literal "__value__" marks where
the value goes.
Mutators run in declaration order:
Registered mutators: add, subtract, multiply, divide, modulo, power. Each
takes one argument and coerces both sides to numbers; anything PHP would refuse to do
arithmetic on yields null rather than raising.
Beyond those, a mutator may name a PHP function — but only one on the allow list, because a template is data and data must never reach arbitrary code. Every listed function is side-effect free and takes no callable, so none can be used to invoke another:
Anything else is refused when the template compiles:
Add your own with withMutator() or withFunctions().
Casts
type |
Result |
|---|---|
integer |
"40" → 40, "40.9" → 40, null → 0 |
float |
"40.5" → 40.5, 40 → 40.0 |
string |
40 → "40", true → "1", false and null → "" |
boolean |
PHP truthiness: "0", "", null, [] → false |
date |
reformatted with the required format; an empty value stays null |
format is required for date, and the template is rejected when it is absent. These
conversions are deliberately forgiving; strict mapping makes them
refuse values they would have to invent an answer for.
The shape of the result
The root is an attribute like any other, so a mapping is not forced to end in an object.
A root list narrows with where, sort and the rest like any other, and a root value is
piped like any other. map() therefore returns mixed.
Validating the payload
Mapping is forgiving by default: a missing path falls back on its default, and a list
whose source is not an array yields []. Two things make it strict where you want it.
required
An explicit null satisfies required — only absence does not. On a list attribute,
required also rejects a source that is not an array, but accepts an empty one. A
failure names the attribute by its full dotted path in the output:
required cannot be combined with default, which already stands in for a missing
value; the template is rejected when it compiles.
Strict mapping
A strict mapper refuses to invent values rather than coercing them:
The bar is faithfulness, not convertibility — (bool) "false" is true in PHP, and that
is exactly the silent answer strictness is there to spare you. A strict integer takes
an int, a bool, a whole float or a whole numeric string; a strict boolean takes a bool,
0/1 or "0"/"1", and refuses "false".
Null is carried through casts untouched rather than becoming 0, "" or false, and a
path the payload has no value for still falls back on its default: absence is not an
error unless the attribute says required. A list attribute whose source is present but
is not a list fails instead of yielding []:
strict() returns a new mapper and survives registering more conditions, casts,
mutators or functions. Custom casts stay trusted unless they implement
Cast\Validating.
Errors
Everything the library throws implements Exception\MappingException, so one catch
covers the lot.
| Exception | Extends | Thrown |
|---|---|---|
InvalidTemplateException |
InvalidArgumentException |
compiling — the template is wrong |
InvalidJsonException |
InvalidArgumentException |
decoding — the JSON is wrong |
InvalidPayloadException |
RuntimeException |
mapping — the payload is wrong |
Whatever a cast or a mutator throws is carried out as an InvalidPayloadException naming
the attribute, with the original kept as previous:
Extending the mapper
Every with*() method returns a new mapper and leaves the original alone.
Custom conditions work anywhere a condition does, including where clauses.
To widen the allow list instead of writing a mutator:
Reusing a compiled mapping
Compiling validates the template and builds the nodes; mapping walks them. When the same template serves many payloads, compile once:
mapMany() maps lazily and preserves keys — nothing is read until the generator is
consumed:
Mapper::map($data, $template) compiles and maps in one call, which is convenient for a
one-off and wasteful in a loop.
What this library does not do
- It does not map back. The transform is lossy by construction — conditions collapse
ranges into labels, casts discard precision,
defaulterases the fact that a key was absent — so a round trip means writing a second template by hand. - It does not hydrate objects. Nodes emit plain arrays and casts produce scalars. Build your DTOs from the result.
Testing
Or against every supported PHP version, with Docker:
License
MIT.
All versions of mapper with dependencies
ext-json Version *