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

clone new

objects

right

**

arithmetic

n/a

unary + -, ++ --, ~, casts (int) …, @

arithmetic, increment, bitwise, casting, error control

left

instanceof

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

yield from, yield, print

generators, output

left

and

logical

left

xor

logical

left

or

logical

Two rows deserve attention:

  • Since PHP 8.0, . has lower precedence than + and -, so "Total: " . $a + $b adds first. In PHP 7 it concatenated first and then tried to add a string.

  • and, or and xor bind more loosely than =, so $ok = true and false; assigns true. 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.

Arithmetic operators

Operator Result

+$a, -$a

identity (converts to int/float), negation

$a + $b, $a - $b, $a * $b

sum, difference, product

$a / $b

quotient: a float, unless both operands are integers that divide evenly

$a % $b

remainder of integer division (operands are converted to int); the sign follows $a

$a ** $b

exponentiation (5.6), right-associative: 2 ** 3 ** 2 is 2 ** 9

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
}

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: ++ gives 1; -- 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 explicit str_increment() and str_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"

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

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.

Comparison operators

Operator True when

$a == $b

equal after type juggling

$a === $b

identical: same type and same value

$a != $b, $a <> $b

not equal after juggling

$a !== $b

not identical

$a < $b, $a > $b, $a <= $b, $a >= $b

ordering (after juggling)

$a <=> $b

returns -1, 0 or 1 (the "spaceship" operator, 7.0)

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)

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

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)

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";
}

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);

Logical operators

Operator Result

!$a

true if $a is falsy

$a && $b, $a and $b

true if both are truthy; $b is not evaluated if $a is falsy (short-circuit)

$a || $b, $a or $b

true if either is truthy; $b is not evaluated if $a is truthy

$a xor $b

true if exactly one is truthy (no short-circuit possible)

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)

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

Array operators

Operator Result

$a + $b

union: the elements of $a, plus those of $b whose keys are not in $a; on key collision $a wins

$a == $b

same key/value pairs (after juggling), in any order

$a === $b

same key/value pairs in the same order and of the same types

$a != $b, $a <> $b, $a !== $b

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(...)), a Closure, 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.

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.

Common pitfalls

  • and/or precedence: $ok = a() and b(); stores only a()'s result.

  • . and +: since 8.0, "x" . 1 + 2 is "x3", not an error.

  • Unparenthesised nested ternaries are a compile error since 8.0.

  • ?: versus ??: ?: falls through on any falsy value (0, "", "0"); ?? only on null/unset.

  • + on arrays is a key-based union, not concatenation.

  • Integer overflow in +, * and ** silently produces a float.

  • Arrow functions in pipes must be parenthesised.

Where older books and tutorials are outdated

  • "Hello " . $a + $b concatenated first before PHP 8.0; it adds first now.

  • Division and modulo by zero were warnings returning false; they throw DivisionByZeroError since 8.0.

  • @ silenced fatal errors before 8.0.

  • Arithmetic on arrays and non-numeric strings throws TypeError since 8.0.

  • The backtick operator and $string++ on non-numeric strings are deprecated since 8.5.

  • throw became 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.