Arrays

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05. The array functions new in recent versions are marked: array_find(), array_find_key(), array_any() and array_all() (8.4), array_first() and array_last() (8.5). The short [] destructuring syntax is preferred over list(), which still works without a deprecation notice in 8.5 and in the upcoming 8.6 (checked against 8.6.0RC2).

The array model

A PHP array is an ordered map: a collection of key/value pairs that remembers insertion order. Keys are int or string; values are anything, including other arrays. The single type serves as a list, a dictionary, a set (keys only), a stack, a queue and a tuple.

<?php

declare(strict_types=1);

$titles = ['Solaris', 'Kindred', 'Dawn'];                    // a list: keys 0, 1, 2
$book = ['isbn' => '978-0-00-000001-1', 'title' => 'Kindred', 'year' => 1979];   // a map
$shelves = ['A' => ['Solaris'], 'B' => ['Kindred', 'Dawn']]; // nested

$titles[] = 'Neuromancer';                                   // append: next integer key (3)
$book['format'] = 'paperback';                               // add a key
unset($titles[1]);                                           // remove: keys are NOT renumbered

print_r($titles);                                            // [0 => Solaris, 2 => Dawn, 3 => Neuromancer]
var_dump(array_is_list($titles));                            // bool(false): keys are 0, 2, 3
var_dump(array_is_list(array_values($titles)));              // bool(true)
echo count($book), ' keys, first key: ', array_key_first($book), "\n";   // 4 keys, first key: isbn

array_is_list() (8.1) is true when the keys are exactly 0, 1, 2, … in order — the arrays that json_encode() writes as JSON arrays rather than objects. After unset() or array_filter(), call array_values() to get a list again.

Source: Arrays and array_is_list

Key casting rules

Keys that are not int or string are converted, and some strings are converted too:

  • a string containing a valid decimal integer without leading zeros or sign padding becomes an int: "8" is key 8, but "08", "8.0" and " 8" stay strings;

  • a float is truncated to int (1.7 becomes 1), which has been deprecated since 8.1 when the float has a fractional part;

  • true becomes 1, false becomes 0;

  • null becomes "" — deprecated since 8.5;

  • arrays and objects cannot be keys (TypeError), and resources become their id with a warning.

<?php

declare(strict_types=1);

$a = [];
$a['8'] = 'string "8"';
$a[8] = 'int 8';              // same key as '8': overwrites
$a['08'] = 'string "08"';     // different key
$a[true] = 'true';            // key 1
$a[1] = 'int 1';              // overwrites the "true" entry

var_dump($a);
var_dump(array_keys(['1' => 'a', '01' => 'b']));   // [1, "01"]: the first key is an int
array(3) {
  [8]=>
  string(5) "int 8"
  ["08"]=>
  string(11) "string "08""
  [1]=>
  string(5) "int 1"
}
array(2) {
  [0]=>
  int(1)
  [1]=>
  string(2) "01"
}

Because numeric-string keys become integers, array_keys() of a map whose keys look like numbers returns ints, and foreach ($map as $key => …) gives you an int $key. Under strict_types=1, passing such a key to a string parameter throws; cast it with (string) $key when needed.

Source: Key casting

Packed and hash arrays, and copy-on-write

Internally, the engine stores a list with keys 0..n-1 as a packed array (a plain vector, very compact), and anything else as a hash table. Converting between the two happens automatically, but a list that stays a list is faster and smaller.

Arrays are values: assigning or passing an array gives the receiver its own logical copy. The engine shares the underlying memory until one side writes to it (copy-on-write), so passing large arrays to functions is cheap as long as the function does not modify them. Writing into a copy duplicates the array once; see References and Memory Management.

<?php

declare(strict_types=1);

function countLong(array $titles): int          // receives a shared copy: no duplication
{
    return count(array_filter($titles, fn (string $t): bool => strlen($t) > 6));
}

$titles = array_fill(0, 100_000, 'Neuromancer');
$before = memory_get_usage();
countLong($titles);
echo memory_get_usage() - $before < 100_000 ? "no copy of the input\n" : "copied\n";

Source: Arrays (copying) and Reference Counting Basics

Creating arrays

<?php

declare(strict_types=1);

print_r(range(1, 5));                 // [1, 2, 3, 4, 5]
print_r(range(0, 20, 5));             // [0, 5, 10, 15, 20]
print_r(range('a', 'e'));             // ["a", "b", "c", "d", "e"]

print_r(array_fill(5, 3, 0));         // [5 => 0, 6 => 0, 7 => 0]
print_r(array_fill_keys(['sf', 'poetry'], []));   // ["sf" => [], "poetry" => []]
print_r(array_combine(['isbn', 'title'], ['978-0-00-000001-1', 'Kindred']));
print_r(array_flip(['sf', 'poetry']));             // ["sf" => 0, "poetry" => 1]: values become keys
$titles = ['Solaris'];
print_r(compact('titles'));                       // ["titles" => ["Solaris"]]: built from variable names

compact() and its inverse extract() work with variable names as strings, which hides data flow from readers and static analysers. Prefer explicit arrays; never call extract() on user input, which can overwrite any local variable.

Destructuring and spread

Destructuring

The short list syntax […] = $array (7.1) assigns elements to variables. Elements can be skipped, keys can be named (7.1), destructuring can be nested, and it works in foreach. Missing elements yield null with a warning.

<?php

declare(strict_types=1);

[$first, , $third] = ['Solaris', 'Kindred', 'Dawn'];                 // skip an element
['title' => $title, 'year' => $year] = ['year' => 1979, 'title' => 'Kindred'];   // by key, any order
[[$a, $b], [$c, $d]] = [[1, 2], [3, 4]];                              // nested

$x = 'left';
$y = 'right';
[$x, $y] = [$y, $x];                                                  // swap without a temporary

echo "{$first} {$third} | {$title} {$year} | {$a}{$b}{$c}{$d} | {$x} {$y}\n";
// Solaris Dawn | Kindred 1979 | 1234 | right left

// By reference (7.3)
$book = ['title' => 'kindred'];
['title' => &$t] = $book;
$t = ucfirst($t);
echo $book['title'], "\n";                                            // Kindred

list($a, $b) = … is the older equivalent of [$a, $b] = …. It still works, with no deprecation notice in 8.5 or in the upcoming 8.6 (8.6.0RC2), but new code uses the short syntax.

Spread

... unpacks an array or Traversable inside an array literal (7.4) or a function call. Since 8.1, arrays with string keys can be unpacked too, with later keys overwriting earlier ones, like array_merge():

<?php

declare(strict_types=1);

$sf = ['Solaris', 'Kindred'];
$poetry = ['Ariel'];
print_r([...$sf, ...$poetry, 'Dawn']);          // ["Solaris", "Kindred", "Ariel", "Dawn"]

$defaults = ['format' => 'paperback', 'copies' => 1];
print_r([...$defaults, 'copies' => 3]);         // ["format" => "paperback", "copies" => 3] (8.1)

function gen(): Generator { yield 'x'; yield 'y'; }
print_r([...gen()]);                            // generators unpack too

Source: Array unpacking

The array API by family

Stack and queue

<?php

declare(strict_types=1);

$returns = ['Solaris'];
array_push($returns, 'Kindred', 'Dawn');   // same as $returns[] = ...
$last = array_pop($returns);               // "Dawn"   -- stack (LIFO)
array_unshift($returns, 'Ariel');          // prepend: renumbers integer keys
$first = array_shift($returns);            // "Ariel"  -- queue (FIFO), also renumbers

var_dump($last, $first, $returns);

array_shift() and array_unshift() reindex the whole array, so they are O(n). For large queues use SplQueue (see SPL Data Structures).

Searching

<?php

declare(strict_types=1);

$books = [
    ['isbn' => '978-0-00-000003-5', 'title' => 'Solaris', 'year' => 1961],
    ['isbn' => '978-0-00-000001-1', 'title' => 'Kindred', 'year' => 1979],
    ['isbn' => '978-0-00-000002-8', 'title' => 'Dawn', 'year' => 1987],
];
$titles = array_column($books, 'title');

var_dump(in_array('Dawn', $titles, true));              // bool(true)  -- always pass true (strict)
var_dump(array_search('Kindred', $titles, true));       // int(1)      -- key, or false
var_dump(array_key_exists(2, $books), isset($books[5])); // bool(true) bool(false)

// PHP 8.4: callback-based search
$firstModern = array_find($books, fn (array $b): bool => $b['year'] > 1970);
var_dump($firstModern['title']);                         // string(7) "Kindred"
var_dump(array_find_key($books, fn (array $b): bool => $b['title'] === 'Dawn'));   // int(2)
var_dump(array_any($books, fn (array $b): bool => $b['year'] < 1965));            // bool(true)
var_dump(array_all($books, fn (array $b): bool => str_starts_with($b['isbn'], '978')));   // bool(true)

// PHP 8.5: first and last values (null for an empty array)
var_dump(array_first($titles), array_last($titles), array_first([]));   // "Solaris" "Dawn" NULL

// PHP 7.3: first and last keys
var_dump(array_key_first($books), array_key_last($books));             // int(0) int(2)

array_find() returns null when nothing matches, which is indistinguishable from a matching null value; use array_find_key() when values can be null. Before 8.4 these were written as foreach loops with break, and before 8.5 the first value was obtained with reset() (which moves the internal pointer) or $a[array_key_first($a)].

Transforming

<?php

declare(strict_types=1);

$books = [
    ['title' => 'Solaris', 'year' => 1961, 'copies' => 2],
    ['title' => 'Kindred', 'year' => 1979, 'copies' => 0],
    ['title' => 'Dawn', 'year' => 1987, 'copies' => 5],
];

// map: one output per input (keys preserved for a single array)
$labels = array_map(fn (array $b): string => "{$b['title']} ({$b['year']})", $books);

// filter: keep elements for which the callback is truthy (keys preserved!)
$available = array_filter($books, fn (array $b): bool => $b['copies'] > 0);
var_dump(array_keys($available));                     // [0, 2]

// filter by key, or by key and value
$byKey = array_filter(['a' => 1, 'b' => 2], fn (string $k): bool => $k !== 'a', ARRAY_FILTER_USE_KEY);
$both = array_filter(['a' => 1, 'b' => 2], fn (int $v, string $k): bool => $v > 1 && $k === 'b', ARRAY_FILTER_USE_BOTH);

// filter without callback: removes falsy values (0, "", "0", null, false, [])
print_r(array_filter([1, 0, '', 'x', null, '0']));    // [0 => 1, 3 => "x"]

// reduce: fold to a single value
$totalCopies = array_reduce($books, fn (int $carry, array $b): int => $carry + $b['copies'], 0);

// column: extract one field, optionally keyed by another
$yearByTitle = array_column($books, 'year', 'title');  // ["Solaris" => 1961, ...]

// map over several arrays in parallel; null callback zips them
print_r(array_map(null, ['a', 'b'], [1, 2]));         // [["a", 1], ["b", 2]]

echo implode('; ', $labels), " | copies: {$totalCopies} | ", json_encode($yearByTitle), "\n";

array_walk() calls a callback with each value by reference and its key, to modify an array in place; array_walk_recursive() descends into nested arrays. array_count_values() counts occurrences, array_sum() and array_product() aggregate numbers, and array_chunk(), array_pad() and array_reverse() reshape.

<?php

declare(strict_types=1);

$prices = ['Solaris' => 12.5, 'Kindred' => 9.99];
array_walk($prices, function (float &$price, string $title): void {
    $price = round($price * 1.21, 2);            // add VAT in place
});
print_r($prices);

print_r(array_count_values(['sf', 'poetry', 'sf', 'sf']));    // ["sf" => 3, "poetry" => 1]
print_r(array_chunk(['a', 'b', 'c', 'd', 'e'], 2));           // [["a","b"],["c","d"],["e"]]
var_dump(array_sum([1, 2, 3.5]));                              // float(6.5)

Set operations: diff and intersect

The diff and intersect families compare arrays by value, by key, or both, with optional user callbacks (u…) for the comparison:

Function Keeps the entries of the first array…

array_diff($a, $b, …)

whose values are in none of the others

array_diff_key($a, $b, …)

whose keys are in none of the others

array_diff_assoc($a, $b, …)

whose key/value pairs are in none of the others

array_intersect, array_intersect_key, array_intersect_assoc

that are present in all of the others

array_udiff, array_uintersect

comparing values with a callback

array_diff_ukey, array_intersect_ukey

comparing keys with a callback

array_udiff_assoc, array_udiff_uassoc, array_uintersect_assoc, …

combinations

Values are compared as strings ((string) $a === (string) $b) unless a callback is given. Keys of the first array are preserved.

<?php

declare(strict_types=1);

$catalogue = ['Solaris', 'Kindred', 'Dawn', 'Neuromancer'];
$onLoan = ['Kindred', 'Dawn'];

print_r(array_values(array_diff($catalogue, $onLoan)));        // ["Solaris", "Neuromancer"]
print_r(array_intersect($catalogue, $onLoan));                 // [1 => "Kindred", 2 => "Dawn"]

$book = ['title' => 'Kindred', 'year' => 1979, 'notes' => 'x', 'internal_id' => 7];
print_r(array_intersect_key($book, array_flip(['title', 'year'])));   // whitelist keys
print_r(array_diff_key($book, ['internal_id' => true]));               // blacklist keys

// Case-insensitive comparison with a callback
print_r(array_udiff(['Solaris', 'KINDRED'], ['kindred'], strcasecmp(...)));   // ["Solaris"]

array_intersect_key($input, array_flip($allowed)) is the idiom for keeping only expected form fields (a whitelist), used in Forms, Validation and Output Escaping.

Merging: array_merge, + and array_replace

Operation Integer keys String keys

array_merge($a, $b)

appended and renumbered

later arrays overwrite

$a + $b

kept; $b's only added if missing

$a wins

array_replace($a, $b)

$b overwrites by key (no renumbering)

$b overwrites

[...$a, ...$b]

appended and renumbered

later overwrite (8.1)

array_merge_recursive($a, $b)

appended

same keys are merged into arrays (rarely what you want)

array_replace_recursive($a, $b)

replaced recursively

replaced recursively (good for nested config)

<?php

declare(strict_types=1);

$a = [0 => 'Solaris', 1 => 'Kindred', 'format' => 'paperback'];
$b = [0 => 'Dawn', 'format' => 'ebook'];

print_r(array_merge($a, $b));     // [0 => Solaris, 1 => Kindred, format => ebook, 2 => Dawn]
print_r($a + $b);                 // [0 => Solaris, 1 => Kindred, format => paperback]
print_r(array_replace($a, $b));   // [0 => Dawn, 1 => Kindred, format => ebook]

$config = ['db' => ['host' => 'localhost', 'port' => 5432]];
$local = ['db' => ['host' => 'db.internal']];
print_r(array_replace_recursive($config, $local));   // host replaced, port kept

Slicing, splicing and uniqueness

<?php

declare(strict_types=1);

$titles = ['Solaris', 'Kindred', 'Dawn', 'Neuromancer', 'Ariel'];

print_r(array_slice($titles, 1, 2));              // ["Kindred", "Dawn"]       -- copy, renumbered
print_r(array_slice($titles, -2, null, true));    // [3 => "Neuromancer", 4 => "Ariel"] -- keys preserved

$removed = array_splice($titles, 1, 2, ['Lilith']);   // remove 2 at offset 1, insert 1 -- in place
print_r($removed);                                // ["Kindred", "Dawn"]
print_r($titles);                                 // ["Solaris", "Lilith", "Neuromancer", "Ariel"]

print_r(array_unique(['sf', 'SF', 'sf', 'poetry']));    // [0 => "sf", 1 => "SF", 3 => "poetry"]

array_unique() compares values as strings by default (SORT_STRING) and keeps the first key of each value.

Sorting

All sort functions sort in place and return true. They differ in what they sort by, the direction, and whether the key/value association is kept:

Function Sorts by Keeps keys? Order

sort / rsort

value

no (renumbered)

ascending / descending

asort / arsort

value

yes

ascending / descending

ksort / krsort

key

yes

ascending / descending

usort

value, with a callback

no

callback

uasort

value, with a callback

yes

callback

uksort

key, with a callback

yes

callback

natsort / natcasesort

value, natural order

yes

ascending

array_multisort

several arrays or columns at once

string keys kept

per column

The non-callback functions accept flags: SORT_REGULAR (default, standard comparison), SORT_NUMERIC, SORT_STRING, SORT_LOCALE_STRING, SORT_NATURAL, and SORT_FLAG_CASE combined with SORT_STRING or SORT_NATURAL. Since PHP 8.0 every sort is stable: elements that compare equal keep their original order, so sorting by one key and then by another works as expected.

<?php

declare(strict_types=1);

$books = [
    ['title' => 'Kindred', 'year' => 1979],
    ['title' => 'Solaris', 'year' => 1961],
    ['title' => 'Dawn', 'year' => 1987],
    ['title' => 'Wild Seed', 'year' => 1980],
];

// usort with the spaceship operator: by year descending
usort($books, fn (array $a, array $b): int => $b['year'] <=> $a['year']);
echo implode(', ', array_column($books, 'title')), "\n";      // Dawn, Wild Seed, Kindred, Solaris

// Multiple criteria: compare arrays of keys
usort($books, fn (array $a, array $b): int => [strlen($a['title']), $a['title']] <=> [strlen($b['title']), $b['title']]);
echo implode(', ', array_column($books, 'title')), "\n";      // Dawn, Kindred, Solaris, Wild Seed

$years = ['Kindred' => 1979, 'Solaris' => 1961];
asort($years);           // keeps title => year association
ksort($years);           // by title
print_r($years);

$files = ['img12.png', 'img10.png', 'IMG9.png'];
natcasesort($files);
print_r($files);                                              // IMG9, img10, img12 (keys kept)

sort($files, SORT_NATURAL | SORT_FLAG_CASE);
print_r($files);                                              // same order, renumbered

Source: Sorting Arrays, usort and sort (flags)

array_multisort

array_multisort() sorts several arrays together, or a table by several columns, like SQL’s ORDER BY a DESC, b ASC:

<?php

declare(strict_types=1);

$books = [
    ['title' => 'Kindred', 'year' => 1979],
    ['title' => 'Dawn', 'year' => 1987],
    ['title' => 'Imago', 'year' => 1989],
    ['title' => 'Adulthood Rites', 'year' => 1987],
];

$years = array_column($books, 'year');
$titles = array_column($books, 'title');
array_multisort($years, SORT_DESC, $titles, SORT_ASC, SORT_STRING, $books);

foreach ($books as $b) {
    echo "{$b['year']} {$b['title']}\n";
}
// 1989 Imago
// 1987 Adulthood Rites
// 1987 Dawn
// 1979 Kindred

Source: array_multisort

Iterating and modifying

foreach iterates over a snapshot of the array (thanks to copy-on-write): appending to the array inside the loop does not extend the loop. To modify elements in place, write back by key, or iterate by reference and unset() the reference afterwards (the pitfall is shown in Control Structures).

<?php

declare(strict_types=1);

$stock = ['Solaris' => 2, 'Kindred' => 0];

foreach ($stock as $title => $copies) {          // by value + write back by key
    if ($copies === 0) {
        $stock[$title] = 1;                      // reorder: one copy requested
    }
}

foreach ($stock as &$copies) {                   // by reference
    $copies *= 2;
}
unset($copies);                                  // always break the reference

print_r($stock);                                 // ["Solaris" => 4, "Kindred" => 2]

The old internal-pointer functions (current(), next(), reset(), end(), key(), prev()) still exist, and reset()/end() were the classic way to get the first/last element; array_first()/array_last() (8.5) and array_key_first()/array_key_last() (7.3) do this without side effects.

Source: foreach and array_key_first

Arrays versus objects

Arrays are flexible but untyped: nothing stops ['titel' => …]. For data with a fixed shape, readonly classes with promoted properties give type checks, IDE completion and immutability at a similar cost; use arrays for collections and for data on its way in and out (JSON, database rows). PHPDoc shapes (array{title: string, year: int}, list<Book>) let static analysers check arrays that remain. ArrayObject and ArrayIterator wrap arrays in objects when an API needs one; see SPL Data Structures.

Common pitfalls

  • array_filter() keeps keys: wrap in array_values() before json_encode() or index access.

  • in_array()/array_search() without true: loose comparison (in_array('abc', [0]) was true before 8.0; '1e1' still equals '10').

  • Numeric-string keys become ints: a map keyed by '1979' has int keys.

  • + versus array_merge(): union by key versus append.

  • array_search() returning 0: compare with !== false.

  • Sorting functions return true, not the sorted array: $sorted = sort($a) is a bug.

  • By-reference foreach without unset().

Where older books and tutorials are outdated

  • each() (Beak ch. 4, Nixon ch. 6) was removed in PHP 8.0: replace while (list($k, $v) = each($a)) with foreach.

  • list() still works and is not deprecated (not even in the upcoming 8.6), but [] (7.1) is the modern syntax.

  • Unstable sorting: before 8.0 equal elements could be reordered; books warn about it, but it no longer applies.

  • First/last element through reset()/end() — prefer array_first()/array_last() (8.5).

  • Hand-written loops for "find the first match", "any" and "all" are replaced by array_find(), array_any() and array_all() (8.4).

  • Spread with string keys was a TypeError before 8.1.

  • array_key_exists() on objects was removed in 8.0; use property_exists() or isset().