Lesson 04 / 14
Operators
What PHP operators say when they bring two types together: arithmetic with numeric strings, loose and strict equality, concatenation and precedence, division by zero, and the null coalescing operator silencing a warning.
Published Updated
In this lesson
- Tells apart, by measurement, the result and the level reported by arithmetic with numeric strings
- Shows in a table the different results that loose and strict equality produce for the same pair
- Verifies how the precedence order of concatenation, addition and logical operators changes the result
- Measures that the null coalescing operator silences the warning and what that hides
Contents
The Variables and Data Types lesson measured that the type of a value can change in assignment, in division and in casts without a signal. This lesson asks the same question of operators: what does the language say when an operator brings two different types together, or when its operand is not suitable for the operator? The answer varies from operator to operator, and the channel of this lesson is warning: the operation usually continues, but it carries a notice with it.
In JavaScript the same question was measured in M06/K01: there, coercion and equality worked by their own rules. PHP’s rules are separate from those, and the measurement keeps the method the same: each file sets up an error handler, the handler prints the level of the notice by name, and the message text is not printed, because the message depends on the version.
Arithmetic with Numeric Strings
PHP can use a string in arithmetic: if the string reads like a number, it is converted to a number. There are three cases. The string is entirely a number, the string starts with a number and continues with other characters, or the string has no number at all. The file below adds each of the three to 1 and prints the result of each or the exception it throws.
<?php // arithmetic.php — numeric string: integer is silent, prefix warns, non-numeric is TypeError set_error_handler(function (int $level): bool { echo 'reported: ', $level === E_WARNING ? 'E_WARNING' : 'other', "\n"; return true; }); foreach (['5', '5 apples', 'apples'] as $input) { try { $result = $input + 1; echo var_export($input, true), ' + 1 = ', var_export($result, true), "\n"; } catch (TypeError $error) { echo var_export($input, true), ' + 1 -> ', get_class($error), "\n"; } }
'5' + 1 = 6 reported: E_WARNING '5 apples' + 1 = 6 'apples' + 1 -> TypeError
LE26. Three inputs gave three different results. The entirely numeric string was silently converted to a number. The string that starts with a number was processed with its numeric prefix and reported an E_WARNING; the operation still finished. The string that carries no number threw a TypeError without doing the addition. This lesson names its channel warning, because the middle case both produces a result and carries a notice; but the three cases show three different channels side by side in a single operator.
Loose and Strict Equality
== tries to bring two values to a common type before comparing them; === looks at both the type and the value. The two can give different answers for the same pair, and no signal ever arrives. The file below compares eight pairs with the two operators. The pairs were chosen to cover most of the common misunderstandings: numeric strings, the empty string and null, zero and a string, the empty array and false.
<?php // equality.php — loose and strict equality answer differently for the same pair $pairs = [ ["'1e3' and '1000'", '1e3', '1000'], ["'abc' and 0", 'abc', 0], ["'0' and false", '0', false], ["'' and null", '', null], ["'1' and '01'", '1', '01'], ["100 and '1e2'", 100, '1e2'], ['[] and false', [], false], ['null and 0', null, 0], ]; printf("%-18s%-7s%s\n", 'pair', '==', '==='); foreach ($pairs as [$name, $a, $b]) { printf("%-18s%-7s%s\n", $name, var_export($a == $b, true), var_export($a === $b, true)); }
pair == === '1e3' and '1000' true false 'abc' and 0 false false '0' and false true false '' and null true false '1' and '01' true false 100 and '1e2' true false [] and false true false null and 0 true false
LE27. '1e3' == '1000' came out true: because both strings are numeric they were compared as numbers, and 1e3 means a thousand. 'abc' == 0 came out false: a non-numeric string is not counted as equal to 0 when compared with a number. '0' == false, '' == null, [] == false and null == 0 came out true. Strict equality gave true for none of the eight pairs, because in all eight the types are different. This table shows that == has no signal but that its result depends on the form of the value.
Comparison, Concatenation and Precedence
The <=> operator (the spaceship operator) compares two values and gives -1, 0 or 1. The . operator concatenates strings. and and && do the same logic with different precedence. The file below runs four expressions: numeric strings with <=>, a non-numeric string with <=>, a mix of + and ., and and with assignment.
<?php // precedence.php — comparison and precedence change the result echo "'10' <=> '9': ", '10' <=> '9', "\n"; echo "'10' <=> '9a': ", '10' <=> '9a', "\n"; echo "1 + 2 . '3': ", 1 + 2 . '3', "\n"; echo "'1' . 2 + 3: ", '1' . 2 + 3, "\n"; $a = true and false; $b = (true and false); echo 'a: ', var_export($a, true), ' b: ', var_export($b, true), "\n";
'10' <=> '9': 1 '10' <=> '9a': -1 1 + 2 . '3': 33 '1' . 2 + 3: 15 a: true b: false
LE28. Two numeric strings were compared as numbers: '10' is greater than '9'. Because '9a' is not numeric, the comparison was made as strings and '10' came out smaller. In the mix of + and . the addition was done first: the expression 1 + 2 . '3' gave '33' and the expression '1' . 2 + 3 gave '15'. The last two lines are the most insidious: because and is processed after assignment, in the expression $a = true and false the value of $a stayed true, while the parenthesized $b became false. None of them produced a notice.
Increment and Decrement
The ++ and -- operators do the expected job on numbers, but their behavior diverges when the operand is a string or null. Strings are incremented alphabetically, as in Perl; when null is incremented it becomes 1, and when decremented it stays null. The file below runs six cases and prints the result and its type, and the handler reports the level.
<?php // increment.php — ++ and -- behave differently on strings and null set_error_handler(function (int $level): bool { echo $level === E_DEPRECATED ? " [E_DEPRECATED]\n" : ($level === E_WARNING ? " [E_WARNING]\n" : " [other]\n"); return true; }); $cases = ['5', 'a', 'z', 'Az', 'a9', '']; foreach ($cases as $start) { $x = $start; $x++; printf("%-6s++ -> %-7s%s\n", var_export($start, true), var_export($x, true), get_debug_type($x)); } $up = null; $up++; $down = null; $down--; echo 'null++ -> ', var_export($up, true), ' null-- -> ', var_export($down, true), "\n";
'5' ++ -> 6 int [E_DEPRECATED] 'a' ++ -> 'b' string [E_DEPRECATED] 'z' ++ -> 'aa' string [E_DEPRECATED] 'Az' ++ -> 'Ba' string [E_DEPRECATED] 'a9' ++ -> 'b0' string [E_DEPRECATED] '' ++ -> '1' string [E_WARNING] null++ -> 1 null-- -> NULL
LE29. '5' became an integer when incremented and stayed silent. 'a' became 'b', 'z' became 'aa', 'Az' became 'Ba', 'a9' became 'b0': on non-numeric strings, increment is an alphabetical rule, and all five strings reported an E_DEPRECATED; this notice was extended to all non-numeric strings with PHP 8.5. null gave 1 when incremented and stayed silent; when decremented it stayed null and reported an E_WARNING. The same operator gave a silent, a deprecated or a warned result depending on the type of the operand.
Remainder and Power
The % operator gives the remainder, but its sign is the sign of the dividend; operands are converted to integers and the fraction is thrown away. The ** operator raises to a power, and if the result exceeds the int limit it becomes a float. The file below prints five expressions.
<?php // remainder.php — % takes its sign from the dividend, ** becomes float on overflow $notices = 0; set_error_handler(function (int $level) use (&$notices): bool { $notices++; return true; }); $expressions = [ '-7 % 3' => -7 % 3, '7 % -3' => 7 % -3, '7.9 % 3' => 7.9 % 3, '2 ** 10' => 2 ** 10, '2 ** -1' => 2 ** -1, ]; foreach ($expressions as $expression => $result) { printf("%-10s%-6s%s\n", $expression, get_debug_type($result), var_export($result, true)); } echo '2 ** 200: ', get_debug_type(2 ** 200), "\n"; echo 'notices: ', $notices, "\n";
-7 % 3 int -1 7 % -3 int 1 7.9 % 3 int 1 2 ** 10 int 1024 2 ** -1 float 0.5 2 ** 200: float notices: 1
LE30. The expression -7 % 3 gave -1 and the expression 7 % -3 gave 1: the sign comes from the dividend. In the expression 7.9 % 3, 7.9 was first truncated to 7, the result was 1, and one notice was counted; the implicit conversion of a fractional number to an integer gives a loss warning. 2 ** 10 is an int and 2 ** -1 a float; 2 ** 200 overflowed and became a float, and the type changed without carrying a notice. The last line is where this lesson meets the Variables and Data Types lesson: exceeding the integer limit is silent here too.
Compound Assignment Operators
Compound assignment operators such as +=, .= and ??= read the variable and write it again. If the variable is undefined, the reading part produces a warning; ??= makes the read safe and assigns only if there is no value. The file below adds to an undefined counter and to an undefined string, and then does the same job with ??=. The handler prints the level name; which notice came from which line is read from the order in the output.
<?php // compound.php — += and .= warn when reading an undefined variable, ??= does not set_error_handler(function (int $level): bool { echo $level === E_WARNING ? " [E_WARNING]\n" : " [other]\n"; return true; }); echo "+= undefined:\n"; $counter += 5; echo ' counter: ', var_export($counter, true), "\n"; echo ".= undefined:\n"; $text .= 'a'; echo ' text: ', var_export($text, true), "\n"; echo "??= undefined:\n"; $timeout ??= 30; echo ' timeout: ', var_export($timeout, true), "\n"; echo "??= defined:\n"; $timeout ??= 99; echo ' timeout: ', var_export($timeout, true), "\n";
+= undefined: [E_WARNING] counter: 5 .= undefined: [E_WARNING] text: 'a' ??= undefined: timeout: 30 ??= defined: timeout: 30
LE31. += and .= each reported an E_WARNING while reading the undefined variable, but the operation continued: the counter became 5 and the string became 'a'. On the first call ??= assigned the variable and produced no notice; on the second call, because the variable was defined, it did nothing and 30 remained. This is the honest way to avoid a warning: expressing the absence of the variable as a decision names the need instead of silencing the warning.
Division by Zero
Division by zero is the first halting behavior of this lesson. The / and % operators and intdiv() throw a DivisionByZeroError when dividing by zero; fdiv() does the operation without an error and gives floating-point infinity. The file below tests four calls. The result is computed and printed separately, because an echo with commas leaves partial output if it is cut in the middle of the operation.
<?php // zero.php — division by zero halts, fdiv gives infinity $zero = 0; $calls = [ '1 / 0' => fn() => 1 / $zero, '1 % 0' => fn() => 1 % $zero, 'intdiv(1, 0)' => fn() => intdiv(1, $zero), 'fdiv(1, 0)' => fn() => fdiv(1, $zero), ]; foreach ($calls as $name => $call) { try { $result = $call(); $text = var_export($result, true); } catch (DivisionByZeroError $error) { $text = get_class($error); } printf("%-14s%s\n", $name, $text); }
1 / 0 DivisionByZeroError 1 % 0 DivisionByZeroError intdiv(1, 0) DivisionByZeroError fdiv(1, 0) INF
LE32. Three operations threw DivisionByZeroError, while fdiv() returned INF. This shows the limit of this lesson’s channel: the language has a kind of error that is not content with a warning. If the error is not caught, execution is cut as in the Syntax and Tags lesson; if it is caught, the program continues. Infinity itself is a silent value and can turn into NAN in later operations.
The Bounding Measurement
There is one operator that can silence all the notices in this lesson: the null coalescing operator ??. When an undefined array key or variable is read, a warning is reported; if the same read is done with ??, no notice is produced at all and that information is lost. The file below reads the same missing key in three ways, and the error handler counts every notice.
<?php // silencing.php — ?? silences the warning and hides the missing key $notices = 0; set_error_handler(function (int $level) use (&$notices): bool { $notices++; return true; }); $settings = ['timeout' => 30]; $a = $settings['timeot']; echo 'direct read: ', var_export($a, true), ' notices: ', $notices, "\n"; $b = $settings['timeot'] ?? 0; echo 'read with ??: ', var_export($b, true), ' notices: ', $notices, "\n"; $c = $settings['timeout'] ?? 0; echo 'correct key: ', var_export($c, true), ' notices: ', $notices, "\n";
direct read: NULL notices: 1 read with ??: 0 notices: 1 correct key: 30 notices: 1
LE33. The direct read gave null and produced a notice. Reading the misspelled key with ?? produced no notice and returned 0: the typo became visible as a valid default value. The third read found the correct key and gave 30; the counter stayed at one again. This is not a bug but the limit of the measurement: ?? silences the warning, so a channel of the warning kind turns into the silent channel behind an operator. If a default value is the desired behavior, ?? is right; if catching the typo is the desired behavior, it should not be used.
Summary
- The channel of operators is warning: a string that starts with a number reports an
E_WARNINGand the operation continues; an entirely numeric string is silent; a string with no number throws aTypeError. ==compares by bringing two values to a common type,===also checks the type; for none of the eight pairs did both come out true, and no comparison produced a notice.+is processed before the.operator, andandruns after assignment; the expression$a = true and falseassigns true to$a./,%andintdiv()throwDivisionByZeroErrorwhen dividing by zero;fdiv()returnsINF.??silences the warning for an undefined key and variable; a typo thus turns into a silent default.
Next Step
It was seen what operators report and what they silence when they bring types together. The Control Flow lesson asks the same question of branches: what does the language do when no arm of a condition matches, that is, when an unmatched branch is left?
To keep your progress and take notes, Log in
My notes
Log in to take notes.