skip to content

In PHP, why does $ok = validate($code) and redeem($code); leave $ok holding only the validation result, and which other precedence traps catch developers?

level: seniorimportance: should knowfreq 38%

answer

  1. keywords bind looser than =
  2. && and || bind tighter than =
  3. nested ternary: compile error since 8.0
  4. == binds tighter than bitwise &
  5. ! wraps the whole instanceof test

basics

~20 s

and, 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 s

PHP 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
<?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

for a junior

Remember that and, or and xor bind more loosely than =, so use && and || whenever the result is assigned or returned.

for a middle

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.

for a senior

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.

for a principal

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.