Algorithms And Data Structures
PHP Arrays, Maps, And Sets
PHP arrays are ordered maps that can act as lists, dictionaries, sets, stacks, or queues, but those roles have different invariants and costs.
Why This Matters
Choosing an explicit data shape makes code easier to review and can replace repeated scans with direct membership or lookup operations.
Working Model
Use sequential integer keys for list semantics, meaningful keys for maps, and keys with boolean-like values for sets. Preserve ordering only when it is part of the requirement.
Practical Rules
- Normalize keys at boundaries.
- Use
array_is_list()when shape matters. - Use
array_key_exists()when null is a valid stored value. - Build lookup maps once for repeated access.
- Document duplicate-key policy.
Failure Modes
- Confusing a missing key with a null value.
- Using
in_array()repeatedly for large membership tests. - Silently overwriting duplicate identifiers.
- Depending on incidental insertion order.
Verification
- Test empty and duplicate input.
- Test string and integer key distinctions.
- Measure memory for large maps.
- Assert the intended list or map shape.
What You Should Be Able To Do
After this lesson, you should be able to explain how PHP arrays represent lists, maps, and sets and when each shape is appropriate, choose a suitable approach for a real PHP project, and verify the result instead of relying on assumptions.
One Type, Several Roles
PHP's array type is flexible enough to represent several different structures, but the program still needs to choose one role at a time. A list is ordered by position. A map is addressed by key. A set answers membership questions. A stack or queue controls where values enter and leave. PHP lets all of those shapes use the same underlying type, so the discipline has to come from naming, boundary checks, and tests.
The danger is not that PHP arrays are weak. The danger is that an array can silently drift from one role into another. A value that begins as a list of rows can become a map after one assignment with a string key. A list with missing integer keys may still print like a sequence, but JSON encoding may treat it differently from a dense list. A map that stores null values can be misread as missing data if the code uses the wrong existence check. These are design problems, not syntax problems.
A good PHP developer therefore describes the intended shape before choosing operations. array<int, User> means the order and numeric positions matter. array<string, User> means the key identifies the user. array<string, true> can represent a set of identifiers. Those comments or PHPDoc annotations do not enforce every rule at runtime, but they give reviewers a concrete claim to verify.
Lists
A list is a sequence with consecutive integer keys starting at zero. Lists are useful when order is part of the meaning: rows in a CSV file, validation messages in display order, middleware in execution order, or search results sorted by relevance. PHP provides array_is_list() to check this shape when it matters.
List operations should preserve list semantics. Appending with $items[] = $value is clear. Removing an element with unset($items[$index]) leaves a gap, so the result is no longer a dense list until it is reindexed with array_values(). That distinction matters when the array is serialized to JSON or handed to code that expects positions to be consecutive.
<?php
declare(strict_types=1);
$names = ['Ada', 'Grace', 'Linus'];
unset($names[1]);
echo array_is_list($names) ? 'list' : 'not list';
echo PHP_EOL;
$names = array_values($names);
echo implode(', ', $names) . PHP_EOL;
// Prints:
// not list
// Ada, Linus
The example is small, but it shows an important rule: deletion policy is part of the data shape. If positions are identifiers, reindexing is wrong because it changes meaning. If positions are only presentation order, reindexing after deletion may be correct.
Maps
A map associates a key with a value. In application code, maps are useful when lookup by identifier is more important than position. Turning a list of products into $productsBySku can replace repeated scans with direct access. Turning user permissions into $permissionByName makes membership checks readable and usually faster for repeated lookups.
Map keys need boundary normalization. External input may contain different casing, whitespace, numeric strings, or alternative spellings. PHP array keys also have conversion rules: integer keys and numeric string keys can interact in ways that surprise code that treats all external identifiers as plain strings. If a key comes from a request, CSV, database, or provider, normalize it before insertion and use the same normalization before lookup.
The existence check matters. isset($map[$key]) returns false when the key exists but the stored value is null. array_key_exists($key, $map) checks whether the key exists regardless of the value. Use the function that matches the meaning of the map. If null means known but not filled, isset() is the wrong test.
<?php
declare(strict_types=1);
$middleNames = [
'ada' => null,
];
echo isset($middleNames['ada']) ? 'set' : 'not set';
echo PHP_EOL;
echo array_key_exists('ada', $middleNames) ? 'known' : 'missing';
echo PHP_EOL;
// Prints:
// not set
// known
Duplicate keys also need a policy. If two rows contain the same SKU, should the importer reject the file, keep the first row, keep the last row, or merge fields? PHP will happily overwrite a previous value when the same key is assigned again. Silent overwrite is rarely acceptable unless it is documented and tested.
Sets
PHP does not have a dedicated built-in set type for normal application arrays, but a map can represent one. Store the normalized member as the key and a simple value such as true as the value. Membership then becomes a key lookup instead of a repeated linear scan.
<?php
declare(strict_types=1);
$allowedRoles = ['admin', 'editor', 'viewer'];
$allowedRoleSet = [];
foreach ($allowedRoles as $role) {
$allowedRoleSet[$role] = true;
}
echo isset($allowedRoleSet['editor']) ? 'allowed' : 'blocked';
echo PHP_EOL;
// Prints:
// allowed
This is useful when the same membership test happens many times. It may be unnecessary for one tiny check. Building the set has a cost in time and memory, so the improvement appears when the set is reused or the input is large enough that repeated scans matter.
A set should still document normalization. Are email addresses lowercased? Are tags trimmed? Are Unicode forms normalized? Are numeric identifiers stored as integers or strings? The set is only as reliable as its member identity rule.
Stacks And Queues
Arrays can also act as stacks and queues. A stack is last-in, first-out: push onto the end and pop from the end. A queue is first-in, first-out: add to one side and remove from the other. The role matters because different operations have different costs and different meanings.
For small in-memory workflows, array operations can be enough. For large or long-running queues, a database table, message queue, or specialized structure may be more appropriate because the workflow needs durability, visibility, retries, or coordination across processes. The fact that a PHP array can model a queue does not mean it is the right queue for background jobs.
When using an array as a queue inside one request, avoid mixing queue operations with random key assignments. The invariant is that items are processed in arrival order. Once unrelated keys are introduced, the data is no longer communicating that invariant clearly.
Serialization And Boundaries
Array shape becomes especially important at boundaries. JSON distinguishes arrays from objects. A dense PHP list normally serializes as a JSON array. A PHP array with non-consecutive numeric keys or string keys serializes as a JSON object. That can break clients even if the PHP code still loops over the values successfully.
Database boundaries have a different concern. A result set is often naturally a list because row order comes from the query. If the application immediately needs lookup by primary key, convert the list into a map in one clear place and name it accordingly. Do not leave later code guessing whether $users is a list of users or a map of users by ID.
Request boundaries need validation. A JSON object submitted by a client should not be accepted as a list merely because PHP can iterate over it. If order and position are part of the contract, check the shape and reject mismatches early. If keys are part of the contract, validate their format before using them as map keys.
Performance Tradeoffs
Repeated in_array() calls over a large list are a common signal that the code wants a set. Repeated array_filter() calls to find rows by ID are a signal that the code wants a map. These changes improve growth by doing one indexing pass and then using direct lookups.
The tradeoff is memory and policy. A map duplicates references to values and may retain more data than a streaming algorithm would. A set loses ordering unless you keep a separate list. A map collapses duplicate keys unless you store a list per key or reject duplicates. Do not hide those choices inside a clever helper name. Make them visible in the calling code or in the helper contract.
For small inputs, readability may dominate. A direct scan through five configured values is often clearer than a precomputed set. For request paths that handle user-controlled or business-growth input, prefer a shape that scales with the actual operation.
Verification And Review
Tests should prove the intended shape, not only the happy result. For lists, test empty lists, single-item lists, deletion, reindexing, and JSON output when the array crosses an API boundary. For maps, test missing keys, present keys with null values, duplicate input, and normalized lookup. For sets, test membership, non-membership, casing or whitespace rules, and repeated lookup behavior.
Review variable names. $items tells very little. $itemsById, $allowedSkuSet, $orderedSteps, and $queuedJobs communicate the role. The name is not a substitute for tests, but it catches drift early because wrong operations look strange against a precise name.
After this lesson, you should be able to choose whether a PHP array is acting as a list, map, set, stack, or queue; state the invariant that belongs to that role; normalize keys at boundaries; avoid repeated scans when a lookup structure is justified; and verify that serialization, duplicates, and missing values behave deliberately.
Practice
Practice: Build A Lookup Map
Transform product rows into an ID-keyed lookup while detecting duplicate IDs.
Your answer must:
- state the intended outcome;
- show the commands, data flow, or implementation shape;
- identify at least one unsafe alternative;
- explain how the result will be verified.
Show solution
Iterate once, reject or record duplicates explicitly, and store each row under a normalized string ID. Verify lookup and duplicate behavior.
The important part is not memorising one command or vendor screen. The solution makes the invariant, failure behavior, and verification evidence explicit.
Practice: Replace Membership Scans
Replace repeated role-list scans with a set.
Your answer must:
- state the intended outcome;
- show the commands, data flow, or implementation shape;
- identify at least one unsafe alternative;
- explain how the result will be verified.
Show solution
Build a set such as $roles[$role] = true, then use isset() for membership when null is not stored. Keep normalization and case policy explicit.
The important part is not memorising one command or vendor screen. The solution makes the invariant, failure behavior, and verification evidence explicit.
Practice: Preserve List Shape
Filter API rows without accidentally returning JSON object keys.
Your answer must:
- state the intended outcome;
- show the commands, data flow, or implementation shape;
- identify at least one unsafe alternative;
- explain how the result will be verified.
Show solution
Use array_values() after filtering when the contract requires a JSON list, then assert array_is_list() before encoding.
The important part is not memorising one command or vendor screen. The solution makes the invariant, failure behavior, and verification evidence explicit.