Scalar Type Declarations

The scalar types bool, int, float, and string can now be used in type declarations:

public function add(int $a, int $b)
{
    return $a + $b;
}

In this section, you will not only learn how the scalar type declarations used in the example above are interpreted but also how their implementation was conceived.

History

Over the course of the eleven years that passed between the release of PHP 5 and PHP 7, multiple attempts were made to add support for scalar types in the optional type declarations for function and method parameters introduced in PHP 5.

The earliest attempt to add this feature to the language is the one initiated by Ilia Alshanetsky in 2009. He proposed to only implement a strict interpretation of scalar type declarations for function and method parameters. That would have meant that a parameter declared to be an integer would only accept an integer argument. A floating point number, a string – even a string containing an integer –, or a boolean would be rejected with a runtime error. This proposal was rejected because its interpretation of scalar type declarations for function and method parameters was considered too strict by the majority of PHP developers.

In 2010, Derick Rethans proposed to allow the usage of scalar types in type declarations for function and method parameters but to not interpret them in any way. The type information would have only been available via the Reflection API or to extensions for the PHP runtime. While this proposal was initially approved and implemented for PHP 5.4, it was rejected in the end, and its implementation was removed before the release of PHP 5.4.0.

Andrea Faulds proposed in 2014 to allow the usage of the scalar types bool, int, float, and string in type declarations for function and method parameters. Her compromise between the earlier proposals by Ilia Alshanetsky and Derick Rethans was to offer two modes of operation for interpreting the parameter declarations for scalar types, coercive and strict, which we will discuss later. After a long and heated debate, Andrea Faulds eventually withdrew her proposal.

In 2015, Zeev Suraski, Francois Laupretre, and Dmitry Stogov proposed an alternative approach for striking a balance between strict and weak interpretation of scalar type declarations. Instead of two modes of operation, their implementation would have applied stricter casting rules than the ones used for explicit cast operations to “coerce” argument values that do not match the expected type of a parameter.

In the example below, the function f() has an int type declaration for its $a parameter. A string that contains an integer is accepted as an argument for that function while a string that does not contain an integer is rejected.

function f(int $a)
{
    var_dump($a);
}

f(1);         // Would have printed int(1)
f(1.0);       // Would have printed int(1)
f(1.2);       // Would have resulted in a type error
f(true);      // Would have resulted in a type error
f('1');       // Would have printed int(1)
f('string');  // Would have resulted in a type error

This proposal was also rejected because its interpretation of scalar type declarations was considered too strict by the majority of PHP developers.

Anthony Ferrara resurrected Andrea’s proposal. This is the proposal that was ultimately accepted for PHP 7. We will now explore its two modes of operation: coercive mode and strict mode.

Coercive Interpretation

By default, scalar type declarations are interpreted in coercive or weak type checking mode. Calls to functions or methods that are built into PHP (or provided by an extension) are handled the same as they were in PHP 5 in this mode. Calls to user-defined functions and methods are treated almost the same as calls to built-in functions: arguments are implicitly converted to match the expected type. The only difference is that null is not accepted as an argument value unless the respective parameter has null as its default value.

Below you find examples for how argument values are converted when they do not match the respective parameter’s type.

bool

When a parameter of a function or method has a bool type declaration then arguments of the types bool, int, float, and string can be passed for it. int, float, and string arguments will be automatically cast to bool. Passing an object will result in a type error.

function f(bool $a)
{
    var_dump($a);
}

f(false);         // Will print bool(false)
f(true);          // Will print bool(true)
f(-1);            // Will print bool(true)
f(0);             // Will print bool(false)
f(1);             // Will print bool(true)
f(1.2);           // Will print bool(true)
f('');            // Will print bool(false)
f('a');           // Will print bool(true)
f('1');           // Will print bool(true)
f('false');       // Will print bool(true)
f('true');        // Will print bool(true)
f(new stdClass);  // Will result in a type error
f(null);          // Will result in a type error

Here is an example of what such a type error looks like:

Fatal error: Uncaught TypeError:
Argument 1 passed to f() must be of the type boolean, object given,
called in ... and defined in ...

When you run into such a type error then you have to explicitly convert the argument to match the expected type. In this case, for instance, this could be as easy as passing along the boolean return value of a method instead of passing the object itself.

int

When a parameter of a function or method has an int type declaration then arguments of the types bool, int, and float can be passed for it. bool and float arguments will be automatically cast to int. Passing a string argument that only contains a numeric string is allowed. Passing a string argument that contains a non-numeric string or passing an object will result in a type error.

function f(int $a)
{
    var_dump($a);
}

f(false);         // Will print int(0)
f(true);          // Will print int(1)
f(0);             // Will print int(0)
f(1);             // Will print int(1)
f(1.2);           // Will print int(1)
f('1234');        // Will print int(1234)
f('1234abcd');    // Will print int(1234)
f('');            // Will result in a type error
f('a');           // Will result in a type error
f('1');           // Will print int(1)
f(new stdClass);  // Will result in a type error

Here is an example of what such a type error looks like:

Fatal error: Uncaught TypeError:
Argument 1 passed to f() must be of the type integer, string given,
called in ... and defined in ...

The automatic cast from float to int only works for floating point numbers between the smallest and the largest integer that the current build of PHP can represent. The valid range can be determined by looking at the PHP_INT_MIN and PHP_INT_MAX constants. The floating point number that is to be cast to integer also must not be float(NAN) or, in other words, must be a number that can actually be represented.

float

When a parameter of a function or method has a float type declaration then arguments of the types bool, int, and float can be passed for it. bool and int arguments will be automatically cast to float. Passing a string argument that only contains a numeric string is allowed. Passing a string argument that contains a non-numeric string or passing an object will result in a type error.

function f(float $a)
{
    var_dump($a);
}

f(false);         // Will print float(0)
f(true);          // Will print float(1)
f(0);             // Will print float(0)
f(1);             // Will print float(1)
f(1.2);           // Will print float(1.2)
f('');            // Will result in a type error
f('a');           // Will result in a type error
f('1');           // Will print float(1)
f(new stdClass);  // Will result in a type error

string

When a parameter of a function or method has a string type declaration then arguments of the types bool, int, float, and string can be passed for it. bool, int, and float arguments will be automatically cast to string. Passing an object will result in a type error unless the object has a __toString() method.

class C
{
    public function __toString()
    {
        return 'c';
    }
}

function f(string $a)
{
    var_dump($a);
}

f(false);         // Will print string(0) ""
f(true);          // Will print string(1) "1"
f(0);             // Will print string(1) "0"
f(1);             // Will print string(1) "1"
f(1.2);           // Will print string(3) "1.2"
f('');            // Will print string(0) ""
f('a');           // Will print string(1) "a"
f('1');           // Will print string(1) "1"
f(new C);         // Will print string(1) "c"
f(new stdClass);  // Will result in a type error

Strict Interpretation

On a per-file basis, a strict interpretation of scalar type declarations can be enabled by making declare(strict_types=1); the first statement of a PHP source code file.

declare(strict_types=1);

function f(bool $a)
{
    // ...
}

f(0);

Executing the code shown above will result in a type error:

Fatal error: Uncaught TypeError:
Argument 1 passed to f() must be of the type boolean, integer given,
called in ... and defined in ...

To fix this error you have to explicitly cast the argument to boolean: f((bool) 0);.

In the example shown above, the f() function is called from within the same source code file in which it is declared. We will have a look at what happens when a function or method is called from a source code file other than the one it is declared in.

strict_caller_weak_callee.php

declare(strict_types=1);

require __DIR__ . '/weak_callee.php';

f(1);  // Will result in a type error

weak_callee.php

function f(bool $a)
{
    var_dump($a);
}

Output

Fatal error: Uncaught TypeError:
Argument 1 passed to f() must be of the type boolean, integer given,
called in strict_caller_weak_callee.php on line ...
and defined in weak_callee.php on line ...

In the examples above, a function f() is declared in a source code file named weak_callee.php. That function is called from strict_caller_weak_callee.php, a source code file that enables the strict interpretation of scalar type declarations. Executing the strict_caller_weak_callee.php source code file will result in a type error because the argument type (int) does not match the parameter type (bool).

The strict_types directive only affects the file it is used in. It does not affect either other files which include the file nor other files that are included by the file. The directive affects the checking of argument types for all function and method calls made from within the file that sets it. The type checks used for a function or method call depend on the type checking mode of the caller’s source code file, not on the callee’s source code file.

weak_caller_strict_callee.php

require __DIR__ . '/strict_callee.php';

f(1);

strict_callee.php

declare(strict_types=1);

function f(bool $a)
{
    var_dump($a);
}

Output

bool(true)

In the example above, a function f() is declared in a source code file named strict_callee.php, a source code file that enables the strict interpretation of scalar type declarations. Executing the weak_caller_strict_callee.php source code file will not result in a type error because the function call is subject to weak type checking and the argument is automatically cast from int to bool.

When scalar type declarations are interpreted strictly then an object that has a __toString() method is not automatically cast to string when it is passed as an argument for a parameter that is expected to be a string:

declare(strict_types=1);
class C
{
    public function __toString()
    {
        return 'string';
    }
}

function f(string $string)
{
    return $string;
}

$o = new C;

var_dump(f($o));

Executing the code shown above will print the error shown below:

Uncaught TypeError: Argument 1 passed to f() must be of the type string,
object given, called in ...

No matter whether arguments are explicitly or implicitly converted between types, be aware of PHP’s rules for casting which are not always intuitive.

The default value for a typed parameter has to match the type of the parameter. The only exception is that float parameters also accept integer default values.

There is a bug in PHP that prevents scalar type declarations to be interpreted strictly when a function or method is invoked using the Reflection API.

Consider a class C that has a method m which expects a parameter $x of type string:

class C
{
    public function m(string $x)
    {
        var_dump($x);
    }
}

Let us try to pass an integer instead of a string, which, when we enable strict interpretation of scalar type declarations, should yield a type error.

When we invoke that method directly, we get the error we expect:

declare(strict_types=1);

$object = new C;
$object->m(1);

Executing the code shown above will print the output shown below:

Fatal error: Uncaught TypeError: Argument 1 passed to C::m()
must be of the type string, integer given, called in ...

If we, however, invoke the method indirectly then PHP will not generate the expected type error.

declare(strict_types=1);

$object    = new C;
$arguments = [1];

$method = new ReflectionMethod($object, 'm');
$method->invokeArgs($object, $arguments);

Instead, executing the code shown above will print the output shown below:

string(1) "1"

While the discussion of that bug mentions that other forms of indirect invocation might also be affected, we were only able to reproduce the bug for the Reflection API. We tried callbacks with functions such as array_map(), the call_user_func() function, and the $object->$method() syntax. For all these forms of indirect invocation a type error was raised, as is expected and correct.

In the example above, the Reflection API is used to invoke a method with arguments coming from an array. The same effect can be achieved using argument unpacking:

declare(strict_types=1);

$object    = new C;
$arguments = [1];

$object->m(...$arguments);

Executing the code shown above will once again print:

Fatal error: Uncaught TypeError: Argument 1 passed to C::m()
must be of the type string, integer given, called in ...

We hope that developers do not use this bug in PHP to bypass type safety in code that they call.