One type for lists and dictionaries

Most languages have separate types for a numbered list and for a set of named values: Python has lists and dictionaries, JavaScript has arrays and objects. PHP has one type for both. A PHP array is an ordered map: a collection of key–value pairs that remembers the order in which you added them. Internally it is a hash table, so looking up a key is fast however large the array is.

You write an array with square brackets. Without keys, PHP numbers the values 0, 1, 2 …, and the array behaves as a list. With key => value pairs, you choose the keys yourself.

<?php
$cities = ['Deventer', 'Zwolle', 'Apeldoorn'];   // a list
$meter = [                                        // an associative array
    'id' => 'MTR-100231',
    'city' => 'Deventer',
    'kwh' => 1542.7,
];

echo $cities[0], ' ', $meter['city'], "\n";
echo count($cities), ' ', count($meter), "\n";
var_dump(array_is_list($cities), array_is_list($meter));
print_r($cities);
Deventer Deventer
3 3
bool(true)
bool(false)
Array
(
    [0] => Deventer
    [1] => Zwolle
    [2] => Apeldoorn
)

count() gives the number of elements. print_r() prints an array in a readable form, var_dump() adds the type of every value, and array_is_list() (PHP 8.1 and later) tells whether the keys are exactly 0, 1, 2 … in order. The older array(…) notation means the same as […]; you will still see it in older code and in WordPress.

Lists: keys 0, 1, 2 …

$array[] = $value appends a value. PHP gives it the next free integer key: one higher than the largest integer key used so far. Keys do not have to be consecutive, and the order of the elements is the order of insertion, not the order of the keys.

<?php
$readings = [318.4, 296.0];
$readings[] = 301.5;            // append: next free integer key
$readings[10] = 290.0;          // keys need not be consecutive
$readings[] = 287.2;            // next key = highest integer key + 1

print_r($readings);
echo $readings[count($readings) - 1] ?? 'none', "\n";
echo array_key_last($readings), ' ', end($readings), "\n";
Array
(
    [0] => 318.4
    [1] => 296
    [2] => 301.5
    [10] => 290
    [11] => 287.2
)
none
11 287.2
The array readings as an ordered map: five slots in insertion order, holding key 0 with value 318.4, key 1 with 296, key 2 with 301.5, key 10 with 290 and key 11 with 287.2. The position of an element and its key are different things. position key value 1st 2nd 3rd 4th 5th 0 1 2 10 11 318.4 296 301.5 290 287.2 insertion order
The array $readings from the example. Each slot holds a key and a value; the order of the slots is the insertion order. count() is 5, but the last key is 11.

That is why $readings[count($readings) - 1] found nothing: count() is 5, and there is no key 4. To get the last element whatever the keys are, use array_key_last() for its key or end() for its value.

Associative arrays

An associative array is an array with keys you choose, usually strings: a meter's properties, the settings of a page, a row from a database. It plays the role of a dictionary in Python or a plain object in JavaScript. Assigning to a key adds it or overwrites its value, and a foreach loop visits the pairs in insertion order.

<?php
$tariffs = ['day' => 0.27, 'night' => 0.21];
$tariffs['weekend'] = 0.19;     // add or overwrite by key
$tariffs['day'] = 0.28;
$tariffs['peak'] = null;

foreach ($tariffs as $period => $price) {
    echo "$period: ", $price ?? 'not set', "\n";
}

var_dump(isset($tariffs['peak']));             // false: the value is null
var_dump(array_key_exists('peak', $tariffs));  // true: the key exists
echo $tariffs['holiday'] ?? 0.25, "\n";        // default for a missing key
day: 0.28
night: 0.21
weekend: 0.19
peak: not set
bool(false)
bool(true)
0.25

Reading a key that does not exist returns null and writes Warning: Undefined array key. The null coalescing operator ?? avoids that and supplies a default in one step. Be careful with isset(): it returns false both for a missing key and for a key whose value is null. When that difference matters, use array_key_exists().

Which keys are allowed

Keys can only be integers or strings. Anything else is converted when you use it as a key, and some strings are converted too. This array key casting follows fixed rules:

  • A string that is a plain decimal integer, such as '8' or '-5', becomes that integer. '08', '1.5' and ' 8' stay strings.
  • true becomes 1 and false becomes 0.
  • null becomes the empty string '' (deprecated since PHP 8.5: write '' instead).
  • A float is cut off to an integer: 1.7 becomes 1. Since PHP 8.1 that gives a Deprecated message when decimals are lost.
  • Arrays and objects cannot be keys: using one throws an error (Illegal offset type on PHP 8.2, Cannot access offset of type array on array since 8.3).
<?php
$keys = [
    '8'   => 'string "8" becomes int 8',
    '08'  => 'string "08" stays a string',
    true  => 'true becomes 1',
    null  => 'null becomes ""',
    '1.5' => 'string "1.5" stays a string',
];
var_dump(array_keys($keys));

$stock = [];
$stock['42'] = 'meter';      // stored under the integer 42
var_dump(isset($stock[42]), array_key_first($stock));
array(5) {
  [0]=>
  int(8)
  [1]=>
  string(2) "08"
  [2]=>
  int(1)
  [3]=>
  string(0) ""
  [4]=>
  string(3) "1.5"
}
bool(true)
int(42)

Two different-looking keys can therefore land on the same element: in one array, '8' and 8 are the same key, and so are true, 1 and 1.7. The later assignment wins. In practice this matters when keys come from outside, such as IDs from a form or a CSV file: $stock['42'] and $stock[42] are the same element, which is usually what you want. But meter numbers with leading zeros ('00042') stay strings and never match the integer 42.

Arrays are copied, not shared

Assigning an array to another variable, or passing it to a function, gives the receiver its own copy. Changing the copy does not change the original. PHP makes this cheap by copying lazily: the data is shared until one side writes to it (copy-on-write).

<?php
$original = ['Deventer', 'Zwolle'];
$copy = $original;       // arrays are copied on assignment
$copy[] = 'Apeldoorn';

echo count($original), ' ', count($copy), "\n";

function addCity(array $list): void
{
    $list[] = 'Enschede';    // changes the function's own copy
}
addCity($original);
echo count($original), "\n";
2 3
2

Adding, removing and searching

array_push() and array_pop() work at the end of a list, array_unshift() and array_shift() at the start (and renumber the integer keys). unset($array[$key]) removes one element by key and does not renumber: it leaves a gap.

<?php
$queue = ['INC-31877', 'INC-31880', 'INC-31902'];

array_push($queue, 'INC-31917');      // same as $queue[] = ...
$last = array_pop($queue);            // remove from the end
$first = array_shift($queue);         // remove from the start (reindexes)
array_unshift($queue, 'INC-31850');   // add at the start
echo "$first $last\n";
echo implode(' ', $queue), "\n";

unset($queue[1]);                     // remove by key: leaves a gap
print_r($queue);
print_r(array_values($queue));        // renumber 0, 1, 2 …
INC-31877 INC-31917
INC-31850 INC-31880 INC-31902
Array
(
    [0] => INC-31850
    [2] => INC-31902
)
Array
(
    [0] => INC-31850
    [1] => INC-31902
)

Searching

in_array() tells whether a value occurs, and array_search() returns its key, or false. Both compare loosely with == unless you pass true as the third argument for a strict === comparison.

<?php
$meterIds = ['100231', '100232', '0'];

var_dump(in_array('100232', $meterIds));        // found
var_dump(in_array(100232, $meterIds, true));    // strict: int is not string
var_dump(array_search('100232', $meterIds));    // the key of the match
var_dump(array_search('999999', $meterIds));    // not found
var_dump(in_array(0, $meterIds));               // loose: 0 == '0'
bool(true)
bool(false)
int(1)
bool(false)
bool(true)

The last line shows why strict mode is safer: the integer 0 matches the string '0'. Since array_search() can return the key 0, test its result with !== false, exactly as with strpos().

Combining arrays

array_merge() joins arrays. String keys from later arrays overwrite earlier ones; integer keys are renumbered, so lists are appended. The + operator is different: it keeps everything from the left array and only adds keys that are missing. That makes $chosen + $defaults handy for filling in default settings, and useless for joining two lists.

<?php
$defaults = ['tariff' => 'day', 'unit' => 'kWh', 'decimals' => 1];
$chosen = ['decimals' => 2, 'currency' => 'EUR'];

print_r(array_merge($defaults, $chosen));   // later values win
print_r($chosen + $defaults);               // left side wins, missing keys added

$north = ['Zwolle', 'Assen'];
$east = ['Deventer', 'Enschede'];
print_r(array_merge($north, $east));        // lists: renumbered
print_r($north + $east);                    // keys 0 and 1 already exist!
Array
(
    [tariff] => day
    [unit] => kWh
    [decimals] => 2
    [currency] => EUR
)
Array
(
    [decimals] => 2
    [currency] => EUR
    [tariff] => day
    [unit] => kWh
)
Array
(
    [0] => Zwolle
    [1] => Assen
    [2] => Deventer
    [3] => Enschede
)
Array
(
    [0] => Zwolle
    [1] => Assen
)

Sorting

PHP has a family of sort functions. All of them sort the array in place and return true, so $sorted = sort($values) is a mistake: $sorted becomes true. They differ in what they sort by and whether they keep the keys:

Function Sorts by Keys
sort() / rsort() Value, ascending / descending Discarded, renumbered 0, 1, 2 …
asort() / arsort() Value, ascending / descending Kept
ksort() / krsort() Key, ascending / descending Kept
usort() Your comparison function Discarded
uasort() / uksort() Your function, on values / keys Kept
<?php
$usage = ['Zwolle' => 1210, 'Deventer' => 1542, 'Assen' => 980];

$values = $usage;
sort($values);              // values sorted, keys thrown away
print_r($values);

asort($usage);              // by value, keys kept
echo implode(', ', array_keys($usage)), "\n";
arsort($usage);             // by value, descending
echo implode(', ', array_keys($usage)), "\n";
ksort($usage);              // by key
echo implode(', ', array_keys($usage)), "\n";

$result = sort($values);
var_dump($result);          // sort() returns true, not the array
Array
(
    [0] => 980
    [1] => 1210
    [2] => 1542
)
Assen, Zwolle, Deventer
Deventer, Zwolle, Assen
Assen, Deventer, Zwolle
bool(true)

Sorting with your own rule

usort() takes a comparison function that receives two elements and returns a negative number, 0 or a positive number. The spaceship operator <=> returns exactly that. Comparing two small arrays with it compares them element by element, which gives a sort on several fields in one line:

<?php
$tickets = [
    ['id' => 'INC-31902', 'priority' => 2, 'opened' => '2026-10-02 08:10'],
    ['id' => 'INC-31877', 'priority' => 3, 'opened' => '2026-10-01 14:30'],
    ['id' => 'INC-31917', 'priority' => 2, 'opened' => '2026-10-01 22:05'],
];

// Priority first (1 = most urgent), then oldest first
usort($tickets, function (array $a, array $b): int {
    return [$a['priority'], $a['opened']] <=> [$b['priority'], $b['opened']];
});

foreach ($tickets as $t) {
    echo "P{$t['priority']} {$t['opened']} {$t['id']}\n";
}
P2 2026-10-01 22:05 INC-31917
P2 2026-10-02 08:10 INC-31902
P3 2026-10-01 14:30 INC-31877

Since PHP 8.0 sorting is stable: elements that compare as equal keep their original order. On older versions their order was not guaranteed.

Numbers, strings and natural order

By default the sort functions compare values with PHP's normal comparison rules, so numeric strings are compared as numbers. A flag changes that: SORT_STRING compares as text, and natsort() uses the natural order people expect for file names.

<?php
$codes = ['10', '9', '2'];
sort($codes);                     // numeric strings compare as numbers
echo implode(' ', $codes), "\n";
sort($codes, SORT_STRING);        // compare as text, character by character
echo implode(' ', $codes), "\n";

$files = ['meter10.csv', 'meter9.csv', 'meter1.csv'];
natsort($files);                  // natural order (keeps keys)
echo implode(' ', $files), "\n";
2 9 10
10 2 9
meter1.csv meter9.csv meter10.csv

array_map, array_filter and friends

Instead of writing a loop for every transformation, you can hand a function to one of the array functions. The short arrow functions (fn($v) => …) are made for this.

  • array_map() calls a function on every element and returns a new array with the results. With one input array, the keys are kept.
  • array_filter() returns the elements for which a function returns a truthy value. Without a function, it drops every falsy element (0, '', '0', null, false, []).
  • array_reduce() combines all elements into one value, step by step; for the common cases array_sum(), max() and min() are shorter.
<?php
$kwh = ['Zwolle' => 1210, 'Deventer' => 1542, 'Assen' => 980];

$costs = array_map(fn($v) => round($v * 0.27, 2), $kwh);
print_r($costs);                          // keys kept

$high = array_filter($kwh, fn($v) => $v > 1000);
print_r($high);

$total = array_reduce($kwh, fn($carry, $v) => $carry + $v, 0);
echo $total, ' ', array_sum($kwh), ' ', max($kwh), "\n";
Array
(
    [Zwolle] => 326.7
    [Deventer] => 416.34
    [Assen] => 264.6
)
Array
(
    [Zwolle] => 1210
    [Deventer] => 1542
)
3732 3732 1542

array_filter keeps the keys

Because a PHP array is a map, array_filter() keeps each surviving element under its original key. Filtering a list therefore leaves gaps, and code that expects keys 0, 1, 2 breaks quietly. A list with gaps also turns into a JSON object instead of a JSON array when you encode it. Wrap the result in array_values() when you need a clean list. The flags ARRAY_FILTER_USE_KEY and ARRAY_FILTER_USE_BOTH pass the key, or the key and the value, to your function.

<?php
$readings = [318.4, 0, 296.0, null, 301.5];

$valid = array_filter($readings);         // no callback: drops falsy values
print_r($valid);                          // note the keys: 0, 2, 4
echo $valid[1] ?? 'no key 1', "\n";
print_r(array_values($valid));            // renumbered from 0

// Filter on keys instead of values
$tariffs = ['day' => 0.27, 'night' => 0.21, 'day_peak' => 0.34];
print_r(array_filter($tariffs, fn($k) => str_starts_with($k, 'day'), ARRAY_FILTER_USE_KEY));
Array
(
    [0] => 318.4
    [2] => 296
    [4] => 301.5
)
no key 1
Array
(
    [0] => 318.4
    [1] => 296
    [2] => 301.5
)
Array
(
    [day] => 0.27
    [day_peak] => 0.34
)

Tables of rows: array_column

Data from a database or a CSV file usually arrives as a list of associative arrays, one per row. array_column() pulls one column out of such a table, optionally keyed by another column. Other useful helpers are array_keys(), array_values(), array_combine() (keys from one array, values from another), array_unique(), array_slice() and array_count_values().

<?php
$customers = [
    ['id' => 501, 'name' => 'Lucía Romero', 'city' => 'Deventer'],
    ['id' => 502, 'name' => 'Joost Bakker', 'city' => 'Zwolle'],
    ['id' => 503, 'name' => 'Mei Chen', 'city' => 'Deventer'],
];

print_r(array_column($customers, 'name'));
print_r(array_column($customers, 'city', 'id'));    // id => city
print_r(array_count_values(array_column($customers, 'city')));
Array
(
    [0] => Lucía Romero
    [1] => Joost Bakker
    [2] => Mei Chen
)
Array
(
    [501] => Deventer
    [502] => Zwolle
    [503] => Deventer
)
Array
(
    [Deventer] => 2
    [Zwolle] => 1
)

Destructuring and spread

Destructuring assignment copies elements of an array into separate variables in one statement. PHP writes it with square brackets on the left of = (or with the older list(…)). You can destructure by position, skip elements with empty slots, destructure by key, swap two variables, and destructure in the head of a foreach loop.

<?php
$reading = ['MTR-100231', '2026-10-01', 1542.7];
[$meter, $date, $kwh] = $reading;           // by position
echo "$meter $date $kwh\n";

[, , $onlyKwh] = $reading;                  // skip elements
echo $onlyKwh, "\n";

['name' => $name, 'city' => $city] = ['id' => 502, 'name' => 'Joost Bakker', 'city' => 'Zwolle'];
echo "$name lives in $city\n";              // by key

$a = 'night'; $b = 'day';
[$a, $b] = [$b, $a];                        // swap
echo "$a $b\n";

$rows = [['Zwolle', 1210], ['Assen', 980]];
foreach ($rows as [$place, $usage]) {       // in a foreach
    echo "$place: $usage\n";
}
MTR-100231 2026-10-01 1542.7
1542.7
Joost Bakker lives in Zwolle
day night
Zwolle: 1210
Assen: 980

The spread syntax ... unpacks an array into a new array literal. Integer keys are renumbered; since PHP 8.1, string keys are allowed too, and a later key overwrites an earlier one, as with array_merge(). The same ... also unpacks an array into the arguments of a function call (see Functions).

<?php
$north = ['Zwolle', 'Assen'];
$east = ['Deventer', 'Enschede'];
$all = ['Utrecht', ...$north, ...$east];     // spread into a new list
echo implode(', ', $all), "\n";

$defaults = ['unit' => 'kWh', 'decimals' => 1];
$settings = [...$defaults, 'decimals' => 2]; // string keys: later wins
print_r($settings);
Utrecht, Zwolle, Assen, Deventer, Enschede
Array
(
    [unit] => kWh
    [decimals] => 2
)