In PHP 8, how do you type a CRM function taking a customer id or a Customer object, and what rules govern union types?
answer
- A|B since PHP 8.0
- narrow with instanceof or is_int
- duplicates are compile errors
- bool|false and object|Customer rejected
- no class hierarchy check
basics
~20 sDeclare int|Customer $customer: a union type (PHP 8.0) accepts a value matching any member, and the body narrows it with instanceof. Unions reject duplicate or trivially redundant members at compile time and cannot contain mixed, void or never.
solid answer
~40 sUse a **union type**: `function resolve(int|Customer $customer): Customer`. PHP 8.0 checks the argument against every member and accepts it if any matches; inside, `if ($customer instanceof Customer)` narrows it, otherwise it is an `int` to look up. The compiler rejects what it can prove redundant **without loading classes**: `int|INT` (`Duplicate type int is redundant`), `bool|false`, `true|false` (use `bool`), `object|Customer`. It does not check class hierarchies, so `Customer|Person` compiles even if one extends the other. `mixed`, `void` and `never` cannot join a union, and `null` is written as a member, not with `?`. For scalar members in coercive mode, a value that matches no member exactly is converted in the order int, float, string, bool. The design cost is that every such function carries both id and object handling.
code
php · 7 lines<?php
// each line is a separate compile-time error in PHP 8.5
function a(int|string|INT $v) {} // Duplicate type int is redundant
function b(): true|false {} // bool must be used instead
function c(object|Customer $v) {} // object plus a class type is redundant
function d(mixed|int $v) {} // mixed only as a standalone typego deeper
Recall the A|B syntax from PHP 8.0 and that the function must check which type arrived, usually with instanceof or is_int.
Explain the compile-time redundancy rules, why class hierarchies are not checked, false as a union member, and the int, float, string, bool coercion order.
Judge whether a union belongs in the signature or whether resolving ids to objects at the boundary keeps the rest of the code simpler.
Set guidance for public APIs: where unions express genuine alternatives and where value objects or separate methods give callers a clearer contract.
## The declaration A **union type**, written `A|B`, declares that a value may be of any one of the listed types. PHP added general unions in **PHP 8.0**. For a CRM where some callers hold a loaded `Customer` and others only its id, the signature says exactly that: ```php <?php declare(strict_types=1); final class Customer { public function __construct(public readonly int $id, public string $name) {} } final class CustomerDirectory { private array $byId = []; public function add(Customer $customer): void { $this->byId[$customer->id] = $customer; } public function resolve(int|Customer $customer): Customer { if ($customer instanceof Customer) { return $customer; } return $this->byId[$customer] ?? throw new OutOfBoundsException("No customer $customer"); } } ``` ## How the check works - At call time PHP checks the value against **each member**; one match is enough. Member order has no meaning. - Inside the function the variable has whichever type arrived, so the body **narrows** it: `instanceof` for classes and interfaces, `is_int()`, `is_string()` and friends for scalars. - For **scalar members in coercive mode**: if the value's own type is in the union it is taken as is; otherwise PHP tries to convert it to `int`, then `float`, then `string`, then `bool`, choosing the first member present that accepts it. `resolve("42")` from a coercive file therefore passes `42`; from a strict file it throws `TypeError`. ## What the compiler rejects PHP runs a **redundancy check** on every union at compile time, limited to what it can prove without loading any class: | Declaration | Result | |---|---| | `int\|string\|INT` | `Duplicate type int is redundant` | | `bool\|false` | rejected: `false` is already in `bool` | | `true\|false` | `Type contains both true and false, bool must be used instead` | | `object\|Customer` | `... contains both object and a class type, which is redundant` | | `iterable\|array` | rejected: `iterable` already includes `array` | | `mixed\|int` | `Type mixed can only be used as a standalone type` | | `?int\|string` | syntax error: `?` applies to one type only | | `Customer\|Person`, where `Customer extends Person` | **compiles** — hierarchies are not checked | `void` and `never` cannot appear in a union either; they are standalone-only return types. ## false as a union member Many built-in functions return a value **or `false`** on failure, a convention older than exceptions: `strpos()` is declared `int|false`, `file_get_contents()` `string|false`, `array_search()` `int|string|false`. That is why `false` has been usable in unions since PHP 8.0 even though it is not a useful type on its own; since PHP 8.2 `false`, `true` and `null` may also stand alone. A caller must compare with `=== false`, because `0` is a valid `strpos()` result. ## Is the union the right design? A union type describes the contract precisely, but it also spreads a choice through the codebase: 1. **Every implementation narrows.** Each function that takes `int|Customer` repeats the "id or object" branch, and each can get it slightly wrong. 2. **Callers lose a signal.** A caller holding a `Customer` cannot tell whether the function will reload it by id anyway. 3. **Alternatives** are two explicit methods (`findById(int $id)` and a method taking `Customer`), resolving ids to objects once at the boundary, or a small `CustomerId` value object so the union disappears. A reasonable rule: accept a union at an API boundary where callers genuinely hold either form, resolve it once, and pass the resolved `Customer` inward. ## Common mistakes - **Assuming member order matters.** `int|Customer` and `Customer|int` are the same type; order changes nothing for objects, and for scalar coercion the fixed int, float, string, bool preference applies regardless of how the members are written. - **Forgetting to narrow.** Calling a `Customer` method on the value without an `instanceof` check throws an `Error` when an `int` arrived. - **Expecting hierarchy checks.** `Customer|Person` compiles; only a static analyser will point out that one member is redundant. - **Loose false checks.** `if (!strpos($haystack, $needle))` treats a match at position `0` as a miss; compare with `=== false`. - **Using `?` with a union.** `?int|Customer` does not parse; write `int|Customer|null`.
- In PHP 8, why does function f(Customer|Person $p) compile even when Customer extends Person?The redundancy check runs at compile time and only uses what it can prove without loading classes: duplicate names, `bool` with `false`, `object` with a class name. Whether `Customer` extends `Person` is only known once both classes are loaded, so the union is accepted. Static analysers are what flag this kind of redundancy.
- In PHP, why do so many built-in functions declare return types like int|false?They predate exceptions for ordinary failures and report failure by returning `false` instead of a value. PHP 8.0 made that honest in the signatures with `false` as a union member. The caller must use `=== false`, since a valid result such as `strpos()` returning `0` is falsy.
- In a coercive PHP file, what does a parameter declared int|float receive when passed the string "1.5"?The float `1.5`. When a string meets an `int|float` union in coercive mode, PHP uses numeric-string rules to pick the member: an integer-looking string becomes `int`, a decimal or exponent string becomes `float`. In a strict file the string is rejected with `TypeError`, since neither member is `string`.
saying these in an interview costs you the question
- Says member order in int|Customer decides which type is tried first for objects.
- Thinks PHP rejects Customer|Person when one class extends the other.
- Writes ?int|Customer to allow null in a union.
- Believes a union argument is automatically converted to the first member type.
- Compares strpos() results with == false instead of === false.