PHP / Basic: values and request foundations / Lesson 3
Arrays, control flow, and explicit absence
Use PHP arrays as ordered maps and distinguish record fields from lists. Combine strict decisions and bounded iteration without hiding missing keys or changing list shape accidentally.
On this page
Lesson goals
- Explain ordered-map keys and list representations.
- Distinguish missing keys from null values.
- Trace loops, strict conditions, and accumulator invariants.
One array type serves several data models
A PHP array is an ordered map whose keys are integers or strings under documented conversion rules. A list is a useful narrower shape with consecutive integer keys starting at zero; a record associates named fields with values. Keep those contracts separate even though the runtime uses one array type. External numeric-looking keys and key collisions need thought rather than assumptions borrowed from JavaScript objects. For stable record access, validate required keys and types once. For list APIs, preserve order and reindex when operations leave gaps that would change JSON representation.
Presence and non-nullness are different questions
array_key_exists reports whether a key exists even when its value is null. isset is false for a missing key or a null value, while null coalescing provides a fallback for those cases without an undefined-key warning. Choose according to the contract. A nullable optional note differs from a required quantity; replacing every missing field with zero can make malformed requests look valid. The example makes the distinction observable without emitting warnings. Never use error suppression merely to make an unknown data shape convenient; repair the boundary that promised the shape.
<?php
declare(strict_types=1);
$record = ["note" => null, "count" => 0];
echo (array_key_exists("note", $record) ? "present" : "missing"), PHP_EOL;
echo (isset($record["note"]) ? "set" : "null-or-missing"), PHP_EOL;
echo $record["count"] ?? 10, PHP_EOL;
present
null-or-missing
0
The note exists but is null. The count remains zero rather than receiving the fallback.
Decisions should reflect normalized domain values
Strict equality compares type as well as value, avoiding loose-comparison surprises at request boundaries. Normalize and validate first rather than scattering casts inside conditions. match expressions, available since PHP 8.0, use strict comparison and return a value; an unmatched input without a default raises an error rather than silently doing nothing. if/elseif is useful when conditions are ranges or compound predicates. Keep rejection before mutation so a failed eligibility check does not consume a seat. Review boundary values and overlapping conditions rather than assuming indentation proves mutually exclusive behavior.
Filtering can preserve keys you no longer want
array_filter preserves original keys, so removing a middle list element can produce a non-list array. JSON encoding then uses an object representation for the resulting keys instead of the expected JSON list. Reindex with array_values when the public contract is a list and positions are not identifiers. Do not reindex an intentional identifier map merely to obtain prettier JSON. The example keeps title order but makes the representation choice explicit. Use a callback with an explicit predicate rather than default truthiness when zero, false, or empty strings can be valid retained values.
<?php
declare(strict_types=1);
$titles = ["DOM", "", "PDO"];
$filtered = array_filter($titles, fn(string $title): bool => $title !== "");
echo json_encode($filtered, JSON_THROW_ON_ERROR), PHP_EOL;
echo json_encode(array_values($filtered), JSON_THROW_ON_ERROR), PHP_EOL;
{"0":"DOM","2":"PDO"}
["DOM","PDO"]
The first output is an object because key one is absent. array_values establishes the intended list shape.
Make progress and mutation visible in loops
foreach is natural for array values or key/value pairs. Accumulators should have an initial value and a simple invariant, such as total equals the sum of processed integer amounts. A by-reference foreach leaves the loop variable referencing an element unless deliberately released, which can surprise later assignments; prefer ordinary iteration when mutation is unnecessary. Bound user-driven loops and avoid altering the collection in ways that make later visits unclear. Early return or continue can simplify rejected-item handling, but document whether one invalid record rejects the whole operation or is collected as a separate error.
Common errors and fixes
A null field is mistaken for a missing key
- Why it happens
- isset is used for a presence-only contract.
- How to fix it
- Use array_key_exists when explicit null must remain distinguishable.
- Prevent it next time
- Test absent, null, zero, and ordinary values.
A JSON list becomes an object unexpectedly
- Why it happens
- Filtering preserved nonconsecutive array keys.
- How to fix it
- Apply array_values for an intended list after filtering.
- Prevent it next time
- Assert output shape as well as item values.
Later assignments change the last loop item
- Why it happens
- A by-reference foreach variable remains aliased.
- How to fix it
- Avoid reference iteration or unset the reference deliberately.
- Prevent it next time
- Review alias lifetime and test a following assignment.
Check your understanding
1. A record contains ["count"=>0]. What should count ?? 10 return?
Show worked solution
It returns zero because the key exists and its value is not null. A truthiness-based fallback would answer a different question. Validate count as an allowed integer separately; coalescing does not establish that negative or string values are acceptable.
2. Filter the empty title from ["A","","B"] for a JSON list.
Show worked solution
Use an explicit nonempty-string predicate, then array_values before JSON encoding. The expected JSON is ["A","B"]. If original numeric keys were external identifiers, keep a map instead and document that different representation rather than silently discarding identity.
Key takeaways
- Array shape and absence are part of the public contract.
- Normalize before strict decisions and make iteration ownership clear.