Arrays
An array is a value that can hold many values. In PHP, the same array type is used for simple lists, key-value maps, and nested records. That flexibility is useful, but it also means you must be clear about the shape of the data.
Arrays appear everywhere in PHP: decoded JSON, request data, configuration, database rows, form errors, route parameters, grouped records, and small scripts. A junior developer needs to do more than create an array. You need to read values safely, check whether keys exist, add and remove items, loop over records, transform values, and sort data without losing track of the keys.
Lists And Keyed Arrays
A list uses numeric keys. PHP assigns the keys automatically when you append values.
<?php
$tags = ['php', 'beginner'];
$tags[] = 'arrays';
print_r($tags);
// Prints:
// Array
// (
// [0] => php
// [1] => beginner
// [2] => arrays
// )
A keyed array uses meaningful keys.
<?php
$product = [
'sku' => 'BOOK-001',
'name' => 'PHP Workbook',
'price_cents' => 2499,
'in_stock' => true,
];
echo $product['name'] . " costs {$product['price_cents']} cents\n";
// Prints:
// PHP Workbook costs 2499 cents
Use keyed arrays when each value has a name. Use lists when order matters and each item has the same meaning.
Reading Optional Keys
Reading a missing key causes a warning. Decide what should happen when a key is missing.
<?php
$product = [
'name' => 'PHP Workbook',
'price_cents' => 2499,
];
$currency = $product['currency'] ?? 'GBP';
echo $currency . "\n";
// Prints:
// GBP
Use ?? when a simple fallback is correct. Use array_key_exists() when you need to distinguish "the key is missing" from "the key exists but contains null".
<?php
$product = ['discount_cents' => null];
var_dump(array_key_exists('discount_cents', $product));
var_dump(isset($product['discount_cents']));
// Prints:
// bool(true)
// bool(false)
isset() is false for null. array_key_exists() only asks whether the key exists.
Nested Arrays
Nested arrays let you represent records inside records.
<?php
$order = [
'id' => 1001,
'customer' => [
'name' => 'Amo',
'email' => 'amo@example.com',
],
'items' => [
['sku' => 'BOOK-001', 'quantity' => 2],
['sku' => 'PEN-002', 'quantity' => 1],
],
];
echo $order['customer']['email'] . "\n";
echo $order['items'][0]['sku'] . "\n";
// Prints:
// amo@example.com
// BOOK-001
Nested arrays are common after decoding JSON or fetching rows grouped by another value. The risk is assuming every nested key exists. Check at the boundary before relying on external data.
Adding, Updating, Removing, And Reindexing
Arrays are often changed in small steps.
<?php
$tags = ['php', 'arrays'];
$tags[] = 'practice';
$tags[1] = 'array-basics';
unset($tags[0]);
print_r($tags);
print_r(array_values($tags));
// Prints:
// Array
// (
// [1] => array-basics
// [2] => practice
// )
// Array
// (
// [0] => array-basics
// [1] => practice
// )
unset() removes an item but does not renumber numeric keys. Use array_values() when you need a clean zero-based list afterward.
Checking Membership And Searching
Use in_array() to check whether a value appears in an array. Pass true as the third argument for strict comparison.
<?php
$allowedStatuses = ['pending', 'paid', 'failed'];
var_dump(in_array('paid', $allowedStatuses, true));
var_dump(in_array('0', [0, 1, 2], true));
// Prints:
// bool(true)
// bool(false)
Use array_search() when you need the key where a value was found.
<?php
$statuses = ['pending', 'paid', 'failed'];
$position = array_search('paid', $statuses, true);
var_dump($position);
// Prints:
// int(1)
Be careful: array_search() can return 0, which is a valid key but falsey. Compare the result with !== false.
Filtering, Mapping, And Walking
array_filter() keeps items that pass a check.
<?php
$products = [
['name' => 'Notebook', 'in_stock' => true],
['name' => 'Pen', 'in_stock' => false],
['name' => 'Mug', 'in_stock' => true],
];
$inStock = array_filter($products, fn (array $product): bool => $product['in_stock']);
print_r(array_column($inStock, 'name'));
// Prints:
// Array
// (
// [0] => Notebook
// [2] => Mug
// )
array_filter() preserves keys. Use array_values() afterward if you need a reindexed list.
array_map() transforms each item and returns a new array.
<?php
$prices = [1299, 2500, 999];
$labels = array_map(
fn (int $cents): string => '£' . number_format($cents / 100, 2),
$prices
);
print_r($labels);
// Prints:
// Array
// (
// [0] => £12.99
// [1] => £25.00
// [2] => £9.99
// )
array_walk() is useful when you want to visit each item, often to modify values by reference.
<?php
$names = [' ada ', ' grace '];
array_walk($names, function (string &$name): void {
$name = trim($name);
});
print_r($names);
// Prints:
// Array
// (
// [0] => ada
// [1] => grace
// )
Use by-reference callbacks carefully. If returning a new array is clearer, prefer array_map().
Pulling Columns And Indexing Records
array_column() pulls one key from each nested array.
<?php
$products = [
['sku' => 'BOOK-001', 'name' => 'PHP Workbook'],
['sku' => 'PEN-002', 'name' => 'Pen'],
];
$namesBySku = array_column($products, 'name', 'sku');
print_r($namesBySku);
// Prints:
// Array
// (
// [BOOK-001] => PHP Workbook
// [PEN-002] => Pen
// )
This pattern is common when you need fast lookup by ID, SKU, email, or another unique value.
Sorting
Use sort() for simple lists when you do not care about preserving keys. Use asort() when keys matter. Use usort() for custom comparison logic.
<?php
$products = [
['name' => 'Notebook', 'price_cents' => 1299],
['name' => 'Mug', 'price_cents' => 899],
['name' => 'Backpack', 'price_cents' => 4200],
];
usort($products, fn (array $a, array $b): int => $a['price_cents'] <=> $b['price_cents']);
print_r(array_column($products, 'name'));
// Prints:
// Array
// (
// [0] => Mug
// [1] => Notebook
// [2] => Backpack
// )
The spaceship operator <=> returns the comparison result that usort() expects.
Merging Settings
Arrays are often used for configuration. Later values can override defaults.
<?php
$defaults = [
'currency' => 'GBP',
'items_per_page' => 20,
];
$userSettings = [
'items_per_page' => 50,
];
$settings = array_merge($defaults, $userSettings);
print_r($settings);
// Prints:
// Array
// (
// [currency] => GBP
// [items_per_page] => 50
// )
For string keys, array_merge() lets later arrays override earlier arrays. Numeric keys behave differently because lists are reindexed.
Count Values And Check List Shape
count() returns the number of top-level elements:
<?php
$tags = ['php', 'arrays', 'practice'];
echo count($tags), PHP_EOL;
A numeric-keyed array is not necessarily a list. PHP defines a list as an array whose keys are consecutive integers from 0:
<?php
$tags = ['php', 'arrays', 'practice'];
unset($tags[1]);
var_dump(array_is_list($tags));
var_dump(array_is_list(array_values($tags)));
Output:
bool(false)
bool(true)
This distinction matters when encoding JSON. A zero-based PHP list becomes a JSON array, while an array with gaps can become a JSON object. Reindex filtered or partially removed lists when the receiving system expects an array shape.
Understand Array Keys
PHP array keys are integers or strings. Some other key values are converted: a numeric string such as '8' becomes integer key 8, while a boolean becomes 1 or 0. Avoid relying on implicit key conversion because it can overwrite an existing entry unexpectedly.
Assigning the same key twice keeps only the later value:
<?php
$prices = [
'BOOK-001' => 1999,
'BOOK-001' => 2499,
];
print_r($prices);
The array contains one key with value 2499. When building a lookup with array_column(), duplicate index values similarly cause later records to replace earlier records. Confirm that IDs, SKUs, or emails are unique before treating them as lookup keys.
Compare Merge With Array Union
For string-keyed settings, array_merge() gives later arrays precedence:
$settings = array_merge($defaults, $userSettings);
The union operator + uses the opposite ownership rule: it keeps values from the left array when a key already exists.
<?php
$defaults = ['currency' => 'GBP', 'page_size' => 20];
$overrides = ['page_size' => 50];
print_r($defaults + $overrides);
print_r(array_merge($defaults, $overrides));
The union keeps page_size as 20; the merge changes it to 50. Neither operation is universally correct. Choose based on which side should win, and add an exercise or test for an overlapping key.
Numeric keys need extra care. array_merge() appends and renumbers numeric entries. Array union preserves left-side numeric keys and ignores conflicting right-side keys. If the data is a list, appending may be right. If numeric keys are meaningful identifiers, neither casual merge behavior may match the intended policy.
Transform Without Losing The Original Accidentally
Functions such as array_map(), array_filter(), array_values(), array_column(), and array_merge() return arrays. They do not replace the source variable unless you assign the result:
<?php
$rawNames = [' ada ', ' grace '];
$cleanNames = array_map('trim', $rawNames);
print_r($rawNames);
print_r($cleanNames);
Keeping raw and cleaned arrays separate can make a data-cleaning pipeline easier to inspect. Reassign deliberately when the original is no longer needed.
By contrast, sorting functions such as sort(), asort(), and usort() modify the supplied array and return a boolean success value. Do not write $sorted = sort($values) expecting $sorted to contain the array. Copy first when both orders are needed.
Common Mistakes
Do not assume a key exists just because one example has it. External data can be missing, null, duplicated, wrongly typed, or shaped differently.
Do not use loose membership checks for security or status decisions. Use strict comparison with in_array() and array_search().
Do not forget that some array functions preserve keys and others reindex. If JSON output needs a list, reindex with array_values() after filtering.
What You Should Be Able To Do
After this lesson, you should be able to:
- create lists, keyed arrays, and nested arrays;
- read required and optional keys deliberately;
- add, update, remove, and reindex values;
- check membership and search by value;
- filter, map, and walk arrays;
- pull columns and build lookup arrays;
- sort records by a field;
- merge default settings with overrides.
Practice
Task: Product Summary
Task
Create this keyed product record:
$product = [
'sku' => 'BOOK-001',
'name' => 'PHP Workbook',
'price_cents' => 2499,
'in_stock' => true,
'discount_cents' => null,
];
Print the summary BOOK-001: PHP Workbook costs 2499 cents, then print Availability: in stock using a ternary expression.
Read an optional currency key with fallback GBP. Finally, use both array_key_exists() and isset() to inspect discount_cents.
Expected final lines:
Currency: GBP
Discount key exists: yes
Discount is set: no
Hints
??is appropriate for the currency fallback.array_key_exists()sees a key whose value isnull.isset()returns false for that same value.
Show solution
Solution
<?php
$product = [
'sku' => 'BOOK-001',
'name' => 'PHP Workbook',
'price_cents' => 2499,
'in_stock' => true,
'discount_cents' => null,
];
$currency = $product['currency'] ?? 'GBP';
$availability = $product['in_stock'] ? 'in stock' : 'out of stock';
$discountKeyExists = array_key_exists('discount_cents', $product) ? 'yes' : 'no';
$discountIsSet = isset($product['discount_cents']) ? 'yes' : 'no';
echo "{$product['sku']}: {$product['name']} costs {$product['price_cents']} cents\n";
echo "Availability: $availability\n";
echo "Currency: $currency\n";
echo "Discount key exists: $discountKeyExists\n";
echo "Discount is set: $discountIsSet\n";
Explanation
Required fields are read directly from the known record shape. Currency uses a documented fallback because the key is optional.
The discount key is present, so array_key_exists() returns true. Its value is null, so isset() returns false. Those checks answer different questions and should not be substituted blindly.
Task: Predict Array Keys
Task
Before running the code, predict both printed arrays and both booleans:
<?php
$tags = ['php', 'arrays', 'practice'];
unset($tags[1]);
var_dump(array_is_list($tags));
print_r($tags);
$tags = array_values($tags);
var_dump(array_is_list($tags));
print_r($tags);
Explain why removing one value does not immediately restore consecutive numeric keys, then run the unchanged code.
Hints
unset()preserves the keys of remaining entries.- A PHP list must use consecutive integer keys starting at
0. array_values()returns a reindexed array.
Show solution
Solution
<?php
$tags = ['php', 'arrays', 'practice'];
unset($tags[1]);
var_dump(array_is_list($tags));
print_r($tags);
$tags = array_values($tags);
var_dump(array_is_list($tags));
print_r($tags);
// First boolean: false
// First keys: 0 and 2
// Second boolean: true
// Second keys: 0 and 1
Explanation
After unset(), php keeps key 0 and practice keeps key 2. The gap means the array is not a list according to array_is_list().
array_values() creates a new array containing the same values under keys 0 and 1. That consecutive zero-based shape is a list again.
Task: Build Order List
Task
Build an $order array with ID 1001 and this nested items list:
BOOK-001, quantity2;PEN-002, quantity1;MUG-003, quantity3.
Loop over the nested items, print <sku> x <quantity> for each one, and accumulate the total quantity. After the loop, print the order ID and total item count.
Expected final line:
Order 1001 contains 6 items
Hints
- Start
$totalQuantityat0before the loop. - Add each item's
quantityduring its iteration. - Print the summary after every item has been visited.
Show solution
Solution
<?php
$order = [
'id' => 1001,
'items' => [
['sku' => 'BOOK-001', 'quantity' => 2],
['sku' => 'PEN-002', 'quantity' => 1],
['sku' => 'MUG-003', 'quantity' => 3],
],
];
$totalQuantity = 0;
foreach ($order['items'] as $item) {
echo "{$item['sku']} x {$item['quantity']}\n";
$totalQuantity += $item['quantity'];
}
echo "Order {$order['id']} contains $totalQuantity items\n";
Explanation
The order is a keyed record containing a nested list of similarly shaped item records. Each iteration reads one item and adds its quantity to a separate accumulator.
The source array remains unchanged. The final summary is printed after the loop because the complete total is not known until all three records have been processed.
Task: Sort And Index Products
Task
Use four products with sku, name, and price_cents. Give MUG-002 and PEN-004 the same price of 899 cents.
Sort by price from low to high. When prices are equal, sort by SKU with strcmp(). Print each sorted line as <sku>: <name> (<price> cents).
Then build $namesBySku with array_column($products, 'name', 'sku') and print the lookup.
Hints
- In the comparator, return the price comparison when it is not zero.
- Use
strcmp($a['sku'], $b['sku'])as the tie-breaker. usort()changes$products; it does not return the sorted array.
Show solution
Solution
<?php
$products = [
['sku' => 'BOOK-001', 'name' => 'PHP Workbook', 'price_cents' => 2499],
['sku' => 'PEN-004', 'name' => 'Pen', 'price_cents' => 899],
['sku' => 'MUG-002', 'name' => 'Mug', 'price_cents' => 899],
['sku' => 'BAG-003', 'name' => 'Backpack', 'price_cents' => 4200],
];
usort($products, function (array $a, array $b): int {
$priceComparison = $a['price_cents'] <=> $b['price_cents'];
if ($priceComparison !== 0) {
return $priceComparison;
}
return strcmp($a['sku'], $b['sku']);
});
foreach ($products as $product) {
echo "{$product['sku']}: {$product['name']} ({$product['price_cents']} cents)\n";
}
$namesBySku = array_column($products, 'name', 'sku');
print_r($namesBySku);
Explanation
The primary comparison orders prices numerically. The SKU tie-breaker makes equal-price ordering deterministic, placing MUG-002 before PEN-004.
usort() mutates and numerically reindexes the product list. array_column() then builds a string-keyed lookup from that sorted data, where each unique SKU points to its name.
Task: Merge And Compare Settings
Task
Create these arrays:
$defaults = [
'currency' => 'GBP',
'items_per_page' => 20,
'show_tax' => true,
];
$userSettings = [
'currency' => 'EUR',
'items_per_page' => 50,
];
Build $merged with array_merge($defaults, $userSettings) and $union with $defaults + $userSettings.
Print the currency and page size from each result in this exact format:
Merge: EUR, 50
Union: GBP, 20
Hints
- Later string keys win in
array_merge(). - Left-side keys win with array union.
show_taxremains present in both arrays even though it is not printed.
Show solution
Solution
<?php
$defaults = [
'currency' => 'GBP',
'items_per_page' => 20,
'show_tax' => true,
];
$userSettings = [
'currency' => 'EUR',
'items_per_page' => 50,
];
$merged = array_merge($defaults, $userSettings);
$union = $defaults + $userSettings;
echo "Merge: {$merged['currency']}, {$merged['items_per_page']}\n";
echo "Union: {$union['currency']}, {$union['items_per_page']}\n";
Explanation
array_merge() processes the user settings later, so their overlapping string keys replace the defaults. This is the usual policy for configurable overrides.
Array union keeps values already present on the left. It adds only right-side keys that the left array lacks, so both overlapping values remain the defaults. The correct operation depends on the intended ownership rule.