php-dextrin

MapBuilder
in package

FinalYes

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
string

toDxnMapKey()

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

describeKey()

private static describeKey(int|string $phpKey) : string
Parameters
$phpKey : int|string
Return values
string

isCanonicalIntegerString()

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
bool

mapKeyToPhpKey()

private static mapKeyToPhpKey(mixed $dxnKey, callable(string): Throwable $errorFactory) : int|string
Parameters
$dxnKey : mixed
$errorFactory : callable(string): Throwable
Return values
int|string
On this page

Search results