Operators and Expressions
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against PHP 8.5, checked on 2026-10-05, and covers every family of the manual’s operators
chapter. Operators that are new or changed since PHP 8.3 are marked with their version: the pipe operator |> is
new in 8.5, as are the deprecations of the backtick operator and of incrementing non-numeric strings.
Expressions
In PHP almost everything you write is an expression: anything that has a value. 5, $book, count($shelf),
$a = 3 (whose value is 3), $x > 1, match (…) {…} and, since PHP 8.0, throw new … are all expressions. That is
why assignments can be chained ($a = $b = 0) and why a match can appear on the right of =.
<?php
declare(strict_types=1);
$copies = $available = 3; // assignment is an expression with a value
$label = $copies > 0 ? 'in stock' : 'lent out';
// Since 8.0, throw is an expression: usable after ??, ?: and in arrow functions.
$isbn = $_GET['isbn'] ?? '978-0-00-000001-1';
$title = ['978-0-00-000001-1' => 'Kindred'][$isbn] ?? throw new InvalidArgumentException("Unknown ISBN {$isbn}");
echo "{$title}: {$label} ({$available})\n"; // Kindred: in stock (3)
Source: Expressions and Exceptions (throw as an expression)
Precedence and associativity
Precedence decides how tightly operators bind (1 + 5 * 3 is 16); associativity decides how operators of equal
precedence group (1 - 2 - 3 is (1 - 2) - 3; $a = $b = $c is $a = ($b = $c)). Non-associative operators cannot
be chained: 1 < 2 > 1 is a syntax error. From highest to lowest precedence:
| Associativity | Operators | Family |
|---|---|---|
n/a |
|
objects |
right |
|
arithmetic |
n/a |
unary |
arithmetic, increment, bitwise, casting, error control |
left |
|
type |
n/a |
|
logical |
left |
|
arithmetic |
left |
|
arithmetic (binary), array union |
left |
|
bitwise |
left |
|
string (since 8.0, below |
left |
|
pipe (8.5) |
non-associative |
|
comparison |
non-associative |
|
comparison |
left |
|
bitwise, references |
left |
|
bitwise |
left |
|
bitwise |
left |
|
logical |
left |
|
logical |
right |
|
null coalescing |
non-associative |
|
ternary (left-associative before 8.0; nesting without parentheses is now an error) |
right |
|
assignment |
n/a |
|
generators, output |
left |
|
logical |
left |
|
logical |
left |
|
logical |
Two rows deserve attention:
-
Since PHP 8.0,
.has lower precedence than+and-, so"Total: " . $a + $badds first. In PHP 7 it concatenated first and then tried to add a string. -
and,orandxorbind more loosely than=, so$ok = true and false;assignstrue. Use&&and||in expressions.
<?php
declare(strict_types=1);
var_dump('a' . 1 + 2); // string(2) "a3" -- (1 + 2) first since PHP 8.0
var_dump(-2 ** 2); // int(-4) -- ** binds tighter than unary minus
$ok = false or true; // ($ok = false) or true
var_dump($ok); // bool(false)
$ok = false || true;
var_dump($ok); // bool(true)
Use parentheses whenever precedence is not obvious to a reader; they cost nothing.
Source: Operator Precedence
Arithmetic operators
| Operator | Result |
|---|---|
|
identity (converts to |
|
sum, difference, product |
|
quotient: a |
|
remainder of integer division (operands are converted to |
|
exponentiation (5.6), right-associative: |
Division and modulo by zero throw DivisionByZeroError (since 8.0; it was a warning before). The related functions
are intdiv() (integer division, rounding towards zero), fmod() (float remainder), fdiv() (IEEE 754 division that
returns INF/NAN instead of throwing) and, since 8.4, fpow() (IEEE 754 power). See
Math, Numbers and Randomness.
<?php
declare(strict_types=1);
var_dump(2 ** 10, 2 ** -1); // int(1024) float(0.5)
var_dump(7 % 3, -7 % 3); // int(1) int(-1) -- sign of the dividend
var_dump(fmod(7.5, 2)); // float(1.5) -- % would truncate 7.5 to 7 (and warn)
var_dump(intdiv(-7, 2)); // int(-3)
var_dump(fdiv(1, 0)); // float(INF)
try {
$perShelf = 120 % 0;
} catch (DivisionByZeroError $e) {
echo get_class($e), ': ', $e->getMessage(), "\n"; // DivisionByZeroError: Modulo by zero
}
Source: Arithmetic Operators
Increment and decrement
++$a and --$a change the variable and return the new value; $a++ and $a-- return the old value. On integers
and floats this is ordinary arithmetic. On other types the rules are historical:
-
null:++gives1;--has no effect and, since 8.3, warns that this will change in a future major version. -
bool: no effect, with the same 8.3 warning. -
Numeric strings: converted to numbers.
-
Non-numeric strings:
++performs "Perl-style" string increment ('a9'becomes'b0','Az'becomes'Ba'),--does nothing. This behaviour was soft-deprecated in 8.3 in favour of the explicitstr_increment()andstr_decrement()functions, and since PHP 8.5 incrementing a non-numeric string emits a deprecation.
<?php
declare(strict_types=1);
$loans = 5;
echo $loans++, ' ', $loans, ' ', ++$loans, "\n"; // 5 6 7
// Shelf codes such as "Az" -> "Ba": use the explicit functions (8.3).
var_dump(str_increment('Az'), str_decrement('Ba')); // string(2) "Ba" string(2) "Az"
$code = 'a9';
$code++; // Deprecated (8.5): Increment on non-numeric string is deprecated, use str_increment() instead
var_dump($code); // string(2) "b0"
Source: Incrementing/Decrementing Operators and str_increment
Assignment operators
= assigns by value ($a = &$b assigns by reference). Every binary arithmetic, bitwise and string operator has a
combined form: +=, -=, *=, /=, %=, **=, .=, &=, |=, ^=, <<=, >>=. The null coalescing
assignment ??= (7.4) assigns only if the left side is unset or null, which is the idiom for lazy defaults and
caches:
<?php
declare(strict_types=1);
$report = 'Loans';
$report .= ': ';
$count = 10;
$count -= 3;
$count **= 2;
echo $report, $count, "\n"; // Loans: 49
$options = ['format' => 'ebook'];
$options['format'] ??= 'paperback'; // kept: already set
$options['language'] ??= 'en'; // added
print_r($options);
// Array destructuring is also an assignment
[$first, , $third] = ['Solaris', 'Kindred', 'Dawn'];
echo "{$first} / {$third}\n"; // Solaris / Dawn
Source: Assignment Operators
Bitwise operators
& (and), | (or), ^ (xor), ~ (not), << and >> (shifts) work on the bits of integers. If both operands
are strings, &, | and ^ work on the bytes of the strings instead. Shifting by a negative amount throws
ArithmeticError. The typical use is flag sets, as in json_encode() options or error_reporting:
<?php
declare(strict_types=1);
const CAN_BORROW = 0b001;
const CAN_RESERVE = 0b010;
const IS_STAFF = 0b100;
$member = CAN_BORROW | CAN_RESERVE; // set two flags
var_dump(($member & CAN_RESERVE) !== 0); // bool(true) test a flag
$member &= ~CAN_RESERVE; // clear a flag
$member ^= IS_STAFF; // toggle a flag
printf("%03b\n", $member); // 101
var_dump(1 << 3, -16 >> 2); // int(8) int(-4) -- arithmetic shift
echo json_encode(['title' => 'Łódź'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), "\n";
For new APIs, an enum or an array of options is usually clearer than integer flags.
Source: Bitwise Operators
Comparison operators
| Operator | True when |
|---|---|
|
equal after type juggling |
|
identical: same type and same value |
|
not equal after juggling |
|
not identical |
|
ordering (after juggling) |
|
returns |
How PHP juggles operands for == and <, the PHP 8 change for string-to-number comparisons, and the full comparison
tables are covered in Type Juggling and
Comparisons.
<?php
declare(strict_types=1);
var_dump(1979 === 1979, '1979' === 1979, '1979' == 1979); // bool(true) bool(false) bool(true)
var_dump('Kindred' <=> 'Dawn'); // int(1)
Source: Comparison Operators
The ternary operator and the short ternary
$cond ? $a : $b evaluates to $a if $cond is truthy, else $b. The short ternary $a ?: $b evaluates to $a
if it is truthy, else $b (note: truthy, so 0 and "" fall through). Nesting ternaries without parentheses was
deprecated in 7.4 and is a compile-time error since 8.0, because the old left-associative behaviour surprised everyone.
<?php
declare(strict_types=1);
$copies = 0;
echo $copies > 0 ? 'available' : 'waiting list', "\n"; // waiting list
echo $copies ?: 'none', "\n"; // none (0 is falsy)
echo $copies ?? 'none', "\n"; // 0 (0 is not null)
$year = 1975;
echo $year < 1970 ? 'classic' : ($year < 2000 ? 'modern' : 'recent'), "\n"; // modern -- parentheses required
Source: Ternary Operator
The null coalescing operator ??
$a ?? $b (7.0) evaluates to $a if it exists and is not null, otherwise to $b, without raising undefined
variable/index warnings. It is right-associative, so $a ?? $b ?? $c returns the first non-null value. Combined with
throw (8.0) it expresses "required value" cleanly.
<?php
declare(strict_types=1);
$config = ['loan_days' => null];
$days = $config['loan_days'] ?? $config['default_days'] ?? 21;
var_dump($days); // int(21)
Source: Null Coalescing Operator
Error control operator @
Prefixing an expression with @ suppresses the diagnostics it would emit (warnings, notices, deprecations); the error
handler is still called, but error_reporting() returns a reduced value during it. Since PHP 8.0, @ no longer
silences fatal errors, and it never catches exceptions.
Avoid @: it hides the reason a call failed and slows down error handling. Check conditions beforehand (is_file(),
isset()), use ??, or let the warning be logged. The few legitimate uses are functions that have no way to report
failure other than a warning and whose failure you handle explicitly, such as @fopen() followed by checking for
false and error_get_last().
<?php
declare(strict_types=1);
$handle = @fopen('/nonexistent/catalogue.csv', 'r'); // warning suppressed...
if ($handle === false) {
$error = error_get_last(); // ...but still retrievable
echo 'Could not open the catalogue: ', $error['message'] ?? 'unknown error', "\n";
}
Source: Error Control Operators
Execution operator (backticks)
Text between backticks is executed as a shell command, like shell_exec(), and evaluates to its output. The
backtick operator is deprecated since PHP 8.5, because it is easy to miss when reading code and invites command
injection. Use shell_exec() if you must, or better proc_open() with an argument array, which bypasses the shell
(see Command-Line PHP).
<?php
declare(strict_types=1);
// Deprecated (8.5): $files = `ls -1`;
$isbn = '978-0-00-000001-1';
// If a shell is unavoidable, escape every argument:
$output = shell_exec('echo ' . escapeshellarg($isbn));
echo $output; // 978-0-00-000001-1
// Better: no shell at all (proc_open() accepts an array command since 7.4)
$process = proc_open(['echo', $isbn], [1 => ['pipe', 'w']], $pipes);
echo stream_get_contents($pipes[1]);
proc_close($process);
Source: Execution Operators and PHP 8.5 deprecated features
Logical operators
| Operator | Result |
|---|---|
|
|
|
|
|
|
|
|
The result of a logical operator is always a bool, not one of the operands (unlike JavaScript); use ?? or ?: to
pick a value. The word forms differ only in precedence: they bind more loosely than assignment, which is why old code
used $file = fopen(...) or die(...). Do not use that idiom: die() with a message is not error handling.
<?php
declare(strict_types=1);
$member = ['active' => true, 'fines' => 0];
$canBorrow = $member['active'] && $member['fines'] === 0;
var_dump($canBorrow); // bool(true)
var_dump('Kindred' || false); // bool(true) -- not the string
var_dump(true xor true); // bool(false)
Source: Logical Operators
String operators
The concatenation operator is . and its assignment form is .=. + never concatenates strings. For anything
longer than a few pieces, interpolation ("{$title} ({$year})"), sprintf() or implode() read better; see
Strings.
<?php
declare(strict_types=1);
$title = 'Kindred';
$line = 'Title: ' . $title;
$line .= ' (1979)';
echo $line, "\n"; // Title: Kindred (1979)
echo 'Copies: ' . 2 + 1, "\n"; // Copies: 3 -- + binds tighter than . since 8.0
Source: String Operators
Array operators
| Operator | Result |
|---|---|
|
union: the elements of |
|
same key/value pairs (after juggling), in any order |
|
same key/value pairs in the same order and of the same types |
|
negations |
+ is often confused with array_merge(): for lists, [1, 2] + [3, 4, 5] gives [1, 2, 5], because keys 0 and 1
already exist. The spread operator ([...$a, ...$b]) or array_merge() append instead; see
Arrays.
<?php
declare(strict_types=1);
$defaults = ['format' => 'paperback', 'language' => 'en'];
$request = ['language' => 'es'];
print_r($request + $defaults); // language => es, format => paperback: request wins
print_r([1, 2] + [3, 4, 5]); // [1, 2, 5] -- union by key, not append
print_r([...[1, 2], ...[3, 4, 5]]); // [1, 2, 3, 4, 5]
Source: Array Operators
Type operator instanceof
$obj instanceof ClassName is true if $obj is an instance of that class, a subclass, or a class implementing that
interface. The right side can be a class name, a string variable holding a class name, or another object. It is
false (not an error) for non-objects and for undefined class names.
<?php
declare(strict_types=1);
interface Lendable {}
final class Book implements Lendable {}
$item = new Book();
$type = 'Lendable';
var_dump($item instanceof Book, $item instanceof Lendable, $item instanceof $type); // all bool(true)
var_dump('Book' instanceof Book); // bool(false): a string is not an object
var_dump($item instanceof Countable); // bool(false)
Source: Type Operators
The pipe operator |> (PHP 8.5)
The pipe operator passes the value on its left as the single argument of the callable on its right and evaluates to
the result: $x |> f(...) is f($x). Chaining pipes turns nested calls, which read inside-out, into a pipeline that
reads in the order things happen.
Rules:
-
The right side must be a callable that accepts exactly one required parameter: a first-class callable (
trim(...)), aClosure, an invokable object, a string function name, or any expression evaluating to one. Functions taking the argument by reference are not allowed. -
Arrow functions on the right must be wrapped in parentheses:
|> (fn ($x) => $x * 2). -
|>binds tighter than comparisons and looser than arithmetic and., and is left-associative.
<?php
declare(strict_types=1);
$rawTitles = " the dispossessed\nKINDRED \n solaris \n\n";
// Without the pipe: read from the inside out.
$titles = array_values(array_filter(array_map(trim(...), explode("\n", $rawTitles))));
// With the pipe (8.5): read from top to bottom.
$titles = $rawTitles
|> (fn (string $s): array => explode("\n", $s))
|> (fn (array $lines): array => array_map(trim(...), $lines))
|> array_filter(...)
|> array_values(...)
|> (fn (array $t): array => array_map(ucwords(...), array_map(strtolower(...), $t)));
print_r($titles);
// Works with any single-argument callable, including invokable objects
final class Slug { public function __invoke(string $s): string { return strtolower(str_replace(' ', '-', $s)); } }
echo 'The Left Hand of Darkness' |> new Slug() |> strrev(...), "\n";
Array
(
[0] => The Dispossessed
[1] => Kindred
[2] => Solaris
)
ssenkrad-fo-dnah-tfel-eht
Functions that need several arguments are wrapped in an arrow function, as above. Upcoming in PHP 8.6, partial
function application (explode("\n", ?)) will produce such single-argument callables directly; see
Closures and Callables.
Source: Functional Operators and RFC: Pipe operator v3
The nullsafe operator ?-> (PHP 8.0)
$a?->b evaluates to null if $a is null, instead of throwing; otherwise it accesses the property or calls the
method. When a link in the chain is null, the rest of the chain is skipped (short-circuit), including method
arguments. Nullsafe access cannot be used for writing ($a?->b = 1 is an error) or by reference.
<?php
declare(strict_types=1);
final class Author { public function __construct(public ?string $country = null) {} }
final class Book
{
public function __construct(public string $title, public ?Author $author = null) {}
public function author(): ?Author { return $this->author; }
}
$books = [new Book('Solaris', new Author('PL')), new Book('Anonymous pamphlet')];
foreach ($books as $book) {
echo $book->title, ': ', $book->author()?->country ?? 'unknown', "\n";
}
// Solaris: PL
// Anonymous pamphlet: unknown
?-> checks only for null; to handle missing array keys or undefined variables, ?? is the right operator. The two
are often combined, as above.
Source: Nullsafe methods and properties
Common pitfalls
-
and/orprecedence:$ok = a() and b();stores onlya()'s result. -
.and+: since 8.0,"x" . 1 + 2is"x3", not an error. -
Unparenthesised nested ternaries are a compile error since 8.0.
-
?:versus??:?:falls through on any falsy value (0,"","0");??only onnull/unset. -
+on arrays is a key-based union, not concatenation. -
Integer overflow in
+,*and**silently produces afloat. -
Arrow functions in pipes must be parenthesised.
Where older books and tutorials are outdated
-
"Hello " . $a + $bconcatenated first before PHP 8.0; it adds first now. -
Division and modulo by zero were warnings returning
false; they throwDivisionByZeroErrorsince 8.0. -
@silenced fatal errors before 8.0. -
Arithmetic on arrays and non-numeric strings throws
TypeErrorsince 8.0. -
The backtick operator and
$string++on non-numeric strings are deprecated since 8.5. -
throwbecame an expression,?->and|>were added (8.0 and 8.5), and??=arrived in 7.4: none of these are in books written for PHP 7.3 or older.
References
-
PHP Manual — Operators: precedence, arithmetic, increment/decrement, assignment, bitwise, comparison, error control, execution, logical, string, array, type, functional (pipe)
-
PHP Manual — The Basics (nullsafe operator)
-
PHP Manual — Exceptions (throw expressions)