PHP Associative Arrays

An associative array in PHP is an array that uses meaningful, custom keys instead of sequential integers to identify its values. Instead of remembering that a customer’s name lives at index 0, you store it under the key "name" and read it back the same way. This makes code far more readable and lets you model real-world data — a user profile, a product, a row from a database — as simple key-value structures without writing a class. Associative arrays are one of the most-used features in everyday PHP code, from configuration files to JSON APIs.

Overview / How it works

In PHP, there is really only one array type under the hood: an ordered map. What people call an “indexed array” (keys 0, 1, 2, ...) and an “associative array” (keys like "name" or "price") are the exact same data structure — PHP just lets you choose your own keys instead of accepting the automatic integer ones. Internally, every PHP array is implemented as a HashTable inside the Zend Engine. Each key is hashed to find a storage bucket, and each bucket also sits inside a doubly linked list that preserves the order in which entries were inserted. That is why foreach always iterates an associative array in insertion order, not in some arbitrary hash order — a detail many other languages do not guarantee, but PHP does.

Keys in a PHP array can only be of type int or string. If you use a string key that looks like a valid decimal integer (for example "5", but not "05" or "5.0"), PHP automatically casts it to an int key. This normalization happens silently and is a frequent source of confusion, especially when merging arrays (covered in Common Mistakes below). Values, on the other hand, can be absolutely anything — strings, numbers, booleans, other arrays (giving you nested/multi-dimensional associative arrays), objects, or even closures.

Because arrays are a core language type rather than an object, PHP uses copy-on-write semantics for them: assigning an array to a new variable or passing it to a function does not immediately duplicate the underlying data. The engine only copies the HashTable when one of the copies is actually modified, which keeps everyday array handling fast even for large associative arrays.

Syntax

You create an associative array with the short array syntax [] (preferred since PHP 5.4) or the older array() function-style syntax, mapping each key to a value with the => operator.

$array = [
    "key1" => "value1",
    "key2" => "value2",
];
Syntax Purpose
["key" => $value] Create an array literal with one or more key/value pairs
$array["key"] Read the value stored under "key"
$array["key"] = $value; Add a new key or overwrite an existing one
unset($array["key"]); Remove a key (and its value) from the array
isset($array["key"]) Check a key exists and is not null
array_key_exists("key", $array) Check a key exists, even if its value is null
foreach ($array as $key => $value) Iterate over every key/value pair in insertion order

Examples

Example 1: Building and modifying a simple associative array

<?php
$person = [
    "name" => "Maria Silva",
    "age" => 29,
    "email" => "maria@example.com",
    "active" => true,
];

echo $person["name"] . "\n";
echo $person["age"] . "\n";
echo ($person["active"] ? "Active" : "Inactive") . "\n";

$person["city"] = "Lisbon";
unset($person["email"]);

foreach ($person as $key => $value) {
    echo $key . ": " . (is_bool($value) ? ($value ? "true" : "false") : $value) . "\n";
}

Output:

Maria Silva
29
Active
name: Maria Silva
age: 29
active: true
city: Lisbon

This example creates an associative array with four keys, reads two of them directly, then mutates the array: $person["city"] adds a brand new key at the end, while unset($person["email"]) removes an existing one. Notice that the final foreach prints the remaining keys in the exact order they were inserted — name, age, active, city — with email gone entirely, confirming that insertion order (not alphabetical or original literal order) is what PHP preserves.

Example 2: Counting word frequency

<?php
$text = "the quick brown fox jumps over the lazy dog the fox runs";
$words = explode(" ", $text);

$frequency = [];
foreach ($words as $word) {
    $frequency[$word] = ($frequency[$word] ?? 0) + 1;
}

arsort($frequency);

foreach ($frequency as $word => $count) {
    echo "$word: $count\n";
}

Output:

the: 3
fox: 2
quick: 1
brown: 1
jumps: 1
over: 1
lazy: 1
dog: 1
runs: 1

Here the array’s keys are the data: each unique word becomes a key, and the null coalescing operator ?? handles the “key might not exist yet” case cleanly — $frequency[$word] ?? 0 returns 0 the first time a word is seen, avoiding an “undefined array key” warning. arsort() then sorts the array by value in descending order while keeping each value attached to its original key (a plain sort() would destroy the word-to-count association entirely).

Example 3: Filtering a catalog of nested associative arrays

<?php
$products = [
    "sku-001" => ["name" => "Wireless Mouse", "price" => 19.99, "stock" => 42],
    "sku-002" => ["name" => "Mechanical Keyboard", "price" => 79.50, "stock" => 15],
    "sku-003" => ["name" => "USB-C Hub", "price" => 24.00, "stock" => 0],
];

$inStock = array_filter($products, fn(array $product) => $product["stock"] > 0);

foreach ($inStock as $sku => $product) {
    printf("%s: %s (\$%.2f) - %d in stock\n", $sku, $product["name"], $product["price"], $product["stock"]);
}

echo json_encode($inStock, JSON_PRETTY_PRINT);

Output:

sku-001: Wireless Mouse ($19.99) - 42 in stock
sku-002: Mechanical Keyboard ($79.50) - 15 in stock
{
    "sku-001": {
        "name": "Wireless Mouse",
        "price": 19.99,
        "stock": 42
    },
    "sku-002": {
        "name": "Mechanical Keyboard",
        "price": 79.5,
        "stock": 15
    }
}

This is a realistic, everyday pattern: a catalog keyed by SKU, where each value is itself an associative array describing that product. array_filter() keeps only the entries whose stock is greater than zero (using a short arrow function), and crucially it preserves the original "sku-xxx" keys instead of renumbering them. Because those keys are strings (not numeric strings), json_encode() naturally produces a JSON object rather than an array — associative arrays with string keys are how you build JSON objects in PHP.

How it works step by step / Under the hood

  • When you write $array["key"] = $value;, PHP computes a hash of the string "key" to locate (or create) a bucket in the array’s internal HashTable.
  • If the key is new, PHP appends a bucket to the table and links it at the end of an internal doubly linked list — this linked list is exactly what makes iteration order match insertion order.
  • If the key already exists, PHP overwrites the value in the existing bucket in place; the key keeps its original position in the iteration order (updating a value does not move it to the end).
  • String keys that are valid decimal integer literals (like "3") are normalized to integer keys automatically; keys like "03" or "3.0" are left as strings because they are not canonical integer representations.
  • On unset($array["key"]), the bucket is removed from the hash table and unlinked from the ordered list, but the remaining keys are not renumbered — only array_values() or similar functions renumber keys.
  • Because arrays use copy-on-write, passing $array to a function by value is cheap until that function actually writes to the array, at which point PHP duplicates the HashTable so the caller’s copy is unaffected.

Common Mistakes

Mistake 1: Using array_merge() with numeric-looking keys

Because numeric string keys are silently cast to integers, array_merge() treats them as indexed (not associative) keys — and indexed keys get renumbered during a merge instead of being matched up and overwritten.

<?php
$a = ["10" => "ten", "20" => "twenty"];
$b = ["10" => "TEN-OVERRIDE", "30" => "thirty"];
$merged = array_merge($a, $b);
print_r($merged);

Output:

Array
(
    [0] => ten
    [1] => twenty
    [2] => TEN-OVERRIDE
    [3] => thirty
)

The developer probably expected key 10 to end up holding "TEN-OVERRIDE". Instead, because "10", "20", and "30" all normalize to integer keys, array_merge() treats every entry as if it came from a plain indexed array and reassigns brand-new sequential keys 0, 1, 2, 3, throwing away the original keys entirely. The fix is to use array_replace(), which merges arrays by key — including integer keys — and overwrites duplicates instead of renumbering them:

<?php
$a = ["10" => "ten", "20" => "twenty"];
$b = ["10" => "TEN-OVERRIDE", "30" => "thirty"];
$merged = array_replace($a, $b);
print_r($merged);

Output:

Array
(
    [10] => TEN-OVERRIDE
    [20] => twenty
    [30] => thirty
)

Mistake 2: Reading a key that might not exist

Accessing a key that isn’t in the array does not throw a fatal error, but since PHP 8.0 it raises an E_WARNING (“Undefined array key”) and evaluates to null, which can silently skew your logic:

<?php
$config = ["debug" => true, "timezone" => "UTC"];

if ($config["cache_driver"] === "redis") {
    echo "Using Redis cache\n";
} else {
    echo "Using default cache\n";
}

Output:

Using default cache

The comparison still “works” here because null === "redis" is false, but PHP also emits an “Undefined array key” warning to the error log (or straight to the page in a misconfigured dev environment), and in a numeric or string-building context a stray null can produce far more confusing bugs than this. Guard the lookup instead, using the null coalescing operator to supply a default:

<?php
$config = ["debug" => true, "timezone" => "UTC"];

$cacheDriver = $config["cache_driver"] ?? "default";

if ($cacheDriver === "redis") {
    echo "Using Redis cache\n";
} else {
    echo "Using default cache\n";
}

Output:

Using default cache

Best Practices

  • Use ?? (null coalescing) or array_key_exists() whenever a key’s presence is not guaranteed, instead of assuming it is always set.
  • Prefer array_replace() over array_merge() when your arrays might contain integer or numeric-string keys you want to preserve and overwrite by key.
  • Use descriptive, consistent key names (snake_case or camelCase, pick one) so nested associative arrays read like self-documenting data structures.
  • Reach for a typed class or enum instead of an associative array once the “shape” of your data becomes fixed and important — arrays give you no protection against typos in key names.
  • Remember that foreach order is insertion order, not sorted order; call ksort(), asort(), or uasort() explicitly if you need a particular order.
  • When converting to JSON, use string keys (not sequential integers) to guarantee json_encode() produces an object {} rather than an array [].

Practice Exercises

  • Create an associative array representing a recipe, mapping ingredient names to quantities (e.g. "flour" => "200g"). Loop over it with foreach and print a shopping list line for each ingredient.
  • Given an associative array of student names mapped to numeric grades, calculate and print the average grade using array_sum() and count(), then print the name of the student with the highest grade.
  • Build two associative arrays: one of default user settings and one of a specific user’s overrides (some keys overlapping). Combine them with array_replace() so the overrides win, and print the final merged settings.

Summary

  • Associative arrays use meaningful string or int keys instead of purely sequential indexes, but they are the same underlying array type as indexed arrays.
  • PHP arrays are ordered hash maps: a HashTable for fast key lookup plus a doubly linked list that preserves insertion order for iteration.
  • Numeric-looking string keys are automatically normalized to integers, which can cause surprising behavior in functions like array_merge().
  • Use isset()/array_key_exists()/?? to safely handle keys that might not exist.
  • Sorting functions like asort(), arsort(), and ksort() reorder an associative array while keeping keys attached to their values.
  • Nested associative arrays are the natural way to model structured, JSON-like data in PHP.