---
title: 'Indexed and Associative Arrays'
source: 'https://academia.sh/en/courses/php-fundamentals/indexed-and-associative-arrays'
course: 'PHP Fundamentals'
language: en
updated: '2026-10-21T05:03:33+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Demonstrates, with an example, that one array structure serves as both an indexed and an associative array and keeps insertion order'
    - "Measures the collapse of the keys '1', 1 and true into a single key as a silent channel and classifies it"
    - 'Measures which strings key normalization applies to and ties the result to the canonical decimal integer rule'
---

# Indexed and Associative Arrays

One array structure used as both a list and a dictionary; the keys '1', 1 and true collapsing into a single key; a decimal key being truncated; normalization applying only to canonical decimal integer strings.

The Functions lesson measured an extra argument being discarded without any signal: `add_two(1, 2, 3)` produced `3` and no word of it reached the caller. This lesson asks whether the same silence appears in a data structure: when the same array receives keys written in different forms and they run together, who says so? The course's measuring question does not change, which is which channel the problem is heard through, and the channel names are only used in this lesson: **silent**, **return**, **warning**, **halts**.

In PHP, `array` is a single structure that serves two uses. An array whose keys are consecutive integers reads as an indexed array, and an array whose keys are strings reads as an associative array. There is no type difference between them; the difference lies only in what the keys are. The variable and type concepts built in the Programming Fundamentals course (M01/K02) apply here as they stand. What is new is that a key is not stored as written; it passes through a rule first.

## Two Uses in One Structure

The file below uses the same structure in three ways: as a list, as a dictionary, and as a mixture of the two. The keys of each array are printed side by side with `array_keys`.

```php
<?php
// arrays.php — the same structure serves as both a list and a dictionary
$items = ['apple', 'pear', 'quince'];
$items[] = 'pomegranate';

$person = ['name' => 'Ada', 'age' => 36];
$person['city'] = 'Izmir';

$mixed = ['one', 'two', 'special' => 'x'];
$mixed[] = 'three';

$ordered = [3 => 'c', 1 => 'a'];
$ordered[] = 'd';

$removed = ['x', 'y', 'z'];
unset($removed[1]);
$removed[] = 'w';

echo 'items: ', implode(',', array_keys($items)), "\n";
echo 'person: ', implode(',', array_keys($person)), "\n";
echo 'mixed: ', implode(',', array_keys($mixed)), "\n";
echo 'ordered: ', implode(',', array_keys($ordered)), "\n";
echo 'removed: ', implode(',', array_keys($removed)), "\n";
echo 'is list: ', var_export(array_is_list($items), true), ' ',
    var_export(array_is_list($person), true), ' ',
    var_export(array_is_list($removed), true), "\n";
```

```
items: 0,1,2,3
person: name,age,city
mixed: 0,1,special,2
ordered: 3,1,4
removed: 0,2,3
is list: true false false
```

Appending with square brackets adds an element without a key, and PHP gives it the next integer key: in the mixed array, the value `'three'` added after the `special` key received the key `2`. The `ordered` line shows that order depends on insertion and not on the key: the key `3` comes first because it was written first, followed by `1` and the appended `4`. The `removed` line shows that keys are not renumbered when an element is deleted, which is why `array_is_list` returns `false`. An array that looks indexed can stop being indexed after an `unset`, and nothing reports it.

**AS1.** In this lesson, "order" means the insertion order of the elements; `foreach` walks over the array in that order. The run above shows that order is independent of key values, and any job that needs numeric order sorts explicitly. Whether an array is copied when it is assigned to another variable is not the question of this lesson: that is the "who sees this change" question measured in M11/K03, and it is not asked here.

## Key Normalization

An array key can only be an `int` or a `string`. A key written with any other type is normalized to one of these two: a `true` key becomes `1`, a decimal number is truncated to an integer, and a string in decimal integer form becomes an integer. That is the whole definition; what matters is that the conversion is invisible at the level of the written code. The file below pulls in `channel.php`; the channel classifier is the shared helper of the course, and its cross-section is used in this lesson.

```php
<?php
// channel.php — helpers that classify which channel a problem is heard through

// Level names of caught warnings collect here; the message text is not kept.
$GLOBALS['heard_warnings'] = [];

set_error_handler(function (int $level): bool {
    $names = [E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE', E_DEPRECATED => 'E_DEPRECATED'];
    $GLOBALS['heard_warnings'][] = $names[$level] ?? 'E_' . $level;
    return true;
});

// Turns a value into a short text together with its type.
function show(mixed $value): string
{
    return match (true) {
        $value === null => 'null',
        is_bool($value) => $value ? 'true' : 'false',
        is_array($value) => 'array(' . count($value) . ')',
        default => get_debug_type($value) . ' ' . $value,
    };
}

// Runs the job and asks, in order, which channel the problem is heard through:
// first a throw, then a warning, then a false/null return; if none of them, silent.
function listen(callable $job): array
{
    $GLOBALS['heard_warnings'] = [];
    try {
        $result = $job();
    } catch (Throwable $error) {
        return ['halts', get_class($error)];
    }
    if ($GLOBALS['heard_warnings']) {
        $levels = implode(',', array_unique($GLOBALS['heard_warnings']));
        return ['warning', $levels . ', result ' . show($result)];
    }
    if ($result === false || $result === null) {
        return ['return', 'result ' . show($result)];
    }
    return ['silent', 'result ' . show($result)];
}

// Runs the code in a separate PHP execution; exit code 255 means execution halted.
function run_separately(string $code): array
{
    exec('php -d display_errors=0 -r ' . escapeshellarg($code) . ' 2>&1', $lines, $exit);
    $channel = $exit === 255 ? 'halts' : 'silent';
    return [$channel, 'lines out ' . count($lines) . ', exit ' . $exit];
}
```

```php
<?php
// normalization.php — differently written keys collapsing into one key
require 'channel.php';

$observations = [
    'three forms, one key' => fn() => ['1' => 'a', 1 => 'b', true => 'c'],
    'decimal key'          => function () {
        $key = 1.7;
        return [$key => 'x'];
    },
];

foreach ($observations as $name => $observe) {
    [$channel, $evidence] = listen($observe);
    printf("%-22s%-9s%s\n", $name, $channel, $evidence);
}

$array = ['1' => 'a', 1 => 'b', true => 'c'];
echo 'remaining key: ', array_key_first($array), ', value: ', $array[1], "\n";
```

```
three forms, one key  silent   result array(1)
decimal key           warning  E_DEPRECATED, result array(1)
remaining key: 1, value: c
```

The first line is the course's `key normalization` observation, and it belongs to the **silent** channel. The keys `'1'`, `1` and `true` are three separate spellings; all three landed on the key `1`, one element remained in the array, and the value of that element is the last one written, `'c'`. `listen()` asked its questions in order: nothing was thrown, no warning was reported, and the result is neither `false` nor `null`; silence is what remains. The problem can be seen only by looking at the result. Someone who expected `array(3)` here instead of `array(1)` heard nothing about the two missing elements. This is the same channel as the discarded extra argument; the difference is that what is discarded here is a value, and the key overwrites it.

The second line shows another face of the same mechanism: the key `1.7` was truncated to `1`, and this time an error level was reported. `E_DEPRECATED` is the deprecation level, and execution continues; the channel is therefore **warning**. The keys `true` and `'1'` reach `1` without losing a value, whereas `1.7` drops its fractional part. The limit of the silence is drawn by information loss: a conversion that loses information is announced, and a conversion that appears lossless is not.

**AS2.** In this lesson `E_DEPRECATED` is recorded under the `warning` channel, because the definition in the core asks only two things: was an error level reported, and did execution continue. The meaning of the level, that the behavior may change in the future, does not alter the channel; it only says that this warning may become a `halts` someday. The lesson does not measure that future.

## Key Types and Merging

Normalization does not work only between strings and integers; Boolean values, the empty value and decimal numbers can also be written in place of a key. To measure which key each one lands on, every candidate is held in a variable and used as the key of a one-element array.

```php
<?php
// types.php — the key that candidates of different types land on
require 'channel.php';

function key_of(mixed $candidate): string
{
    $single = [$candidate => 'x'];
    return var_export(array_key_first($single), true);
}

$candidates = [
    'null'        => null,
    'false'       => false,
    'true'        => true,
    'whole float' => 2.0,
    'empty string' => '',
];

foreach ($candidates as $name => $candidate) {
    [$channel, $evidence] = listen(fn() => key_of($candidate));
    printf("%-14s%-9s%s\n", $name, $channel, $evidence);
}

[$channel, $evidence] = listen(function () {
    $none = null;
    return count([$none => 'a', '' => 'b']);
});
echo 'null and empty string together: ', $channel, ' ', $evidence, "\n";
```

```
null          warning  E_DEPRECATED, result string ''
false         silent   result string 0
true          silent   result string 1
whole float   silent   result string 2
empty string  silent   result string ''
null and empty string together: warning E_DEPRECATED, result int 1
```

A `null` key lands on the empty string and reports `E_DEPRECATED` (**warning**); `false` lands on `0`, `true` on `1`, and `2.0`, which has no fraction, on `2`, and these three, together with the empty string, are **silent**. The last line ties the result together: `null` and `''` are the same key, and an array that writes both carries a single element. Most of these conversions look free of information loss, yet a source that carries `null` and the empty string as separate data loses that distinction once the data enters the array. A decimal number with a fraction, as measured in the previous section, is truncated with a warning.

**AS3.** The five candidates in the table illustrate the language's conversion rule; types that are not valid as keys (another array, an object) are outside this lesson and are not measured here. The lesson reads which type lands on which key from the executed output, not from the written rule.

When two arrays are merged, a key collision is a second point of normalization. Three different merges applied to the same two arrays produce three different fates for the keys.

```php
<?php
// merging.php — merging the same two arrays in three different ways
$a = ['name' => 'Ada', 5 => 'x', 6 => 'y'];
$b = ['name' => 'Can', 5 => 'z'];

echo 'addition: ', json_encode($a + $b), "\n";
echo 'array_merge: ', json_encode(array_merge($a, $b)), "\n";
echo 'array_replace: ', json_encode(array_replace($a, $b)), "\n";
echo 'spread: ', json_encode([...$a, ...$b]), "\n";
echo 'keys: ', implode(',', array_keys(array_merge($a, $b))), "\n";
```

```
addition: {"name":"Ada","5":"x","6":"y"}
array_merge: {"name":"Can","0":"x","1":"y","2":"z"}
array_replace: {"name":"Can","5":"z","6":"y"}
spread: {"name":"Can","0":"x","1":"y","2":"z"}
keys: name,0,1,2
```

The addition operator keeps the values of the left array and adds only the new keys from the right: the `name` key stayed `'Ada'` and the `5` key stayed `'x'`. `array_merge` takes the right value for string keys (`'Can'`) but renumbers integer keys: the keys `5` and `6` disappeared, and all three elements from the two sides were kept under the keys `0, 1, 2`. `array_replace`, by contrast, writes the right value without touching the integer keys. The spread syntax gives the same result as `array_merge`. Four spellings produced three different results for the same two arrays, and none of them reported a level: the channel is **silent**.

**AS4.** The difference between `array_merge` and addition is measured in this lesson only through key collisions. Which is faster, or which should be preferred, is not measured; the lesson does not rank one function above the other, and only shows that the same input gives different results.

## The Does-the-Key-Exist Question

Whether a key is present in an array can be asked with two separate questions, and the questions do not give the same answer. `isset` asks whether the key exists and its value is not `null`, `array_key_exists` asks only whether the key exists, and `??` supplies the fallback when the value is missing or `null`.

```php
<?php
// exists.php — the difference between isset, array_key_exists and ??
$data = ['full' => 'x', 'empty' => null, 'zero' => 0];

foreach (['full', 'empty', 'zero', 'absent'] as $key) {
    printf(
        "%-6s isset=%-5s exists=%-5s coalesce=%s\n",
        $key,
        var_export(isset($data[$key]), true),
        var_export(array_key_exists($key, $data), true),
        var_export($data[$key] ?? 'fallback', true)
    );
}
```

```
full   isset=true  exists=true  coalesce='x'
empty  isset=false exists=true  coalesce='fallback'
zero   isset=true  exists=true  coalesce=0
absent isset=false exists=false coalesce='fallback'
```

The `empty` line shows the distinction: the key exists, and the value is `null`. `isset` treats it as missing, `array_key_exists` treats it as present, and `??` supplies the fallback. On the `zero` line, all three accept the value as present; `0` is not an empty value. On the `absent` line, where the key truly does not exist, `isset` and `array_key_exists` both give `false`. All three queries returned a `bool` or a fallback value without raising a warning; code that wants to hear about a missing key from a warning must choose the plain read, and the plain read is measured in the next lesson.

## The Bounding Measurement

Normalization is not applied to every string. To measure which strings are converted to an integer key, string keys can be tried one by one: each is made the key of a one-element array, and `array_key_first` reports the type of that key.

```php
<?php
// bounds.php — which string keys are converted to integers
$candidates = ['0', '1', '-1', '01', '1.5', '-0', ' 1'];

foreach ($candidates as $candidate) {
    $single = [$candidate => 'x'];
    $key = array_key_first($single);
    printf("%-6s%s\n", "'" . $candidate . "'", get_debug_type($key));
}

$two = ['1' => 'a', '01' => 'b'];
echo 'two spellings, element count: ', count($two), "\n";
```

```
'0'   int
'1'   int
'-1'  int
'01'  string
'1.5' string
'-0'  string
' 1'  string
two spellings, element count: 2
```

The rule is read from the output: only strings in **decimal, canonical integer form** are converted to an integer key. `'1'` and `'-1'` become integers; strings with a leading zero (`'01'`), a decimal point (`'1.5'`), a minus-signed zero (`'-0'`) or a space (`' 1'`) stay strings. This distinction draws the limit of the silence in the previous section: if `'1'` and `1` are the same key, `'01'` is a different key, and the two spellings on the last line leave two elements this time. Normalization is not a rule of "merging whatever writes the same number differently"; it is a rule of "converting the canonical spelling to an integer". A data source that writes the same number in two forms produces two separate elements in the array, and that too is reported by no signal.

**AS5.** The types and keys in this section are the result of the language's own rule and rest on a class, not on a single number. The seven candidates measured illustrate the class the language documents and do not claim to be exhaustive. If a string key is not converted to an integer, the reason is that the string is not in canonical decimal form; the lesson does not move on to other bases or to very large integers.

## Summary

- In PHP, `array` is a single structure: an array whose keys are consecutive integers reads as indexed, and one with string keys reads as associative; order is insertion order.
- `unset` does not renumber keys; an array that looks indexed does not report whether it is still indexed unless it is tested with `array_is_list`.
- `'1'`, `1` and `true` normalize to a single key and one element remains in the array; the measuring channel of this lesson is **silent** (`key normalization`).
- A decimal key such as `1.7` is truncated and `E_DEPRECATED` is reported; a conversion that loses information is heard through the **warning** channel.
- Normalization applies only to strings in decimal, canonical integer form; `'01'`, `'1.5'` and `'-0'` stay strings.

## Next Step

This lesson measured the keys of a single-level array. When an element of an array is itself another array, the key is no longer a single value but a path, and if one link of the path is missing, `null` arrives. The Multidimensional Arrays lesson measures which channel a missing inner key is heard through, and why writing to the same path is heard through a different channel.
