PHP (^8.3) port of JavaScript's Array and Map APIs with TypeScript-style generics.
Golden rule: behave exactly like JavaScript Array, not like idiomatic PHP.
When unsure what JS does, do not guess — run it: node is available.
node -e 'console.log([1, , 3].findIndex(x => x === undefined))' # prints 1| Path | Purpose |
|---|---|
src/Arr.php |
The whole Arr implementation (one class) |
src/Map.php |
The whole Map implementation (one class) |
src/FloatToString.php |
ECMAScript Number::toString helper used by Arr |
src/RangeError.php, src/NumberFormatError.php |
Project exception classes |
tests/Unit/ArrTest.php |
Main Arr spec, organized in JS API order (see ordering rule) |
tests/Unit/MapTest.php |
Main Map spec, organized in JS API order |
tests/Unit/Test262*Test.php |
Ports of the official test262 suite, one file per Array/Map method |
tests/Integration/DocumentationExamplesTest.php |
Mirrors every PHP example block in README.md / doc/Arr.md / doc/Map.md |
tests/Integration/MapProcessingTest.php |
Complex end-to-end Map integration example |
tests/Integration/UserProcessingTest.php |
Complex end-to-end Arr integration example |
tests/Stub/*.php |
Shared test fixtures |
doc/Arr.md |
Arr API documentation with runnable examples |
doc/Map.md |
Map API documentation with runnable examples |
| Command | What it checks |
|---|---|
composer test |
Everything below, in order — must exit 0 before you are done |
composer test:lint |
php -l on all files |
composer test:static-analysis |
PHPStan level 9 (analyzes src/ only, not tests) |
composer test:cs |
php-cs-fixer dry-run (covers src/ AND tests/) |
composer test:unit |
PHPUnit Unit suite with coverage |
composer test:integration |
PHPUnit Integration suite |
composer test:infection |
Mutation testing, fails below 95% MSI (needs test:unit coverage first) |
composer fix:cs |
Auto-fix code style |
Quality gates to preserve:
- 100% line and method coverage of
src/Arr.phpandsrc/Map.php(check the coverage summarycomposer test:unitprints). - Mutation score ≥ 95%. Kill mutants with exact assertions: assert exact strings/values,
test boundary values (
0,-1,length,length - 1,21,-6), and test negative numbers, empty arrays, and sparse arrays for every new branch.
- Keep
declare(strict_types=1);,finalclasses, typed properties, and the detailed PHPDoc generics/callable signatures (@template T,callable(null|T, int, self<T>): bool, ...). - Method ordering in
Arr.phpandMap.php: (1) magic methods__*; (2) interface methods in the order listed inimplements; (3) JS API methods in MDN order (statics first, then instance methods); (4) non-JS helpers (toArray(), private helpers) at the end. Mirror the same order in the matching test file (ArrTest.php/MapTest.php),doc/Arr.md/doc/Map.md, andtests/Integration/DocumentationExamplesTest.php. thisArgsupport: bind only non-staticClosurecallables viaClosure::bindTo; every other callable type silently ignores$thisArg.
- PHP
nullplays the role of JSundefinedeverywhere. - Sparse arrays:
lengthcan exceed the number of stored indexes ("holes"). Reading a hole returnsnull, butoffsetExists()/isset()returnsfalsefor holes andtruefor explicitnullvalues. Never collapse that distinction. - Methods that SKIP holes (callback not invoked / index not matched):
every,some,filter,forEach,map(keeps holes in result),reduce,reduceRight,flat,indexOf,lastIndexOf. - Methods that DO NOT skip holes (hole is treated as
null):find,findIndex,findLast,findLastIndex,includes,join,keys,entries,values,fill,at. - Always-dense results:
toReversed,toSorted,toSpliced,withnever return sparse arrays — holes materialize as explicitnulls. - Iteration methods capture
$len = $this->internalLengthbefore the loop (JS uses the initial length even if the callback mutates the array). $this->internalArraykeys are in insertion order, not index order ($a[2] = ...; $a[0] = ...;). When order matters, neverforeachover it directly; loopfor ($i = 0; $i < $len; ++$i)and check\array_key_exists($i, ...).- Floats stringify via
FloatToString::floatToString()which implements ECMAScriptNumber::toString(shortest round-trip):0.1→"0.1",1.0→"1",-0.0→"0",INF→"Infinity",NAN→"NaN",1e21→"1e+21",1e-7→"1e-7". Do not usesprintf('%g')or(string)casts. sort()moves explicitnulls (JSundefined) to the end without calling the comparator; holes end up after those (length stays, trailing indexes unset).- Out-of-range / non-numeric
ArrayAccessoffsets become string "properties" stored separately (they never affectlength), mirroring JS property access; offsets that cannot be stringified throw.
- PHP
nullplays the role of JSundefinedeverywhere, including as a valid stored value.Mapdistinguishes a missing key (returnsnull) from a key explicitly mapped tonull. - Key equality is
SameValueZero:NaNmatchesNaN,-0matches+0, object keys are compared by identity, and string/numeric lookalikes are not equal. -0is canonicalized to+0on insertion (set, constructor,getOrInsert,getOrInsertComputed,groupBy), matching ECMAScript.- Insertion order is preserved;
keys(),values(),entries(),forEach, andgetIterator()all iterate in insertion order and skip deleted-entry sentinels. - Deletion uses a
nullsentinel rather than removing the record, so live iterators andforEachbehave like JSMapDataeven when entries are deleted during iteration. sizeis live: it counts non-deleted entries. Access it via the magic$map->sizeproperty.- Iteration methods walk the live entry list; entries added during iteration may be visited,
while entries deleted during iteration are skipped via the
nullsentinel without shortening the boundary. - Callback signatures follow JS order:
forEach(value, key, map),groupBy(value, index),getOrInsertComputed(key). thisArgsupport: bind only non-staticClosurecallables viaClosure::bindTo; every other callable type silently ignores$thisArg.Map.groupBygroups values intoArrinstances (the project's JSArrayequivalent). ForArrinputs, holes materialize asnullentries, matching JS array iteration.
- The test suite is the spec. PHPUnit runs tests in random order — no hidden coupling between tests.
tests/Unit/Test262*Test.php: each test method names the original test262 file in its PHPDoc. Non-portable test262 cases are recorded as two comment lines:// SKIPPED: test/built-ins/Array/...jsfollowed by// Reason: .... Adapted cases explain the adaptation in a comment. Keep that style when adding or changing tests.- If you change public behavior: update
tests/Unit/ArrTest.php/tests/Unit/MapTest.php, the matchingTest262*Test.php,doc/Arr.md/doc/Map.md, andREADME.mdtogether. - Every PHP example block in
README.md,doc/Arr.md, anddoc/Map.mdmust stay executable and have exactly one matching test intests/Integration/DocumentationExamplesTest.php(testDoc<Method>Example). - PHPStan does not analyze
tests/, but php-cs-fixer does — runcomposer fix:csafter writing tests.
- JS behavior verified against
node(not assumed). composer testexits 0 (includes 95% MSI and code style).- Coverage still 100% for
src/Arr.phpandsrc/Map.php. - Docs + integration examples updated if public behavior changed.