In PHP, why does $ok = validate($code) and redeem($code); leave $ok holding only the validation result, and which other precedence traps catch developers?
answer
- keywords bind looser than =
- && and || bind tighter than =
- nested ternary: compile error since 8.0
- == binds tighter than bitwise &
- ! wraps the whole instanceof test
basics
~20 sand, or and xor bind more loosely than =, so the line runs as ($ok = validate($code)) and redeem($code), discarding redeem's result. && and || bind tighter than =. Nested unparenthesised ternaries and == inside bitwise & are the other classic traps.
solid answer
~40 sPHP has two sets of logical operators that differ only in **precedence**. `&&` and `||` bind tighter than assignment; `and`, `or` and `xor` bind looser. So `$ok = validate($code) and redeem($code);` parses as `($ok = validate($code)) and redeem($code)`: `$ok` gets the validation result, `redeem()` runs only if that was truthy, and its return value is thrown away. `$f = false or true;` likewise leaves `$f` false. Other traps: since PHP 8.0 an unparenthesised nested ternary such as `$a ? $b : $c ? $d : $e` is a **compile-time error** (it was left-associative before, deprecated in 7.4); `==` binds tighter than `&`, so `$flags & MASK == MASK` tests `MASK == MASK`; and `??` binds looser than `.`. The fix everywhere is `&&`/`||` in expressions and explicit parentheses.
code
php · 9 lines<?php
function validate(string $c): bool { return $c === 'SPRING25'; }
function redeem(string $c): bool { return false; } // e.g. already used
$ok = validate('SPRING25') and redeem('SPRING25');
var_dump($ok); // bool(true): redeem() ran, its false was discarded
$ok = validate('SPRING25') && redeem('SPRING25');
var_dump($ok); // bool(false)go deeper
Remember that and, or and xor bind more loosely than =, so use && and || whenever the result is assigned or returned.
Walk through the precedence table for assignment, logical, ternary, equality and bitwise operators, and explain the PHP 8.0 changes to the ternary and concatenation.
Recognise these bugs in legacy code during a PHP 8 upgrade, explain why a silent success was reported, and make the fix a lint rule rather than a one-off patch.
Weigh banning the keyword operators across a code base against the or throw idiom, and decide how style rules enforce explicit parentheses.
## Two ANDs, two ORs PHP inherited both the symbolic logical operators `&&` and `||` and the keyword operators `and`, `or` and `xor`. They compute the same truth values and both short-circuit, but they sit at very different places in the **precedence table** (the rules that decide which operator grabs its operands first): | Operators (high to low, excerpt) | Associativity | |---|---| | `**` | right | | unary `-`, casts, `@` | n/a | | `instanceof` | left | | `!` | n/a | | `*` `/` `%` | left | | `+` `-` | left | | `.` | left | | `<` `<=` `>` `>=` | non-associative | | `==` `!=` `===` `!==` `<=>` | non-associative | | `&` | left | | `&&` | left | | `||` | left | | `??` | right | | `? :` | non-associative (since 8.0) | | `=` `+=` `??=` and other assignments | right | | `and` | left | | `xor` | left | | `or` | left | Because `=` sits **between** the two families, the same-looking lines behave differently: - `$ok = validate($code) && redeem($code);` stores the combined result. - `$ok = validate($code) and redeem($code);` stores only `validate()`'s result. `redeem()` still runs when validation passed, but a failed redemption leaves `$ok` true, and the checkout reports success. - `$f = false or true;` leaves `$f` as `false`; the manual shows exactly this example. ## The one idiom that relies on it The low precedence of `or` is why the old pattern `$x = something() or die('failed');` works: the value is assigned first, and the right side runs only when it was falsy. Since PHP 8.0 `throw` is an expression, so the modern form is: ```php $coupon = loadCoupon($code) or throw new DomainException('Unknown coupon'); ``` Many style guides still forbid the keyword operators outright, because a reader cannot tell whether the author meant the idiom or made the mistake. ## Nested ternaries Before PHP 8.0 the ternary operator was **left-associative**, unlike most C-family languages. The manual's example `true ? 'true' : false ? 't' : 'f'` grouped as `(true ? 'true' : false) ? 't' : 'f'` and printed `t`. PHP 7.4 deprecated relying on that, and PHP 8.0 made the ternary **non-associative**: the unparenthesised form now stops compilation with a fatal error: ```text Unparenthesized `a ? b : c ? d : e` is not supported. Use either `(a ? b : c) ? d : e` or `a ? b : (c ? d : e)` ``` The same compile error covers `a ? b : c ?: d` and `a ?: b ? c : d`. Chaining only short ternaries, `$a ?: $b ?: $c`, is allowed because both groupings give the same result. For multi-way choices, a `match` expression is usually clearer than nested ternaries. ## Comparison against bitwise operators Equality binds tighter than `&`, `^` and `|`. A flag test written as 1. `if ($flags & FLAG_ACTIVE == FLAG_ACTIVE)` is parsed as `$flags & (FLAG_ACTIVE == FLAG_ACTIVE)`, which is `$flags & true`; 2. the intended test needs `if (($flags & FLAG_ACTIVE) === FLAG_ACTIVE)`. ## instanceof and ! `instanceof` binds **tighter** than `!`, so `!$coupon instanceof Coupon` groups as `!($coupon instanceof Coupon)`, which is what people mean. Casts bind tighter still: `(int) $x instanceof Foo` groups as `((int) $x) instanceof Foo`. Two related facts are worth knowing: `instanceof` returns `false` without any error when its left side is not an object, and its right side may be a class name, an object, a string variable or, since PHP 8.0, a parenthesised expression producing a string. ## Concatenation and null coalescing - Since PHP 8.0, `.` binds **looser** than `+` and `-`, so `'Total: ' . $a + $b` means `'Total: ' . ($a + $b)`. In PHP 7 it meant `('Total: ' . $a) + $b`. - `??` binds looser than `.`, so `'Hi ' . $name ?? 'guest'` never falls back. ## How to avoid the whole class - Use `&&` and `||` in expressions; reserve the keywords, if at all, for the `or throw` idiom. - Parenthesise any mix of ternary, `??`, bitwise and comparison operators. - Let a code-style fixer or static analyser flag the keyword operators and bitwise-comparison mixes in review.
- Why does $coupon = loadCoupon($code) or throw new DomainException(); still assign the coupon?`or` has lower precedence than `=`, so PHP first evaluates `$coupon = loadCoupon($code)`. If that value is truthy, `or` short-circuits and the throw never runs; if it is falsy, the right side runs and throws. It works because `throw` is an expression since PHP 8.0. The same line with `||` would assign a `bool` instead of the coupon.
- What does instanceof return when its left operand is not an object?It returns `false` and raises no error, whether the left side is an `int`, `null`, a string or a resource. The right side can be a class name, an object (its class is used), a string variable holding a class name, or since PHP 8.0 any parenthesised expression that produces a string.
saying these in an interview costs you the question
- Says and and && are interchangeable apart from style.
- Thinks $f = false or true; leaves $f true.
- Believes PHP 8 still evaluates unparenthesised nested ternaries left to right.
- Thinks the keyword operators do not short-circuit.
- Reads $flags & MASK == MASK as a masked comparison.