Each 8.N.x line targets exactly one PHP version: 8.0.x runs on PHP 8.0, 8.1.x on PHP 8.1, and so on. Moving from one 8.x line to another never changes the public API or results, so no code changes are needed.
To upgrade, install the line that matches your PHP version. Composer picks it for you: composer require pharaonic/php-dot-array resolves the latest line your PHP version supports.
| Line | PHP | pharaonic/php-readable |
|---|---|---|
8.5.x |
8.5 | ~8.5.0 |
8.4.x |
8.4 | ~8.4.0 |
8.3.x |
8.3 | ~8.3.0 |
8.2.x |
8.2 | ~8.2.0 |
8.1.x |
8.1 | ~8.1.0 |
8.0.x |
8.0 | ~8.0.1 |
8.5.0 was released before the 8.x rebuild and still ran the 2.x engine. 8.5.1 is a patch release, so composer update installs it automatically, but it brings every result change listed in From 2.x to 8.0. The API is the same. Review that section before updating, or pin 8.5.0 until you have:
composer require pharaonic/php-dot-array:8.5.0Differences from 8.5.0 that are not in the 2.x list:
8.5.0already deleted integer keys correctly, so section 10'sdelete($int)fix doesn't apply.dot()acceptsnulland anotherDotArrayagain;8.5.0accepted arrays only.- PHP
>=8.5 <8.6instead of^8.5.
Version 8.0 rebuilds the path engine for PHP 8.0. The public API is unchanged: every method and helper still exists with the same parameters, and delete() still returns bool. What changed is the result of some calls, mostly edge cases that were bugs, but some of them could be relied on.
- PHP
>=8.0 <8.1(one release line per PHP version:8.0.x,8.1.x, ...). pharaonic/php-readable~8.0.1(installed automatically), which requiresext-mbstring.
| Before | Now | Status |
|---|---|---|
get(), has(), set(), delete() |
unchanged | Supported |
all(), clear(), count(), isEmpty(), toJson() |
unchanged | Supported |
setArray(), setReference(), ArrayAccess, dot() |
unchanged | Supported |
set(), clear(), setArray(), setReference() returned nothing |
return the instance (chainable) | Supported |
get() then delete() |
pull($path, $default) |
New |
isNumericKeys(), array_is_numeric() |
Pharaonic\Readable\Arr::isList() |
Deprecated (works, no runtime warning) |
isMultidimensional(), array_is_multidimensional() |
Pharaonic\Readable\Arr::isMultidimensional() |
Deprecated (works, no runtime warning) |
isNulledValues(), array_is_null() |
Pharaonic\Readable\Arr::isNull() |
Deprecated (works, no runtime warning) |
third argument of get(), has(), set(), delete() |
dot($array) or setReference($array) |
Deprecated (works, no runtime warning) |
Subclasses of DotArray keep working: no parameter or declared return type changed.
get() still gives the default for a missing path or a null value, as before. It no longer treats [] or an array holding only nulls as missing, and the other methods now see null values.
| Data | Call | Before | Now |
|---|---|---|---|
['a' => null] |
get('a', 'x') |
'x' |
'x' (unchanged) |
['a' => []] |
get('a', 'x') |
'x' |
[] |
['a' => [null, null]] |
get('a', 'x') |
'x' |
[null, null] |
['a' => null] |
delete('a') |
false, kept |
true, deleted |
[null] |
has(0) |
null |
true |
If you need the old fallback for empty arrays, apply it yourself:
$dot->get('a') ?: 'x'; // default for anything emptyResults used to be merged with array_merge_recursive() whenever the values were arrays. Now every matched element gives exactly one entry, in order. Scalar results and nested wildcards are unchanged.
| Data | Call | Before | Now |
|---|---|---|---|
['users' => [['p' => ['e' => 1]], ['p' => ['e' => 2]]]] |
get('users.*.p') |
['e' => [1, 2]] |
[['e' => 1], ['e' => 2]] |
['a' => [['tags' => [1, 2]], ['tags' => [3]]]] |
get('a.*.tags') |
[1, 2, 3] |
[[1, 2], [3]] |
['g' => [['u' => [['n' => 'A']]], ['u' => [['n' => 'B']]]]] |
get('g.*.u') |
['n' => ['A', 'B']] |
[[['n' => 'A']], [['n' => 'B']]] |
| same | get('g.*.u.*.n') |
['A', 'B'] |
['A', 'B'] (unchanged) |
To flatten a list of lists yourself:
array_merge(...$dot->get('a.*.tags')); // [1, 2, 3]A trailing .* used to be stripped, so users.* meant users.
| Data | Call | Before | Now |
|---|---|---|---|
['users' => [1, 2]] |
delete('users.*') |
removes the users key |
users becomes [] |
['a' => [1, 2]] |
set('a.*', 9) |
['a' => 9] |
['a' => [9, 9]] |
['users' => []] |
has('users.*') |
true |
false |
['r' => ['x' => 1, 'y' => 2]] |
get('r.*') |
['x' => 1, 'y' => 2] |
[1, 2] (keys dropped) |
['a' => 1] |
get('*') |
['a' => 1] |
[1] |
If you meant the array itself, drop the .*: delete('users'), set('a', 9), has('users'), get('r'), all().
| Data | Call | Before | Now |
|---|---|---|---|
[] |
set('users.*.active', true) |
['users' => [['active' => true]]] |
unchanged |
['users' => []] |
set('users.*.active', true) |
['users' => [['active' => true]]] |
unchanged |
To create the element, use its index: set('users.0.active', true).
has()isfalsewhen the wildcard matches nothing (it wastruefor an empty array). When elements match, every one of them must still have the path, as before.- Branches that don't reach the last
*are skipped. Withgroups.*.users.*.name, a group withoutusersused to makehas()returnfalse; now it is ignored, the same wayget()ignores it. delete()returnstruewhen at least one value was deleted. It used to returnfalseunless every element had the key, even though it deleted the ones it found.
| Data | Call | Before | Now |
|---|---|---|---|
['a' => 'hello'] |
set('a.b', 1) |
TypeError |
['a' => ['b' => 1]] |
['a' => 5] |
set('a.b', 1) |
Error |
['a' => ['b' => 1]] |
['a' => 1] |
count('x') |
TypeError |
0 |
['a' => 1] |
count('a') |
TypeError |
0 |
['i' => [1, ['b' => 2]]] |
get('i.*.b') |
TypeError |
[2] (non-arrays skipped) |
['i' => [1, ['b' => 2]]] |
has('i.*.b') |
TypeError |
true |
['i' => [1, ['b' => 2]]] |
set('i.*.b', 9) |
TypeError |
['i' => [1, ['b' => 9]]] |
If you caught these errors to detect a wrong shape, check the value instead, for example is_array($dot->get('a')).
\.,\*and\\are now escapes:get('config.app\.name')reads the keyapp.name. A path that used these sequences for literal backslashes now means something else. Other backslashes are unchanged (App\Models\Useris still one key).*is a wildcard only as a whole segment.get('a.b*')reads the keyb*; it used to readb.
| Data | Call | Before | Now |
|---|---|---|---|
['a', 'b'] |
get('0') |
['a', 'b'] |
'a' |
['a', 'b'] |
set('0', 'x') |
nothing | ['x', 'b'] |
['a', 'b'] |
delete('0') |
false, nothing deleted |
true, deleted |
[[1], [2]] |
toJson(0) |
'[[1],[2]]' |
'[1]' |
['', 'b'] |
isEmpty('0') |
false (whole array) |
true |
They now use Pharaonic\Readable\Arr, which gives a different result in two edge cases. isNulledValues() / array_is_null() are unchanged.
| Data | Call | Before | Now |
|---|---|---|---|
[] |
isNumericKeys(), array_is_numeric($a) |
false |
true (same as PHP's array_is_list()) |
['a' => []] |
isMultidimensional(), array_is_multidimensional($a) |
false |
true (any array value counts, even an empty one) |
delete('x.b')with a missingxdeleted the root keyb. It now deletes nothing and returnsfalse.delete($int)raised warnings and deleted nothing (2.x).
get('')returns the whole array,has('')istrue, andset('')/delete('')do nothing.- Leading and trailing dots and spaces are ignored, and
a..bmeans an empty-string key. toJson()returns''when encoding fails; passJSON_THROW_ON_ERRORto get an exception.setArray()aftersetReference()replaces the referenced array.get()returns the default for a missing path or anullvalue.- Missing and
nullvalues inside a wildcard read are filled with the default:get('users.*.email', '-')gives['a@x', '-'].