skip to content

In PHP, how are array keys like "8", "08", true, 8.7 and null converted, and why do ZIP-code keys end up with mixed types?

level: middleimportance: must knowfreq 55%

answer

  1. canonical decimal integer strings become int
  2. leading zero, plus sign or spaces stay string
  3. true to 1, 8.7 to 8 with a deprecation
  4. null key is "", deprecated in 8.5
  5. "90210" comes back as int 90210

basics

~20 s

A string key that is a canonical decimal integer, like "8" or "-8", is stored as int; "08" stays a string. true becomes 1, 8.7 becomes 8 and null becomes "", so ZIP code "90210" returns as int while "02134" stays string.

solid answer

~40 s

Array keys can only be `int` or `string`, so PHP converts others when storing and reading. A string that is exactly a canonical decimal integer (optional `-`, no leading zeros, no `+`, no spaces, within the `int` range) becomes an `int`: `"8"` and `"-8"` do, `"08"`, `"+8"`, `" 8"`, `"8.0"` and `"-0"` stay strings. `true` and `false` become `1` and `0`. A float is truncated, so `8.7` is stored as `8`, and since PHP 8.1 a fractional float key raises a deprecation. `null` becomes `""`, and PHP 8.5 deprecates that. Arrays and objects throw `TypeError`. In a ZIP-code table, `'90210'` is therefore stored as `int` while `'02134'` stays a string, so `foreach` hands back mixed types and a `string` parameter under `strict_types` rejects the int keys.

code

php · 17 lines
php
<?php
declare(strict_types=1);

$zips = ['02134' => 'Boston', '90210' => 'Beverly Hills'];
var_dump(array_keys($zips));  // [0 => string(5) "02134", 1 => int(90210)]

function label(string $zip, string $city): string
{
    return "$zip $city";
}

foreach ($zips as $zip => $city) {
    echo label((string) $zip, $city), "\n"; // cast: $zip may be int
}

var_dump(array_key_exists('90210', $zips));             // bool(true)
var_dump(in_array('90210', array_keys($zips), true));    // bool(false)

go deeper

for a junior

Recall that array keys are only int or string, and that "8" becomes the int 8 while "08" stays a string.

for a middle

Explain the full conversion table, including bool, float truncation with its 8.1 deprecation, null with its 8.5 deprecation, and TypeError for arrays and objects.

for a senior

Diagnose bugs where read-back keys change type, such as ZIP codes or IDs under strict_types, and choose casting, prefixed keys or records as the fix.

for a principal

Set a team convention for identifier-like keys, deciding where typed value objects or explicit casts belong so mixed key types never reach domain code.

## Why keys get converted A PHP array stores keys as either an integer or a string, nothing else. When code uses any other value as a key, or a string that looks like an integer, the engine normalises it before storing or looking it up. The rules are applied both on write (`$a[$k] = $v`, array literals) and on read (`$a[$k]`, `array_key_exists($k, $a)`), so lookups stay consistent with how the key was stored. ## The conversion table (PHP 8.5) | Key as written | Stored as | Notes | |---|---|---| | `"8"`, `"-8"`, `"0"` | `int` 8, -8, 0 | canonical decimal integer strings | | `"08"`, `"007"` | `string` | leading zero, so not canonical | | `"+8"`, `" 8"`, `"8 "`, `"8.0"`, `"1e3"` | `string` | sign, whitespace, decimal point or exponent | | `"-0"` | `string` | not the canonical form of zero | | `"9223372036854775808"` | `string` | too large for a 64-bit `int` | | `true`, `false` | `int` 1, 0 | | | `8.7` | `int` 8 | truncated; deprecated since 8.1 (*Implicit conversion from float 8.7 to int loses precision*) | | `8.0` | `int` 8 | no fractional part, no deprecation | | `null` | `string` `""` | deprecated since 8.5 (*Using null as an array offset is deprecated, use an empty string instead*) | | an array or object | nothing | `TypeError`: *Cannot access offset of type array on array* | The string rule is stricter than PHP's general notion of a numeric string. `" 8"` and `"8.0"` are numeric strings for arithmetic and comparison, yet they are **not** converted when used as keys. Only the exact form an `int` would print as qualifies. ## Collisions Because conversion happens before storage, keys that look different can land on the same slot. The PHP manual's own example: ```php $a = [1 => 'a', '1' => 'b', 1.5 => 'c', true => 'd']; // array(1) { [1] => 'd' } ``` All four keys become `int` 1, each assignment overwrites the previous one, and only `'d'` survives. The `1.5` also triggers the 8.1 deprecation. Collisions like this appear in real code when keys come from mixed sources, for example IDs read from a CSV file as strings and IDs from a database driver as integers. ## The ZIP-code lookup table A table of US ZIP codes shows the trap clearly. ZIP codes are identifiers, not numbers, and several start with a zero: 1. The table is built from strings: `['02134' => 'Boston', '90210' => 'Beverly Hills']`. 2. `'02134'` has a leading zero, so it stays a string key. 3. `'90210'` is a canonical integer string, so it is stored as `int` 90210. 4. `array_keys()` and `foreach` now return a `string` for one key and an `int` for the other. 5. A function declared `function normalise(string $zip)` in a file with `declare(strict_types=1)` throws `TypeError` when called with the int key; without strict types it silently coerces. 6. `in_array('90210', array_keys($zips), true)` is `false`, because the strict search compares a string with an int. Lookups by key still work, because `$zips['90210']` and `array_key_exists('90210', $zips)` convert the string the same way. The damage appears only when keys are **read back** and treated as strings. ## Ways to avoid it - **Cast on the way out.** Treat keys as untyped and write `(string) $zip` whenever a key leaves the array. - **Make keys non-numeric.** A prefix such as `'zip:90210'` keeps every key a string. - **Store identifiers as values.** Keep a list of records like `['zip' => '90210', 'city' => '...']`, and build a keyed index only for lookups. - **Let static analysis see it.** Declaring the array type as `array<int|string, City>` in a docblock tells analysers the truth, which forces callers to handle both. ## array_key_exists() and conversion `array_key_exists($key, $array)` applies the same conversions to `$key`: `'1'`, `1`, `true` and `1.0` all find the `int` key 1. PHP 8.5 deprecates passing `null` as the key, with the replacement being an explicit `''`. Unlike checks that treat a `null` value as absent, it reports `true` for any key that exists, whatever its value.

  • How does array_key_exists() differ from isset() when the key holds null?
    `array_key_exists()` reports whether the key is present, so it returns `true` for a key whose value is `null`. `isset()` also requires the value to be non-null, so it returns `false` for that key. Use `array_key_exists()` when a stored `null` is meaningful.
  • Why is " 8" a numeric string for comparison but not converted to an int array key?
    Array keys use a narrower rule than numeric strings: only the canonical decimal form of an `int`, with an optional minus, no leading zeros, no whitespace, no sign or decimal point, is converted. Anything else stays a string key, so `" 8"` and `8` are different keys even though `" 8" == 8` is true.

saying these in an interview costs you the question

  • Numeric string keys keep their string type inside a PHP array.
  • A key like "08" is converted to the integer 8.
  • Float keys are rounded to the nearest integer.
  • Using true as a key stores the string "1".
  • An object can be used as an array key and is compared by identity.
  • Lookups with '90210' fail once the key has been stored as an int.