Array String Keys And Dynamic Access
PHP arrays are the language's workhorse data structure, and accessing them is always done with square brackets and a key. This lesson covers the key rules that surprise people — how string and integer keys behave, and how to access arrays dynamically when the key itself is computed at runtime, which is the mechanism behind a lot of framework and configuration code.
Bracket Access And Key Types
Every array element is reached with [] and a key. Keys are either integers or strings (the two legal key types), and PHP applies some normalization you need to know about:
<?php
declare(strict_types=1);
$data = [
'name' => 'Ada',
42 => 'answer',
'active' => true,
];
echo $data['name'] . PHP_EOL; // string key
echo $data[42] . PHP_EOL; // integer key
// Key normalization surprises:
$a = [];
$a['1'] = 'x'; // the string '1' becomes integer 1
$a[1] = 'y'; // same slot -> overwrites
var_dump(count($a)); // 1, not 2
// Prints:
// Ada
// answer
// int(1)
The rule that bites: a string key that looks like a canonical integer ('1', '42') is silently cast to an integer key, so $a['1'] and $a[1] are the same element. Non-canonical strings ('01', '1.0', ' 1') stay strings. Booleans and floats used as keys are cast to integers (true → 1, 1.9 → 1), and null becomes the empty string ''. Most of the time you use clean string or integer keys and never notice; the surprises show up when keys come from external data.
Safe Access For Missing Keys
Reading a key that does not exist emits a warning and yields null. Two idioms avoid that:
<?php
declare(strict_types=1);
$config = ['host' => 'localhost'];
// Null coalescing: value if set, else a default. The standard way.
$port = $config['port'] ?? 3306;
// array_key_exists when you must distinguish "missing" from "set to null".
$hasHost = array_key_exists('host', $config);
echo $port . PHP_EOL;
echo $hasHost ? 'has host' : 'no host';
echo PHP_EOL;
// Prints:
// 3306
// has host
Prefer ?? for "give me the value or a default." Reach for array_key_exists() only in the rarer case where a key explicitly set to null must be told apart from a key that is absent — because ?? treats both as "use the default."
Dynamic Keys
The powerful part: the key can be any expression, not just a literal. This is how you access arrays when the key is decided at runtime.
<?php
declare(strict_types=1);
$user = ['name' => 'Ada', 'role' => 'admin', 'email' => 'ada@example.com'];
$field = 'role'; // key held in a variable
echo $user[$field] . PHP_EOL; // -> 'admin'
// Build a key from an expression:
$prefix = 'na';
echo $user[$prefix . 'me'] . PHP_EOL; // $user['name'] -> 'Ada'
// Iterate keys and values dynamically:
foreach ($user as $key => $value) {
echo $key . '=' . $value . PHP_EOL;
}
// Prints:
// admin
// Ada
// name=Ada
// role=admin
// email=ada@example.com
Dynamic keys are what let a config loader read $settings[$requested], a form handler pull $_POST[$fieldName], or a serializer walk arbitrary data. With that power comes the safety point: when the key originates from user input, a missing or attacker-chosen key is a real case — combine dynamic access with ?? defaults and validate that the key is one you expect, so untrusted input cannot reach data you did not intend to expose.
What To Check
Before moving on, make sure you can:
- access array elements with
[]and explain that keys are int or string only - explain integer-string key normalization (
'1'becomes1) and when it bites - use
??for defaults andarray_key_existsfor the null-versus-absent case - access arrays with a dynamic key held in a variable or built from an expression
- validate and default dynamic keys that come from untrusted input
What You Should Be Able To Do
After this lesson, you should be able to access array data safely with the right idiom for missing keys, predict PHP's key-normalization behavior, and use dynamic keys to write flexible data-driven code without exposing yourself to untrusted-key bugs.
Practice
Practice: Write Safe Dynamic Array Access
Show solution
<?php
declare(strict_types=1);
function pluck(array $data, array $keys): array
{
$result = [];
foreach ($keys as $key) {
$result[$key] = $data[$key] ?? null; // dynamic key + safe default
}
return $result;
}
$user = ['name' => 'Ada', 'role' => 'admin', 'email' => 'ada@example.com'];
$picked = pluck($user, ['name', 'email', 'phone']);
echo $picked['name'] . PHP_EOL;
echo $picked['email'] . PHP_EOL;
var_dump($picked['phone']); // missing key -> null, no warning
// Prints:
// Ada
// ada@example.com
// NULL
The key points: $data[$key] uses a dynamic key (the value held in $key on each iteration), and ?? null supplies a default so a missing key yields null rather than emitting an undefined-key warning. The phone key is absent from the source array and comes back as null cleanly. If $keys came from untrusted input, this function is already safe against missing keys, but a caller exposing sensitive data should also validate $keys against an allow-list so a request cannot pluck fields it should not see.
Practice: Diagnose A Key Normalization Bug
$byId = [];
foreach ($rows as $row) {
$byId[$row['id']] = $row; // ids are strings like '1', '2', '07', '10'
}
// Later, some lookups by the original string id fail or collide.
var_dump(count($rows)); // 4
var_dump(count($byId)); // fewer than 4 in some data sets
Task
Explain why entries are lost or collide, and how to key the lookup reliably.
Show solution
- If the source data ever produces both a canonical form and something that normalizes to the same integer (or the same id appears twice in different string forms), they overwrite each other, dropping entries — hence a lower count.
- Keys like
'07'are not canonical integers (leading zero), so they stay the string'07'. Now the array has a mix of integer keys (from'1','10') and string keys (from'07'), and later code that assumes all keys are the original strings — e.g.$byId['1']versus$byId['07']— behaves inconsistently:'1'gets normalized on lookup too (so it works by luck), while code comparing key types or iterating breaks.
The reliable fixes:
- If ids are truly strings and must stay strings, prefix them so they never look like integers:
$byId['id_' . $row['id']]. This forces every key to remain a string, eliminating normalization entirely. - Or accept integer keys deliberately by casting ids to
inton both insert and lookup, so the behavior is uniform and intentional rather than accidental.
The root lesson: PHP array keys are int-or-string with automatic normalization of integer-looking strings, so when keys come from external data, either guarantee they cannot look like integers (prefixing) or normalize them yourself consistently — never rely on the original string form surviving.
Practice: Define Array Access Review Checks
List the array-access issues to check for in a PHP code review, covering missing keys, key normalization, and dynamic keys from untrusted input.
Show solution
- Unguarded access to keys that may be absent.
$arr['x']wherexmight not exist emits a warning and yields null; require?? default(or a priorisset/array_key_exists) anywhere the key is not guaranteed present — especially for$_GET/$_POST/config reads. ??used where null-vs-absent matters. If code needs to distinguish "key set to null" from "key missing,"??is wrong (it treats both as default);array_key_existsis correct. Flag??on keys that can legitimately hold null.- Keys derived from external data that could normalize. Any array keyed by user/DB-supplied ids risks integer-string normalization. Check that such keys are either prefixed to stay strings or deliberately cast to a single type on both write and read.
- Dynamic keys from untrusted input without an allow-list.
$data[$_GET['field']]lets a request choose which field to read. Confirm the key is validated against expected values before use, so untrusted input cannot reach unintended data. - Assumptions about key type in iteration. Code that compares keys with
===or relies on keys being strings can break when normalization produced integer keys. Check for type-sensitive key handling.
Static analysis (PHPStan/Psalm) catches many possibly-undefined-key accesses automatically; the human review adds the untrusted-key and normalization concerns that tools cannot judge from types alone.