0

Multidimensional arrays and nested access patterns

Beginner5 min read·php-04-004

Concept

Multidimensional arrays in PHP are arrays whose values are themselves arrays, to arbitrary depth. Because PHP arrays are hash maps rather than typed containers, a "2D array" is just an array of arrays — there is no native matrix type, no shape enforcement, and no requirement that inner arrays share the same keys or length. This flexibility is powerful for representing heterogeneous data (e.g., a list of user records from a database), but it also means bugs can silently produce inconsistent shapes.

Accessing nested elements uses chained bracket notation: $data['users'][0]['email']. Each bracket dereference performs an O(1) HashTable lookup. Deep chains like $a['b']['c']['d']['e'] are perfectly valid and common in configuration parsing and API response handling. The risk is accessing a key that does not exist at any level — PHP will emit an E_WARNING (or E_NOTICE in older versions) and return null. The null coalescing operator ?? was introduced in PHP 7.0 precisely to handle this: $data['user']['address']['city'] ?? 'Unknown' short-circuits safely without triggering notices.

In PHP 8.0, the nullsafe operator ?-> applies to method chains on objects, not arrays. For nested arrays, the idiomatic safe access pattern remains ?? chaining or a dedicated helper. Laravel's data_get() and Arr::get() helpers (from Illuminate\Support\Arr) use dot notation and handle null at any level gracefully — Arr::get($data, 'user.address.city', 'Unknown').

When building multidimensional arrays programmatically — for example, grouping database rows by a category — the pattern $result[$category][] = $item is idiomatic. It auto-initializes the inner array on first access via PHP's automatic creation of nested arrays when using [] on the right-hand side. However, this only works when using [] for push — $result[$category]['key'] = $value on a non-existent $result[$category] will create the inner array automatically starting PHP 7.

Performance note: because each level of nesting is a separate HashTable allocation, a 1,000-element array of 10-field associative arrays consumes approximately 1,000 × (56 bytes/bucket × 10 + 336 bytes overhead) ≈ 896 KB. For truly large datasets, consider SplFixedArray, generators, or reading from a database cursor rather than materializing the entire structure.

Code Example

php
<?php
declare(strict_types=1);

// --- Basic 2D array ---
$matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];
echo $matrix[1][2]; // 6

// --- Array of associative records ---
$users = [
    ['id' => 1, 'name' => 'Alice', 'role' => 'admin'],
    ['id' => 2, 'name' => 'Bob',   'role' => 'viewer'],
];
echo $users[0]['name']; // Alice

// --- Safe nested access with null coalescing ---
$config = ['db' => ['host' => 'localhost']];
$port = $config['db']['port'] ?? 3306;       // 3306 — key missing at level 2
$user = $config['cache']['host'] ?? 'redis'; // 'redis' — level 1 missing

// --- Building nested arrays programmatically ---
$rows = [
    ['category' => 'fruit',  'name' => 'apple'],
    ['category' => 'fruit',  'name' => 'banana'],
    ['category' => 'veggie', 'name' => 'carrot'],
];

$grouped = [];
foreach ($rows as $row) {
    $grouped[$row['category']][] = $row['name'];
}
// $grouped = ['fruit' => ['apple', 'banana'], 'veggie' => ['carrot']]

// --- Iterating multidimensional arrays ---
foreach ($users as $index => $user) {
    foreach ($user as $key => $value) {
        echo "users[$index][$key] = $value\n";
    }
}

// --- Modifying nested values ---
// This DOES NOT work as expected — creates a copy of inner array:
foreach ($matrix as $row) {
    $row[0] = 999; // modifies local copy only
}

// Correct: use reference or index
foreach ($matrix as &$row) {
    $row[0] *= 2;
}
unset($row); // CRITICAL: always unset the reference after a foreach by reference

// --- Deep access with a helper (manual Arr::get equivalent) ---
function array_get(array $array, string $path, mixed $default = null): mixed
{
    $keys = explode('.', $path);
    $current = $array;
    foreach ($keys as $key) {
        if (!is_array($current) || !array_key_exists($key, $current)) {
            return $default;
        }
        $current = $current[$key];
    }
    return $current;
}

$data = ['user' => ['address' => ['city' => 'Paris']]];
echo array_get($data, 'user.address.city', 'Unknown'); // Paris
echo array_get($data, 'user.phone', 'N/A');            // N/A

Interview Q&A

Q: When iterating a multidimensional array with foreach ($matrix as &$row) and modifying $row, why is unset($row) after the loop critical, and what bug does omitting it cause?

After foreach ($matrix as &$row), the variable $row holds a reference to the last element of $matrix. If you then run any subsequent code that modifies $row — including another foreach on the same array — the last element of $matrix will be overwritten. The classic bug is looping twice: the second foreach ($matrix as $row) overwrites $matrix[last] with each element it iterates, leaving the last element as a copy of the second-to-last. unset($row) immediately after the loop breaks the reference, making $row a regular variable again. This is one of the most common PHP bug patterns in legacy code.


Q: What is the memory impact of storing 10,000 rows (each with 5 string fields) in a PHP array versus using a database cursor with PDO::FETCH_LAZY?

A PHP array of 10,000 associative arrays, each with 5 string keys and short string values, consumes roughly 10,000 × (5 buckets × 56 bytes + 336 bytes overhead + string overhead) ≈ 6–8 MB. Every element is a live PHP value in memory simultaneously. A PDO cursor with PDO::FETCH_LAZY fetches one row at a time and discards each row after use, keeping memory consumption flat at approximately one row (a few hundred bytes). For large result sets, always prefer cursor() in Laravel (which uses PDO's buffered cursor) or iterate with PDO::FETCH_LAZY directly to avoid materializing the entire result in memory.


Q: You need to group database rows by a composite key — e.g., by both country and year. What PHP array pattern do you use, and what is the time complexity?

Use nested $result[$country][$year][] = $row. PHP auto-creates each nesting level as a new HashTable on first access. The time complexity is O(n) for n rows, with each insertion being O(1) amortized (hash lookup at each level). The result is a three-level nested array: country → year → list of rows. This pattern is idiomatic and fast. For more flexible grouping, array_column() combined with custom grouping, or using collect() in Laravel with ->groupBy(), achieves the same in a more composable way, though with slightly higher constant-factor overhead from the Collection wrapper.