Operators #

Operators are symbols that tell PHP to perform a specific operation on one or more values. PHP has more than a dozen categories of operators — far more than what basic tutorials usually teach. What sets PHP apart from many other languages is the unintuitive behavior of some of its operators: the == operator silently converts types, the and and && operators have different precedence even though both mean “and”, and the + operator on arrays behaves very differently from what you might expect. This article covers all PHP operators in depth — not just the list, but also the traps that often cause bugs and how to use them correctly.

Arithmetic Operators #

Arithmetic operators perform basic mathematical operations. They all work on numeric values (integers and floats), with some important edge-case behaviors.

<?php
$a = 17;
$b = 5;

echo $a + $b;   // 22  — addition
echo $a - $b;   // 12  — subtraction
echo $a * $b;   // 85  — multiplication
echo $a / $b;   // 3.4 — division (produces float if not evenly divisible)
echo $a % $b;   // 2   — modulo (remainder)
echo $a ** $b;  // 1419857 — exponentiation (PHP 5.6+)

Division Behavior #

The / operator always produces the correct type — integer if evenly divisible, float if not:

<?php
var_dump(10 / 2);   // int(5)    — evenly divisible, integer result
var_dump(10 / 3);   // float(3.333...) — not even, float result
var_dump(10 / 3.0); // float(3.333...) — one is float, result is float

// If you need integer division (discard the remainder):
echo intdiv(10, 3); // 3 — dedicated integer division function (PHP 7+)
echo (int)(10 / 3); // 3 — cast to int (truncates, doesn't round)
echo floor(10 / 3); // 3 — floor division

Modulo and Negative Values #

<?php
// The sign of a modulo result follows the dividend (not the divisor)
echo  10 %  3;  //  1
echo -10 %  3;  // -1 — negative because the dividend is negative
echo  10 % -3;  //  1 — positive because the dividend is positive
echo -10 % -3;  // -1

// Common modulo use: checking multiples
$number = 15;
echo ($number % 3 === 0) ? "multiple of 3" : "not a multiple of 3"; // multiple of 3
echo ($number % 2 === 0) ? "even" : "odd";                          // odd

// Array index rotation — modulo prevents out-of-bounds
$colors = ["red", "green", "blue"];
$count  = count($colors);
for ($i = 0; $i < 9; $i++) {
    echo $colors[$i % $count] . " "; // red green blue red green blue...
}

The Exponentiation Operator #

<?php
echo 2 ** 8;    // 256 — 2 to the power of 8
echo 2 ** 0.5;  // 1.4142... — square root (2^0.5 = √2)
echo (-2) ** 3; // -8

// Exponentiation associativity is RIGHT-TO-LEFT
echo 2 ** 3 ** 2; // 512 — evaluated as 2 ** (3 ** 2) = 2 ** 9 = 512
                  // NOT (2 ** 3) ** 2 = 8 ** 2 = 64!

Assignment Operators #

Assignment operators store values into variables. Besides the basic =, PHP provides compound assignment operators to shorten common expressions.

<?php
// Basic assignment
$x = 10;

// Compound assignment operators — all equivalent to $x = $x OP value
$x += 5;   // $x = $x + 5  → 15
$x -= 3;   // $x = $x - 3  → 12
$x *= 2;   // $x = $x * 2  → 24
$x /= 4;   // $x = $x / 4  → 6
$x %= 4;   // $x = $x % 4  → 2
$x **= 3;  // $x = $x ** 3 → 8

// String assignment
$text  = "Hello";
$text .= " World"; // $text = $text . " World" → "Hello World"

// Null coalescing assignment (PHP 7.4+)
$config = [];
$config['timeout'] ??= 30;  // assign 30 only if null/missing
$config['timeout'] ??= 60;  // unchanged — it already has a value (30)
echo $config['timeout'];     // 30

Chained Assignment #

PHP supports chained assignment — values are assigned from right to left:

<?php
// Chained to the left — all variables get the same value
$a = $b = $c = 0;
echo "$a $b $c"; // 0 0 0

// Be careful: this is not a comparison!
// ANTI-PATTERN: a very common typo
$active = false;
if ($active = true) {    // ✗ this is ASSIGNMENT, not comparison!
    echo "always enters here because $active = true always evaluates to true";
}

// CORRECT: use == or ===
if ($active === true) {  // ✓ comparison
    echo "active";
}

Comparison Operators #

Comparison operators return true or false. This is the most critical part to understand correctly in PHP, because the difference between == and === can cause bugs that are very hard to find.

Complete Comparison Operator Table #

OperatorNameExampleResultNotes
==Equal (loose)1 == "1"trueAutomatic type conversion
===Identical (strict)1 === "1"falseTypes must match
!=Not equal (loose)1 != "2"trueAutomatic type conversion
<>Not equal (loose)1 <> "2"trueAlias of !=
!==Not identical (strict)1 !== "1"trueTypes must differ
<Less than1 < 2true
>Greater than2 > 1true
<=Less than or equal1 <= 1true
>=Greater than or equal2 >= 1true
<=>Spaceship1 <=> 2-1Returns -1, 0, or 1

Loose Comparison == — Type Juggling Traps #

The == operator performs type conversion before comparing. The results can be very surprising:

<?php
// The most common == traps
var_dump(0    == "foo");   // PHP 7: true  | PHP 8: false (big change!)
var_dump(0    == "");      // PHP 7: true  | PHP 8: false
var_dump(0    == "0");     // true  (both versions)
var_dump(0    == false);   // true
var_dump(0    == null);    // true
var_dump(""   == false);   // true
var_dump(""   == null);    // true
var_dump("1"  == "01");    // true  — both converted to int 1
var_dump("10" == "1e1");   // true  — scientific notation!
var_dump(100  == "1e2");   // true
var_dump(null == false);   // true
var_dump(null == 0);       // true
var_dump(null == "");      // true
var_dump(null == "0");     // false — this one is different!

// A very dangerous case:
$password_hash = "0e123456789"; // certain MD5 hashes start with "0e..."
$input_hash    = "0e987654321"; // a different MD5 hash that also starts with "0e..."
var_dump($password_hash == $input_hash); // true! — "magic hash" attack!
// PHP converts both to float 0 * 10^xxx = 0
// ALWAYS use === to compare hashes
Don’t use == to compare cryptographic hashes, tokens, or passwords. Strings starting with 0e are interpreted by PHP as scientific notation (0 * 10^n = 0), so two different hashes can be considered equal. Use === or the hash_equals() function, which is specifically designed to resist timing attacks.

Strict Comparison === #

The === operator compares values and types without any conversion:

<?php
var_dump(1   === 1);      // true  — same value, same type
var_dump(1   === "1");    // false — same value, different types (int vs string)
var_dump(1   === 1.0);    // false — int vs float
var_dump(null === false); // false — null vs bool
var_dump([]  === []);     // true  — identical empty arrays
var_dump([1] === [1]);    // true
var_dump([1] === ["1"]);  // false — elements have different types

// For objects: === checks whether it's the SAME instance (not just equal values)
$a = new stdClass();
$b = new stdClass();
$c = $a;

var_dump($a === $b); // false — two different instances even with the same properties
var_dump($a === $c); // true  — the same variable referencing the same instance
var_dump($a == $b);  // true  — == on objects checks whether type and properties match

The Spaceship Operator <=> #

The spaceship returns -1 (less than), 0 (equal), or 1 (greater than) — very useful as a sorting callback:

<?php
// Return values
echo 1 <=> 2;   // -1 (left is smaller)
echo 2 <=> 2;   //  0 (equal)
echo 3 <=> 2;   //  1 (left is greater)

// The most common use: usort() callback
$products = [
    ["name" => "Laptop",  "price" => 15000000],
    ["name" => "Mouse",   "price" => 250000],
    ["name" => "Monitor", "price" => 5000000],
];

// Sort ascending by price
usort($products, fn($a, $b) => $a["price"] <=> $b["price"]);

// Sort descending (swap the operands)
usort($products, fn($a, $b) => $b["price"] <=> $a["price"]);

// Multi-criteria: sort by price ascending, then by name ascending if prices match
usort($products, function($a, $b) {
    $priceCmp = $a["price"] <=> $b["price"];
    if ($priceCmp !== 0) return $priceCmp;
    return $a["name"] <=> $b["name"];
});

// More concise with an arrow function
usort($products, fn($a, $b)
    => [$a["price"], $a["name"]] <=> [$b["price"], $b["name"]]
);
// PHP compares arrays element-by-element — an elegant tuple-sorting technique

Logical Operators #

Logical operators are used to combine or invert boolean conditions. PHP has two sets of logical operators that look the same but have different precedence — this is a fairly common source of bugs.

<?php
$a = true;
$b = false;

// Symbol operators (high precedence)
var_dump($a && $b);  // false — AND
var_dump($a || $b);  // true  — OR
var_dump(!$a);       // false — NOT
var_dump($a xor $b); // true  — XOR (true if exactly one is true)

// Word operators (low precedence — below = )
var_dump($a and $b); // false
var_dump($a or $b);  // true
var_dump($a xor $b); // true

The Precedence Trap: and/or vs &&/|| #

This is one of the most common precedence traps in PHP:

<?php
// ANTI-PATTERN: assuming 'and' and '&&' are identical
$result = true and false;
var_dump($result); // bool(true) — SURPRISING!

// This happens because '=' has higher precedence than 'and':
// PHP reads it as: ($result = true) and false
// So $result = true, then the expression 'and false' is evaluated but discarded

$result = true && false;
var_dump($result); // bool(false) — this is what you'd expect
// '&&' has higher precedence than '=':
// PHP reads it as: $result = (true && false)

// CORRECT: always use && and ||, avoid and/or unless you really need them
$active   = true;
$verified = false;

if ($active && $verified) {
    echo "access granted";
}

Short-Circuit Evaluation #

PHP uses short-circuit evaluation — an expression stops as soon as its result is certain:

<?php
function checkA(): bool
{
    echo "checkA called\n";
    return false;
}

function checkB(): bool
{
    echo "checkB called\n";
    return true;
}

// && stops at checkA() because false && anything = false
if (checkA() && checkB()) { }
// Output: "checkA called" — checkB is never called!

// || stops at the first true condition
$user = null;
$name = $user?->getName() || "Guest";
// If user is null, the expression after || is never evaluated

// Idiomatic pattern: use short-circuit for guards
function processFile(string $path): void
{
    file_exists($path) || throw new \InvalidArgumentException("File not found: $path");
    is_readable($path) || throw new \RuntimeException("File not readable: $path");

    // continue processing...
}

Increment and Decrement Operators #

The increment (++) and decrement (--) operators add or subtract 1 from a value. The operator’s position (before or after the variable) determines when the change happens relative to expression evaluation.

<?php
$x = 5;

// Pre-increment: increment first, then return the new value
echo ++$x; // 6 — $x is now 6

// Post-increment: return the old value first, then increment
echo $x++; // 6 — old value returned, $x is now 7
echo $x;   // 7

// Pre-decrement
echo --$x; // 6
// Post-decrement
echo $x--; // 6 — old value, $x is now 5
echo $x;   // 5

Behavior on Strings and Null #

PHP has interesting behavior when increment/decrement is applied to strings and null — inherited from Perl conventions:

<?php
// String increment — "advances" to the next character
$s = "a";
$s++;
echo $s; // "b"

$s = "z";
$s++;
echo $s; // "aa"

$s = "A9";
$s++;
echo $s; // "B0"

$s = "Az";
$s++;
echo $s; // "Ba"

// NULL increment produces 1
$n = null;
$n++;
echo $n; // 1

// WARNING: decrement on strings and null does NOT behave symmetrically!
$s = "b";
$s--;
echo $s; // "b" — unchanged! decrement doesn't apply to strings

$n = null;
$n--;
echo $n; // -1 — decrement on null produces -1? No! this is an error in PHP 8+
// In PHP 8.3+, decrementing null produces -1 (behavior changed from older versions)

String Operators #

PHP has only two dedicated string operators:

<?php
// The concatenation operator (.) — joins two strings
$first  = "Hello";
$middle = ", ";
$last   = "World!";

$sentence = $first . $middle . $last;
echo $sentence; // "Hello, World!"

// Non-strings are automatically converted to strings
echo "Value: " . 42;        // "Value: 42"
echo "Pi: " . 3.14;         // "Pi: 3.14"
echo "Active: " . true;     // "Active: 1"
echo "Inactive: " . false;  // "Inactive: " (false becomes an empty string)

// The concatenation assignment operator (.=)
$log = "[2024-01-01] ";
$log .= "Server started. ";
$log .= "Port 8080.";
echo $log; // "[2024-01-01] Server started. Port 8080."

// For building long strings in a loop, .= is more efficient than re-assigning
$html = "";
foreach ($items as $item) {
    $html .= "<li>{$item['name']}</li>\n";
}

Concatenation vs Interpolation #

<?php
$name = "Budi";
$city = "Jakarta";

// ANTI-PATTERN: repeated concatenation for many variables — verbose
$message = "Hello " . $name . ", you're from " . $city . "!";

// CORRECT: string interpolation is cleaner
$message = "Hello {$name}, you're from {$city}!";

// For more complex operations (functions, expressions), keep concatenation
$message = "Total: Rp " . number_format($total, 0, ',', '.') . " (VAT included)";

// Or sprintf for more structured formatting
$message = sprintf("Total: Rp %s (VAT included)", number_format($total, 0, ',', '.'));

Bitwise Operators #

Bitwise operators work directly on the binary representation of integers. Though rarely used in ordinary web work, they’re very useful for permission flags, simple encryption, and bit manipulation.

<?php
// Binary representation: 5 = 0101, 3 = 0011

echo 5 & 3;   // 1  (0101 AND 0011 = 0001) — both are 1
echo 5 | 3;   // 7  (0101 OR  0011 = 0111) — either is 1
echo 5 ^ 3;   // 6  (0101 XOR 0011 = 0110) — exactly one is 1
echo ~5;       // -6 (NOT 0101 = ...11111010 in two's complement)
echo 5 << 1;  // 10 (shift left 1 bit: 0101 → 1010)
echo 5 >> 1;  // 2  (shift right 1 bit: 0101 → 0010)

The Flag Pattern with Bitwise #

The most common use of bitwise in PHP is permission/flag systems:

<?php
// Define flags as power-of-2 constants
const PERM_READ   = 0b001; // 1
const PERM_WRITE  = 0b010; // 2
const PERM_DELETE = 0b100; // 4

// Combine several permissions with OR
$adminPerm  = PERM_READ | PERM_WRITE | PERM_DELETE; // 7 (0b111)
$userPerm   = PERM_READ;                             // 1 (0b001)
$editorPerm = PERM_READ | PERM_WRITE;                // 3 (0b011)

// Check whether a specific permission is active with AND
function hasPermission(int $permission, int $flag): bool
{
    return ($permission & $flag) === $flag;
}

var_dump(hasPermission($adminPerm, PERM_DELETE));  // true
var_dump(hasPermission($userPerm, PERM_DELETE));   // false
var_dump(hasPermission($editorPerm, PERM_WRITE));  // true

// Add a permission with OR
$userPerm |= PERM_WRITE;
var_dump(hasPermission($userPerm, PERM_WRITE)); // true — now added

// Remove a permission with AND NOT
$editorPerm &= ~PERM_WRITE;
var_dump(hasPermission($editorPerm, PERM_WRITE)); // false — now removed

This pattern is also widely used in PHP’s built-in functions — for example json_encode(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), where JSON_PRETTY_PRINT and JSON_UNESCAPED_UNICODE are bitwise flag constants.


Array Operators #

PHP has dedicated operators for working with arrays. Their behavior differs from ordinary comparison operators and needs to be understood carefully.

<?php
$a = ["x" => 1, "y" => 2];
$b = ["y" => 9, "z" => 3];

// Union (+): merge arrays, left keys win on duplicates
$union = $a + $b;
print_r($union);
// Array ( [x] => 1 [y] => 2 [z] => 3 )
// Note: [y] stays 2 (from $a), not 9 (from $b)!

// ANTI-PATTERN: using + when you want a merge with override
// CORRECT: use array_merge() when right keys should win
$merge = array_merge($a, $b);
print_r($merge);
// Array ( [x] => 1 [y] => 9 [z] => 3 )
// [y] becomes 9 (from $b, overriding $a)

Array Comparison #

<?php
$a = ["a" => 1, "b" => 2];
$b = ["b" => 2, "a" => 1]; // different order
$c = ["a" => 1, "b" => "2"]; // different value types

// == : equal if key-values match, order doesn't matter, type conversion applies
var_dump($a == $b);  // true  — same key-values even though the order differs
var_dump($a == $c);  // true  — "2" == 2 after conversion

// === : equal if key-values match, order MUST match, types must match
var_dump($a === $b); // false — different order!
var_dump($a === $c); // false — different types (int vs string)

$d = ["a" => 1, "b" => 2]; // identical to $a
var_dump($a === $d); // true

The Ternary Operator and Its Variants #

The ternary operator and its variants provide a concise way to write conditional expressions as expressions (not statements), so the result can be assigned directly or used in a larger expression.

<?php
$age = 20;

// Full ternary: condition ? true_value : false_value
$status = $age >= 18 ? "adult" : "minor";

// The Elvis operator (?:) — shorthand ternary without a middle expression
// Returns the left operand if truthy, the right operand if falsy
$name  = "";
$display = $name ?: "Guest"; // "Guest" — because $name is falsy (empty string)

$name2  = "Budi";
$display2 = $name2 ?: "Guest"; // "Budi" — because $name2 is truthy

// Equivalent to: $display = $name ? $name : "Guest";
// but more concise because you don't write $name twice

// Null coalescing (??) — only checks null/undefined, not falsy
$config = ["host" => "localhost", "port" => null];

echo $config["host"] ?? "127.0.0.1"; // "localhost" — exists and isn't null
echo $config["port"] ?? 3306;         // 3306 — its value is null, use the default
echo $config["user"] ?? "root";       // "root" — key doesn't exist

// The difference between ?: and ??
$value = 0;
echo $value ?: "default";  // "default" — 0 is falsy!
echo $value ?? "default";  // 0 — 0 isn't null, so return $value

Null Coalescing Assignment ??= #

<?php
// Assign a value only if the variable is null or undefined
$options = [];
$options['debug']   ??= false;  // set to false because it doesn't exist yet
$options['timeout'] ??= 30;     // set to 30
$options['timeout'] ??= 60;     // UNCHANGED — it already has a value (30)

// Useful when building a config array incrementally
function defaultConfig(array &$config): void
{
    $config['debug']     ??= false;
    $config['timeout']   ??= 30;
    $config['max_retry'] ??= 3;
    $config['cache_ttl'] ??= 3600;
}

$userConfig = ['debug' => true, 'timeout' => 10];
defaultConfig($userConfig);

// debug and timeout are unchanged (already set), but max_retry and cache_ttl are added
print_r($userConfig);

Nested Ternary — When to Stop #

<?php
// ANTI-PATTERN: hard-to-read nested ternaries
$level = 3;
$label = $level === 1 ? "Junior"
       : $level === 2 ? "Mid"
       : $level === 3 ? "Senior"
       : "Unknown";

// CORRECT: use match for cases like this — far clearer
$label = match($level) {
    1 => "Junior",
    2 => "Mid",
    3 => "Senior",
    default => "Unknown",
};

// Or an array lookup for simple mappings
$labels = [1 => "Junior", 2 => "Mid", 3 => "Senior"];
$label  = $labels[$level] ?? "Unknown";

Precedence and Associativity #

Precedence determines which operator is evaluated first when several operators appear in one expression. Associativity determines the evaluation direction when operators have the same precedence.

PHP’s precedence table (from highest to lowest, the most important groups):

PrecedenceOperatorsAssociativity
Highestclone, new
**Right to left
++, --, ~, (int), (string), etc.
instanceof
!
*, /, %Left to right
+, -, .Left to right
<<, >>Left to right
<, <=, >, >=
==, !=, ===, !==, <=>
&Left to right
^Left to right
|Left to right
&&Left to right
||Left to right
??Right to left
? :
=, +=, -=, etc.Right to left
yield
andLeft to right
xorLeft to right
LowestorLeft to right
<?php
// Examples that often confuse:

// 1. Concatenation and addition — same precedence, left to right
echo "Total: " . 2 + 3;  // PHP error or "3" — WRONG!
// Evaluated as: ("Total: " . 2) + 3 → "Total: 2" + 3
// PHP converts the string to int → 0 + 3 = 3

echo "Total: " . (2 + 3); // "Total: 5" — CORRECT with parentheses

// 2. && vs and
$a = true;
$b = false;
$c = $a && $b;   // $c = false (&& has higher precedence than =)
$d = $a and $b;  // $d = true  (=   has higher precedence than and!)

// 3. ?? vs ?:
$x = null;
echo $x ?? "null-default" ?: "falsy-default";
// Evaluated as: ($x ?? "null-default") ?: "falsy-default"
// → "null-default" ?: "falsy-default"
// → "null-default" (truthy, returned)

// When in doubt, use parentheses
echo ($x ?? "null-default");
Always use parentheses () when combining operators from different categories. PHP’s precedence isn’t always intuitive — especially around . (concatenation), ??, ?:, and the && vs and difference. Parentheses make your intent explicit and prevent subtle precedence bugs.

Summary #

  • Always use === instead of == for comparisons — == performs implicit type conversion whose results are often unpredictable, and its behavior changed between PHP 7 and PHP 8.
  • && and and are not synonymsand has lower precedence than =, so $x = true and false produces $x = true, not false. Use && and || consistently.
  • The spaceship <=> is the cleanest way to write usort() callbacks — it supports multi-criteria sorting by comparing arrays directly.
  • Short-circuit evaluation — in &&, the right expression isn’t evaluated if the left is already false. In ||, the right side isn’t evaluated if the left is already true. Use this for efficient guard conditions.
  • ?? vs ?:?? only checks null/undefined; ?: checks falsy (including 0, “”, false). Use ?? for defaults that replace null, ?: for fallbacks from falsy values.
  • The + operator on arrays is not the same as array_merge()+ keeps the left key on duplicates; array_merge() lets the right key win.
  • Bitwise operators for permission flags — the PERM_READ | PERM_WRITE pattern is the idiomatic way to store multiple boolean flags in a single integer.
  • Use parentheses when combining operators from different categories — PHP’s precedence isn’t always intuitive, and parentheses make your intent explicit.

← Previous: Data Types   Next: Conditional Statements →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact