PHP Operators: Arithmetic to Null-Coalescing Assignment
Master every category of PHP operator — arithmetic, assignment, comparison, spaceship, logical, bitwise, string, array, null-coalescing, and ternary — along with how precedence and associativity decide what a mixed expression actually evaluates to.
An operator is a symbol that tells PHP to perform a specific operation on one or more values (its operands). PHP has an unusually rich operator set compared to many languages — it includes not just the arithmetic and comparison operators you'd expect, but also array operators, a dedicated spaceship operator for sorting, and two null-coalescing operators built specifically for PHP's very common 'this might not be set' data shapes. Reading operator-heavy code fluently means knowing both what each operator does in isolation and how PHP resolves an expression that combines several of them without parentheses.
Arithmetic, Assignment, and Comparison
The arithmetic operators are +, -, *, /, % (modulo, the remainder after division), and ** (exponentiation, added in PHP 5.6). Division with / always produces a float unless both operands are integers that divide evenly, in which case PHP still promotes the result to float only when there's a remainder — 10 / 2 is int(5)... actually PHP's / always returns float unless the result is a whole number that fits, so in practice you should not rely on the return type of / at all and cast explicitly with intdiv() when you need guaranteed integer division, since intdiv(10, 3) gives 3 while 10 / 3 gives 3.3333333333333.
Assignment starts with the plain = operator, which copies a value (or, for objects, copies a handle to the same object) into a variable. Every arithmetic and string operator has a compound assignment shorthand that combines the operation with the assignment: +=, -=, *=, /=, %=, **=, and .= for string concatenation. $total += $price; is exactly equivalent to $total = $total + $price;, just shorter and, once you're used to reading it, clearer about intent.
Comparison operators split into loose and strict families. == and != compare values after type juggling — coercing operands to a common type before comparing — which is the source of PHP's infamous surprises like '0' == false being true and 'abc' == 0 being false in modern PHP (this exact case was fixed in PHP 8: string-to-number comparisons now compare as strings when the string isn't numeric). === and !== compare both value and type with no coercion at all, and should be your default in almost all application code. Ordering comparisons <, >, <=, >= follow the same loose-comparison rules as == when the operand types differ.
• intdiv(10, 3) === 3 — guaranteed integer division, throws on divide-by-zero. • 2 ** 10 === 1024 — exponentiation operator. • $count += 1; is shorthand for $count = $count + 1; • '10' == '1e1' is true (both are numeric strings compared numerically) but '10' === '1e1' is false (different string values). • Always prefer === and !== unless you have a specific, documented reason to allow type coercion.
Spaceship, Logical, and Bitwise Operators
The spaceship operator <=> (introduced in PHP 7) returns -1, 0, or 1 depending on whether the left operand is less than, equal to, or greater than the right — exactly the three-way result that sort callbacks need. Instead of writing if ($a < $b) return -1; elseif ($a > $b) return 1; else return 0; inside a usort() callback, you write return $a <=> $b; and get the identical behavior in one expression. It works on numbers, strings, and arrays (element by element), using the same comparison rules as the other comparison operators.
Logical operators come in two spellings with different precedence: the symbolic &&, ||, ! bind tightly (higher precedence, close to arithmetic), while the word forms and, or, xor bind extremely loosely — lower precedence than =. This is not a stylistic choice; $result = false or true; assigns false to $result (because = binds tighter than or), which surprises almost everyone the first time they see it. The safe, conventional rule is: always use && and || in conditional expressions, and reserve and/or for rare cases where you deliberately want that low-precedence control-flow behavior (like $file = fopen($path, 'r') or die('cannot open file')).
Bitwise operators (&, |, ^, ~, <<, >>) operate on the binary representation of integers. & (AND) and | (OR) are commonly used to combine flag constants — PHP's own standard library does this constantly, e.g. json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) combines two flag bits with | so the function can accept any combination of options through one integer parameter. << and >> shift bits left or right, effectively multiplying or dividing by powers of two.
• usort($items, fn($a, $b) => $a->price <=> $b->price); — spaceship in a sort callback. • $canEdit = $isAdmin && $isLoggedIn; — always use && / || in conditions, not and/or. • $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE; — combining flags with bitwise OR. • 1 << 3 === 8 — left shift multiplies by 2 per shifted bit.
String, Array, Null-Coalescing, Ternary, and Error-Control
The string concatenation operator is a single dot: ., and its compound form is .=. Unlike some languages, PHP does not overload + for string concatenation — '5' + '3' performs numeric addition and gives 8, while '5' . '3' concatenates and gives '53'; mixing these up is a common source of bugs when refactoring between numeric and string contexts. Array operators include + (union — merges arrays, but keeps the *left* array's value whenever a key exists in both, unlike array_merge() which lets the right array's value win for string keys) and the equality operators == (same key/value pairs, any order) and === (same key/value pairs in the same order with the same types).
The null-coalescing operator ?? (PHP 7+) returns its left operand if that operand exists and is not null, otherwise it returns the right operand — and critically, it does this without emitting a warning even if the left side is a completely undefined variable or array key, which is why $name = $data['name'] ?? 'Guest'; became the idiomatic replacement for verbose isset() checks. Its assignment form, ??= (PHP 7.4+), assigns the right-hand value only if the left-hand variable is currently null or unset: $cache['key'] ??= computeExpensiveValue(); computes the value only the first time.
The ternary operator $a ? $b : $c evaluates $a; if truthy it evaluates to $b, otherwise $c. PHP also has a 'short ternary' $a ?: $c, which evaluates to $a itself when $a is truthy (skipping the need to repeat it), or $c otherwise — this is different from ??, because ?: checks truthiness (so 0, '', and false all trigger the fallback) while ?? checks only for null/unset. Finally, the error-control operator @ suppresses error output from the expression it prefixes ($value = @$array['maybe_missing'];), but it is widely considered bad practice in modern PHP because it silences *all* errors, including genuine bugs, and has a real performance cost — reach for isset(), ??, or proper exception handling instead.
• '5' . '3' === '53' (concatenation) vs '5' + '3' === 8 (numeric addition, string is coerced). • [1, 2] + [0 => 9, 2 => 3] === [1, 2, 3] — array union keeps left values for overlapping keys. • $port = $config['port'] ?? 8080; — safe default without warnings. • $settings['theme'] ??= 'light'; — assign only if null/unset. • $label = $nickname ?: $fullName; — short ternary falls back on any falsy value, not just null.
Precedence and Associativity
When an expression mixes multiple operators without parentheses, PHP resolves it using a precedence table: operators higher in the table bind tighter and are evaluated first. From highest to lowest (a simplified but practically complete ordering): ** (exponentiation), unary !/-/~/@, instanceof, multiplicative * / %, additive + -, bitwise shifts << >>, concatenation . (note: since PHP 8, . was demoted below <</>> and now sits below additive but above shifts — always parenthesize mixed ./+ expressions to be safe), comparison < <= > >=, equality == != === !==, bitwise &, bitwise ^, bitwise |, logical &&, logical ||, null-coalescing ??, ternary ? :, assignment operators, and finally the low-precedence word operators and, xor, or.
Associativity decides how operators of the *same* precedence are grouped when there are several in a row. Most arithmetic operators are left-associative (10 - 3 - 2 is (10 - 3) - 2 = 5), but ` is right-associative (2 3 2 is 2 (3 2) = 512, not (2 3) ** 2 = 64), and the ternary operator's associativity was deliberately made non-associative in PHP 8 — chained ternaries without explicit parentheses ($a ? $b : $c ? $d : $e`) now throw a fatal parse error instead of silently picking a grouping, because the old left-associative behavior was a frequent source of bugs. The practical lesson is not to memorize the whole table but to parenthesize any expression where the grouping matters to a reader — clarity costs nothing at runtime.
• 2 + 3 * 4 === 14 — multiplication binds tighter than addition. • 2 3 2 === 512 — ** is right-associative. • Chained ternaries without parentheses are a fatal error in PHP 8+ — always write $a ? $b : ($c ? $d : $e). • $x = $y = 5; works because = is right-associative — $y is assigned first, then $x.
Operator families at a glance
Prefer === / !== and && / || as your defaults; reach for the loose/word variants only deliberately. ?? checks for null/unset; ?: (short ternary) checks for truthiness — they are not interchangeable. Parenthesize mixed-operator expressions for readability even when you know the precedence rules.
// arithmetic + compound assignment
$total = 0;
$total += 9.99;
$remainder = 17 % 5; // 2
$power = 2 ** 8; // 256
// comparison: strict vs loose
var_dump(0 == 'abc'); // false in PHP 8 (was true in PHP 7)
var_dump(0 === 'abc'); // false, always
// spaceship for sorting
usort($people, fn($a, $b) => $a['age'] <=> $b['age']);
// null-coalescing family
$name = $_GET['name'] ?? 'anonymous';
$cache['user'] ??= fetchUser($id);
// ternary vs short ternary
$status = $isActive ? 'on' : 'off';
$label = $customLabel ?: 'Untitled';Code Examples
The spaceship operator also works element-by-element on arrays, which lets you express a multi-key comparison in one line by comparing two small tuple arrays instead of writing a chain of if/elseif branches. Expected result: Keyboard: $45 (stock 12) Monitor: $45 (stock 3) Mouse: $25 (stock 0) return [$b['stock'] > 0, $a['price']] <=> [$a['stock'] > 0, $b['price']];
This is the single most common source of operator confusion in real PHP code: ?? only falls through to the default when the value is null or entirely unset, so a legitimate 0 is preserved. The short ternary ?: instead checks truthiness, so it discards a real 0 just like it would discard '' or false. Expected result: int(0) int(30) int(5)
Common mistake: Using ?: to supply a default for a numeric setting that could legitimately be 0 (like 'retries' => 0, meaning 'do not retry') silently overwrites that intentional value.
<?php
$products = [
['name' => 'Mouse', 'price' => 25, 'stock' => 0],
['name' => 'Keyboard', 'price' => 45, 'stock' => 12],
['name' => 'Monitor', 'price' => 45, 'stock' => 3],
];
usort($products, function ($a, $b) {
// primary: in-stock items first; secondary: cheaper first
return [$b['stock'] > 0, $a['price']] <=> [$a['stock'] > 0, $b['price']];
});
foreach ($products as $p) {
echo "{$p['name']}: \${$p['price']} (stock {$p['stock']})\n";
}<?php
$config = ['retries' => 0, 'timeout' => null];
$retries = $config['retries'] ?? 5; // 0, because 'retries' IS set and not null
$timeout = $config['timeout'] ?? 30; // 30, because 'timeout' is set but null
$label = $config['retries'] ?: 5; // 5, because ?: treats 0 as falsy too
var_dump($retries, $timeout, $label);Where ?? and ?: diverge
?? only reacts to null or a missing key; ?: reacts to any falsy value at all, including a legitimate 0, '', or false.
value: null unset key 0 / '' / false "real"
| | | |
$x ?? 'D' 'D' 'D' value kept value kept
$x ?: 'D' 'D' 'D' 'D' value keptImportant Rules
Default to === and !== for comparisons; only use == when type coercion is explicitly what you want.
Use && / || inside conditional expressions; never rely on the low-precedence and/or/xor for control flow.
Reach for ?? (and ??= for caching-style assignment) when a value might be null or unset; use ?: only when any falsy value should trigger the fallback.
Avoid the @ error-control operator in application code — use isset()/??, try/catch, or explicit checks instead.
Parenthesize any expression mixing more than two different operator families, even when you're confident about precedence.
Where this shows up
Every controller that reads request input leans on ?? for safe defaults, every comparator passed to usort()/uasort() leans on <=>, and every flag-accepting standard library call (json_encode, preg_match, array_filter with ARRAY_FILTER_USE_KEY) leans on bitwise | to combine option constants — these operators aren't academic, they're in nearly every file of a real PHP codebase.
$discount = $couponValue ?: 0; // couponValue of 0 (a valid 0% coupon) is silently replaced
$discount = $couponValue ?? 0; // only replaces null/unset, so a real 0 is preserved
?: treats 0 as falsy and substitutes the fallback, which is wrong whenever 0 is a meaningful, intentional value rather than 'missing data'. ?? is the correct choice whenever the concern is specifically 'is this null or unset', not 'is this falsy'.
if ($user = authenticate($creds) or die('failed')) if (!($user = authenticate($creds))) { die('failed'); } or has lower precedence than =, so $user = authenticate($creds) or die('failed') actually parses as ($user = authenticate($creds)) or die('failed') — which happens to work here, but relying on that subtlety is fragile; the explicit form is unambiguous to every future reader.
What does $result = false or true; assign to $result? A. true B. false C. It's a syntax error D. 1
B. false — or has lower precedence than =, so this parses as ($result = false) or true — $result is assigned false, and the or true part has no effect on it.
Given $data = ['count' => 0], what does $data['count'] ?? 10 evaluate to? A. 10 B. 0 C. null D. It throws an undefined key warning
B. 0 — ?? only falls back when the value is null or the key is unset. Here 'count' is set to 0, which is neither, so the operator returns the actual value 0.
What does the spaceship operator <=> return when its left operand is greater than its right operand? A. true B. -1 C. 1 D. 0
C. 1 — <=> returns -1 if left < right, 0 if equal, and 1 if left > right — the standard three-way comparison result expected by sort callbacks like usort().
Write a function normalizePage(array $query): int that reads $query['page'], returning 1 if the key is missing or null, but preserving an explicit value of 0 as 0 (do not treat 0 as 'missing'). Use the correct null-handling operator, not the short ternary.
function normalizePage(array $query): int { return $query['page'] ?? 1; }
?? checks specifically for null/unset — that's exactly the 'missing vs explicit 0' distinction this needs, unlike ?: which would also replace an explicit 0.