Operators and Comparison
Arithmetic, string and logical operators, why === is safer than ==, the comparison rules that changed in PHP 8, the spaceship operator, and the null coalescing operator.
Arithmetic operators
PHP has the usual
operators: +, -, *, /,
% (remainder) and ** (power). A few
details differ from other languages:
<?php
var_dump(10 / 4); // not exact: float
var_dump(10 / 5); // exact: int
var_dump(intdiv(10, 4)); // whole-number division
var_dump(-7 % 3); // the sign follows the left operand
var_dump(7 % -3);
var_dump(fmod(7.5, 2)); // remainder for floats
var_dump(2 ** 10, 2 ** -1);
float(2.5) int(2) int(2) int(-1) int(1) float(1.5) int(1024) float(0.5)
-
/returns anintonly when both operands are integers and the division is exact; otherwise afloat. For whole-number division useintdiv(), which rounds toward zero. -
%works on integers (floats are cut to integers first, with aDeprecatedmessage since PHP 8.1 when decimals are lost) and its result has the sign of the left operand, so-7 % 3is-1. Python gives2here; see negative operands. Usefmod()for floats. -
Integers that grow past
PHP_INT_MAXturn into floats (see Integers and floats).
Division by zero
Dividing by zero with /, % or
intdiv() throws a DivisionByZeroError (for
/ since PHP 8; % and
intdiv() already did in PHP 7). Older tutorials say it
returns false or INF with a warning; that
is no longer true.
<?php
$tickets = 12;
$customersAffected = 0;
echo "Tickets: $tickets\n";
$perCustomer = $tickets / $customersAffected;
echo "Tickets per customer: $perCustomer\n";
Tickets: 12 Fatal error: Uncaught DivisionByZeroError: Division by zero in example.php:5
Check the divisor first (if ($customersAffected > 0)) or catch the error; see
Errors and Exceptions.
Assignment and increment
= assigns. The combined operators (augmented assignment) do an operation and assign the result: +=,
-=, *=, /=, %=,
**=, .= (append to a string) and
??= (assign only if missing, see
below).
$i++ and $i-- add or subtract one; as in
JavaScript, ++$i changes the variable first and then
gives its value, $i++ gives the old value.
Joining strings
The
concatenation operator
in PHP is the dot, .: it converts both sides to strings
and joins them. .= appends to a string variable.
Because the dot is reserved for strings, + never joins
text — it always tries to calculate.
<?php
$meter = "MTR-100231";
$kwh = 1542.7;
$line = "Meter " . $meter . ": " . $kwh . " kWh";
$line .= " (estimated)";
echo $line, "\n";
echo "5" . "5", "\n"; // joins
echo "5" + "5", "\n"; // adds
echo "Sum: " . 1 + 2, "\n";
Meter MTR-100231: 1542.7 kWh (estimated) 55 10 Sum: 3
The last line shows a change in PHP 8: + and
- now bind tighter than ., so
"Sum: " . 1 + 2 means "Sum: " . (1 + 2).
In PHP 7 the dot went first, and the line printed
2 with a warning. Parentheses make the intent clear in
either version. For text with many variables, string interpolation
("Meter $meter: $kwh kWh") or sprintf() is
often easier to read; see Strings.
Comparison operators
| Operator | True when |
|---|---|
$a === $b |
Same type and same value (identical) |
$a !== $b |
Not identical |
$a == $b |
Equal after type juggling |
$a != $b, $a <> $b |
Not equal after type juggling |
<, >, <=,
>=
|
Ordering, also after type juggling |
$a <=> $b |
Not a test: returns -1, 0 or 1 (see below) |
Identical (===) and equal (==)
=== is PHP's
strict equality: 1 === 1 is true, 1 === "1" and
1 === 1.0 are false, because the types differ. There
are no surprises, which is why it is the operator to use by default.
For arrays, === also requires the same order and the
same types of elements.
Loose comparison (==) and the PHP 8 rules
Loose comparison is PHP's
== (and !=, <,
> …): when the two values have different types, PHP
first juggles them to a common type and then compares. The main
rules since PHP 8:
-
Two numeric strings are compared as numbers:
"1" == "01"and"10" == "1e1"are true. -
A number and a numeric string are compared as
numbers:
100 == "1e2"is true. -
A number and a non-numeric string are compared as
strings: the number is turned into text. So
0 == "a"is false, and so is42 == "42abc". -
A
boolon either side: both sides become booleans.nullis compared the same way, except against a string, where it becomes"". Sonull == false,null == 0and"0" == falseare all true, butnull == "0"is false.
<?php
$pairs = [
[0, "a"], ["1", "01"], ["10", "1e1"], [100, "1e2"], ["abc", 0], [42, "42abc"],
[null, false], [null, 0], ["", null], [0, ""], ["0", false], ["0e123", "0e456"],
];
foreach ($pairs as [$a, $b]) {
printf("%-7s == %-7s %-5s === %s\n", var_export($a, true), var_export($b, true),
var_export($a == $b, true), var_export($a === $b, true));
}
0 == 'a' false === false '1' == '01' true === false '10' == '1e1' true === false 100 == '1e2' true === false 'abc' == 0 false === false 42 == '42abc' false === false NULL == false true === false NULL == 0 true === false '' == NULL true === false 0 == '' false === false '0' == false true === false '0e123' == '0e456' true === false
Before PHP 8, a number and a non-numeric string were compared as
numbers, so the string became 0 and 0 == "abc" was
true. Code written for PHP 7 that relied on this
can behave differently after an upgrade; it is one of the
version changes
worth testing for.
Everyday pitfalls
Several built-in functions use loose comparison or return values
that are easy to mistake for false. The classic one is
strpos(), which returns the position of a substring —
and position 0 is loosely equal to false:
<?php
$ticket = "INC-31902 portal login fails";
if (strpos($ticket, "INC") == false) {
echo "wrong check: no INC found\n";
}
if (strpos($ticket, "INC") !== false) {
echo "right check: INC at position ", strpos($ticket, "INC"), "\n";
}
var_dump(str_starts_with($ticket, "INC")); // PHP 8: says what you mean
var_dump(in_array("1e1", ["10"])); // loose: "1e1" == "10"
var_dump(in_array("1e1", ["10"], true)); // strict
var_dump(in_array(null, [0])); // loose: null == 0
wrong check: no INC found right check: INC at position 0 bool(true) bool(true) bool(false) bool(true)
Pass true as the third argument of
in_array() and array_search() to make them
strict, and compare the result of strpos() and
array_search() with !== false. Since PHP
8, str_contains(), str_starts_with() and
str_ends_with() avoid the problem altogether.
Ordering: <, > and strings
< and > follow the same juggling
rules. Two numeric strings are compared as numbers ("10" < "9"
is false), but as soon as one string is not numeric, PHP compares
byte by byte, like a dictionary that puts all capitals before
lowercase letters: "10" < "9a" and
"Zwolle" < "amsterdam" are both true. Convert input
to numbers before you compare it as numbers, and use
strcmp(), strcasecmp() or the sort flags
described in Arrays when you mean text.
The spaceship operator <=>
The spaceship operator,
$a <=> $b (PHP 7+), compares two values and
returns -1 if $a is smaller,
0 if they are equal and 1 if
$a is larger — exactly what a sort function's callback
must return. It uses the same rules as < and
==. Arrays are compared element by element, which makes
sorting on several fields a one-liner:
<?php
var_dump(1 <=> 2, 2 <=> 2, 3 <=> 2, "apple" <=> "banana");
$customers = [
['name' => 'Visser', 'kwh' => 2950],
['name' => 'Mulder', 'kwh' => 1870],
['name' => 'Meijer', 'kwh' => 2950],
['name' => 'Smit', 'kwh' => 3410],
];
// Highest use first; equal use sorted by name A–Z
usort($customers, fn($a, $b) => [$b['kwh'], $a['name']] <=> [$a['kwh'], $b['name']]);
foreach ($customers as $c) {
echo $c['name'], ": ", $c['kwh'], " kWh\n";
}
int(-1) int(0) int(1) int(-1) Smit: 3410 kWh Meijer: 2950 kWh Visser: 2950 kWh Mulder: 1870 kWh
Swapping $a and $b on the two sides
reverses the order, which is how the example sorts the use from high
to low and the names from A to Z. usort() and the short
fn syntax are explained in
Arrays and
Functions.
Logical operators
| Operator | Meaning |
|---|---|
!$a |
Not |
$a && $b, $a and $b |
Both true |
$a || $b, $a or $b |
At least one true |
$a xor $b |
Exactly one true |
The operands are converted to booleans with the truthiness rules, and evaluation stops as soon as the answer is known (short-circuit evaluation). Two things are different from Python and JavaScript:
<?php
// 1. || and && always return true or false, never one of the operands
var_dump("Deventer" || "default");
var_dump("" ?: "guest"); // use ?: to get "first truthy value"
// 2. and / or bind more weakly than =
$isAdmin = false;
$allowed = true and $isAdmin; // means: ($allowed = true) and $isAdmin
var_dump($allowed);
$allowed = true && $isAdmin;
var_dump($allowed);
bool(true) string(5) "guest" bool(true) bool(false)
The word forms and, or and
xor have a lower precedence than assignment, so
$allowed = true and $isAdmin assigns
true and then throws the rest away — a real security
bug when it decides access. Use && and
|| in expressions. You will still see
or in older code in the idiom
$file = fopen(…) or die(…), where the low precedence is
intended.
The null coalescing operator ??
The
null coalescing operator, $a ?? $b (PHP 7+), returns $a if it
exists and is not null, and $b otherwise.
It does not raise a warning for a missing variable or array key,
which makes it the standard way to read optional input.
??= (PHP 7.4+) assigns a value only if the variable or
key is missing or null. It works like JavaScript's
nullish coalescing operator.
<?php
// As if the page was requested with ?area=Deventer&page=0
$_GET = ['area' => 'Deventer', 'page' => '0'];
$area = $_GET['area'] ?? 'all';
$sort = $_GET['sort'] ?? 'date'; // missing key: no warning
$page = $_GET['page'] ?? '1'; // keeps "0"
$pageWrong = $_GET['page'] ?: '1'; // ?: replaces "0" as well
echo "$area | $sort | $page | $pageWrong\n";
echo $_GET['filter']['region'] ?? 'any region', "\n"; // nested keys work too
$settings = [];
$settings['theme'] ??= 'light';
$settings['theme'] ??= 'dark'; // already set: no change
echo $settings['theme'], "\n";
Deventer | date | 0 | 1 any region light
Compare ?? with the short ternary
?: (sometimes called the Elvis operator):
$a ?: $b returns $a if it is
truthy, so it also replaces 0,
"0" and "" — and it does warn
when $a does not exist. Choose ?? for
"missing or null", ?: for "empty".
A related operator is the nullsafe operator ?-> (PHP
8+), which stops a chain of method or property calls at the first
null instead of failing. It is the PHP version of
optional chaining:
$city = $customer?->getAddress()?->city ?? 'unknown';
The ternary operator
$condition ? $a : $b chooses between two values, like a
short if/else that produces a value:
$level = $kwh > 3000 ? 'high' : 'normal';. Since PHP
8, nesting ternaries without parentheses ($a ? $b : $c ? $d : $e) is a compile error, because PHP used to group them in an
unexpected order. Add parentheses, or use
match
for more than two choices.
Other operators
-
Bitwise operators
&,|,^,~,<<and>>work on the bits of integers (see bitwise operators). In PHP you meet them mostly when combining option flags:JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES, orE_ALL & ~E_DEPRECATEDin php.ini. -
instanceoftests whether an object belongs to a class or interface:$date instanceof DateTimeInterface. -
@in front of an expression hides its warnings. It also hides the information you need to fix a problem; see Errors and Exceptions. -
Backticks (
`ls`) run a shell command and return its output, likeshell_exec(). They are deprecated since PHP 8.5; avoid them, because passing user input to a shell is a classic injection hole. -
...(spread) unpacks an array into arguments or into another array; see Functions.
Operator precedence
When an expression has several operators, precedence decides which goes first. A shortened table, from strongest to weakest:
| Operators | Notes |
|---|---|
** |
Right to left: 2 ** 3 ** 2 is 512 |
++ -- ~, casts such as (int),
@
|
Unary operators |
instanceof |
|
! |
|
* / % |
|
+ - |
|
<< >> |
|
. |
Below + and - since PHP 8 |
< <= > >= |
|
== != === !== <> <=>
|
|
&, then ^, then |
|
Bitwise; weaker than comparison |
&&, then || |
|
?? |
Right to left: $a ?? $b ?? 'x' |
? : |
Nesting needs parentheses |
= += -= .= ??= … |
Assignment, right to left |
and, then xor, then
or
|
Weaker than assignment |
Nobody remembers the whole table. When an expression mixes more than two kinds of operators, add parentheses: they cost nothing and make the intent clear to the next reader.