Types & type juggling
The ten types, how PHP converts between them, why `0 == "abc"` changed in PHP 8, and what `declare(strict_types=1)` really does.
PHP is dynamically typed: a variable has no type, the value in it does. Assigning a new value can change the type at any moment.
The types
| Type | Example | Notes |
|---|---|---|
null | null | the only value of its type β "no value" |
bool | true, false | |
int | 42, -7, 0x2A | platform-sized, 64-bit almost everywhere |
float | 3.14, 1e3 | IEEE-754 double |
string | 'hi', "hi" | a byte string, not a character string |
array | [1, 2] | an ordered map |
object | new stdClass | |
callable | 'strlen', fn() => 1 | a type hint, not a storable type |
resource | a file handle | a handle to something outside PHP |
never, void | β | return types only |
<?php
foreach ([null, true, 42, 3.14, 'hi', [1, 2], new stdClass] as $v) {
printf("%-10s %s\n", get_debug_type($v), var_export($v, true));
}π‘ Tip: get_debug_type() is almost always what you want over gettype(). It returns the names you actually write β int, bool, float, and the class name for objects β where gettype() returns the historical integer, boolean, double.
Type juggling
PHP converts values automatically when the context demands a particular type. The value itself never changes β the conversion is temporary.
<?php echo 5 + '3', "\n"; // numeric context: 8 echo 5 . '3', "\n"; // string context: 53 var_dump((bool) '0'); // logical context: false var_dump((bool) '0.0'); // ...but this one is true
What counts as false
Only these are falsy. Everything else β including '0.0', 'false', -1 and [0] β is truthy.
<?php
$falsy = [false, 0, -0.0, '', '0', [], null];
foreach ($falsy as $v) {
printf("%-8s β %s\n", var_export($v, true), var_export((bool) $v, true));
}Comparison: == versus ===
=== compares type and value. == converts first. Prefer ===.
<?php
var_dump(1 === '1'); // false β different types
var_dump(1 == '1'); // true β '1' is a numeric string
var_dump('1' == '01'); // true β both numeric, compared as numbers
var_dump('10' == '1e1'); // true β 1e1 is numeric notation for 10
var_dump(100 == '1e2'); // trueThe PHP 8 change everyone should know
Before PHP 8, comparing a number with a string converted the string to a number β so 0 == "abc" was true, because "abc" became 0. Since PHP 8 the number is converted to a string when the string is not numeric, so the comparison is a string comparison.
<?php
var_dump(0 == 'abc'); // false in PHP 8+ (was true before)
var_dump(0 == ''); // false in PHP 8+ (was true before)
var_dump('abc' == '0'); // false
var_dump(0 == '0'); // true β '0' IS numericπ§ Scenario: Old code guarded a login with if ($password == 0). On PHP 7 any non-numeric password matched β a real vulnerability that shipped more than once. PHP 8 closes it, and === would have closed it a decade earlier.
Casting explicitly
<?php var_dump((int) '42 apples'); // 42 β leading digits are taken var_dump((int) 'apples 42'); // 0 β no leading digits var_dump((float) '1.5e3'); // 1500.0 var_dump((string) true); // "1" var_dump((string) false); // "" β not "0" var_dump((array) 'solo'); // ["solo"]
A non-numeric string in arithmetic is different from a cast: it throws.
<?php
try {
echo 'apples' + 1; // throws: Unsupported operand types
} catch (\TypeError $e) {
echo 'TypeError: ', $e->getMessage(), "\n";
}
echo '5 apples' + 1, "\n"; // 6, plus a Warning: the ' apples' was discardedDeclared types and strict_types
Type declarations on parameters, returns and properties are checked at runtime. In the default coercive mode PHP will convert a value if it safely can.
<?php
function half(int $n): float {
return $n / 2;
}
echo half(7), "\n"; // 3.5
echo half('8'), "\n"; // 4 β the numeric string is coerced to intdeclare(strict_types=1) β which must be the very first statement in a file β turns coercion off for calls made from that file. Only int β float widening survives.
<?php
declare(strict_types=1);
function half(int $n): float {
return $n / 2;
}
echo half(7), "\n";
try {
echo half('8'); // throws: must be of type int, string given
} catch (\TypeError $e) {
echo 'TypeError: ', $e->getMessage(), "\n";
}Union, nullable and intersection types
<?php
function parseId(int|string $id): ?int {
return is_int($id) ? $id : (ctype_digit($id) ? (int) $id : null);
}
var_dump(parseId(7), parseId('12'), parseId('x7'));?int is shorthand for int|null. A&B requires a value satisfying both interfaces. mixed accepts anything and is the explicit way to say "no constraint".
Reference: Types and Type juggling in the PHP Manual.