Arithmetic, and the Parts That Are Not Obvious
The arithmetic operators look exactly as you would expect — +, -, *, /, % for remainder and ** for powers — but three of them behave in ways worth knowing before they cost you an afternoon.
Division is the first. / gives you a float whenever the answer is not a whole number, so 7 / 2 is 3.5, not 3. If you want whole-number division, say so with intdiv(7, 2), which returns 3. Students coming from C or Java expect integer division by default and quietly get decimals instead.
The remainder operator % converts its operands to integers first, so 10.5 % 3 is the same as 10 % 3, which is 1. Its most common real use is testing divisibility — $n % 2 === 0 for even numbers, or alternating row colours in a results table with $i % 2. With negative numbers the result takes the sign of the left-hand operand, so -10 % 3 is -1, which surprises people expecting 2.
Dividing by zero is an error in PHP 8, not a warning. Both / and % throw a DivisionByZeroError, which will stop your script unless you catch it. In practice this means checking the divisor before you use it, which you should be doing anyway — an average over an empty result set is exactly this bug.
<?php
echo 10 + 3; // 13
echo 10 - 3; // 7
echo 10 * 3; // 30
echo 10 / 4; // 2.5 <- a float, not 2
echo intdiv(10, 4); // 2 <- whole-number division
echo 10 % 3; // 1
echo 2 ** 10; // 1024
// A real case: percentage of students who passed
$total = 0;
$passed = 0;
// echo ($passed / $total) * 100; // DivisionByZeroError
$percentage = $total > 0 ? round(($passed / $total) * 100, 2) : 0;
echo $percentage; // 0, and no crash
// Increment and decrement
$count = 5;
echo $count++; // prints 5, THEN makes it 6 (post-increment)
echo ++$count; // makes it 7, THEN prints 7 (pre-increment) $i++and++$idiffer only when you use the result in the same expression. Inside a plainforloop header they behave identically, so the common advice to "always use++$ibecause it is faster" is folklore, not something you will ever measure.
Joining Strings, and a Change PHP 8 Made
PHP joins strings with a dot, not a plus. "Hello" . " " . $name produces one string; "Hello" + $name tries to do arithmetic and is an error if the strings hold no numbers. Coming from JavaScript or Python, this is the single most common muscle-memory mistake, and PHP 8's error message about unsupported operand types is at least clear about what went wrong.
The shorthand .= appends to an existing string, which is the natural way to build up a block of HTML in a loop before echoing it once.
PHP 8 changed one detail here that is worth understanding, because it silently fixed a class of bugs. In PHP 7, . and + had equal precedence and were evaluated left to right, so "Total: " . $a + $b meant ("Total: " . $a) + $b — it built a string and then tried to add a number to it, which almost never did what the author intended. In PHP 8, + and - bind more tightly than ., so the same line now means "Total: " . ($a + $b), which is what everyone always wanted.
The lesson to take from that is not the rule itself but the habit: when an expression mixes concatenation with arithmetic, put the arithmetic in brackets. It costs two characters and removes any need to remember precedence tables.
<?php
$first = "Ananya";
$last = "Sharma";
$full = $first . " " . $last; // "Ananya Sharma"
$full .= " (Batch 2026)"; // "Ananya Sharma (Batch 2026)"
// Building HTML in a loop
$subjects = ["Physics", "Chemistry", "Maths"];
$html = "<ul>";
foreach ($subjects as $s) {
$html .= "<li>" . htmlspecialchars($s) . "</li>";
}
$html .= "</ul>";
echo $html;
// Precedence: always bracket the arithmetic
$a = 5;
$b = 3;
echo "Total: " . ($a + $b); // "Total: 8" - clear in any PHP version
// Interpolation is often cleaner than concatenation
$marks = 87;
echo "Ananya scored {$marks} marks"; - For anything longer than two or three pieces, string interpolation or
sprintf()reads better than a chain of dots.sprintf('%s scored %d out of %d', $name, $got, $max)keeps the sentence structure visible instead of burying it in quotes and dots.
Comparison Operators and the Spaceship
Comparison operators return a boolean, and you will use them constantly inside conditions. The pair worth being deliberate about is == versus ===. Loose equality converts types before comparing; strict equality requires the same type as well as the same value. Use === unless you have a specific reason not to — it removes an entire family of bugs at the cost of one keystroke.
The same distinction applies to their negatives: != (which also has the older spelling <>) is loose, and !== is strict. Mixing != with a function that can return 0 or false is a reliable way to produce a bug that only appears with certain input.
The spaceship operator <=> answers a different question. Instead of true or false, it returns -1, 0 or 1 depending on whether the left side is less than, equal to, or greater than the right. On its own that seems academic; its purpose is sorting. PHP's usort() expects a comparison function returning exactly those three values, so <=> turns a custom sort into a one-line comparison. Reversing the operands reverses the sort order.
For sorting by more than one field, chain with ?:: compare the primary field first, and if it returns 0 — meaning a tie — fall through to the secondary field. That pattern appears in almost every real listing page.
<?php
var_dump(5 == "5"); // true - loose, string converted
var_dump(5 === "5"); // false - strict, different types
var_dump(5 != "5"); // false
var_dump(5 !== "5"); // true
var_dump(10 > 3); // true
var_dump(10 <= 10); // true
// The spaceship: -1, 0 or 1
var_dump(3 <=> 5); // -1
var_dump(5 <=> 5); // 0
var_dump(7 <=> 5); // 1
// Its real purpose: sorting
$students = [
['name' => 'Ravi', 'marks' => 78],
['name' => 'Ananya', 'marks' => 91],
['name' => 'Meera', 'marks' => 78],
];
// Highest marks first, then name A-Z for ties
usort($students, fn($a, $b) =>
($b['marks'] <=> $a['marks']) ?: ($a['name'] <=> $b['name'])
);
// Ananya 91, Meera 78, Ravi 78 <=>works on strings and arrays too, not just numbers. On strings it compares alphabetically, which is exactly what you want for sorting names — though note it compares byte by byte, so uppercase letters sort before lowercase ones.
Logical Operators and Short-Circuiting
&& means and, || means or, and ! means not. What makes them more than syntax is short-circuit evaluation: PHP stops as soon as the answer is certain. In $a && $b, if $a is false then $b is never evaluated at all, because the result cannot change.
That is not a performance trick, it is a correctness tool. Writing if (isset($user) && $user['role'] === 'admin') is safe precisely because the second half never runs when the first fails. Reverse the order and the same line throws a warning on every visitor who is not logged in. Whenever you check that something exists and then use it, the existence check must come first.
PHP also has the word forms and, or and xor, and they are not interchangeable with the symbols. They have much lower precedence — lower even than = — which produces one of PHP's nastiest silent bugs. $ok = true and false; assigns true to $ok, because it is parsed as ($ok = true) and false. The assignment happens first and the and is evaluated afterwards on a value nobody uses.
The simple defence is to never use the word forms. && and || behave the way you expect in every position, and there is no situation in ordinary code where the word forms are the better choice.
<?php
$loggedIn = true;
$isAdmin = false;
var_dump($loggedIn && $isAdmin); // false
var_dump($loggedIn || $isAdmin); // true
var_dump(!$isAdmin); // true
// Short-circuiting protects the second check
$user = null;
if ($user !== null && $user['role'] === 'admin') {
echo "Admin panel";
}
// The second condition never runs, so no error
// The word forms are a trap
$ok = true and false;
var_dump($ok); // bool(true) <- almost certainly not what you meant
$ok = (true and false);
var_dump($ok); // bool(false) - brackets fix it, but just use &&
// Guard clauses read well with short-circuiting
$file = $_FILES['photo'] ?? null;
if ($file === null || $file['error'] !== UPLOAD_ERR_OK) {
echo "No usable file was uploaded.";
} - The one place you will still see
oris the old idiom$conn = connect() or die('failed');. It works because of that same low precedence, butdie()in the middle of an application is poor error handling. Throw an exception instead — the error-handling lesson covers this.
Ternary, Null Coalescing, and Nullsafe
The ternary operator condition ? a : b is an if that produces a value, which makes it perfect inside HTML templates where a full if block would be clumsy. The short form $a ?: $b returns $a when it is truthy and $b otherwise.
The null coalescing operator ?? looks similar but asks a different question, and the difference matters. ?: tests for truthiness and complains if the left side does not exist. ?? tests only for null, and it stays silent when the left side is an undefined variable or a missing array key. That is why $_GET['page'] ?? 1 is the standard way to read optional input: no warning, and a clean default.
The distinction bites with values that are legitimately falsy. If a user sets their display count to 0, then $_POST['count'] ?: 10 silently replaces it with 10, because 0 is falsy — while $_POST['count'] ?? 10 correctly keeps the 0. Whenever zero or an empty string is a valid answer, you want ??.
Two related pieces complete the set. ??= assigns only if the left side is null or missing, which is a compact way to fill in defaults. And the nullsafe operator ?->, added in PHP 8, calls a method or reads a property only if the object is not null, returning null instead of throwing. It saves the nested null checks that pile up when one object holds another.
One PHP 8 change to be aware of: chained ternaries without brackets are now a fatal error rather than a confusing silent behaviour. $a ? 1 : $b ? 2 : 3 will not compile. Add brackets, or better, use match — which the conditionals lesson covers.
<?php
$marks = 0;
// Ternary
$status = $marks >= 40 ? "Pass" : "Fail";
// Short ternary: truthy check
$displayName = $nickname ?: "Guest"; // warns if $nickname is undefined
// Null coalescing: null / missing check, no warning
$page = $_GET['page'] ?? 1;
$sort = $_GET['sort'] ?? 'name';
// The difference that matters
$count = 0;
var_dump($count ?: 10); // int(10) <- lost the user's 0
var_dump($count ?? 10); // int(0) <- correct
// Assign only if not already set
$options = [];
$options['perPage'] ??= 20;
$options['sort'] ??= 'created_at';
// Nullsafe: no error if getProfile() returns null
// $city = $user?->getProfile()?->city ?? 'Not set';
// Chained ternary needs brackets in PHP 8
$grade = $marks >= 90 ? 'A' : ($marks >= 75 ? 'B' : 'C'); ??only suppresses the warning for undefined variables and array keys — it does not suppress errors.$user->name ?? 'x'is fine when$useris null, but calling a method on null is still fatal, which is exactly the gap?->fills.
Precedence: When to Reach for Brackets
Precedence decides which operator is applied first when you do not say. Multiplication before addition is the rule everybody remembers from school, and PHP follows the same idea across a much larger set of operators.
Nobody memorises the full table, and nobody should. What experienced developers actually do is recognise the handful of combinations where the answer is not obvious and add brackets there. Mixing arithmetic with concatenation, mixing && with ||, and mixing assignment with anything are the three that account for most real confusion.
The cost of a bracket is nothing. The cost of a misread expression in a fee calculation or a permission check can be significant. When in doubt, be explicit — a reader six months from now, including you, will thank you.
- Highest:
**, then!, then*/%, then+- - Then the concatenation operator
.— which in PHP 8 sits below+and- - Then comparisons:
<><=>=, then==!====!==<=> - Then
&&, then||, then??, then the ternary - Then assignment:
=+=.=??= - Lowest of all: the word forms
and,or,xor— which is precisely why they are a trap
- A quick sanity check while learning: wrap the part you are unsure about in brackets and
var_dump()both versions. If they differ, you just found a bug you would otherwise have shipped.
