Home PHP PHP Operators — Why Loose Equality Exposes User Accounts
Beginner 9 min · March 06, 2026

PHP Operators — Why Loose Equality Exposes User Accounts

A stored hash starting with '0e' plus digits made any password valid in PHP—a classic loose equality bug that strict === avoids.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 20 min
  • Basic programming fundamentals
  • A computer with internet access
  • Willingness to follow along with examples
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • PHP operators are symbols that perform operations on values: arithmetic, comparison, logical, assignment, and more.
  • Every comparison has two versions: loose (==) and strict (===). Mixing them causes most security bugs.
  • Assignment operators (+=, -=, .=) combine operation and assignment in one step — use them to signal mutation.
  • The null coalescing operator (??) and null-safe operator (?->) eliminate null errors in modern PHP.
  • Performance insight: Loose comparisons are slower than strict because PHP must coerce types first.
  • Production insight: Using == for password checks can bypass authentication when hash starts with '0e' (PHP type juggling).
✦ Definition~90s read
What is PHP Operators?

PHP operators are the fundamental building blocks that transform static variables into dynamic logic, but they also represent a critical security boundary that many developers underestimate. Operators in PHP are not just syntactic sugar — they directly control how data flows through your application, how comparisons are made, and ultimately how authentication, authorization, and data integrity decisions are executed.

Think of PHP operators as the verbs of your code — they're the action words that tell PHP what to DO with your data.

The infamous loose equality operator (==) versus strict equality (===) is a prime example: using == can cause type juggling that treats "0" as equal to 0, or even worse, "0e12345" as equal to "0e67890" because PHP coerces both to zero when they look like scientific notation. This has historically exposed user accounts in authentication systems where password hashes (which often start with 0e followed by digits) could be bypassed by an attacker providing a matching hash string.

Understanding operators as a security boundary means recognizing that every ==, !=, or loose comparison in a conditional is a potential vulnerability that strict operators (===, !==) can eliminate.

PHP operators span several categories that every developer must internalize. Arithmetic operators (+, -, , /, %, *) work as expected but with PHP's dynamic typing — dividing integers can produce floats, and modulus on negatives follows the dividend's sign.

Assignment operators (=, +=, -=, etc.) combine operation and assignment in one step, but the simple = is easily confused with == in conditionals, a classic source of bugs. Comparison operators (==, ===, !=, !==, <, >, <=, >=, <=>) are where the security landmines live: the spaceship operator (<=>) returns -1, 0, or 1 and is safe, but loose comparisons with == can produce unexpected results like "php" == 0 being true because PHP converts the string to an integer (0 if it doesn't start with a number).

Logical operators (&&, ||, !, and, or, xor) short-circuit, meaning false && expensiveFunction() never calls the function — a performance optimization that can also mask bugs if you rely on side effects.

String operators are deceptively simple: . concatenates, .= appends. But concatenation in loops without using an array and implode() is a performance anti-pattern that creates O(n²) memory allocations. Increment/decrement operators (++, --) work on strings in a Perl-like way — ++ on "a" gives "b", on "z" gives "aa" — but -- does not, which catches many developers off guard.

The ternary operator (?:) and null coalescing operator (??) are essential for concise defaults, but the ternary's left-associativity in PHP (unlike most languages) means nested ternaries often evaluate in unexpected order; always parenthesize them. The null-safe operator (?->) introduced in PHP 8.0 short-circuits on null: $user?->profile?->email returns null if $user or $user->profile is null, avoiding fatal errors on chained method/property access.

In production systems handling millions of requests, choosing ?? over ?: or === over == isn't pedantry — it's the difference between a secure login flow and a CVE in your authentication logic.

Plain-English First

Think of PHP operators as the verbs of your code — they're the action words that tell PHP what to DO with your data. Just like a calculator has buttons for +, -, × and ÷, PHP has symbols that add numbers, compare values, combine conditions, and assign data to variables. Without operators, you'd have data sitting around doing absolutely nothing — operators are what make things happen.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Every useful program ever written does at least one of three things: it calculates something, it compares something, or it makes a decision. None of those three things are possible without operators. Whether you're building a shopping cart that totals an order, a login form that checks if a password matches, or a news feed that filters posts by date — operators are working silently behind every single line of that logic. They are the engine of your code.

Before operators existed in programming languages, developers had to write verbose machine-level instructions just to add two numbers. Operators solve this by giving us a clean, readable shorthand. Instead of writing a function call like add($price, $tax), you just write $price + $tax. Instead of a function like isGreaterThan($age, 18), you write $age > 18. Operators compress complex intentions into a single character or short symbol — and PHP has a rich set of them covering arithmetic, comparison, logic, assignment, string manipulation and more.

By the end of this article you'll understand every major category of PHP operator, know exactly when to reach for each one, be able to read and write real PHP logic without second-guessing yourself, and you'll know the two or three sneaky mistakes that trip up even experienced developers. Let's build this up from the ground floor.

Why PHP Operators Are a Security Boundary, Not Just Syntax

PHP operators are symbols that perform operations on values and variables. The core mechanic is that they define how values are compared, combined, or transformed — but the critical distinction is between loose (==) and strict (===) equality. Loose equality performs type juggling before comparison, meaning '1' == 1 is true, while strict equality checks both value and type. This is not a minor detail; it's a fundamental behavior that determines how your application handles authentication, authorization, and data validation. Loose equality coerces strings to numbers, objects to arrays, and nulls to empty strings, creating implicit conversion paths that often bypass intended logic. In practice, this means a user input of '0e12345' compared with == to a password hash starting with '0e' can evaluate as true because PHP interprets both as scientific notation numbers equal to zero. This is not theoretical — it has been exploited in real-world authentication bypasses. Use strict equality (===) for all security-sensitive comparisons, including password verification, session tokens, and user IDs. Loose equality is only safe when you explicitly intend type coercion, such as checking if a form field is empty with == ''.

⚠ Type Juggling Is Not a Bug — It's a Design Choice That Bites
Loose equality (==) does not 'guess' types; it follows a strict coercion table that often produces unexpected results like '0' == false being true.
📊 Production Insight
A login endpoint using loose equality (==) to compare a user-supplied password hash against the stored hash allowed an attacker to bypass authentication by sending '0e12345' as the password, because both the stored hash and the input were interpreted as zero in scientific notation.
The symptom: users with password hashes starting with '0e' followed by digits could be logged into with any similarly structured string, silently granting unauthorized access.
Rule of thumb: always use strict comparison (===) for any value that originates from user input or a database — type coercion is a liability, not a convenience.
🎯 Key Takeaway
Loose equality (==) performs type juggling and can cause security vulnerabilities like authentication bypasses.
Strict equality (===) checks both value and type — use it for all comparisons involving user input, hashes, or tokens.
The only safe use of loose equality is when you explicitly need type coercion, e.g., checking if a string is empty with == ''.
php-operators PHP Operator Security Layers Hierarchy of operator risks from input to output Input Layer User Data | HTTP Requests | Form Values Comparison Layer Loose Equality (==) | Strict Equality (===) | Logical Operators Type Juggling Layer String to Int | Boolean Coercion | Null Handling Security Boundary Authentication Check | Authorization Logic | Session Validation Output Layer Account Access | Data Exposure | Privilege Escalation THECODEFORGE.IO
thecodeforge.io
Php Operators

Arithmetic Operators — PHP as Your Calculator

Arithmetic operators are the ones you already know from primary school maths — they perform mathematical calculations on numbers. PHP gives you addition (+), subtraction (-), multiplication (), division (/), modulus (%), and exponentiation (*).

The one you might not recognise is modulus (%). It gives you the remainder after a division. So 10 % 3 is 1, because 3 goes into 10 three times (that's 9), leaving 1 left over. This is incredibly useful for things like checking whether a number is even or odd — an even number always has a remainder of 0 when divided by 2.

Exponentiation () is also worth highlighting. 2 8 means '2 to the power of 8', which is 256. You'll see this in security calculations, image sizing logic, and anywhere exponential growth matters.

PHP follows standard mathematical order of operations — multiplication and division before addition and subtraction. Use parentheses to override that order and make your intention crystal clear to anyone reading your code later.

arithmetic_operators.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php

// Imagine you're building a simple invoice calculator
$itemPrice   = 49.99;   // price of one item in dollars
$quantity    = 3;        // how many the customer ordered
$discountPct = 10;       // 10% discount
$taxRate     = 0.08;     // 8% sales tax as a decimal

// --- Addition ---
$shippingFee = 5.99;
$subtotalWithShipping = $itemPrice + $shippingFee; // adds two values
echo "Item + Shipping: $" . $subtotalWithShipping . "\n"; // 55.98

// --- Subtraction ---
$refundAmount = 10.00;
$amountAfterRefund = $itemPrice - $refundAmount;  // removes the refund
echo "After Refund: $" . $amountAfterRefund . "\n"; // 39.99

// --- Multiplication ---
$lineTotal = $itemPrice * $quantity; // price times quantity = line total
echo "Line Total: $" . $lineTotal . "\n"; // 149.97

// --- Division ---
$discountAmount = ($discountPct / 100) * $lineTotal; // calculate discount value
echo "Discount Amount: $" . $discountAmount . "\n"; // 14.997

// --- Modulus (remainder after division) ---
$totalItems = 10;
$itemsPerBox = 3;
$leftoverItems = $totalItems % $itemsPerBox; // 10 divided by 3 = 3 remainder 1
echo "Items that won't fit in a full box: " . $leftoverItems . "\n"; // 1

// A classic use: check if a number is even or odd
$orderNumber = 4582;
if ($orderNumber % 2 === 0) {
    echo "Order #" . $orderNumber . " is an even-numbered order.\n";
} else {
    echo "Order #" . $orderNumber . " is an odd-numbered order.\n";
}

// --- Exponentiation ---
$base = 2;
$power = 10;
$result = $base ** $power; // 2 to the power of 10
echo "2 to the power of 10 is: " . $result . "\n"; // 1024

// --- Order of operations matters! ---
$wrongTotal  = $itemPrice * $quantity + $discountPct / 100; // PHP does * and / first
$correctTotal = ($itemPrice * $quantity) + ($discountPct / 100); // parentheses = intention is clear
echo "Wrong-looking (but technically same here): $" . $wrongTotal . "\n";
echo "With parentheses (explicit intention): $" . $correctTotal . "\n";
Output
Item + Shipping: $55.98
After Refund: $39.99
Line Total: $149.97
Discount Amount: $14.997
Items that won't fit in a full box: 1
Order #4582 is an even-numbered order.
2 to the power of 10 is: 1024
Wrong-looking (but technically same here): $150.07
With parentheses (explicit intention): $150.07
⚠ Watch Out: Division Doesn't Always Return What You Expect
In PHP, 10 / 3 returns 3.3333... (a float), not 3. If you need a whole number result from division, use intdiv(10, 3) which returns 3, or cast with (int)(10 / 3). Never assume PHP integer division truncates like it does in some other languages.
📊 Production Insight
Floating point division can introduce rounding errors in financial calculations.
Use bcdiv() for precise decimal arithmetic in payment systems.
Rule: never compare floats with ==; use a tolerance instead.
🎯 Key Takeaway
Arithmetic operators follow standard precedence.
Modulus is great for even/odd checks and cycling values.
Always parenthesize complex expressions to avoid surprises.

Assignment Operators — Storing and Updating Values in One Move

The basic assignment operator is =. But here's something beginners always get confused about: in PHP (and most programming languages), = does NOT mean 'equals'. It means 'take the value on the right and store it in the variable on the left'. Think of it like putting a label on a jar.

$score = 100 means: create a jar called score, and put the number 100 inside it.

PHP also gives you compound assignment operators that combine a maths operation with an assignment in one step. Instead of writing $score = $score + 10, you can write $score += 10. These aren't just shortcuts — they make your code easier to read because they communicate 'I am changing this existing value' rather than 'I am creating a new value'.

These compound operators exist for all arithmetic operations: +=, -=, =, /=, %=, *=. There's also .= for strings, which appends text to an existing string variable — something you'll use constantly when building HTML output or log messages.

Using these operators correctly is a sign of confident, fluent PHP code.

assignment_operators.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php

// --- Basic Assignment ---
$playerName  = "Alex";    // store the string "Alex" in $playerName
$playerScore = 0;          // player starts with zero points
$playerLevel = 1;          // player starts at level 1

echo "Starting state — Player: $playerName, Score: $playerScore, Level: $playerLevel\n";

// --- += (add and assign) ---
// Player picks up a coin worth 50 points
$playerScore += 50; // same as: $playerScore = $playerScore + 50
echo "After coin pickup — Score: $playerScore\n"; // 50

// Player gets a time bonus of 30 points
$playerScore += 30;
echo "After time bonus — Score: $playerScore\n"; // 80

// --- -= (subtract and assign) ---
// Player takes damage and loses 15 points
$playerScore -= 15; // same as: $playerScore = $playerScore - 15
echo "After taking damage — Score: $playerScore\n"; // 65

// --- *= (multiply and assign) ---
// Player activates a double-score power-up!
$playerScore *= 2; // same as: $playerScore = $playerScore * 2
echo "After double-score power-up — Score: $playerScore\n"; // 130

// --- /= (divide and assign) ---
// Score gets halved for using a hint
$playerScore /= 2; // same as: $playerScore = $playerScore / 2
echo "After hint penalty — Score: $playerScore\n"; // 65

// --- %= (modulus and assign) ---
// Rarely used, but here's a real example:
// "Wrap" the level number to stay within 1-5 cycle
$currentLevel = 7;
$currentLevel %= 5; // 7 % 5 = 2, wraps back into range 0-4
echo "Wrapped level (0-indexed): $currentLevel\n"; // 2

// --- .= (concatenate and assign) — extremely useful for building strings ---
$gameLog  = "[LOG] Game started. ";
$gameLog .= "Player $playerName joined. ";  // appends to existing string
$gameLog .= "Final score: $playerScore.";   // appends again
echo $gameLog . "\n";
Output
Starting state — Player: Alex, Score: 0, Level: 1
After coin pickup — Score: 50
After time bonus — Score: 80
After taking damage — Score: 65
After double-score power-up — Score: 130
After hint penalty — Score: 65
Wrapped level (0-indexed): 2
[LOG] Game started. Player Alex joined. Final score: 65.
💡Pro Tip: Use .= When Building HTML in a Loop
When generating HTML output inside a loop, do NOT echo each line separately — it's slow and messy. Instead, build the full string with .= and echo once at the end: $html = ''; foreach($items as $item) { $html .= "<li>$item</li>"; } echo $html;. This is cleaner, testable, and easier to extend.
📊 Production Insight
Forgetting to use .= and using = instead inside a loop overwrites the variable each iteration.
You'll only see the last item. This bug silently eats data.
Rule: when accumulating strings, always use .=.
🎯 Key Takeaway
Compound assignment operators signal mutation.
They make code shorter and clearer.
.= is essential for string building — use it everywhere.
php-operators Loose vs Strict Equality in PHP Why strict operators prevent account exposure Loose Equality (==) Strict Equality (===) Type coercion Converts types automatically No type conversion Example: 0 == 'admin' True (string becomes 0) False (different types) Authentication risk High - bypass possible Low - type mismatch blocks Performance Slightly slower due to conversion Faster, direct comparison Best practice Avoid in security contexts Always use for passwords THECODEFORGE.IO
thecodeforge.io
Php Operators

Comparison and Logical Operators — The Decision-Making Duo

Comparison operators let PHP evaluate whether something is true or false by comparing two values. Every if statement, every loop condition, every filter you write depends on them. They always return a boolean — either true or false.

The most important distinction in PHP comparisons is == versus ===. Double-equals (==) checks if two values are loosely equal — PHP will try to convert types before comparing. Triple-equals (===) checks if two values are strictly equal — same value AND same type. This difference causes some of the nastiest bugs in PHP.

Logical operators combine multiple comparisons into one decision. && (AND) means both conditions must be true. || (OR) means at least one must be true. ! (NOT) flips a boolean — true becomes false and vice versa.

Think of && and || like real-world logic: 'You can enter the club if you have ID AND you're over 18' is &&. 'You get a discount if you're a student OR a senior' is ||.

There's also the spaceship operator <=>, which returns -1, 0, or 1 — perfect for sorting. And the null coalescing operator ??, which is one of PHP's most practical modern features.

comparison_logical_operators.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php

// ===========================
// COMPARISON OPERATORS
// ===========================

$userAge        = 20;
$minimumAge     = 18;
$membershipTier = "gold";

// == (loose equality — compares VALUE only, converts types)
$numericString = "20";  // this is a STRING, not an integer
var_dump($userAge == $numericString);   // true — PHP converts "20" to 20 before comparing

// === (strict equality — compares VALUE and TYPE)
var_dump($userAge === $numericString);  // false — 20 (int) is not identical to "20" (string)

// != (not equal, loose)
var_dump($userAge != 18);  // true — 20 is not equal to 18

// !== (not identical, strict)
var_dump($userAge !== $numericString); // true — different types

// > < >= <=
var_dump($userAge > $minimumAge);   // true — 20 is greater than 18
var_dump($userAge >= $minimumAge);  // true — 20 is greater than or equal to 18
var_dump($userAge < 25);            // true — 20 is less than 25
var_dump($userAge <= 20);           // true — 20 is less than or equal to 20

echo "\n";

// Spaceship operator <=> (returns -1, 0, or 1)
// Great for custom sort functions
$priceA = 29.99;
$priceB = 49.99;
$comparison = $priceA <=> $priceB; // -1 means left is LESS THAN right
echo "Spaceship result: " . $comparison . "\n"; // -1

// ===========================
// LOGICAL OPERATORS
// ===========================

$isLoggedIn      = true;
$hasVerifiedEmail = true;
$isAdminUser     = false;
$isBannedUser    = false;

// && (AND) — both must be true
if ($isLoggedIn && $hasVerifiedEmail) {
    echo "Access granted: user is logged in AND has verified email.\n";
}

// || (OR) — at least one must be true
if ($isAdminUser || $hasVerifiedEmail) {
    echo "Can post content: user is admin OR has verified email.\n";
}

// ! (NOT) — flips the boolean
if (!$isBannedUser) {
    echo "User is not banned — showing dashboard.\n";
}

// Combining conditions
$canPublishPost = $isLoggedIn && $hasVerifiedEmail && !$isBannedUser;
echo "Can publish post: " . ($canPublishPost ? 'Yes' : 'No') . "\n"; // Yes

// Null coalescing operator ?? (PHP 7+)
// Returns the LEFT side if it's set and not null, otherwise returns the RIGHT side
$username = null;
$displayName = $username ?? "Guest"; // $username is null, so use "Guest"
echo "Welcome, " . $displayName . "!\n"; // Welcome, Guest!

$loggedInUsername = "sarah_dev";
$displayName2 = $loggedInUsername ?? "Guest"; // $loggedInUsername is set, so use it
echo "Welcome, " . $displayName2 . "!\n"; // Welcome, sarah_dev!
Output
bool(true)
bool(false)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
Spaceship result: -1
Access granted: user is logged in AND has verified email.
Can post content: user is admin OR has verified email.
User is not banned — showing dashboard.
Can publish post: Yes
Welcome, Guest!
Welcome, sarah_dev!
⚠ Watch Out: == vs === Is PHP's #1 Gotcha
In PHP, 0 == 'hello' evaluates to TRUE in PHP versions before 8.0, because PHP converted the string 'hello' to the integer 0. This caused serious security bugs in login systems where an empty password hash compared loosely to 0. Always use === for comparisons unless you have a specific reason not to. Make strict comparison your default habit from day one.
📊 Production Insight
Loose comparison (==) can cause authentication bypass when string hashes start with '0e'.
PHP converts '0e12345' and '0e67890' to 0, so 0 == 0 is true.
Rule: always use strict (===) for passwords, hashes, and IDs.
🎯 Key Takeaway
Strict comparison (===) checks value and type.
Loose comparison (==) coerces types — dangerous.
Use ?? for default values and <=> for custom sorting.

String Operators and Concatenation

The dot operator (.) is PHP's string concatenation operator. It joins two strings into one. 'Hello' . ' World' produces 'Hello World'. You'll use it constantly when building messages, HTML, or any textual output.

There's also the compound assignment version (.=) which appends a string to the end of an existing variable. $str .= ' more text' is equivalent to $str = $str . ' more text'. This is your go-to tool for building strings in loops, constructing SQL queries, or accumulating log messages.

One subtle behavior: PHP automatically converts numbers to strings when using the dot operator. So 42 . ' apples' becomes '42 apples'. But be aware that concatenation has lower precedence than arithmetic, so 'Result: ' . 5 + 3 gives 'Result: 8'? Actually it gives '8' because the addition happens first — always wrap arithmetic in parentheses when concatenating.

string_operators.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

// --- Basic Concatenation ---
$firstName = "John";
$lastName  = "Doe";
$fullName  = $firstName . " " . $lastName;
echo "Full name: " . $fullName . "\n"; // John Doe

// Building a sentence
$noun = "fox";
$verb = "jumps";
$sentence = "The quick brown " . $noun . " " . $verb . " over the lazy dog.";
echo $sentence . "\n";

// Number to string conversion
$count = 5;
echo "You have " . $count . " new messages.\n"; // You have 5 new messages.

// ----- Compound assignment .= -----
$logMessage = "[START] ";
$logMessage .= "User logged in. ";
$logMessage .= "IP 192.168.1.1. ";
$logMessage .= "[END]";
echo $logMessage . "\n"; // [START] User logged in. IP 192.168.1.1. [END]

// ----- Precedence Gotcha -----
$price = 10;
$tax = 2;
// Intent: "Total: 12"
$result1 = "Total: " . $price + $tax;  // Warning: + has higher precedence than .
echo "Without parentheses: " . $result1 . "\n"; // 12 (but produces a warning in PHP 8)
$result2 = "Total: " . ($price + $tax); // Correct
echo "With parentheses: " . $result2 . "\n"; // Total: 12
Output
Full name: John Doe
The quick brown fox jumps over the lazy dog.
You have 5 new messages.
[START] User logged in. IP 192.168.1.1. [END]
Without parentheses: 12
With parentheses: Total: 12
⚠ Precedence Trap: `.` vs `+`
In PHP, the concatenation operator (.) has LOWER precedence than addition (+). So "Total: " . $price + $tax is evaluated as ("Total: " . $price) + $tax, which produces an unexpected result (and a warning in PHP 8). Always wrap arithmetic in parentheses when concatenating: "Total: " . ($price + $tax).
📊 Production Insight
Using .= inside a loop is efficient, but forgetting to initialise the variable with an empty string causes undefined variable warnings.
In PHP 8, this throws a TypeError if the first use is .= with a non-string.
Rule: always initialise string variables before using .=.
🎯 Key Takeaway
Dot (.) joins strings; .= appends.
Wrap arithmetic in parentheses when concatenating.
Always initialise string variables before using .=.

Increment, Decrement, Ternary, and Null-Safe Operators

PHP has a few more operator types that you'll use every day, and skipping them would leave real gaps in your understanding.

Increment and decrement operators (++ and --) increase or decrease a number by exactly 1. They come in two flavours: pre (++$count) changes the value first, then returns it. Post ($count++) returns the current value first, then changes it. This subtlety matters inside expressions.

The ternary operator (?:) is a compact if/else on one line. $result = condition ? 'if true' : 'if false'. Use it for simple assignments — not for complex logic where a full if/else is clearer.

The null-safe operator (?->, PHP 8.0+) lets you safely call methods on an object that might be null, without throwing an error. Before it existed, you'd need an if ($obj !== null) check every single time. Now one ?-> handles it gracefully.

increment_ternary_null_safe.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?php

// ===========================
// INCREMENT / DECREMENT
// ===========================

$pageViews = 100;

// Post-increment: RETURNS current value, THEN increments
$viewsBeforeIncrement = $pageViews++;  // $viewsBeforeIncrement gets 100, THEN $pageViews becomes 101
echo "Returned value (post-increment): " . $viewsBeforeIncrement . "\n"; // 100
echo "pageViews after post-increment:  " . $pageViews . "\n";             // 101

// Pre-increment: INCREMENTS first, THEN returns new value
$newPageViews = ++$pageViews;  // $pageViews becomes 102, THEN $newPageViews gets 102
echo "Returned value (pre-increment):  " . $newPageViews . "\n"; // 102
echo "pageViews after pre-increment:   " . $pageViews . "\n";    // 102

// Decrement works the same way
$stockCount = 5;
$stockCount--; // post-decrement — one item sold
echo "Stock remaining: " . $stockCount . "\n"; // 4

echo "\n";

// ===========================
// TERNARY OPERATOR (?:)
// ===========================

$cartItemCount = 3;

// Long way:
if ($cartItemCount > 0) {
    $cartStatus = "Items in cart";
} else {
    $cartStatus = "Cart is empty";
}

// Ternary — same logic, one line:
$cartStatus = ($cartItemCount > 0) ? "Items in cart" : "Cart is empty";
echo "Cart status: " . $cartStatus . "\n"; // Items in cart

// Real use — displaying user role label
$isAdmin   = false;
$roleLabel = $isAdmin ? "Administrator" : "Standard User";
echo "Role: " . $roleLabel . "\n"; // Standard User

echo "\n";

// ===========================
// NULL-SAFE OPERATOR (?->) — PHP 8.0+
// ===========================

class UserProfile {
    public string $bio;
    public function __construct(string $bio) {
        $this->bio = $bio;
    }
    public function getBio(): string {
        return $this->bio;
    }
}

// Simulate a user that exists and one that doesn't
$activeUser  = new UserProfile("Full-stack developer from Toronto.");
$deletedUser = null; // this user no longer exists

// Without null-safe operator, you'd need:
// if ($deletedUser !== null) { $bio = $deletedUser->getBio(); } else { $bio = null; }

// With null-safe operator (?->) — clean and safe:
$activeBio  = $activeUser?->getBio();  // works fine, returns the bio
$deletedBio = $deletedUser?->getBio(); // $deletedUser is null — returns null safely, no error

echo "Active user bio: " . ($activeBio ?? 'No bio available') . "\n";
echo "Deleted user bio: " . ($deletedBio ?? 'No bio available') . "\n";
Output
Returned value (post-increment): 100
pageViews after post-increment: 101
Returned value (pre-increment): 102
pageViews after pre-increment: 102
Stock remaining: 4
Cart status: Items in cart
Role: Standard User
Active user bio: Full-stack developer from Toronto.
Deleted user bio: No bio available
💡Pro Tip: Prefer ?? Over Ternary for Null Checks
When you're just checking if a value is null and want a fallback, use ?? instead of a ternary. $name = $input ?? 'default' is cleaner and more intentional than $name = ($input !== null) ? $input : 'default'. PHP 7.4+ even gives you ??= — the null coalescing assignment: $name ??= 'default' only assigns if $name is currently null.
📊 Production Insight
Post-increment inside a comparison can cause off-by-one bugs in pagination.
For example, if ($i++ < $limit) uses the old value, so it may run one extra iteration.
Rule: use pre-increment (++$i) in loop conditions, or increment after the condition.
🎯 Key Takeaway
Pre-increment returns the new value; post-increment returns the old.
Use ternary for simple assignments only.
Null-safe operator (?->) prevents errors from null objects.

Bitwise and Type Operators — Low-Level Power and Type Safety

PHP also provides bitwise operators (&, |, ^, ~, <<, >>) that work on the binary representation of integers. These are often used for flags, permissions, or low‑level data manipulation. For example, you can pack multiple boolean flags into a single integer using bitwise OR and check them with AND.

Type operators help you inspect and enforce variable types. instanceof checks whether an object is an instance of a specific class (or implements an interface). The @ error control operator suppresses errors from an expression — but use it sparingly, as it often hides real problems.

Bitwise operators have lower precedence than comparison operators, so always parenthesize your expressions when mixing them.

bitwise_type_operators.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php

// ===========================
// BITWISE OPERATORS
// ===========================

// Define permission flags as powers of 2
$PERMISSION_READ   = 1;   // 001
$PERMISSION_WRITE  = 2;   // 010
$PERMISSION_EXECUTE = 4;  // 100

// Assign user permissions using bitwise OR
$userPermissions = $PERMISSION_READ | $PERMISSION_EXECUTE; // 101 = 5

// Check permissions using bitwise AND
if ($userPermissions & $PERMISSION_READ) {
    echo "User has READ permission.\n";
}
if ($userPermissions & $PERMISSION_WRITE) {
    echo "User has WRITE permission.\n";
} else {
    echo "User does NOT have WRITE permission.\n";
}

// Bitwise shift
$value = 4; // 100 in binary
$shiftedLeft = $value << 1;  // 1000 = 8
$shiftedRight = $value >> 1; // 10   = 2
echo "4 << 1 = " . $shiftedLeft . "\n";
echo "4 >> 1 = " . $shiftedRight . "\n";

// ===========================
// TYPE OPERATORS
// ===========================

class User {};
class Admin extends User {};

$user = new Admin();
echo "Is user an instance of User? " . ($user instanceof User ? "Yes" : "No") . "\n"; // Yes
echo "Is user an instance of Admin? " . ($user instanceof Admin ? "Yes" : "No") . "\n"; // Yes

// Error control operator @ — suppress warnings (use sparingly)
$result = @file_get_contents("nonexistent.txt"); // Suppresses warning, $result = false
if ($result === false) {
    echo "File not found (error suppressed).\n";
}
Output
User has READ permission.
User does NOT have WRITE permission.
4 << 1 = 8
4 >> 1 = 2
Is user an instance of User? Yes
Is user an instance of Admin? Yes
File not found (error suppressed).
🔥When to Use Bitwise vs Permissions Database
Bitwise flags are fast and memory‑efficient for simple yes/no permissions (max ~30 flags per integer). For complex ACLs or role‑based systems with many users, use a database table instead. Bitwise permissions become unreadable when you have more than a handful of options.
📊 Production Insight
Overusing @ error suppression hides critical failures like database connection errors.
It makes debugging a nightmare because no error is logged.
Rule: never suppress errors in production code unless you explicitly handle the failure.
🎯 Key Takeaway
Bitwise operators are great for flags and low‑level manipulation.
instanceof ensures type safety for operations.
Avoid @ — it hides bugs that will bite you later.

Array Operators — Merging Without the Side Effects

Array operators don't get enough respect until you're debugging why a merge silently dropped a key. The + operator is not array_merge. It's a union that keeps the left operand's keys if they already exist. That means $a + $b is the same as writing: take everything from $a, then append anything from $b that isn't already set. No overwrites. No surprises. Use this when you want default values that never get overridden — like config arrays. == and === compare arrays structurally. === checks order and types too. That catches you when two arrays have the same values but different internal pointers. If you're ever tempted to serialize() just to compare arrays, stop. Use ===. It's faster and doesn't serialize the whole object graph. The null coalescing operator ?? also works on arrays: $config['timeout'] ?? 30 won't throw an undefined index notice. Use that instead of isset() chains.

ConfigFallbackMerge.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — php tutorial

$defaults = [
    'host' => 'localhost',
    'port' => 5432,
    'timeout' => 30,
];

$user_config = [
    'host' => 'prod-db.internal',
    'port' => 5432,
    'dbname' => 'orders',
];

// Union keeps first-match keys — user config wins
$merged = $user_config + $defaults;

var_dump($merged);
/*
array(4) {
  ["host"]=> string(19) "prod-db.internal"
  ["port"]=> int(5432)
  ["dbname"]=> string(6) "orders"
  ["timeout"]=> int(30)
}
*/
Output
array(4) {
["host"]=> string(19) "prod-db.internal"
["port"]=> int(5432)
["dbname"]=> string(6) "orders"
["timeout"]=> int(30)
}
⚠ Never Do This:
Using array_merge() when you meant the union operator. array_merge reindexes numeric keys and overwrites string keys from the right. That's two different behaviours — pick the right tool.
🎯 Key Takeaway
Remember: + for defaults you keep, array_merge() for overwrites you control.

Execution Operators — Backticks Are a Production Footgun

Backticks in PHP run shell commands. $output = \ls -la\; returns stdout as a string. That's all it does. No error handling. No stderr. No timeout. No control. In production, that's a footgun. One runaway gzip process or a missing binary and your script hangs indefinitely — or worse, exposes internal data if you accidentally interpolate user input. The shell_exec() function does the same thing but is more explicit. At least it's easier to grep for in a code review. Never, ever pass unsanitised input into a backtick expression. That's a command injection vulnerability waiting to happen. If you must execute a system command, use proc_open() or symfony/process. Those give you timeout, stderr capture, and proper exit code handling. Backticks are fine for one-liner dev scripts on your local machine. That's it.

PingCheckDangerous.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — php tutorial

$target = $_GET['host'] ?? '';
// NEVER DO THIS — direct user input into backticks
$result = `ping -c 2 $target`;
echo $result;

// Safer but still not great — no timeout, no stderr
$escaped = escapeshellarg($target);
$result = `ping -c 2 $escaped`;
echo $result;

// The right way: use symfony/process or proc_open
// proc_open gives you pipes for stdin/stdout/stderr + exit codes
Output
PING google.com (142.250.80.14): 56 data bytes
64 bytes from 142.250.80.14: icmp_seq=0 ttl=118 time=2.214 ms
--- google.com ping statistics ---
2 packets transmitted, 2 packets received, 0.0% packet loss
⚠ Production Trap:
Backticks with user input = command injection. Even with escapeshellarg(), you still can't set a timeout. A slow command blocks every subsequent request on that process.
🎯 Key Takeaway
Backticks are a code smell in any file that touches user data. Use proc_open() or symfony/process instead.

Nullsafe Operator ?-> (PHP 8.0)

The nullsafe operator ?-> was introduced in PHP 8.0 to simplify chained method calls or property accesses on potentially null values. Instead of writing nested null checks, you can use ?-> to short-circuit the chain if any element is null, returning null immediately. This is particularly useful when working with deeply nested objects from APIs or databases.

Consider a scenario where you have a user object that may have an address, and that address may have a city. Without the nullsafe operator, you'd write:

$city = null;
if ($user !== null) {
    $address = $user->getAddress();
    if ($address !== null) {
        $city = $address->getCity();
    }
}
$city = $user?->getAddress()?->getCity();

If $user is null, the entire expression returns null without throwing an error. Similarly, if getAddress() returns null, the chain stops and returns null.

Important: The nullsafe operator only works for method calls and property accesses, not for array accesses or function calls. Also, it cannot be used on the left side of an assignment.

This operator is a game-changer for readability and reduces boilerplate null-checking code. However, be cautious: overusing it can mask logic errors where a null value is unexpected. Use it when null is a valid and expected state.

nullsafe.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

class Address {
    public function getCity(): ?string {
        return 'New York';
    }
}

class User {
    public function getAddress(): ?Address {
        return new Address();
    }
}

$user = new User();
$city = $user?->getAddress()?->getCity();
echo $city; // Output: New York

$user = null;
$city = $user?->getAddress()?->getCity();
echo $city; // Output: (nothing, null)
💡Use Nullsafe for Optional Chaining Only
📊 Production Insight
In production, nullsafe operator reduces the need for repetitive null checks, making code cleaner. However, ensure that null is a legitimate state; otherwise, use explicit error handling.
🎯 Key Takeaway
The nullsafe operator ?-> allows safe chaining of method calls and property accesses on potentially null values, returning null if any part of the chain is null.

Null Coalescing Assignment ??= (PHP 7.4)

The null coalescing assignment operator ??= was introduced in PHP 7.4 as a shorthand for assigning a value only if the variable is null. It combines the null coalescing operator ?? with assignment. The syntax $var ??= $value is equivalent to $var = $var ?? $value. This is extremely useful for setting default values without overwriting existing non-null values.

For example, consider a configuration array where you want to set a default value for a key only if it's not already set:

``php $config['timeout'] ??= 30; ``

``php if (!isset($config['timeout'])) { $config['timeout'] = 30; } ``

Or using the ternary operator:

``php $config['timeout'] = $config['timeout'] ?? 30; ``

The ??= operator works with any variable, including array elements and object properties. It only assigns if the current value is null (or undefined, which is treated as null). Note that it does not check for empty strings or zero; only null (and undefined) triggers the assignment.

This operator is particularly handy in loops or when processing data where you want to ensure a default value without overwriting existing data. It improves readability and reduces the chance of errors from manual null checks.

null_coalescing_assignment.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

$config = [
    'host' => 'localhost',
    'port' => null,
];

$config['port'] ??= 3306;
$config['timeout'] ??= 30;

var_dump($config);
// Output:
// array(3) {
//   ["host"]=> string(9) "localhost"
//   ["port"]=> int(3306)
//   ["timeout"]=> int(30)
// }
🔥Only Null Triggers Assignment
📊 Production Insight
In production, ??= is ideal for setting defaults in configuration arrays or initializing optional fields without overwriting existing data. It reduces boilerplate and potential bugs from manual isset checks.
🎯 Key Takeaway
The null coalescing assignment ??= sets a default value only if the variable is null, simplifying code that initializes variables with fallbacks.

Match Expression vs Switch (PHP 8.0)

PHP 8.0 introduced the match expression as a more powerful and concise alternative to the traditional switch statement. Unlike switch, match is an expression (it returns a value) and uses strict comparison (===) instead of loose comparison (==). This eliminates common bugs where type juggling causes unexpected matches.

Key Differences: - match returns a value; switch does not. - match uses strict comparison; switch uses loose comparison. - match can combine multiple conditions with a comma; switch requires fall-through or break. - match throws an UnhandledMatchError if no arm matches (unless a default is provided); switch silently continues.

Example:

```php $statusCode = 404;

// Switch (loose comparison) switch ($statusCode) { case 200: case 302: $message = 'OK'; break; case 404: $message = 'Not Found'; break; case 500: $message = 'Server Error'; break; default: $message = 'Unknown'; }

// Match (strict comparison) $message = match ($statusCode) { 200, 302 => 'OK', 404 => 'Not Found', 500 => 'Server Error', default => 'Unknown', }; ```

Notice how match is more concise and eliminates the need for break statements. It also forces you to handle all cases explicitly (or provide a default), reducing bugs from forgotten cases.

When to use each: - Use match when you need to return a value and want strict comparison. - Use switch when you need side effects (like multiple statements per case) or when loose comparison is intentional.

match is generally preferred for new code due to its safety and readability.

match_vs_switch.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

$statusCode = '404'; // string

// Switch: loose comparison, so '404' == 404 is true
switch ($statusCode) {
    case 404:
        echo 'Switch: Not Found';
        break;
    default:
        echo 'Switch: Unknown';
}
// Output: Switch: Not Found

// Match: strict comparison, so '404' === 404 is false
$message = match ($statusCode) {
    404 => 'Match: Not Found',
    default => 'Match: Unknown',
};
echo $message;
// Output: Match: Unknown
⚠ Strict Comparison Can Break Old Code
📊 Production Insight
In production, prefer match over switch for new code to avoid type juggling bugs and improve readability. However, keep switch for cases where you need fall-through or multiple statements per case.
🎯 Key Takeaway
The match expression in PHP 8.0 provides strict comparison, returns a value, and is more concise than switch, making it the safer and more readable choice for most conditional logic.
● Production incidentPOST-MORTEMseverity: high

Loose Equality Bug Exposed User Accounts

Symptom
Users could log in with any password if their stored hash started with '0e' followed by digits.
Assumption
Using == for password comparison is safe because both values are strings.
Root cause
PHP's type juggling: when comparing a string like '0e12345' to another string like '0e67890', PHP treats both as numeric strings and converts to floats, resulting in 0 == 0 (true).
Fix
Replace == with === for all password hash comparisons. Use password_verify() which uses strict comparison internally.
Key lesson
  • Never use loose comparison (==) for security-sensitive checks.
  • Always use strict comparison (===) by default.
  • Use PHP's built-in password functions which handle comparison safely.
Production debug guideSymptom → Action Guide for Common Operator-Related Failures5 entries
Symptom · 01
Condition always evaluates to true regardless of variable value
Fix
Check for assignment (=) inside if instead of comparison (==). Look for if ($var = 'admin') vs if ($var === 'admin').
Symptom · 02
Unexpected type-coercion results (e.g., '0' == 'hello' returns true in PHP 7)
Fix
Replace all ==/!= with ===/!== and test again. Use var_dump() on the operands to see actual types.
Symptom · 03
Null pointer errors when accessing method on possibly null object
Fix
Upgrade to PHP 8+ and use null-safe operator (?->) or wrap in if ($obj !== null) check.
Symptom · 04
String concatenation not working as expected in loop
Fix
Ensure you're using .= (append) not = (assignment) inside the loop. var_dump the string each iteration.
Symptom · 05
Rounding errors in financial calculations
Fix
Use bcmath functions (bcadd, bccomp) for precise decimal arithmetic. Avoid float comparisons with ==.
★ PHP Operator Debug Cheat SheetQuick commands and fixes for the most common operator-related production issues in PHP.
Strict comparison fails unexpectedly
Immediate action
Check both operand types with var_dump().
Commands
var_dump($a, gettype($a)); var_dump($b, gettype($b));
Compare using === after ensuring types match: if ($a === $b) { ... }
Fix now
Cast the operands to the same type: if ((int)$a === (int)$b) { ... } or use is_int(), is_string().
Assignment inside if statement+
Immediate action
Review condition for single = instead of == or ===.
Commands
grep -n 'if (\$[a-zA-Z_][a-zA-Z0-9_]* = ' yourfile.php
Add parentheses to highlight: if (($var = 'admin')) will warn in PHP 8+.
Fix now
Replace = with === in the condition.
Null safe operator not working (PHP 7 and below)+
Immediate action
Check PHP version. ?-> requires PHP 8.0+.
Commands
php -v | grep -E '^PHP'
Replace with null check: if ($obj !== null) { $obj->method(); } else { ... }
Fix now
Upgrade to PHP 8+ or use ternary with null check: $result = $obj ? $obj->method() : null;
PHP Operator Comparison
OperatorSymbolWhat It DoesReturnsWhen To Use It
Loose Equality==Compares values after type coercionboolAlmost never — prefer ===
Strict Equality===Compares value AND type — no coercionboolDefault choice for all equality checks
Loose Inequality!=True if values differ after coercionboolAlmost never — prefer !==
Strict Inequality!==True if value OR type differsboolDefault choice for inequality checks
Ternary?:Compact if/else for simple assignmentsmixedShort, readable one-liners only
Null Coalescing??Returns left side if set and non-null, else rightmixedDefault values, especially from $_GET/$_POST
Spaceship<=>Returns -1, 0, or 1 for orderingint (-1/0/1)Custom usort() comparison callbacks
Null-Safe?->Calls method/property only if object is not nullmixed or nullOptional relationships — e.g. $user?->getProfile()
Concatenation.Joins two strings into onestringBuilding strings, HTML output, messages
Logical AND&&True only if BOTH sides are trueboolMulti-condition if statements
Logical OR||True if AT LEAST ONE side is trueboolFallback conditions, permission checks
InstanceofinstanceofChecks if object is of a specific class/interfaceboolType checks, especially in dependency injection
Bitwise AND&Performs bitwise AND on two integersintFlag checking, low-level data manipulation
Bitwise OR|Performs bitwise OR on two integersintBuilding permission flags from enumerated values
Error Control@Suppresses errors from an expressionmixedRarely; only when you handle failure explicitly
⚙ Quick Reference
11 commands from this guide
FileCommand / CodePurpose
arithmetic_operators.php$itemPrice = 49.99; // price of one item in dollarsArithmetic Operators
assignment_operators.php$playerName = "Alex"; // store the string "Alex" in $playerNameAssignment Operators
comparison_logical_operators.php$userAge = 20;Comparison and Logical Operators
string_operators.php$firstName = "John";String Operators and Concatenation
increment_ternary_null_safe.php$pageViews = 100;Increment, Decrement, Ternary, and Null-Safe Operators
bitwise_type_operators.php$PERMISSION_READ = 1; // 001Bitwise and Type Operators
ConfigFallbackMerge.php$defaults = [Array Operators
PingCheckDangerous.php$target = $_GET['host'] ?? '';Execution Operators
nullsafe.phpclass Address {Nullsafe Operator ?-> (PHP 8.0)
null_coalescing_assignment.php$config = [Null Coalescing Assignment ??= (PHP 7.4)
match_vs_switch.php$statusCode = '404'; // stringMatch Expression vs Switch (PHP 8.0)

Key takeaways

1
Always use === (strict equality) by default
== silently converts types and causes subtle authentication and logic bugs that are extremely hard to track down.
2
The modulus operator % is not just a curiosity
it's the standard way to check even/odd numbers, cycle through arrays by index, and create pagination calculations.
3
The null coalescing operator ?? (PHP 7+) and null-safe operator ?-> (PHP 8+) are must-know modern PHP
they eliminate entire classes of null-related crashes with clean, readable syntax.
4
Post-increment ($x++) and pre-increment (++$x) both add 1, but return different values inside expressions
post returns the OLD value, pre returns the NEW value. Keep increments on their own line when clarity matters.
5
Floating point comparisons with == are unreliable. Use bccomp() or a tolerance for financial calculations.
6
The concatenation operator (.) has lower precedence than addition
always parenthesize arithmetic inside string concatenation.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between == and === in PHP, and can you give an ex...
Q02SENIOR
What does the spaceship operator <=> return, and in what real-world scen...
Q03SENIOR
If $a = 0 and $b = 'hello', what does var_dump($a == $b) output in PHP 7...
Q04SENIOR
Explain the difference between post-increment ($x++) and pre-increment (...
Q05JUNIOR
What is the null coalescing operator (??) and how does it differ from th...
Q01 of 05JUNIOR

What is the difference between == and === in PHP, and can you give an example where using == would produce a surprising result?

ANSWER
== performs loose equality with type coercion; === performs strict equality checking both value and type. A surprising example: 0 == 'hello' returns true in PHP 7 because 'hello' is converted to 0. Always use === unless you explicitly need coercion.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between == and === in PHP?
02
What does the PHP modulus operator % actually do?
03
When should I use the ternary operator vs a full if/else in PHP?
04
What is the null-safe operator (?->) and how is it different from the null coalescing operator (??)?
05
Why does `0.1 + 0.2 == 0.3` return false in PHP?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's PHP Basics. Mark it forged?

9 min read · try the examples if you haven't

Previous
PHP Variables and Data Types
3 / 14 · PHP Basics
Next
PHP Control Flow