Indexed arrays, associative arrays, and mixed keys
Concept
PHP recognizes three array styles that map onto the same underlying HashTable: indexed arrays (integer keys), associative arrays (string keys), and mixed arrays (both key types). From the engine's perspective, there is no separate type for each — every array is a HashTable, and the difference lies only in what keys you use. This simplicity is both a feature and a potential trap.
Indexed arrays use integer keys starting at 0 by default. When you push an element without specifying a key, PHP sets the key to max_numeric_key + 1. If you unset an element and then push a new one, the new element does NOT reuse the gap — it takes the next integer after the highest key ever used. This is a common source of bugs when developers expect a "compact" sequential array after unset() and then iterate assuming $arr[0] through $arr[count($arr)-1] are all valid.
Associative arrays use string keys. Any string is valid as a key, including empty strings, numeric-looking strings, and strings with special characters. However, PHP silently coerces numeric string keys to integers: $arr['1'] and $arr[1] refer to the same slot. The strings '0' through '9223372036854775807' (PHP_INT_MAX as a string) are all cast to integer keys. This is a well-known gotcha when using data from JSON or databases as array keys.
Mixed arrays are valid but should be used deliberately. When you mix integer and string keys, count(), array_values(), and sort() behave differently than you might expect. count() counts all elements regardless of key type. sort() re-indexes from 0. array_values() extracts only the values and re-creates a pure integer-indexed array. Understanding these behaviors prevents subtle data corruption when array data passes through transformation functions.
| Key type | Storage key | Example |
|---|---|---|
| Integer literal | zend_ulong | $a[0], $a[42] |
| String | zend_string | $a['name'] |
| Numeric string | Cast to zend_ulong | $a['7'] → same as $a[7] |
| Float (cast) | Truncated to int | $a[1.9] → $a[1] |
true / false | Cast to 1 / 0 | $a[true] → $a[1] |
null | Cast to '' | $a[null] → $a[''] |
Code Example
<?php
declare(strict_types=1);
// --- Indexed array ---
$fruits = ['apple', 'banana', 'cherry'];
// Keys: 0, 1, 2
// After unset + push, the gap is NOT reused
unset($fruits[1]);
$fruits[] = 'date';
print_r($fruits);
// Array ( [0] => apple [2] => cherry [3] => date )
// Key 1 is gone, next push used key 3 (max+1)
// --- Associative array ---
$user = [
'id' => 42,
'name' => 'Alice',
'email' => 'alice@example.com',
];
// Numeric string keys are silently cast to int
$scores = [];
$scores['10'] = 'A';
$scores['20'] = 'B';
var_dump(array_key_exists(10, $scores)); // true — '10' was cast to 10
var_dump(array_key_exists('10', $scores)); // true — same slot
// --- Mixed array ---
$mixed = ['x' => 1, 0 => 'zero', 'y' => 2, 1 => 'one'];
count($mixed); // 4
// array_values() collapses to sequential int keys, losing string keys entirely
$values = array_values($mixed);
print_r($values);
// Array ( [0] => 1 [1] => zero [2] => 2 [3] => one )
// --- Casting gotchas as keys ---
$a = [];
$a[1.9] = 'float truncated to 1';
$a[true] = 'bool true → 1'; // overwrites above
$a[false] = 'bool false → 0';
$a[null] = 'null → empty string';
print_r($a);
// Array ( [1] => bool true → 1 [0] => bool false → 0 [] => null → empty string )
// --- Iterating safely after unset ---
$items = ['a' => 1, 'b' => 2, 'c' => 3];
unset($items['b']);
foreach ($items as $key => $value) {
// foreach always works correctly — no index assumption needed
echo "$key: $value\n";
}Interview Q&A
Q: You receive a JSON array from an API, decode it with json_decode($json, true), and use the values as keys in a new array. Under what conditions will PHP silently drop or merge data?
json_decode with true returns a plain PHP array. If the JSON object has keys that are numeric strings (e.g., "0", "42"), PHP will cast them to integer keys upon assignment. If two JSON keys differ only in representation — for instance "1" and 1 (which JSON allows in objects) — they will collide in the PHP array and one value will be silently overwritten. The last value assigned wins. Always validate that external data used as array keys is what you expect, and use array_key_first() / array_key_last() to inspect the actual key types after decoding.
Q: After calling unset($arr[2]) on a 5-element indexed array, count($arr) returns 4 but $arr[2] is undefined. Why doesn't PHP reindex automatically, and how do you get a compact array back?
PHP preserves the original integer keys after unset() because reindexing every time would be an O(n) operation that breaks existing references and iterators pointing into the array. The design choice prioritizes predictability — a key that existed before unset() is simply removed; nothing else shifts. To get a compact 0-based array, call array_values($arr), which creates a new array with sequential integer keys starting at 0. If you need to reindex in-place (without creating a new variable), sort() does reindex but also sorts; $arr = array_values($arr) is the idiomatic approach.
Q: What is the difference between $arr = [] and $arr = array(), and which should you use in PHP 8.4 codebases?
They are semantically identical — both produce an empty HashTable. The short syntax [] was introduced in PHP 5.4 and is now universally preferred in PSR-12 and modern codebases. array() is the old procedural-style syntax, still valid but treated as legacy. In PHP 8.4 codebases, always use [] for declarations, [...] for literals, and [...$spread] for unpacking. The only place array() might still appear is in PHP 5.x compatibility layers or legacy code being migrated.