Type Juggling & Casting

PHP · Lesson 6 of 50 8 min read Updated

PHP lets you mix types freely. You can add a string to a number, compare a number with a string, or pass '72' to a function that wants an int. Behind the scenes PHP converts the values for you. This automatic conversion is called type juggling, and it is the reason '1' == '01' is true in PHP.

You met the basics in Data Types. In this lesson you will learn exactly when PHP juggles, read a full table of loose and strict comparisons, see what changed in PHP 8, and learn to convert values on purpose with casts, intval() and settype(). You will finish with strict_types, which stops juggling at function boundaries.

When does PHP juggle?

PHP converts a value whenever an operation needs a different type. The most common places are:

  • Arithmetic (+, -, *, /): strings become numbers.
  • Joining strings with .: numbers become strings.
  • Conditions such as if ($x): any value becomes a bool.
  • Loose comparison with ==: both sides may be converted.
  • Function arguments, unless strict_types is on.

Everything from a form, the URL or a CSV file arrives as a string, so juggling happens all the time in web code:

PHP
<?php
declare(strict_types=1);

$qty = '3';           // from a form, so it is a string
$price = 249.5;

var_dump($qty * $price);     // string becomes a number
var_dump($qty . ' items');   // stays a string
var_dump('5' + '10');        // two numeric strings, int result
var_dump('1.5' + 1);         // float result
var_dump(7 / 2);             // int / int can give a float
var_dump(10 / 2);            // ... or an int
Output
float(748.5)
string(7) "3 items"
int(15)
float(2.5)
float(3.5)
int(5)

Numeric strings

For arithmetic, PHP sorts strings into three groups:

  • A numeric string looks exactly like a number: '42', '4.5', '4.2e1'. Spaces before or after are allowed (trailing spaces only since PHP 8).
  • A leading-numeric string starts with a number and then has other text, such as '250 rupees'. PHP uses the number but shows a warning.
  • A non-numeric string such as 'abc' cannot be used in arithmetic at all: PHP 8 throws a TypeError.

is_numeric() tells you which strings are fully numeric. Notice that hexadecimal text like '0x1A' is not numeric:

PHP
<?php
declare(strict_types=1);

$inputs = ['42', ' 42', '42 ', '4.2e1', '0x1A', '42abc', 'abc', ''];

foreach ($inputs as $value) {
    printf("%-8s %s\n", var_export($value, true), is_numeric($value) ? 'numeric' : 'not numeric');
}

$total = '250 rupees' + 50;
var_dump($total);
Output
'42'     numeric
' 42'    numeric
'42 '    numeric
'4.2e1'  numeric
'0x1A'   not numeric
'42abc'  not numeric
'abc'    not numeric
''       not numeric

Warning: A non-numeric value encountered in ... on line 10
int(300)

The warning is your friend: it tells you that dirty input reached your maths. Writing 'two hundred' + 50 instead stops the script with TypeError: Unsupported operand types: string + int.

Loose and strict comparison

The loose operator == juggles types before comparing. The strict operator === never converts: different types are simply not equal. These results come from a real PHP 8.4 run:

Expression=====
'0' and 0truefalse
'1' and '01'truefalse
'10' and '1e1'truefalse
100 and '1e2'truefalse
42 and ' 42'truefalse
42 and '42abc'falsefalse
0 and ''falsefalse
0 and 'a'falsefalse
null and falsetruefalse
null and ''truefalse
'0' and falsetruefalse
[] and falsetruefalse
true and 'false'truefalse
-1 and truetruefalse
1.0 and 1truefalse

Some rows deserve a second look. true == 'false' is true, because the non-empty string 'false' is truthy. Two numeric strings such as '10' and '1e1' are compared as numbers, even though both sides are strings. That is why comparing passwords, tokens or PIN codes with == is a bug: always use ===, or hash_equals() for secrets.

Loose comparison also hides inside other features. in_array() and switch compare loosely, while match compares strictly:

PHP
<?php
declare(strict_types=1);

var_dump(in_array('1e1', ['10', '20']));         // loose: true
var_dump(in_array('1e1', ['10', '20'], true));   // strict: false

$code = '0';
switch ($code) {
    case false:
        echo "switch: matched false\n";
        break;
    default:
        echo "switch: default\n";
}

echo match ($code) {
    false => 'match: false',
    '0' => 'match: string zero',
    default => 'match: default',
}, "\n";
Output
bool(true)
bool(false)
switch: matched false
match: string zero

Pass true as the third argument to in_array() and array_search(), and prefer match over switch when types matter.

What changed in PHP 8

PHP 8 changed how a number is compared with a string. If the string is numeric, PHP still compares them as numbers. If the string is not numeric, PHP 8 turns the number into a string and compares two strings. So 0 == 'a' asks "is '0' the same text as 'a'?", and the answer is false.

Casting on purpose

A cast converts a value when you decide to. Write the type in brackets: (int), (float), (string), (bool), (array) or (object). The function forms intval(), floatval(), strval() and boolval() do the same job, and intval() can also read other number bases:

PHP
<?php
declare(strict_types=1);

var_dump((int) '1e3');          // scientific notation is read
var_dump((int) 9.99);           // cuts off the fraction
var_dump((int) round(9.99));    // round first if you want 10
var_dump((string) false);       // false becomes an empty string
var_dump((string) 2.50);        // the trailing zero is not kept
var_dump((array) 'Pune');       // a scalar becomes a one-item array
var_dump((array) null);         // null becomes an empty array
var_dump(intval('0x1A', 16));   // hexadecimal
var_dump(intval('101', 2));     // binary
Output
int(1000)
int(9)
int(10)
string(0) ""
string(3) "2.5"
array(1) {
  [0]=>
  string(4) "Pune"
}
array(0) {
}
int(26)
int(5)

settype() converts a variable in place instead of returning a new value. It returns true on success. You will see it in older code; a cast with an assignment, $pin = (int) $pin;, is usually clearer.

PHP
<?php
declare(strict_types=1);

$pin = '124001';
settype($pin, 'int');
var_dump($pin);

echo gettype(1.5), ' / ', get_debug_type(1.5), "\n";
Output
int(124001)
double / float

strict_types: no juggling for function arguments

By default PHP runs in coercive mode: if a function wants an int and you pass '72', PHP converts it. With declare(strict_types=1); as the first statement of a file, calls made from that file must pass the exact type, or PHP throws a TypeError. The one allowed exception is passing an int where a float is expected, because no information is lost.

PHP
<?php
declare(strict_types=1);

function seatsLeft(int $total, int $booked): int
{
    return $total - $booked;
}

function fare(float $base): float
{
    return $base * 1.05;
}

var_dump(fare(400));               // int to float is still allowed

try {
    var_dump(seatsLeft('72', '70'));
} catch (TypeError $e) {
    echo $e->getMessage(), "\n";
}

$booked = (int) '70';              // convert on purpose, then call
var_dump(seatsLeft(72, $booked));
Output
float(420)
seatsLeft(): Argument #1 ($total) must be of type int, string given, called in ... on line 17
int(2)

Without the declare line, seatsLeft('72', '70') would quietly return 2. Strict mode makes you convert at one clear place, usually where input enters your program, so the rest of your code can trust its types. Remember that strict_types does not change operators: '5' + 1 still juggles.

Advanced: validate, then convert

A cast never fails. (int) '3 kg' is 3, and (int) '' is 0, with no warning at all. For user input, check the value first. filter_var() with FILTER_VALIDATE_INT validates and converts in one step:

PHP
<?php
declare(strict_types=1);

function toQuantity(string $raw): ?int
{
    $qty = filter_var($raw, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 1, 'max_range' => 10],
    ]);

    return $qty === false ? null : $qty;
}

foreach (['3', ' 4 ', '12', '2.5', '3 kg', '1e1', ''] as $raw) {
    $qty = toQuantity($raw);
    $cast = (int) $raw;
    printf("%-7s cast: %-3d checked: %s\n", "'$raw'", $cast, $qty === null ? 'rejected' : $qty);
}
Output
'3'     cast: 3   checked: 3
' 4 '   cast: 4   checked: 4
'12'    cast: 12  checked: rejected
'2.5'   cast: 2   checked: rejected
'3 kg'  cast: 3   checked: rejected
'1e1'   cast: 10  checked: rejected
''      cast: 0   checked: rejected

The cast accepted every value, including nonsense. The validated version accepts only whole numbers from 1 to 10. You will learn all the options of filter_var() in Filters & filter_var().

Common mistakes

  • Comparing secrets with ==. '0e123' == '0e456' is true because both look like the number zero. Use === or hash_equals().
  • Forgetting the strict flag in in_array(). Without true as the third argument, '1e1' is found in ['10'].
  • Casting user input without checking it. (int) 'abc' is 0, which may be a valid id. Validate first.
  • Expecting (int) to round. It cuts off the fraction. Use round() first.
  • Thinking strict_types affects operators. It only affects function and method calls (and return values) in that file.

Key points

  • Type juggling is PHP's automatic conversion in arithmetic, joining, conditions, == and function calls.
  • === compares value and type with no conversion; use it by default, plus in_array(..., true) and match.
  • Since PHP 8, comparing a number with a non-numeric string compares them as strings, so 0 == 'a' is false.
  • Casts, intval() and settype() convert on purpose, but never complain about bad input.
  • declare(strict_types=1); makes function calls in that file throw a TypeError for the wrong scalar type.
  • Validate input (for example with filter_var()) before you convert it.

Exercise

Write a file with declare(strict_types=1); and a function discountedPrice(int $price, int $percent): int that returns the price after the discount, rounded down with intdiv(). A form sends $price = '1299' and $percent = '10%'. Convert both values safely: remove a trailing % with rtrim(), then validate each with FILTER_VALIDATE_INT (percent must be 0 to 90). Print the new price, or "Invalid input" if either value fails.

Show solution
PHP
<?php
declare(strict_types=1);

function discountedPrice(int $price, int $percent): int
{
    return intdiv($price * (100 - $percent), 100);
}

$price = '1299';
$percent = '10%';

$priceInt = filter_var($price, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
$percentInt = filter_var(rtrim($percent, '%'), FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 0, 'max_range' => 90],
]);

if ($priceInt === false || $percentInt === false) {
    echo "Invalid input\n";
} else {
    echo 'New price: ₹' . discountedPrice($priceInt, $percentInt) . "\n";
}
Output
New price: ₹1169

Because the file uses strict_types, passing the raw strings would throw a TypeError. Validating first turns them into real ints and rejects values like 'abc' or '95%'.

Quick Quiz

1. What is the result of 0 == 'a' in PHP 8?

2. Which call finds nothing in ['10', '20']?

3. What does (int) 9.99 give?

4. With declare(strict_types=1);, which call to function f(float $x) is allowed?

Learn PHP with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call