skip to content

Scalar & Compound Types

PHP's type system at runtime: juggling and casts, declarations under coercive or strict mode, union and DNF types, and number precision. Interviewers probe it because coercion hides bugs.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

21

In PHP, what does a nullable type such as ?Customer mean, and is it the same as Customer|null or an optional parameter?

level: juniorimportance: must knowfreq 62%

answer

  1. question mark before one type
  2. ?T and T|null are identical
  3. nullable is not optional
  4. return null; explicitly, not return;
  5. T $x = null deprecated in 8.4

basics

~20 s

?Customer accepts a Customer object or null, and is exactly the same type as Customer|null. Nullable does not mean optional: the argument must still be passed unless the parameter also has a default, written ?Customer $c = null.

solid answer

~40 s

A leading `?` (PHP 7.1) marks a **single** type as nullable: `?Customer` accepts a `Customer` or `null`, and the manual states that `?T` and `T|null` are identical. The `?` cannot prefix a union — `?int|string` is a syntax error, so you write `int|string|null`. **Nullable is not optional**: `function assign(?Customer $c)` still requires an argument, and calling it with none throws `ArgumentCountError`; `?Customer $c = null` is both. On a return type, `?Customer` requires an explicit `return null;` — a bare `return;` does not compile, and falling off the end throws `TypeError`. The old shortcut `Customer $c = null`, which made the type nullable implicitly, is deprecated since PHP 8.4.

code

php · 20 lines
php
<?php
declare(strict_types=1);

final class Customer
{
    public function __construct(public string $name) {}
}

function greet(?Customer $customer): string   // nullable, but required
{
    return 'Hello, ' . ($customer?->name ?? 'guest');
}

echo greet(null), PHP_EOL;                    // Hello, guest

try {
    echo greet();
} catch (ArgumentCountError $e) {
    echo 'missing argument', PHP_EOL;         // nullable is not optional
}

go deeper

for a junior

Recall that ?Customer accepts a Customer or null, that it equals Customer|null, and that you still must pass the argument unless there is a default.

for a middle

Explain why ? works on one type only, the explicit return null rule for nullable returns, and why PHP 8.4 deprecated the implicit T $x = null form.

for a senior

Decide where null is a legitimate result and where absence should throw, since every nullable return pushes a check onto every caller.

for a principal

Set a codebase convention for representing absence — nullable returns, exceptions or dedicated result objects — and keep it consistent across modules.

## What the question mark does In a PHP type declaration, a leading `?` means **"this type, or null"**. It was added in PHP 7.1, before PHP had general union types: ```php <?php declare(strict_types=1); function describe(?Customer $customer): string { return $customer === null ? 'guest' : $customer->name; } ``` `describe(new Customer(...))` and `describe(null)` both work; `describe('bob')` throws `TypeError`. ## ?T and T|null are the same type PHP 8.0 added union types, and with them a second spelling. The manual says it directly: `?T` and `T|null` are identical. Reflection, error messages and the engine treat them as one type. Which spelling to use is style: - `?Customer` is shorter and the common choice for one type plus null. - `int|string|null` is **required** when more than one non-null type is involved, because `?` applies only to a single type. `?int|string` does not parse. - For an intersection plus null, PHP 8.2+ needs the DNF form `(A&B)|null`; `?` cannot prefix an intersection either. Two spellings are rejected outright: `?mixed` (mixed already includes null) and `?null`. ## Nullable is not optional This is the part candidates most often blur. **Nullable** is about which values are accepted; **optional** is about whether the argument may be omitted. They are independent: | Declaration | `f(null)` | `f()` | |---|---|---| | `function f(?Customer $c)` | accepted | `ArgumentCountError` | | `function f(?Customer $c = null)` | accepted | accepted, `$c` is `null` | | `function f(Customer $c)` | `TypeError` | `ArgumentCountError` | | `function f(Customer $c = null)` | accepted (declaration deprecated since 8.4) | accepted, `$c` is `null` | The last row is the old implicit form: a `null` default silently widened the type to nullable. PHP 8.4 deprecated it because the declared type no longer says what the parameter accepts. The fix is to write the `?` explicitly. ## Nullable return types On a return type, `?Customer` means the function returns a `Customer` or `null` — but it must **say** which: 1. `return null;` is fine. 2. A bare `return;` is a compile error: `A function with return type must return a value (did you mean "return null;" instead of "return;"?)`. 3. Reaching the end of the body without any `return` throws `TypeError` at runtime (`Return value must be of type ?Customer, none returned`). PHP does not treat "no return" as "return null" for a declared return type. The rule exists to separate a deliberate "not found" from a forgotten return. ## When to use a nullable type - **Lookups that can miss**: `findCustomer(int $id): ?Customer` returns `null` for "no such customer". The caller must handle it, and the nullsafe operator `?->` or an explicit check makes that visible. - **Truly optional inputs**: `?string $note = null` for an argument that may be absent. - **Avoid it where absence is an error.** If a missing customer is a bug, return `Customer` and throw; a nullable return pushes a check onto every caller. ## Handling a nullable value Once a value may be `null`, the code that receives it has to deal with that before using it as an object: - an explicit check: `if ($customer === null) { ... }`; - the **nullsafe operator** (PHP 8.0): `$customer?->name` evaluates to `null` instead of throwing when `$customer` is `null`; - the **null coalescing operator**: `$customer?->name ?? 'guest'` supplies a fallback. Calling a method directly on a `null` value, `$customer->name()`, throws an `Error`. That is the practical cost of a nullable type: every consumer carries a branch, which is why it should mark genuine absence rather than serve as a convenient default. ## Common mistakes 1. Treating `?Customer $c` as optional and then calling the function without an argument. 2. Writing `?int|string` and getting a syntax error, instead of `int|string|null`. 3. Returning with a bare `return;` from a `?Customer` function, which does not compile. 4. Keeping `Customer $c = null` in new code, which raises a deprecation on PHP 8.4 and later. ## Quick reference - `?T` = `T|null`, since PHP 7.1; the union spelling since 8.0. - `?` works on one type only. - Nullable ≠ optional; add `= null` for optional. - `T $x = null` without `?` is deprecated since PHP 8.4. - A `?T` return needs an explicit `return null;`.

  • In PHP, how do you declare a parameter that accepts an int, a string or null?
    Write the union with `null` as a member: `int|string|null $key`. The `?` shorthand applies to one type only, so `?int|string` is a syntax error. If the argument may also be omitted, add a default: `int|string|null $key = null`.
  • In PHP, what happens when a function declared : ?Customer reaches the end of its body without a return statement?
    It throws `TypeError` saying the return value must be of type `?Customer` and none was returned. A declared return type is never satisfied by an implicit return, even when `null` is allowed. Write `return null;` for the not-found path; a bare `return;` would not compile.

saying these in an interview costs you the question

  • Says ?Customer makes the parameter optional, so it can be left out.
  • Thinks ?Customer and Customer|null behave differently at runtime.
  • Writes ?int|string for an int, a string or null.
  • Uses a bare return; to return null from a function declared ?Customer.
  • Recommends Customer $c = null as the normal way to declare a nullable parameter.
open as a page

In PHP, what does declare(strict_types=1) change about how scalar parameter and return type declarations are checked?

level: juniorimportance: must knowfreq 74%

basics

~20 s

By default PHP coerces a wrong-typed scalar into the declared type when it can, so "12" passed to an int parameter becomes 12. With declare(strict_types=1) the value must already match, or a TypeError is thrown; only int-to-float widening survives.

open as a page

In PHP, which values convert to false in a boolean context, and why does the string '0' cause bugs?

level: juniorimportance: must knowfreq 58%

basics

~20 s

false, 0, 0.0, -0.0, '', '0', an empty array and null are falsy; everything else is true. '0' is the trap: a real value such as a zero quantity or the name '0' fails an if check.

open as a page

In PHP, what does an (int) cast return for strings such as '42', ' 42 ', '42abc', '1e3', '0x1A' and 'abc'?

level: juniorimportance: must knowfreq 64%

basics

~20 s

(int) takes the leading numeric part of a string and never fails: '42', ' 42 ' and '42abc' give 42, '1e3' gives 1000, while '0x1A' and 'abc' give 0. No warning is raised for the leading-numeric or non-numeric cases.

open as a page

In PHP, why is 0.1 + 0.2 == 0.3 false, and how should you compare two floats?

level: juniorimportance: must knowfreq 64%

basics

~20 s

PHP floats are IEEE 754 binary doubles, and 0.1, 0.2 and 0.3 have no exact binary form, so the sum is 0.30000000000000004. Compare floats with a tolerance, abs($a - $b) < $epsilon, or avoid floats where exact decimals matter.

open as a page

In PHP, when a strict_types file declares a function and a file without strict_types calls it, which checking mode applies?

level: middleimportance: must knowfreq 58%

basics

~20 s

Coercive mode applies: for arguments, PHP uses the mode of the file that makes the call, not the file that declares the function. Return values are checked in the declaring file's mode, and callbacks invoked by built-in functions are always coercive.

open as a page

In PHP 8, what does mixed accept, how does it differ from no type, and what are the null, false, true, iterable and callable types?

level: middleimportance: should knowfreq 30%

basics

~20 s

mixed (PHP 8.0) accepts every value including null, like an untyped parameter, but states the intent explicitly and, as a return type, requires an actual return. null, false and true are literal types usable alone since 8.2; iterable means array|Traversable; callable cannot type a property.

open as a page

In PHP 8, how do you type a CRM function taking a customer id or a Customer object, and what rules govern union types?

level: middleimportance: should knowfreq 44%

basics

~20 s

Declare 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.

open as a page

In PHP 8, how do built-in functions such as strlen() handle wrong-typed and null arguments in coercive and strict mode?

level: middleimportance: should knowfreq 34%

basics

~20 s

Since PHP 8.0 built-in functions check arguments against their declared types: a value that cannot be accepted throws TypeError. In coercive mode null passed to a non-nullable scalar parameter still works but is deprecated since 8.1; in strict mode it is a TypeError.

open as a page

In PHP 8.1 and later, what is the difference between the void and never return types, and when is each used?

level: middleimportance: should knowfreq 46%

basics

~20 s

void means the function returns normally but hands back no value, so callers see null. never, added in PHP 8.1, means the function does not return at all: it must throw, exit or loop forever, and falling off its end throws TypeError.

open as a page

In PHP 8, what happens when arithmetic uses a numeric string, a leading-numeric string such as '5 boxes', a non-numeric string or an array?

level: middleimportance: should knowfreq 44%

basics

~20 s

A numeric string behaves as a number silently. A leading-numeric string like '5 boxes' uses its numeric prefix and raises E_WARNING 'A non-numeric value encountered'. A non-numeric string, an array or a plain object throws TypeError 'Unsupported operand types'.

open as a page

In PHP, how do intval(), settype() and the (array) cast differ from a plain (int) cast?

level: middleimportance: should knowfreq 24%

basics

~20 s

intval() equals (int) but takes a base, so intval('1A', 16) is 26. settype() converts a variable in place and returns true. (array) wraps a scalar in a one-element array, turns null into [] and lists an object's properties.

open as a page

In PHP, what happens when an integer calculation exceeds PHP_INT_MAX, and how do you work with integers that large?

level: middleimportance: should knowfreq 40%

basics

~20 s

PHP does not throw or wrap on integer overflow: the result silently becomes a float. On 64-bit builds PHP_INT_MAX is 9223372036854775807, and floats above 2^53 cannot represent every integer, so precision is lost; use GMP or bcmath strings for larger values.

open as a page

In PHP 8.4+, how does round() handle halves by default, and what does the RoundingMode enum add?

level: middleimportance: should knowfreq 30%

basics

~20 s

round() rounds halves away from zero by default, so round(2.5) is 3.0 and round(-2.5) is -3.0, and it always returns a float. PHP 8.4 added the RoundingMode enum: the four half modes plus TowardsZero, AwayFromZero, PositiveInfinity and NegativeInfinity.

open as a page

PHP 8.4 deprecated parameters like Customer $customer = null; why, and how do you clean them up across a large CRM codebase?

level: seniorimportance: should knowfreq 28%

basics

~20 s

A null default used to make the type nullable silently, so Customer $customer = null accepted null while the type said Customer. PHP 8.4 deprecates that; fix each site by writing ?Customer or Customer|null explicitly, found with a static scan rather than runtime logs.

open as a page

Your team is adding declare(strict_types=1) file by file to a legacy PHP library-fines calculator; what breaks, and how do you roll it out safely?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Flipping a file makes its own calls and return checks strict, so request strings passed to int parameters, numeric strings passed to built-ins and round()'s float returned as int throw TypeError. Add types first, validate input at the boundary, then flip files under tests.

open as a page

A warehouse CSV import stores (int) $row['qty'], so '1,200' becomes 1 and 'n/a' becomes 0. How do you parse quantities safely in PHP 8?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Validate before converting: filter_var($raw, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]) returns an int or false, so '1,200' and 'n/a' are rejected instead of becoming 1 and 0. Normalise known formats like thousands separators explicitly, and report bad rows.

open as a page

A PHP point-of-sale system's float currency totals drift by a cent; what causes it, and how do you fix the money arithmetic?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Floats cannot store most decimal prices exactly, so sums, tax multiplications and (int) conversions drift by a cent. Keep money as integer cents or as bcmath decimal strings (BcMath\Number since PHP 8.4), and round once, at a defined step, with an explicit RoundingMode.

open as a page

In PHP 8.5, which cast spellings are deprecated or removed, and which new warnings can an explicit cast now raise?

level: middleimportance: nice to knowfreq 16%

basics

~20 s

PHP 8.5 deprecates the casts (integer), (boolean), (double) and (binary); (real) and (unset) were removed in 8.0. Casting an out-of-range float to int, or NAN, now warns. The new (void) cast discards a value, converting nothing.

open as a page

In PHP, how do intdiv(), the % operator, fmod() and fdiv() differ in result type, sign and division by zero?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

intdiv() and % work on ints, truncate toward zero and throw DivisionByZeroError for a zero divisor. fmod() gives a float remainder and returns NAN for zero. fdiv() (PHP 8.0) performs IEEE float division, returning INF, -INF or NAN instead of throwing.

open as a page

In PHP 8.2+, what does a DNF type such as (HasEmail&HasConsent)|null express, and which intersection and DNF forms does PHP reject?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

A&B, an intersection type (PHP 8.1), accepts only objects that are instances of every listed class or interface. DNF types (PHP 8.2) combine them with unions as an OR of parenthesised ANDs, so (HasEmail&HasConsent)|null means an object implementing both interfaces, or null.

open as a page