MapBuilder
in package
Builds a DXN `map` as a bare PHP array -- shared by the text {@see \JOetjen\Dextrin\Text\Parser} and the binary {@see \JOetjen\Dextrin\Binary\Decoder}, since both need identical logic for the exact same reason: `map` is the one collection type this port decodes to bare native `array` rather than a wrapper class (see the project's own collections decision), and every PHP-array-key coercion hazard that follows from that applies equally regardless of which pipeline produced the raw key/value pairs.
Every key reduces to whatever PHP scalar it naturally coerces to --
everything follows PHP's own int/string/bool/float/null array-key
coercion (including PHP's own further auto-coercion of a canonical
decimal-integer string key to an int key, e.g. "1" and 1
already share one slot before this class does anything at all),
except DXNKeyword, which needs one more bit of care: a
DXNKeyword reduces to its bare name string (matching
map_entry's shorthand form exactly, and keeping the overwhelmingly
common case -- a keyword-keyed map, e.g. %{x: 1} -- looking up as
the ergonomic, unprefixed $map['x']), while a genuinely
string-typed key with the same text is marked with a single
leading self::STRING_KEY_MARKER byte first -- unless it's
already a canonical-integer-string PHP would coerce to an int key
on its own, which is left alone so it keeps colliding with a bare
integer key exactly as before (see the class-level doc's own
example of that, a deliberately-kept error case, not something this
class's own keyword/string marking is about).
Without the mark, :x and "x" would collapse to the exact same
PHP array slot with no way to tell them apart again on encode --
which used to mean Text\Printer/Text\Formatter and
Binary\Encoder disagreed with each other about which one to
assume (the text side always guessed "keyword" for an
identifier-shaped key; the binary side always assumed "string"), so
the exact same decoded PHP value could produce a .dxn and a
.dxnb that decoded back to two different things.
self::toDxnMapKey() is the reverse direction, used by both
encoders to recover which one a given PHP key actually was.
Two distinct DXN keys that would still coerce to the same PHP
slot after all of the above (1 and "1" as a bare int/canonical-
integer-string pair) is a decode error, not silent data loss --
ordinary duplicate keys (the same key appearing twice) still follow
normal last-write-wins map semantics.
Table of Contents
Constants
- STRING_KEY_MARKER : mixed = "\x00"
- Marks a genuinely string-typed map key, distinguishing it from a `DXNKeyword` key with the same text -- see this class's own doc for why. A single NUL byte: it can't be the first character of any *other* PHP-array-key-representable DXN value (int/bool/float/nil all coerce to a plain `int` key or the bare empty string, never a leading-NUL string), and prepending exactly one byte is trivially, unambiguously reversible by stripping exactly one byte back -- no escaping needed even for a string that itself already starts with a NUL byte.
Methods
- build() : array<int|string, mixed>
- markStringKey() : string
- Marks `$s` as a genuinely `string`-typed map key, the same rule `mapKeyToPhpKey()` applies to a decoded `string` key -- exposed for callers that build a map-shaped PHP array directly from a known-string source without going through `build()`'s DXN-value dispatch. The one caller today: {@see \JOetjen\Dextrin\Schema\Validator}'s default (no custom materializer registered) struct decode, which resolves a `DXNStruct`'s field *names* into a bare associative array -- exactly the same "materialize a struct with no registered module/materializer into a generic map" shape Elixir's own default (`Atom.to_string/1` on each field name) and node-dextrin's default both produce, keyed by the field name as a genuine string, not a keyword. Left unmarked, those keys would silently re-encode as `keyword` instead on the way back out -- diverging from what a schema-free reader of the same document in either of those other two languages would produce.
- toDxnMapKey() : DXNKeyword|string|int
- The reverse of {@see self::mapKeyToPhpKey()}'s `DXNKeyword`/ `string` handling, for `Text\Printer`/`Text\Formatter`/ `Binary\Encoder` to recover which one a bare `map`'s PHP array key actually started out as. An `int` key needs no translation -- it only ever came from a `DXNInteger`/`bool`/`float`/ canonical-integer-`string` key to begin with, and re-encoding it as a plain integer is correct either way (see this class's own doc on why a canonical-integer-string key is deliberately left to collide with an `integer` key rather than marked apart).
- __construct() : mixed
- describeKey() : string
- isCanonicalIntegerString() : bool
- Whether PHP would auto-coerce `$s` to an `int` array key on its own (e.g. `"1"`, `"0"`, `"-5"` -- but not `"01"`, `"1.0"`, `""`, or a string past `int` range) -- left un-marked so it keeps colliding with a bare `integer` key of the same value exactly as before, rather than this class's own keyword/string marking accidentally hiding that collision instead of reporting it.
- mapKeyToPhpKey() : int|string
Constants
STRING_KEY_MARKER
Marks a genuinely string-typed map key, distinguishing it from a `DXNKeyword` key with the same text -- see this class's own doc for why. A single NUL byte: it can't be the first character of any *other* PHP-array-key-representable DXN value (int/bool/float/nil all coerce to a plain `int` key or the bare empty string, never a leading-NUL string), and prepending exactly one byte is trivially, unambiguously reversible by stripping exactly one byte back -- no escaping needed even for a string that itself already starts with a NUL byte.
private
mixed
STRING_KEY_MARKER
= "\x00"
The one residual ambiguity this can't resolve: a keyword
literally named starting with a NUL byte (only reachable via an
explicit \x{0} escape inside a quoted keyword literal) is
indistinguishable from a marked string key. Accepted as a
documented, exceedingly rare edge case, same spirit as the
int-vs-numeric-string collision this class already detects
rather than silently resolving.
Methods
build()
public
static build(array<int, array{0: mixed, 1: mixed}> $pairs, callable(string): Throwable $errorFactory) : array<int|string, mixed>
Parameters
- $pairs : array<int, array{0: mixed, 1: mixed}>
- $errorFactory : callable(string): Throwable
-
builds the caller's own exception type (
Text\ParseError/Binary\BinaryError) from a message.
Return values
array<int|string, mixed>markStringKey()
Marks `$s` as a genuinely `string`-typed map key, the same rule `mapKeyToPhpKey()` applies to a decoded `string` key -- exposed for callers that build a map-shaped PHP array directly from a known-string source without going through `build()`'s DXN-value dispatch. The one caller today: {@see \JOetjen\Dextrin\Schema\Validator}'s default (no custom materializer registered) struct decode, which resolves a `DXNStruct`'s field *names* into a bare associative array -- exactly the same "materialize a struct with no registered module/materializer into a generic map" shape Elixir's own default (`Atom.to_string/1` on each field name) and node-dextrin's default both produce, keyed by the field name as a genuine string, not a keyword. Left unmarked, those keys would silently re-encode as `keyword` instead on the way back out -- diverging from what a schema-free reader of the same document in either of those other two languages would produce.
public
static markStringKey(string $s) : string
Parameters
- $s : string
Return values
stringtoDxnMapKey()
The reverse of {@see self::mapKeyToPhpKey()}'s `DXNKeyword`/ `string` handling, for `Text\Printer`/`Text\Formatter`/ `Binary\Encoder` to recover which one a bare `map`'s PHP array key actually started out as. An `int` key needs no translation -- it only ever came from a `DXNInteger`/`bool`/`float`/ canonical-integer-`string` key to begin with, and re-encoding it as a plain integer is correct either way (see this class's own doc on why a canonical-integer-string key is deliberately left to collide with an `integer` key rather than marked apart).
public
static toDxnMapKey(int|string $phpKey) : DXNKeyword|string|int
Parameters
- $phpKey : int|string
Return values
DXNKeyword|string|int__construct()
private
__construct() : mixed
describeKey()
private
static describeKey(int|string $phpKey) : string
Parameters
- $phpKey : int|string
Return values
stringisCanonicalIntegerString()
Whether PHP would auto-coerce `$s` to an `int` array key on its own (e.g. `"1"`, `"0"`, `"-5"` -- but not `"01"`, `"1.0"`, `""`, or a string past `int` range) -- left un-marked so it keeps colliding with a bare `integer` key of the same value exactly as before, rather than this class's own keyword/string marking accidentally hiding that collision instead of reporting it.
private
static isCanonicalIntegerString(string $s) : bool
Parameters
- $s : string
Return values
boolmapKeyToPhpKey()
private
static mapKeyToPhpKey(mixed $dxnKey, callable(string): Throwable $errorFactory) : int|string
Parameters
- $dxnKey : mixed
- $errorFactory : callable(string): Throwable