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 an int only when both operands are integers and the division is exact; otherwise a float. For whole-number division use intdiv(), which rounds toward zero.
  • % works on integers (floats are cut to integers first, with a Deprecated message since PHP 8.1 when decimals are lost) and its result has the sign of the left operand, so -7 % 3 is -1. Python gives 2 here; see negative operands. Use fmod() for floats.
  • Integers that grow past PHP_INT_MAX turn 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 is 42 == "42abc".
  • A bool on either side: both sides become booleans. null is compared the same way, except against a string, where it becomes "". So null == false, null == 0 and "0" == false are all true, but null == "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, or E_ALL & ~E_DEPRECATED in php.ini.
  • instanceof tests 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, like shell_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.