Dot Array
Read, write, check and delete values in deeply nested PHP arrays using dot-notation paths (user.profile.name) and * wildcards (users.*.email). It is small and framework-independent. One path parser and one set of wildcard rules are shared by every operation, so get(), set(), has() and delete() always agree on what a path means.
Dot-Notation Paths
get('user.profile.name'), set('a.b.c', 1): missing keys are created on write.
Wildcards
users.*.email reaches the value in every element, nested wildcards included.
Safe Defaults
get('user.phone', 'n/a') falls back for missing and null values; has() still sees a null key.
Escaping
config.app\.name reads the key app.name; items.\* reads the key *.
Reference Mode
setReference($array) writes every change straight to your own array.
ArrayAccess & Countable
$dot['user.name'], isset(), unset(), count(), iteration and json_encode() all work.
Wrap any array with the dot() helper and chain your writes: dot($data)->set('user.active', true)->delete('user.password').
Installation
Install the package with Composer. There is nothing to register or configure.
Requirements
- PHP 8.3.x (each
8.xrelease line targets the matching PHP version) ext-mbstringpharaonic/php-readable~8.3.0 (installed automatically)
Composer Installation
composer require pharaonic/php-dot-arrayComposer autoloads the Pharaonic\DotArray namespace and the global dot() helper.
You're all set! Try dot(['user' => ['name' => 'Raggi']])->get('user.name'), which returns "Raggi".
Basic Usage
Wrap an array with the dot() helper, or create a DotArray yourself. Both accept an array, another DotArray, or nothing for an empty one:
1use Pharaonic\DotArray\DotArray;2 3$dot = dot(['user' => ['name' => 'Raggi']]);4$dot = new DotArray(['user' => ['name' => 'Raggi']]);The instance works on its own copy of the array. To change your array directly, see Reference Mode.
Reading Values
get() follows the path one key at a time. Numeric indexes are keys like any other:
1$dot = dot([ 2 'user' => ['profile' => ['name' => 'Raggi']], 3 'matrix' => [[1, 2], [3, 4]], 4]); 5 6$dot->get('user.profile.name'); // "Raggi" 7$dot->get('user.profile'); // ["name" => "Raggi"] 8$dot->get('matrix.1.0'); // 3 9$dot->all(); // the whole array10 11dot(['first', 'second'])->get(1); // "second" (integer keys work too)Default Values
The second argument of get() is returned when the path is missing or its value is null. A path is missing when a key doesn't exist, or when a value on the way isn't an array:
1$dot = dot(['user' => ['email' => null, 'name' => 'Raggi', 'active' => false]]);2 3$dot->get('user.phone'); // null (missing, no default given)4$dot->get('user.phone', 'n/a'); // "n/a" (missing)5$dot->get('user.name.first', 'x'); // "x" ("Raggi" is not an array)6$dot->get('user.email', 'n/a'); // "n/a" (null)Other empty values, false, 0, '' and [], are returned as they are:
1$dot->get('user.active', true); // falseTo tell a null value from a missing key, use has().
With a * in the path, the default works per element. See Wildcards.
Writing Values
set() creates missing keys and returns the instance, so calls can be chained:
1$dot = dot();2 3$dot->set('user.name', 'Raggi')4 ->set('user.roles.0', 'admin');5 6$dot->all(); // ["user" => ["name" => "Raggi", "roles" => ["admin"]]]A value that isn't an array and sits in the way of the path is replaced by an array:
1$dot = dot(['a' => 'hello']);2$dot->set('a.b', 1);3 4$dot->all(); // ["a" => ["b" => 1]]Checking Paths
has() is true when the path exists, even when its value is null:
1$dot = dot(['user' => ['email' => null]]);2 3$dot->has('user.email'); // true4$dot->has('user.phone'); // falseDeleting Values
delete() returns true when something was deleted. pull() returns the value (or the default) and deletes it:
1$dot = dot(['token' => 'abc', 'user' => ['name' => 'Raggi', 'password' => 'secret']]);2 3$dot->delete('user.password'); // true4$dot->delete('user.password'); // false (already gone)5 6$dot->pull('token'); // "abc"7$dot->pull('token', 'none'); // "none"8 9$dot->all(); // ["user" => ["name" => "Raggi"]]Clearing and Counting
1$dot = dot(['a' => [1, 2, 3], 'b' => 'x']);2 3$dot->count(); // 2 (top-level items)4$dot->count('a'); // 35$dot->count('b'); // 0 (not an array)6$dot->isEmpty('a'); // false7 8$dot->clear();9$dot->isEmpty(); // trueAn empty path never deletes anything; use clear() to empty the whole array.
Wildcards
A * segment matches every element of the array it is applied to. Every operation supports it:
1$dot = dot([ 2 'users' => [ 4 ['name' => 'Sara', 'profile' => []], 5 ], 6]); 7 8$dot->get('users.*.name'); // ["Ahmed", "Sara"]11 12$dot->has('users.*.name'); // true: every user has a name13$dot->has('users.*.profile.email'); // false: Sara has no email14 15$dot->set('users.*.active', true); // adds "active" => true to every user16$dot->delete('users.*.profile'); // removes "profile" from every user17$dot->count('users.*.name'); // 2Nested Wildcards
Nested wildcards give one flat list, in order:
1$dot = dot([ 2 'groups' => [ 3 ['users' => [['name' => 'A'], ['name' => 'B']]], 4 ['users' => [['name' => 'C']]], 5 ], 6]); 7 8$dot->get('groups.*.users.*.name'); // ["A", "B", "C"] 9 10$dot->set('groups.*.users.*.active', true);11$dot->get('groups.*.users.*.active'); // [true, true, true]How Matching Works
The rules are the same for get(), has(), set(), delete() and pull():
- The elements reached by the last
*are matched. For each of them, the rest of the path is read, written or deleted as usual. - Branches that don't reach the last
*are skipped: a missing key, or a value that isn't an array, before it. When keys follow the last*, elements that aren't arrays are skipped too. - A trailing
*means the elements themselves. *is a wildcard only as a whole segment:a.b*is the literal keyb*.
1$dot = dot([ 2 'groups' => [ 3 ['users' => [['name' => 'A']]], 4 ['title' => 'no users'], // skipped: no "users" key 5 ['users' => [['name' => 'B'], 'x']], // "x" skipped: not an array 6 ], 7]); 8 9$dot->get('groups.*.users.*.name'); // ["A", "B"]10$dot->has('groups.*.users.*.name'); // trueResults and Defaults
| Operation | Result with a wildcard |
|---|---|
get($path, $default) | A list with one entry per matched element: its value, or $default where the rest of the path is missing or null. $default itself when nothing matches. Keys of associative arrays are not kept. |
has($path) | true when at least one element matches and every matched element has the rest of the path. |
set($path, $value) | Sets the value in every matched element. Never creates elements for a *. |
delete($path) | Deletes the key from every matched element; true when at least one was deleted. |
pull($path, $default) | Returns what get() returns, then deletes like delete(). |
count($path) | The number of entries get() returns, or 0. |
1$dot = dot(['users' => [], 'roles' => ['admin' => ['level' => 1], 'editor' => ['level' => 2]]]);2 3$dot->get('users.*.name', 'none'); // "none" (nothing matched)4$dot->get('roles.*.level'); // [1, 2]5$dot->get('roles.*'); // [["level" => 1], ["level" => 2]]6 7$dot->set('users.*.active', true); // nothing to update, nothing created8$dot->get('users'); // []Trailing Wildcards
A trailing * reaches the elements themselves:
1$dot = dot(['flags' => ['beta' => true, 'dark' => true], 'tags' => ['a', 'b']]);2 3$dot->set('flags.*', false); // every flag becomes false4$dot->delete('tags.*'); // "tags" becomes []5$dot->all(); // ["flags" => ["beta" => false, "dark" => false], "tags" => []]delete('tags.*') empties tags but keeps the key. To remove the key, use delete('tags').
Paths & Escaping
A path is a list of keys separated by .. The same parser is used by every method.
Escaping Dots
Use \. for a dot that is part of a key:
1$dot = dot(['config' => ['app.name' => 'Pharaonic']]);2 3$dot->get('config.app\.name'); // "Pharaonic"4$dot->get('config.app.name'); // null (looks for ["app"]["name"])5 6$dot->set('hosts.example\.com.port', 443);7$dot->get('hosts'); // ["example.com" => ["port" => 443]]Escaping Wildcards
Use \* for a key that is literally *:
1$dot = dot(['items' => ['*' => ['name' => 'Literal Star'], 'x' => ['name' => 'X']]]);2 3$dot->get('items.\*.name'); // "Literal Star"4$dot->get('items.*.name'); // ["Literal Star", "X"]Backslashes
\\ is a literal backslash. Any other backslash is kept as it is, so class names work as keys:
1$dot = dot(['App\Models\User' => ['table' => 'users']]);2 3$dot->get('App\Models\User.table'); // "users"In single-quoted PHP strings, '\.' and '\*' are already a backslash followed by the character, so you can write the paths exactly as shown. In double-quoted strings, write "config.app\\.name".
Numeric Keys
Numeric segments reach numeric keys. PHP itself stores '1' as the integer key 1, so both spellings reach the same element, while '01' stays a string key:
1$dot = dot([0 => 'A', '1' => 'B', '01' => 'C']);2 3$dot->get('1'); // "B"4$dot->get(1); // "B"5$dot->get('01'); // "C"6$dot->get('0'); // "A"Unusual Paths
| Path | Meaning |
|---|---|
'', '.' | The whole array: get() returns it, has() is true, set() and delete() do nothing. |
'.user.', ' user ' | Leading and trailing dots and spaces are ignored: user. |
'user..name' | An empty-string key between user and name. |
'a\.' | The key a. (an escaped trailing dot is kept). |
ArrayAccess & Interfaces
DotArray implements ArrayAccess, Countable, IteratorAggregate and JsonSerializable.
ArrayAccess
Array syntax maps directly to the methods, paths and wildcards included:
| Syntax | Same as |
|---|---|
$dot['user.name'] | $dot->get('user.name') |
$dot['user.name'] = 'Raggi' | $dot->set('user.name', 'Raggi') |
$dot[] = 'value' | Appends to the root array. |
isset($dot['user.name']) | $dot->has('user.name') |
unset($dot['user.name']) | $dot->delete('user.name') |
1$dot = dot(); 2 3$dot['user.name'] = 'Raggi'; 4$dot['user.email'] = null; 5$dot[] = 'appended'; 6 7$dot['user.name']; // "Raggi" 8isset($dot['user.email']); // true (null still exists) 9$dot['users.*.name']; // null (no users yet)10 11unset($dot['user.name']);Unlike a plain array, isset($dot['user.email']) is true when the value is null, because it uses has().
Countable
count($dot) counts the top-level items. $dot->count($path) counts the items at a path, and is 0 when the path is missing or not an array:
1$dot = dot(['users' => [['name' => 'A'], ['name' => 'B']], 'title' => 'x']);2 3count($dot); // 24$dot->count('users'); // 25$dot->count('users.*.name'); // 26$dot->count('title'); // 0Iteration
foreach walks the top-level items:
1foreach (dot(['a' => 1, 'b' => 2]) as $key => $value) {2 echo "$key=$value "; // a=1 b=23}JSON
toJson() encodes the whole array, or the value at a path, with optional json_encode() flags. json_encode($dot) gives the same result as toJson():
1$dot = dot(['user' => ['name' => 'Raggi', 'url' => 'https://pharaonic.dev']]);2 3$dot->toJson(); // the whole array4$dot->toJson('user.name'); // '"Raggi"'5$dot->toJson('user', JSON_UNESCAPED_SLASHES); // '{"name":"Raggi","url":"https://pharaonic.dev"}'6json_encode($dot); // same as toJson()When encoding fails, toJson() returns an empty string. Pass JSON_THROW_ON_ERROR to get a JsonException instead.
Reference Mode
By default, a DotArray works on its own copy, so your array is never changed:
1$array = ['user' => ['name' => 'Old']];2 3$dot = dot($array);4$dot->set('user.name', 'New');5 6$array['user']['name']; // "Old"setReference() binds the instance to your array instead, so every write lands there:
1use Pharaonic\DotArray\DotArray;2 3$array = ['user' => ['name' => 'Old']];4 5$dot = (new DotArray())->setReference($array);6$dot->set('user.name', 'New');7 8$array['user']['name']; // "New"set(), delete(), pull(), clear(), setArray() and ArrayAccess writes, wildcards included, all write to the referenced array. Changes you make to the array yourself are visible through the instance too. Reads never modify it.
1$config = ['users' => [['name' => 'A', 'password' => 'x']]];2 3(new DotArray())->setReference($config)4 ->set('users.*.active', true)5 ->delete('users.*.password');6 7$config; // ["users" => [["name" => "A", "active" => true]]]After setReference(), setArray($items) replaces the contents of your array and clear() empties it. To stop working on your array, create a new instance.
API Reference
Pharaonic\DotArray\DotArray
| Method | Description | Returns |
|---|---|---|
__construct(mixed $items = []) | Create an instance from an array, another DotArray, or any value castable to an array. | |
get(string|int $key, mixed $default = null) | The value at a path, or $default when it doesn't exist. A list with a wildcard. | mixed |
set(string $key, mixed $value = null) | Set a value, creating missing keys. | $this |
has(string|int $key) | Whether the path exists, even when its value is null. | bool |
delete(string|int $key) | Delete a path. | bool (true when something was deleted) |
pull(string|int $key, mixed $default = null) | Return the value like get(), then delete it. | mixed |
all() | The whole array. | array |
clear() | Empty the array. | $this |
setArray(mixed $items) | Replace the array. | $this |
setReference(array &$items) | Work on the given array by reference. | $this |
count(string|int|null $key = null) | The number of top-level items, or of items at a path (0 when missing or not an array). | int |
isEmpty(?string $key = null) | Whether the array, or the value at a path, is empty. | bool |
toJson(int|string|null $key = null, int $options = 0) | The array, or the value at a path, as JSON ('' on failure). | string |
getIterator() | Iterator over the top-level items. | ArrayIterator |
jsonSerialize() | The array, for json_encode(). | array |
offsetGet() / offsetSet() / offsetExists() / offsetUnset() | ArrayAccess: see ArrayAccess & Interfaces. |
Helper
| Function | Description | Returns |
|---|---|---|
dot(mixed $items = []) | Same as new DotArray($items). | DotArray |
Deprecated
These still work, but will be removed in a future major release:
| Name | Use instead |
|---|---|
isNumericKeys(), array_is_numeric(array $arr) | Pharaonic\Readable\Arr::isList($array) |
isMultidimensional(), array_is_multidimensional(array $arr) | Pharaonic\Readable\Arr::isMultidimensional($array) |
isNulledValues(), array_is_null(array $arr) | Pharaonic\Readable\Arr::isNull($array) |
The third argument of get(), has(), set() and delete() (an array to work on) | dot($array) or setReference($array) |
Between 8.x Release Lines
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. Composer picks the line that matches your PHP version.
Upgrading from 2.x
v8.3.1 rebuilt the path engine. Every method and helper from 2.x still exists with the same parameters, and delete() still returns bool, but some results changed. Most of them were bugs.
The full list, with before and after values for every change, is in UPGRADE.md.
What Changed
| Change | Before | Now |
|---|---|---|
Existing [], [null, null] | get('a', 'x') gave 'x' | gives the value (null still gives 'x') |
Existing null | has()/delete() ignored it | has() is true, delete() removes it |
| Wildcard values that are arrays | merged: get('users.*.p') gave ["e" => [1, 2]] | one entry per element: [["e" => 1], ["e" => 2]] |
Trailing * | stripped: delete('users.*') deleted users | the elements: users becomes [] |
set() with a * on a missing or empty array | created [["active" => true]] | does nothing |
has() with a * and no matches | true for an empty array | false |
delete() with a * | false unless every element had the key | true when anything was deleted |
Path '0' | treated as an empty path | the key 0 |
set('a.b', 1) on ["a" => "hello"], count('missing'), wildcards over scalars | threw TypeError / Error | work (see Basic Usage and Wildcards) |
\., \*, \\ in a path | literal backslashes | escapes (see Paths & Escaping) |
isNumericKeys() on [], isMultidimensional() on ['a' => []] | false | true (now backed by Pharaonic\Readable\Arr) |
New
pull($path, $default)returns a value and deletes it.set(),clear(),setArray()andsetReference()return the instance, so calls can be chained.
Deprecated
isNumericKeys(), isMultidimensional(), isNulledValues() and the array_is_*() helpers (use Pharaonic\Readable\Arr instead), and the third argument of get(), has(), set() and delete(). See the API Reference for replacements.
Examples
1. Reading an API Response
Pull what you need from a deeply nested JSON response without isset() chains:
1$response = dot(json_decode($json, true));2 3$orderId = $response->get('data.order.id');4$currency = $response->get('data.order.currency', 'USD');5$skus = $response->get('data.order.items.*.sku', []);6$total = array_sum($response->get('data.order.items.*.price', []));The [] default keeps array_sum() safe when the order has no items.
2. Cleaning Data Before Output
Remove sensitive fields from every record, then encode the result:
1$payload = dot(['users' => $users])2 ->set('users.*.profile.public', true);3 4$payload->delete('users.*.password');5$payload->delete('users.*.remember_token');6 7return $payload->toJson('users', JSON_UNESCAPED_UNICODE);3. Editing a Config Array in Place
Use reference mode to update a configuration array that the rest of the code already holds:
1use Pharaonic\DotArray\DotArray;2 3$config = require __DIR__ . '/../config/app.php';4 5$settings = (new DotArray())->setReference($config);6$settings->set('cache.driver', getenv('CACHE_DRIVER') ?: 'file');7$settings->set('mail.hosts.smtp\.example\.com.port', 587);1return [2 'cache' => ['driver' => 'array'],3 'mail' => ['hosts' => []],4];After this, $config['cache']['driver'] holds the new driver, and the SMTP host is stored under the key smtp.example.com.
4. Validating Nested Input
Check that every line of a submitted form has the fields you need:
1$input = dot($_POST);2 3if (! $input->has('lines.*.product_id') || ! $input->has('lines.*.quantity')) {4 throw new InvalidArgumentException('Every line needs a product and a quantity.');5}6 7$input->set('lines.*.tax_rate', 0.14);has() with a wildcard is false when there are no lines at all, and when any line misses the field.
5. Array Syntax in Templates
Pass a DotArray to code that expects array access:
1$view = dot(['page' => ['title' => 'Home', 'meta' => ['og.title' => 'Welcome']]]);2 3echo $view['page.title']; // Home4echo $view['page.meta.og\.title']; // Welcome5echo isset($view['page.subtitle']) ? 'yes' : 'no'; // noTroubleshooting
get() returns my default although the key exists
The key holds null, and get() gives the default for null values as well as for missing paths. Use has() to check whether the key exists:
1$dot = dot(['a' => null]);2 3$dot->get('a', 'x'); // "x"4$dot->has('a'); // trueget() returns [] or false instead of my default
The key exists with that value. Only missing paths and null values give the default. Use ?: for a fallback on any empty value:
1dot(['a' => []])->get('a') ?: 'x'; // "x"A key with a dot in its name can't be found
config.app.name looks for ["config"]["app"]["name"]. Escape the dot: config.app\.name. In a double-quoted PHP string, write "config.app\\.name".
A wildcard read returns the default instead of a list
Nothing matched: the array before the * is missing, empty, or not an array, or no element reached the last *. Pass [] as the default when you want an empty list:
1dot(['users' => []])->get('users.*.name', []); // []A wildcard read has fewer entries than the array has elements
When keys follow the last *, elements that aren't arrays are skipped. Branches that miss a key before the last * are skipped too. Elements that are arrays but miss the rest of the path are kept, with the default as their value (the same as a null value).
set() with a wildcard didn't add anything
Wildcards only update existing elements; they never create them. Write to an index instead: set('users.0.active', true).
has() with a wildcard is false although some elements have the key
With a *, has() is true only when every matched element has the path. Check the values instead when one is enough:
1$emails = dot($data)->get('users.*.email', []);2$anyEmail = array_filter($emails, fn ($email) => $email !== null) !== [];delete('users.*') didn't remove the users key
A trailing * means the elements, so users becomes []. Use delete('users') to remove the key.
My original array didn't change
A DotArray works on its own copy. Use Reference Mode to write to your array.
toJson() returns an empty string
Encoding failed, usually because of invalid UTF-8. Pass JSON_THROW_ON_ERROR to see why:
1$dot->toJson(null, JSON_THROW_ON_ERROR); // throws JsonException with the reasonContributors
Amazing people who made this package possible
This package wouldn't exist without the contributions of our amazing community. A huge thank you to everyone who has helped improve Dot Array!
Want to Contribute?
We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.