PHP array functions package common transformations, filters, aggregations, merges, and sorts into standard APIs. Choose among them by checking how each function treats keys and whether it changes its input.
array_filter() preserves old keys, array_merge() renumbers integer keys, and usort() modifies its array in place while discarding every original key.
Decide whether the result is a list or a map, give array_reduce() a correctly typed initial value, and return a negative integer, 0, or a positive integer from sort comparators.
What it is and why it exists
A PHP array acts as both a list and an ordered map. Array functions perform standard operations on that container: array_map() transforms values, array_filter() selects elements, array_reduce() accumulates state, array_merge() combines inputs, array_keys() extracts keys, and usort() applies a custom order.
These functions replace repeated traversal code, not every loop. Their names state the operation and PHP handles iteration, but each API still defines its own rules for keys, empty values, and mutation. If you guess from map, filter, or reduce in another language, you can get valid PHP that quietly changes the shape of your data.
array_map(), array_filter(), array_reduce(), and usort() accept a callback . A callback is a function passed into another function, which invokes it while processing data. Arrow functions suit one-expression callbacks; an ordinary anonymous function is usually clearer when validation or state updates take several steps.
The result contract matters more than the syntax:
| Function | Main operation | Key behavior | Changes input |
|---|---|---|---|
array_map() | Transform every value | Preserves keys with one input array | No |
array_filter() | Retain matching elements | Preserves keys and may leave gaps | No |
array_reduce() | Fold all values into one result | Does not pass keys to the callback | No |
array_merge() | Combine arrays in order | Overwrites string keys; renumbers integer keys | No |
array_keys() | Get all keys or keys for matching values | Returns a list starting at 0 | No |
usort() | Sort values with a comparator | Discards original keys and renumbers | Yes |
These functions fit data that is already in memory and has a clear processing boundary. A foreach loop or generator is often more direct when you need early exit, several pieces of state, streaming input, or careful key handling. Array functions don’t guarantee readability; three or four nested callbacks are already beyond their comfortable range.
How it works
All six functions traverse arrays, but they produce different kinds of results. Track values, keys, callback arguments, return values, and mutation separately instead of looking only at the final element values.
array_map() transforms values
array_map($callback, $array) calls the callback once for each value and builds a new array from its return values. With exactly one input array, the new array preserves the original keys. With two or more arrays, PHP combines values by position and gives the result sequential integer keys.
If the input arrays have different lengths, missing positions in shorter arrays are filled with null. The callback’s parameter count should match the number of arrays; PHP throws ArgumentCountError when the callback requires more arguments than the function supplies. If a transformation needs keys, combine array_keys() and array_values() explicitly or use foreach.
array_filter() selects elements
array_filter($array, $callback) retains elements whose callback result converts to true. The default mode passes only the value, ARRAY_FILTER_USE_KEY passes only the key, and ARRAY_FILTER_USE_BOTH passes the value followed by the key. Every mode preserves input keys in the returned array.
Without a callback, array_filter() removes values PHP considers empty, including false, null, integer 0, string "0", the empty string, and an empty array. That shortcut is safe only when all of those values are invalid in your domain. A zero quantity, form value, or status code can be legitimate, so write an explicit predicate for such data.
array_reduce() accumulates a result
array_reduce($array, $callback, $initial) starts with $initial as the accumulator, then passes the accumulator and each value to the callback in array iteration order. Each callback result becomes the next accumulator. The last result is the function result, and keys are never passed to the callback.
PHP differs from JavaScript here: omitting $initial doesn’t use the first element as the starting value. PHP starts with null and still processes the first element. An empty array without an initial value also returns null. Use 0 for a sum, '' for string concatenation, or [] when building a map; the initial value defines both the empty result and accumulator type.
array_merge() combines arrays
array_merge(...$arrays) processes its inputs from left to right. When the same string key occurs more than once, the value on the right overwrites the value on the left. Values with integer keys are appended instead of overwritten, then numbered from 0 in the result. Calling the function without arguments returns an empty array.
The array union operator + uses different rules. It retains every key already present on the left, ignores a right-side value with the same key, and doesn’t renumber integer keys. Use array_merge() to concatenate lists or let later configuration override earlier configuration; use + to preserve left-side keys and values. Neither operation discovers the right deep-merge policy for your domain.
array_keys() extracts keys
array_keys($array) returns every integer and string key in iteration order. With a second argument, it returns only keys whose values match $filterValue; a third argument of true selects ===, while the default uses loose comparison.
Use array_key_exists() to check whether one key exists instead of allocating the full key list and then calling in_array(). isset($array[$key]) asks a different question: it returns false when the key exists but its value is null. Pick the function according to whether you mean “the key exists” or “the value isn’t null.”
usort() defines an order
usort(&$array, $callback) rearranges values in place and replaces every key with a sequential integer. A comparator returns a negative integer, 0, or a positive integer when the left value belongs before, equal to, or after the right value. The <=> operator has exactly that contract.
Since PHP 8, elements that compare equal retain their original relative order, so usort() is a stable sort . Stability cannot repair an inconsistent comparator: if the callback contradicts itself or returns only a Boolean, the order is still unreliable. Since PHP 8.2, the function itself has return type true; the useful result is in the array it modified.
Examples
The four examples cover transformation and filtering, reduction, merging, and sorting. Every output below was produced with the local PHP 8.3.33 runtime.
Filter orders and transform display values
The orders use business identifiers as keys. Filtering and mapping both preserve those keys, so the first JSON encoding produces an object. Reindex with array_values() only when a JSON list is actually required.
<?php
$orders = [
104 => ['customer' => 'Mina', 'paid' => true, 'cents' => 1599],
207 => ['customer' => 'Omar', 'paid' => false, 'cents' => 2400],
310 => ['customer' => 'Liu', 'paid' => true, 'cents' => 800],
];
$paid = array_filter(
$orders,
fn(array $order): bool => $order['paid'],
);
$labels = array_map(
fn(array $order): string => sprintf('%s:%.2f', $order['customer'], $order['cents'] / 100),
$paid,
);
echo json_encode($labels, JSON_UNESCAPED_UNICODE), PHP_EOL;
echo json_encode(array_values($labels), JSON_UNESCAPED_UNICODE), PHP_EOL;{"104":"Mina:15.99","310":"Liu:8.00"}
["Mina:15.99","Liu:8.00"]Only keys 104 and 310 remain in $paid, and $labels keeps both keys. PHP’s json_encode() emits a JSON array only for arrays with sequential keys starting at 0, which is why the first line has object shape.
The second line deliberately calls array_values() at the serialization boundary. That placement is easy to review: processing retains the order identifiers, and only the external list format discards them.
Reduce order amounts by region
array_reduce() can return an array rather than a number. The empty initial array handles an empty order list and declares that the accumulator is a map from region to amount.
<?php
$orders = [
['region' => 'east', 'cents' => 1200],
['region' => 'west', 'cents' => 750],
['region' => 'east', 'cents' => 750],
];
$totals = array_reduce(
$orders,
function (array $carry, array $order): array {
$region = $order['region'];
$carry[$region] = ($carry[$region] ?? 0) + $order['cents'];
return $carry;
},
[],
);
foreach (array_keys($totals) as $region) {
printf("%s=%d\n", $region, $totals[$region]);
}east=1950
west=750The callback returns the updated $carry on every iteration. If it changes a local variable but forgets to return it, the next iteration receives null; the declared array type exposes that mistake quickly.
The first appearance of each region determines iteration order in $totals. array_keys() extracts the region-name list, while the loop still reads amounts from the original map. It doesn’t copy the amounts into the key list.
Distinguish merge from union
The user settings appear on the right of array_merge(), so they override the defaults. The nested features array is replaced as one value because this function handles only top-level keys.
<?php
$defaults = [
'theme' => 'light',
'page_size' => 20,
'features' => ['search'],
];
$user = ['theme' => 'dark', 'features' => ['export']];
$settings = array_merge($defaults, $user);
$states = [10 => 'draft', 20 => 'review']
+ [20 => 'ignored', 30 => 'published'];
echo json_encode($settings), PHP_EOL;
echo json_encode($states), PHP_EOL;{"theme":"dark","page_size":20,"features":["export"]}
{"10":"draft","20":"review","30":"published"}The second expression uses +, so the left-side review value wins at key 20, and integer keys 10, 20, and 30 remain intact. Those keys aren’t sequential list indexes, so the JSON result is still an object.
Swapping the two operations would still produce valid code, but it would implement a different data policy. Configuration merge code should make override direction clear through names or tests rather than forcing readers to infer it from argument position.
Make a copy, then sort stably
usort() modifies its argument, so the code first copies $tasks. The two priority-1 tasks compare equal and retain their input order on PHP 8.3.
<?php
$tasks = [
['name' => 'write', 'priority' => 2],
['name' => 'test', 'priority' => 1],
['name' => 'deploy', 'priority' => 1],
];
$sorted = $tasks;
usort(
$sorted,
fn(array $left, array $right): int =>
$left['priority'] <=> $right['priority'],
);
echo implode(',', array_column($sorted, 'name')), PHP_EOL;
echo implode(',', array_column($tasks, 'name')), PHP_EOL;test,deploy,write
write,test,deployThe first line proves both ascending priority order and preservation of the test, deploy tie. The second line shows that the original array stayed in its original order because the PHP array passed to usort() was a copy.
If the domain requires names to break equal-priority ties, put that second comparison in the callback. A stable sort preserves input order for equal items; it doesn’t invent a business tie-breaker.
Pitfalls
The most dangerous array-function errors don’t throw. They quietly change keys or classify valid data as empty. Tests should assert values, key order, and whether the input changed.
Treating a filtered map as a list
Fix: decide whether the result is a business map or a list. Call array_values() only when the consumer requires sequential indexes. If keys carry order numbers or database IDs, retain them and use a serialization shape that clearly represents a map.
Deleting zero with the default filter
Fix: express the domain condition in a callback, such as fn($value) => $value !== null. Don’t use array_filter($values) as input validation because PHP’s set of empty values is usually broader than your domain’s set of invalid values.
Assuming reduction starts with the first item
Fix: give possibly empty input an identity value of the right type. Omitting the initial value for multiplication lets null enter the calculation rather than starting from the first item; use 1 to state the multiplicative identity.
Mistaking a shallow merge for a policy
Fix: decide whether each configuration node should replace, append, or recurse before choosing an API or writing a small domain function. Test a repeated string key, an integer key, and a nested array. The word “recursive” doesn’t mean “correct for configuration overrides.”
Reversing callback arguments or return types
Fix: give callback parameters domain names and types, then verify comparator signs with lesser, equal, and greater input pairs. A Boolean converted to an integer provides only 0 and 1, so it can’t express both “before” and “equal.”
Forgetting that sorting mutates
Fix: assign the array to a new variable first when you need both orders. Consider uasort() when key-value association must survive. Don’t assign the true return value from usort() to $sorted; the sorted data remains in the argument.
Edge semantics
When these six functions are composed, boundary behavior deserves more attention than the ordinary path through one function. Check the number of inputs, callback declarations, PHP key conversion, and sort ties.
Mapping several arrays changes the key contract
The one-array form of array_map() preserves keys, while the multiple-array form aligns by position and returns sequential integer keys. It doesn’t join on keys even when both inputs use the same string keys; only their iteration positions matter.
Shorter inputs are padded with null, which can make a non-nullable callback throw TypeError. If you need to join two maps by a business key, iterate over keys explicitly and handle missing entries instead of treating multi-array array_map() as a join.
Callbacks see only what the API promises
array_reduce() passes the accumulator and value, but not the current key. Reducing over array_keys() can work, though the callback must then capture the source array. Once that indirection grows complicated, foreach ($array as $key => $value) is clearer.
The array_filter() mode changes callback arguments, not the result’s key-preservation rule. With ARRAY_FILTER_USE_KEY, the callback cannot see the value. With ARRAY_FILTER_USE_BOTH, the value comes first and the key second; type declarations expose a reversal sooner.
PHP keys determine JSON shape
PHP has no separate runtime list type; a list is an array with keys 0 through n - 1. Filtering leaves gaps and union retains non-sequential integer keys, while array_merge() and usort() renumber. The same value set can therefore produce different JSON shapes.
Don’t discard keys blindly just to get a JSON array. First decide whether they are business identifiers. If they are, make each key an explicit object field or deliberately return a JSON object. Serialization tests should assert the full string or decoded structure, not merely the presence of values.
Stable sorting still needs a complete rule
PHP 8 stability constrains only elements for which the comparator returns 0. If input comes from a source with nondeterministic order, preserving relative input order doesn’t create repeatable business output. Add an explicit secondary field when the output must be deterministic across runs.
A comparator must also be consistent: its answer for one pair cannot depend on changing external state, and compare(a, b) should have the opposite sign from compare(b, a). Don’t make network calls, modify compared elements, or read a changing clock inside it.
Strictness in value lookup
array_keys($array, $value) uses loose comparison by default, so values of different types can match. Numeric values and numeric strings commonly mix after form, JSON, or database input. Pass true as the third argument when the type itself carries meaning.
A strict value search does not make keys “strictly typed.” PHP array keys can only be integers or strings, and some valid decimal integer strings are converted to integer keys when inserted. Choosing value-comparison semantics and understanding key normalization are separate tasks.
Choosing array functions or an explicit loop
A single transformation or filter usually fits a standard function, and reduction can express a natural accumulator. An explicit loop is often shorter when a pipeline traverses the same large array several times, must stop at the first match, or needs both keys and values while handling errors.
Judge readability by whether the data policy is obvious. A well-named foreach isn’t inferior to nested array_map(array_filter(...)). Choose the form that makes keys, empty input, and mutation boundaries easiest to review.
How to review composed operations
A pipeline can connect several individually correct functions and still produce the wrong result. Don’t verify only the final values. Write down the intermediate key set, type, and ownership after each step.
A useful method is to give every step a minimal counterexample instead of testing only tidy sequential lists. Missing keys, valid zero values, and duplicate keys tend to expose a mistaken default immediately.
Reindex as late as possible
array_values() permanently discards original keys, so later steps can’t recover an order identifier or source position. Keep reindexing at the output boundary that truly requires a list, leaving more diagnostic information available beforehand.
If later code must use positional access, put the shape in the variable name, such as $paidOrderList. That name says the keys no longer carry business meaning and discourages later code from treating a position as an ID.
Define empty input explicitly
Every aggregate needs an explainable empty result: a money total is 0, a grouping result is [], and concatenated text may be ''. Use that value as the array_reduce() initial argument so ordinary and empty input share a return type.
If no sensible empty result exists, validate the input and throw a domain exception. Don’t let an omitted initial argument quietly choose null for you. That value is an API default, not a business decision.
Separate transformation from validation
array_map() suits converting each valid input, while array_filter() suits selecting with a defined predicate. Collapsing parse failures, missing fields, and authorization failures to null, then filtering without a callback, loses the failure reason and can delete legitimate zeros.
When every failure must be reported, use an explicit loop to collect results and errors or have the callback throw an exception with context. The caller should be able to distinguish “no match” from “input could not be processed.”
Name complex stages first
Nested array functions execute from the inside out, while people tend to read the code from the outside in. Naming intermediate values $eligibleOrders or $totalsByRegion exposes the shape and policy of each stage.
Named stages are also easier to inspect in a debugger and test separately. If one callback captures several variables and contains several branches, extracting a named function or writing an ordinary loop is usually less work.
Protect boundaries with shape assertions
For list results, assert array_is_list($result) as well as element order. For maps, assert exact keys and their types so a refactor cannot quietly introduce reindexing through array_values() or array_merge().
A sort test should include at least one equal pair and keep the pre-sort array for comparison. A merge test should include a string-key collision, an integer-key collision, and a nested array. Those cases directly expose a wrong function choice.
Further reading
5 questions · 1 predict-the-output · 1 spot-the-bug