0

Arithmetic, assignment, comparison, logical operators

Beginner5 min read·php-02-011

Concept

PHP's operator set covers arithmetic, assignment, comparison, string, logical, and bitwise categories. Understanding operator precedence and the subtle differences between superficially similar operators is essential for reading and writing correct PHP.

Arithmetic operators (+, -, *, /, %, **) behave mostly as expected. Division with / returns a float when the result is not evenly divisible, or an int when it is — 10 / 2 is int(5), 10 / 3 is float(3.3333…). The modulo % operator casts both operands to int first, so 5.9 % 3.1 is 5 % 3 = 2. The exponentiation operator ** (PHP 5.6+) is right-associative: 2 ** 3 ** 2 means 2 ** (3 ** 2) = 512.

Assignment operators include the standard = plus compound forms for every arithmetic and bitwise operator: +=, -=, *=, /=, %=, **=, .=, &=, |=, ^=, <<=, >>=. Assignment returns the assigned value, enabling chained assignment $a = $b = 0 (both become 0, right-to-left).

Comparison operators fall into two families. Strict comparisons (===, !==) check both value and type. Loose comparisons (==, !=, <, >, <=, >=) perform type juggling — PHP tries to coerce both operands to a common type. The rules for == changed slightly between PHP 7 and PHP 8: in PHP 8, comparing a string to an integer now converts the int to string (unless the string is numeric) rather than converting the string to int, fixing the famous 0 == 'foo' being true bug.

Logical operators exist in two precedence levels: &&, ||, ! (high precedence) and and, or, not (low precedence, lower than assignment). The difference matters: $a = true && false assigns false to $a (because && binds before =), but $a = true and false assigns true to $a (because = binds before and, then and is evaluated but the assignment already happened).

Operator precedence follows a fixed hierarchy. The most common surprises: ** is right-associative; ?: (ternary) is left-associative and was deprecated for chaining in PHP 7.4, removed in PHP 8.0; instanceof has higher precedence than ==; and the null coalescing ?? is right-associative.

Code Example

php
<?php
declare(strict_types=1);

// Arithmetic — division returns int or float
var_dump(10 / 2);    // int(5)
var_dump(10 / 3);    // float(3.3333...)
var_dump(intdiv(10, 3));  // int(3) — explicit integer division
var_dump(10 % 3);    // int(1)
var_dump(5.9 % 3.1); // int(2) — operands cast to int
var_dump(2 ** 10);   // int(1024)
var_dump(2 ** 3 ** 2); // int(512) — right-associative: 2 ** 9

// Compound assignment
$str = 'Hello';
$str .= ', World';  // 'Hello, World'

$score = 100;
$score -= 25;  // 75
$score **= 2;  // 5625

// PHP 8 comparison change: string vs int
var_dump(0 == 'foo');   // bool(false) — PHP 8 converts int to string "0", "0" != "foo"
var_dump(0 == '');      // bool(false) — PHP 8: "0" != ""
var_dump(0 == '0');     // bool(true)  — numeric string
var_dump(100 == '1e2'); // bool(true)  — '1e2' is numeric

// and vs && precedence trap
$a = true && false;  // $a = false (&&  before =)
$b = true and false; // $b = true  (=   before and)
var_dump($a, $b);    // bool(false), bool(true)

// or vs || precedence trap
$result = false || true;   // $result = true  (|| before =)
$result = false or true;   // $result = false (= before or, then 'or' doesn't affect $result)

// Ternary — chaining removed in PHP 8.0
// $val = $a ? 'a' : $b ? 'b' : 'c';  // Parse error in PHP 8.0+
// Must use explicit parentheses:
$a = 0;
$b = 1;
$val = $a ? 'a' : ($b ? 'b' : 'c');  // 'b'

// Null coalescing — right-associative, safe on undefined
$config = ['db' => ['host' => 'localhost']];
$host = $config['db']['host']    ?? 'default';  // 'localhost'
$port = $config['db']['port']    ?? 3306;        // 3306 — key missing, no notice
$user = $undefined_variable      ?? 'guest';     // 'guest' — no notice

// Spaceship operator — returns -1, 0, or 1
echo 1    <=> 2;    // -1
echo 2    <=> 2;    //  0
echo 3    <=> 2;    //  1
echo 'b'  <=> 'a';  //  1

usort($items = [3, 1, 2], fn($a, $b) => $a <=> $b);
// $items = [1, 2, 3]

// instanceof precedence is high
$obj = new stdClass();
var_dump($obj instanceof stdClass == true);   // bool(true) — instanceof first, then ==
var_dump(($obj instanceof stdClass) == true); // identical, parentheses make intent clear

Interview Q&A

Q: PHP 8.0 changed how string-to-integer comparison works with ==. What was the old behaviour, what is the new behaviour, and why did it matter for security?

In PHP 7 and earlier, when comparing a string to an integer with ==, PHP would convert the string to a number first. The string 'foo' would convert to 0, making 0 == 'foo' evaluate to true. This was a serious security vulnerability in authentication code: if a developer accidentally compared a hash digest (an integer-ish string) against user input without strict equality, an attacker could supply 0 or a string that converts to zero and bypass the check. In PHP 8, the rule was reversed: when comparing a non-numeric string to an integer, the integer is converted to string instead. So 0 == 'foo' is now '0' == 'foo' which is false. Applications upgrading from PHP 7 to PHP 8 need to audit any loose == comparisons involving integers and strings — some valid comparisons may also have changed, such as 0 == '' (now false) and 0 == 'abc' (now false).


Q: What is the precedence difference between and/or and &&/||, and when does it cause a real bug?

&& and || have higher precedence than assignment (=), while and and or have lower precedence. The classic bug: $handle = fopen('file.txt', 'r') or die('fail'). This works correctly because the intent is ($handle = fopen(...)) or die(...)= happens first, then or checks if $handle is falsy and calls die. But $handle = fopen('file.txt', 'r') || die('fail') is $handle = (fopen(...) || die('fail')), assigning a boolean true/false to $handle, which is wrong. The rule of thumb: prefer && and || in all modern code for clarity, and use and/or only in the specific $x = something() or abort() idiom where the low precedence is intentional.


Q: What does the spaceship operator <=> return, and why is it the correct tool for custom usort comparators?

<=> returns -1, 0, or 1 when the left operand is less than, equal to, or greater than the right operand respectively, using the same comparison rules as < and >. It is ideal for usort, uasort, and uksort callbacks because those functions expect exactly that return contract from their comparator. Before <=>, developers wrote return $a < $b ? -1 : ($a > $b ? 1 : 0), which is verbose and error-prone. The spaceship operator handles all types comparisons consistently: numbers numerically, strings lexicographically, arrays by size then element-by-element, and objects by property comparison. A critical note: returning $a - $b as a comparator works for integers but is incorrect for floats (may return a non-integer that gets truncated to 0) and strings; $a <=> $b is always safe.