Type Juggling and Comparisons

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05, with every result shown below produced by PHP 8.5.11; the before/after table also shows PHP 7.4’s results. PHP converts values between types automatically in many places — type juggling — and lets you convert them explicitly with casts. Knowing exactly when and how this happens is what separates a working comparison from a security bug. The types themselves are described in Types and the Type System.

Type juggling contexts

PHP does not require you to declare the type of a variable; instead, an operation determines the type its operands are converted to. The manual distinguishes these contexts:

Context What happens

Numeric

Arithmetic operators (+ - * / % **): bool, null and numeric strings are converted to int or float; an array or a non-numeric string throws TypeError (8.0).

String

echo, ., interpolation, string functions: the value is converted to string (arrays produce "Array" with a warning; objects need __toString()).

Logical

if, while, !, &&, ||, the ternary condition: converted to bool with the falsy rules.

Integral and string

Bitwise operators: both operands are converted to int, unless both are strings, in which case the operation works byte by byte.

Comparative

==, <, <=>, …: the rules of the next sections.

Function

Passing an argument to a typed parameter or returning a value: governed by strict_types (see Types and the Type System).

<?php

declare(strict_types=1);

$copies = '3';
var_dump($copies + 2);        // int(5)        numeric context
var_dump($copies . ' left');  // string(6) "3 left"   string context
var_dump(!$copies);           // bool(false)   logical context
var_dump('12' | 1);           // int(13)       integral context
var_dump('a' | 'b');          // string(1) "c" two strings: bytewise

Note that strict_types does not change these operator conversions: it applies to parameters, returns and typed properties only.

Source: Type Juggling

Explicit casts

A cast is written as the target type in parentheses before the value. PHP 8.5 accepts only the canonical names below; the aliases (integer), (boolean), (double) and (binary) are deprecated since 8.5, and (real) and (unset) were removed in 8.0.

Cast Deprecated alias Notes

(int)

(integer)

floats are truncated towards zero; see Float to string and float to int below for NAN, INF and out-of-range values

(float)

(double), (real) (removed)

(string)

(binary)

(bool)

(boolean)

the falsy rules

(array)

a scalar becomes a one-element array; null becomes []; an object becomes its properties (private ones with mangled keys)

(object)

an array becomes a stdClass; a scalar becomes a stdClass with a scalar property

<?php

declare(strict_types=1);

$inputs = [
    "'42'" => '42', "'42abc'" => '42abc', "'abc'" => 'abc', "' 42'" => ' 42', "'4.9'" => '4.9',
    "'1e3'" => '1e3', "'0x1A'" => '0x1A', "'012'" => '012', 'null' => null, 'true' => true,
    '4.9' => 4.9, '-4.9' => -4.9, '[]' => [], '[1]' => [1], "''" => '',
];

foreach ($inputs as $label => $v) {
    printf("%-8s int:%-5s float:%-7s bool:%s\n",
        $label, var_export((int) $v, true), var_export((float) $v, true), var_export((bool) $v, true));
}
'42'     int:42    float:42.0    bool:true
'42abc'  int:42    float:42.0    bool:true
'abc'    int:0     float:0.0     bool:true
' 42'    int:42    float:42.0    bool:true
'4.9'    int:4     float:4.9     bool:true
'1e3'    int:1000  float:1000.0  bool:true
'0x1A'   int:0     float:0.0     bool:true
'012'    int:12    float:12.0    bool:true
null     int:0     float:0.0     bool:false
true     int:1     float:1.0     bool:true
4.9      int:4     float:4.9     bool:true
-4.9     int:-4    float:-4.9    bool:true
[]       int:0     float:0.0     bool:false
[1]      int:1     float:1.0     bool:true
''       int:0     float:0.0     bool:false

Three things to notice: casts never fail (they convert as much of a leading-numeric string as they can, and silently produce 0 for a non-numeric one), hexadecimal and octal strings are not parsed by casts ('0x1A' is 0, '012' is 12), and (int) truncates instead of rounding. Casting is therefore the wrong tool for validating input; use filter_var($v, FILTER_VALIDATE_INT) or is_numeric() first (see Forms, Validation and Output Escaping).

Source: Type Casting and PHP 8.5 deprecated features (non-canonical cast names)

Conversion functions

<?php

declare(strict_types=1);

// intval() can parse other bases; base 0 detects the prefix (0x, 0, 0b).
var_dump(intval('0x1A', 16), intval('0x1A', 0), intval('012', 0), intval('101', 2));   // 26 26 10 5

var_dump(floatval('1.5e3 copies'));   // float(1500)
var_dump(boolval('0'), strval(3.0));  // bool(false) string(1) "3"

// settype() changes the variable in place and returns true on success.
$year = '1979';
settype($year, 'int');
var_dump($year);                      // int(1979)

// gettype() uses historical names; get_debug_type() uses declaration names.
var_dump(gettype(1.5), get_debug_type(1.5));   // string(6) "double" string(5) "float"

The is_* functions test without converting: is_int, is_float, is_string, is_bool, is_array, is_object, is_null, is_callable, is_iterable, is_countable, is_resource, is_scalar and the looser is_numeric (true for numeric strings).

Float to string and float to int

Converting a float to a string uses the shortest representation that round-trips (0.1 becomes "0.1", 1.0 becomes "1"), independent of the locale since PHP 8.0. Converting a float to int truncates. NAN and INF become 0, and a float outside the int range wraps around to a meaningless value; since PHP 8.5 such casts emit a warning ("The float INF is not representable as an int, cast occurred"). Check is_finite() and the range before casting. Implicitly converting a float with a fractional part to int (for example as an array key or a coercive int parameter) is deprecated since 8.1: "Implicit conversion from float 4.9 to int loses precision".

Loose and strict comparison

PHP has two equality operators:

  • === (identical) is true only if both operands have the same type and the same value. No conversion happens.

  • == (equal) first juggles the operands to a common type, then compares.

!== and != (or <>) are their negations. Use === by default. Reach for == only when you genuinely want juggling, for example comparing two objects' properties.

The PHP 8 change: saner string-to-number comparisons

Before PHP 8.0, comparing a number with a string always converted the string to a number, so any non-numeric string compared equal to 0. Since PHP 8.0, a number and a string are compared as numbers only if the string is numeric; otherwise the number is converted to a string and the two are compared as strings. The RFC that introduced this is "Saner string to number comparisons".

Expression PHP 7.4 PHP 8.5 Why

0 == 'foo'

true

false

'foo' is not numeric: compared as strings '0' and 'foo'

0 == ''

true

false

the empty string is not numeric

'abc' == 0

true

false

same rule, operands reversed

42 == '42abc'

true

false

leading-numeric is not numeric

"12 o'clock" == 12

true

false

leading-numeric

'1' == '1 '

false

true

trailing whitespace became allowed in numeric strings

INF == 'INF'

false

true

'INF' is not numeric, so INF is compared as the string "INF"

42 == ' 42'

true

true

numeric string (leading whitespace)

'1' == '01'

true

true

two numeric strings are compared as numbers

'10' == '1e1'

true

true

numeric strings, 1e1 is 10

100 == '1e2'

true

true

numeric string

The last three rows show that the rule still bites with two strings: if both are numeric, == compares them numerically. Two different password hashes or tokens that both look like 0e… numbers ("magic hashes") compare equal with ==. Always compare secrets with hash_equals(), and strings with ===.

<?php

declare(strict_types=1);

var_dump(0 == 'foo');               // bool(false) since 8.0 (true in PHP 7)
var_dump('1e3' == '1000');          // bool(true)  -- both numeric strings!
var_dump('0e1234' == '0e5678');     // bool(true)  -- both are "0" in scientific notation
var_dump('0e1234' === '0e5678');    // bool(false)
var_dump(hash_equals('0e1234', '0e5678'));   // bool(false) -- and timing-safe

How == compares each pair of types

Operands Comparison

null or string with string

null is converted to "", then numeric or lexical comparison

bool or null with anything

both sides converted to bool

object with object

same class and equal properties (==); same instance (===); built-in classes may define their own comparison (for example DateTime)

string, resource, int or float with string, resource, int or float

numeric strings and resources are converted to numbers, then usual math; non-numeric strings compared as strings (8.0 rule)

array with array

the array with fewer elements is smaller; if a key from operand 1 is not in operand 2 they are uncomparable; otherwise compared value by value

object with anything

the object is always greater

array with anything

the array is always greater

<?php

declare(strict_types=1);

var_dump(null == false, null == 0, null == '', '0' == false, '' == false, [] == false);
// all bool(true): the bool conversion rule

var_dump('0.0' == false);   // bool(false): '0.0' is a non-empty string other than '0', so it is true

var_dump('abc' == 'ABC');   // bool(false): case-sensitive

// Arrays: == ignores order of keys, === does not
var_dump(['a' => 1, 'b' => 2] == ['b' => 2, 'a' => 1]);    // bool(true)
var_dump(['a' => 1, 'b' => 2] === ['b' => 2, 'a' => 1]);   // bool(false)

The loose comparison table

The manual’s type comparison tables appendix gives == for every pair of common values. An abridged PHP 8 version (T = true, F = false):

== true false 1 0 -1 "1" "0" "" null []

true

T

F

T

F

T

T

F

F

F

F

false

F

T

F

T

F

F

T

T

T

T

1

T

F

T

F

F

T

F

F

F

F

0

F

T

F

T

F

F

T

F

T

F

-1

T

F

F

F

T

F

F

F

F

F

"1"

T

F

T

F

F

T

F

F

F

F

"0"

F

T

F

T

F

F

T

F

F

F

""

F

T

F

F

F

F

F

T

T

F

null

F

T

F

T

F

F

F

T

T

T

[]

F

T

F

F

F

F

F

F

T

T

In the strict (===) table only the diagonal is true.

Ordering: <, > and the spaceship operator <=>

<, <=, >, >= use the same juggling rules as ==. The spaceship operator $a <=> $b (7.0) returns -1, 0 or 1 when $a is less than, equal to or greater than $b, which is exactly what sorting callbacks must return:

<?php

declare(strict_types=1);

var_dump(1 <=> 2, 'b' <=> 'a', [1, 2] <=> [1, 3], [1, 2, 3] <=> [1, 2]);   // -1 1 -1 1

$books = [
    ['title' => 'Solaris', 'year' => 1961],
    ['title' => 'Kindred', 'year' => 1979],
    ['title' => 'Dawn', 'year' => 1979],
];

// Sort by year descending, then by title ascending: compare arrays element by element.
usort($books, fn (array $a, array $b): int => [$b['year'], $a['title']] <=> [$a['year'], $b['title']]);

echo implode(', ', array_column($books, 'title')), "\n";   // Dawn, Kindred, Solaris

Source: Comparison Operators (spaceship operator)

Comparing objects

== on two objects of the same class compares all properties with ==; === is true only for the same instance. Enum cases are singletons, so === is the natural comparison. DateTime and DateTimeImmutable compare by the instant they represent with <, == and >, even across the two classes.

<?php

declare(strict_types=1);

final class Isbn { public function __construct(public string $value) {} }

$a = new Isbn('978-0-00-000001-1');
$b = new Isbn('978-0-00-000001-1');

var_dump($a == $b, $a === $b, $a === $a);    // bool(true) bool(false) bool(true)

var_dump(new DateTimeImmutable('2026-01-01') < new DateTime('2026-06-01'));   // bool(true)

Pitfalls in searching, sorting and switching

Several functions and constructs use == by default:

  • in_array() and array_search() compare loosely unless the third argument $strict is true.

  • array_keys($a, $value) and array_unique() (which compares the string representation by default) also juggle.

  • switch compares with ==; match compares with ===, which is one of the reasons to prefer match (see Control Structures).

  • sort() without flags compares values with the standard rules, so mixing numbers and strings gives orders that are hard to predict; pass SORT_STRING, SORT_NUMERIC or SORT_NATURAL to state the intent.

  • Array keys are juggled too: '1', 1, 1.7 (with a deprecation) and true all become the key 1 (see Arrays).

<?php

declare(strict_types=1);

var_dump(in_array('1e1', ['10']));          // bool(true)  -- '1e1' == '10'
var_dump(in_array('1e1', ['10'], true));    // bool(false)
var_dump(array_search('0', [false, 0]));    // int(0)      -- '0' == false

$shelfCodes = ['10', '9', '2', '1'];
sort($shelfCodes);                          // numeric strings compared as numbers
echo implode(' ', $shelfCodes), "\n";       // 1 2 9 10
sort($shelfCodes, SORT_STRING);
echo implode(' ', $shelfCodes), "\n";       // 1 10 2 9

$status = '0';
echo match ($status) {                      // strict comparison: '0' !== 0
    0 => "zero (int)\n",
    '0' => "zero (string)\n",
};

Source: in_array, sort and match

Where older books and tutorials are outdated

  • String-to-number comparisons. Books written for PHP 5 and 7 (the four consulted for this section among them) teach that 0 == "any string" is true and give examples such as "12 o'clock" == 12 evaluating to true. Since PHP 8.0 both are false; the before/after table above lists the changes.

  • Arithmetic on non-numeric strings. Before 8.0 it produced a warning and used 0; since 8.0 it throws TypeError. Leading-numeric strings ("12 books" + 1) still work, with a warning.

  • Casts. The (unset) cast and (real) were removed in 8.0; (integer), (boolean), (double) and (binary) are deprecated since 8.5.

  • Float to string. Before 8.0, (string) 3.5 depended on the locale set with setlocale() ("3,5" in a German locale); it is always "3.5" now.