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.