Table of Contents

PHP RFC: Scalar Type Hints

Summary

This RFC proposes the addition of four type hints for scalar types: int, float, string and bool. These type hints would have “weak” type-checking by default, following the same casting rules traditionally used for the parameters of extension and built-in PHP functions.

This RFC further proposes the addition of a new optional per-file directive, declare(strict_types=1), which makes all function calls and return statements within a file have “strict” type-checking for scalar type hints, including for extension and built-in PHP functions. In addition, calls to extension and built-in PHP functions with this directive produce an E_RECOVERABLE_ERROR on parameter parsing failure, bringing them into line with userland type hints.

With these two features, it is hoped that more correct and self-documenting PHP programs can be written.

Example

Let's say we have a PHP class that represents an ElePHPant. We put scalar type hints on our constructor arguments:

ElePHPant.php
<?php
class ElePHPant {
    public $name, $age, $cuteness, $evil;
    public function __construct(string $name, int $age, float $cuteness, bool $evil) {
        $this->name = $name;
        $this->age = $age;
        $this->cuteness = $cuteness;
        $this->evil = $evil;
    }
}

In a separate file we might try to make a new instance like so:

main.php
<?php
require "ElePHPant.php";
 
$sara = new ElePHPant("Sara", 7, 0.99, FALSE);
var_dump($sara); /*
object(ElePHPant)#1 (4) {
  ["name"]=>
  string(4) "Sara"
  ["age"]=>
  int(7)
  ["cuteness"]=>
  float(0.99)
  ["evil"]=>
  bool(false)
} */

This call succeeds, because the types of the arguments passed exactly match the type hints.

By default, weak type hints that permit some conversions are used, so we could also pass values that are convertible and they'll be converted, just like with extension and built-in PHP functions:

main2.php
<?php
require "ElePHPant.php";
 
$nelly = new ElePHPant(12345, "7 years", "0.9", "1");
var_dump($nelly); /*
object(ElePHPant)#2 (4) {
  ["name"]=>
  string(5) "12345"
  ["age"]=>
  int(7)
  ["cuteness"]=>
  float(0.9)
  ["evil"]=>
  bool(true)
}
Notice: A non well formed numeric value encountered
*/

However, it is also possible to turn on strict type checking with an optional directive. In this mode, the same call would fail:

main3.php
<?php
require "ElePHPant.php";
 
declare(strict_types=1);
 
$nelly = new ElePHPant(12345, "7 years", "0.9", "1");
// Catchable fatal error: Argument 1 passed to ElePHPant::__construct() must be of the type string, integer given

The directive affects all function calls in the file (or declare() block if specified), regardless of whether the functions being called were declared in files which used strict type checking. So:

strict_mix.php
<?php
require "ElePHPant.php";
 
// implicitly weakly type-checked code (default)
$nelly = new ElePHPant(12345, "7 years", "0.9", "1"); // succeeds
 
declare(strict_types=1) {
    // explicitly strictly type-checked code
 
    $nelly = new ElePHPant(12345, "7 years", "0.9", "1");
    // Catchable fatal error: Argument 1 passed to ElePHPant::__construct() must be of the type string, integer given
}

This applies equally to nested function calls, which also use the strictness setting of the file:

strict_mix2.php
<?php
require "ElePHPant.php";
 
// implicitly weakly type-checked code (default)
function makeEllie() {
    return new ElePHPant(42, "19", "0.7", 1); // will succeed, no matter where makeEllie() is called from
}
 
makeEllie(); // no error
 
declare(strict_types=1) {
    // explicitly strictly type-checked code
 
    makeEllie(); // no error
 
    function makeEllie_strict() {
        return new ElePHPant(42, "19", "0.7", 1); // will fail, no matter where makeEllie_strict() is called from
    }
 
    makeEllie_strict(); // error
}
 
// implicitly weakly type-checked code, again
 
makeEllie_strict(); // error

In addition to userland functions, the strict type checking mode also affects extension and built-in PHP functions:

main4.php
<?php
 
declare(strict_types=1);
 
$foo = sin(1);
// Catchable fatal error: sin() expects parameter 1 to be float, integer given

Scalar type hints would also work for return values, as does strict type checking mode:

returns.php
<?php
 
function foobar(): int {
    return 1.0;
}
 
var_dump(foobar()); // int(1)
 
declare(strict_types=1) {
    function foobar2(): int {
        return 1.0;
    }
}
 
var_dump(foobar2());
// Catchable fatal error: Return value of foobar() must be of the type integer, float returned

However, there is a key difference between parameter and return type hints. The type-checking mode used for parameters is the one used by the file containing the function call, while the type-checking mode used for return values is the one used by the file containing the return statement (i.e. the file defining the function). So:

returns.php
<?php
 
declare(strict_types=1) {
    function foobar3(): int {
        return 1.0; // error, regardless of where it is called from
    }
 
    foobar3(); // error
}
 
foobar3(); // also error

Background and Rationale

History

PHP has had parameter type hints for interface and class names since PHP 5.0, arrays since PHP 5.1 and callables since PHP 5.4. These type hints allow the PHP runtime to ensure that correctly-typed arguments are passed to functions, and make function signatures more informative. Unfortunately, PHP's scalar types haven't been hintable.

There have been some previous attempts at adding scalar type hints, such as the Scalar Type Hints with Casts RFC. From what I can see, that specific RFC failed primarily for three reasons:

In creating this RFC, I have attempted to learn from these failings.

Weak typing and strict typing

There are two major approaches to how to check parameter and return type hints that have been proposed for PHP:

Both approaches have their advantages and disadvantages, and in fact PHP already has a mix of both. We use strict type checking for non-scalars such as arrays, objects and resources, and this applies to both userland type hints, and extension and built-in PHP function parameter types. We use weak type checking for scalar parameter types, but only for extension and built-in PHP functions, as PHP does not currently have scalar type hints.

In both approaches, the function will always get exactly the argument type it asks for. In the case of strict type-checking, this is done by rejecting incorrectly-typed values. In the case of weak type-checking, this is done by rejecting some values, and converting others. Therefore, the following code will always work, regardless of mode:

function foobar(int $i) {
    if (!is_int($i)) {
        throw new Exception("Not an integer."); // this will never happen
    }
}

Similarly, in both approaches, a function will always return exactly the return type it claims to:

function barfoo(): int {
   /* ... */
}
if (!is_int(barfoo())) {
    throw new Exception("Not an integer."); // this will also never happen
}

Why both?

So far, most advocates of scalar type hints have asked for either strict type checking, or weak type checking. Rather than picking one approach or the other, this RFC instead makes weak type checking the default, and adds an optional directive to use strict type checking within a file. There were several reasons behind this choice.

By and large the PHP community, myself included, seems to be in favour of strict type checking. However, adding strictly type-checked scalar type hints would cause a few problems:

There is also a significant group of people (including, at times, my past self) who are in favour of weak type checking. But, like adding strictly type-checked hints, adding weakly type-checked scalar type hints would also cause problems:

A third approach has also been suggested, which is to add separate weakly- and strictly-checked type hints with different syntax. It would present its own set of issues:

In order to avoid the issues with these three approaches, this RFC proposes a fourth approach: per-file strict or weak type-checking. This has the following advantages:

Type hint choices

No type hint for resources is added, as this would prevent moving from resources to objects for existing extensions, which some have already done (e.g. GMP).

For the integer typehint, both the int and integer syntaxes are allowed, and for the boolean typehint, both bool and boolean are allowed. This has been done because PHP uses both throughout the manual and error messages, so there is no clear choice of syntax that wouldn't cause problems. While in an ideal world we would not need to support these aliases, the likelihood of people being caught out by integer or boolean not working is very high, so I feel we ought to support both the short and long forms of these type names.

Details

Scalar type hints

No new reserved words are added. The names int, integer, float, string, bool and boolean are recognised and allowed as type hints, and prohibited from use as class/interface/trait names (including with use and class_alias).

The new userland scalar type hints are implemented internally by calling the Fast Parameter Parsing API functions.

strict_types declare() directive

By default, all PHP files are in weak type-checking mode. A new declare() directive is added, strict_types, which takes either 1 or 0. If 1, strict type-checking mode is used for function calls and return statements in the remainder of the file. If 0, weak type-checking mode is used.

This directive also supports the declare() block syntax (e.g. declare(strict_types=1) { foo(); }), in which case it will only affect function calls and return statements within the block.

Like the encoding directive, but unlike the ticks directive, the strict_types directive only affects the specific file it is used in, and does not affect either other files which include the file, nor other files that are included by the file.

The directive is entirely compile-time and cannot be controlled at runtime. It works by setting a flag on the opcodes for function calls (for parameter type hints) and return type checks (for return type hints).

Parameter type hints

The directive affects any function call, including those within a function or method. For example:

strict_types_scope.php
<?php
 
declare(strict_types=1) {
    foo(); // strictly type-checked function call
 
    function foobar() {
        foo(); // strictly type-checked function call
    }
 
    class baz {
        function foobar() {
            foo(); // strictly type-checked function call
        }
    }
}
 
foo(); // weakly type-checked function call
 
function foobar() {
    foo(); // weakly type-checked function call
}
 
class baz {
    function foobar() {
        foo(); // weakly type-checked function call
    }
}

Whether or not the function being called was declared in a file that uses strict or weak type checking is irrelevant. The type checking mode depends on the file where the function is called.

Return type hints

The directive affects any return statement in any function or method within a file. For example:

strict_types_scope.php
<?php
 
declare(strict_types=1) {
    function foobar(): int {
        return 1.0; // strictly type-checked return
    }
 
    class baz {
        function foobar(): int {
            return 1.0; // strictly type-checked return
        }
    }
}
 
function foobar(): int {
    return 1.0; // weakly type-checked return
}
 
class baz {
    function foobar() {
        return 1.0; // weakly type-checked return
    }
}

Unlike parameter type hints, the type checking mode used for return types depends on the file where the function is defined, not where the function is called. This is because returning the wrong type is a problem with the callee, while passing the wrong type is a problem with the caller.

Behaviour of weak type checks

A weakly type-checked call to an extension or built-in PHP function has exactly the same behaviour as it did in previous PHP versions.

The weak type checking rules for the new scalar type hints are mostly the same as those of extension and built-in PHP functions. The only exception to this is the handling of NULL: in order to be consistent with our existing type hints for classes, callables and arrays, NULL is not accepted by default, unless it is a parameter and is explicitly given a default value of NULL. This would work well with the draft Declaring Nullable Types RFC. If that RFC were to pass, it would be possible to mark return types as nullable, and so they would accept NULL.

For the reference of readers who may not be familiar with PHP's existing weak scalar parameter type rules, the following brief summary is provided.

The table shows which types are accepted and converted for scalar type hints. NULL, arrays and resources are never accepted for scalar type hints, and so are not included in the table.

Type hint integer float string boolean object
integer yes yes* yes† yes no
float yes yes yes† yes no
string yes yes yes yes yes‡
boolean yes yes yes yes no

*Only non-NaN floats between PHP_INT_MIN and PHP_INT_MAX accepted. (New in PHP 7, see the ZPP Failure on Overflow RFC)

†Non-numeric strings not accepted. Numeric strings with trailing characters are accepted, but produce a notice.

‡Only if it has a __toString method.

Behaviour of strict type checks

A strictly type-checked call to an extension or built-in PHP function changes the behaviour of zend_parse_parameters. In particular, it will produce E_RECOVERABLE_ERROR rather than E_WARNING on failure, and it follows strict type checking rules for scalar typed parameters, rather than the traditional weak type checking rules.

The strict type checking rules are quite straightforward: when the type of the value matches that specified by the type hint it is accepted, otherwise it is not.

These strict type checking rules are used for userland scalar type hints, and for extension and built-in PHP functions.

Backward Incompatible Changes

int, integer, float, string, bool and boolean are no longer permitted as class/interface/trait names (including with use and class_alias).

Because the weak type-checking rules for scalar hints are quite permissive in the values they accept and behave similarly to PHP's type juggling for operators, it should be possible for existing userland libraries to add scalar type hints without breaking compatibility.

Since the strict type-checking mode is off by default and must be explicitly used, it does not break backwards-compatibility.

Proposed PHP Version(s)

This is proposed for the next PHP x, currently PHP 7.

RFC Impact

To Existing Extensions

ext/reflection will need to be updated in order to support scalar type hint reflection for parameters. This hasn't yet been done.

Unaffected PHP Functionality

This doesn't affect the behaviour of cast operators.

When the strict type-checking mode isn't in use (which is the default), function calls to built-in and extension PHP functions behave identically to previous PHP versions.

Open Issues

There is currently an open issue related to naming, described below. This will go to a vote.

This RFC and patch allows the aliases integer and boolean in addition to int and bool. Should we only allow int and bool? It is probably not a good idea to add too many new reserved class names. On the other hand, we use integer and boolean in many places in the manual, and programmers would be forgiven for expecting integer and boolean to work. We could opt to reserve them but prevent their use, telling people to use int and bool instead. That wouldn't reduce the number of prohibited class names, but it would prevent confusion and ensure consistency.

TODO

Future Scope

Because scalar type hints guarantee that a passed argument will be of a certain type within a function body (at least initially), this could be used in the Zend Engine for optimisations. For example, if a function takes two float-hinted arguments and does arithmetic with them, there is no need for the arithmetic operators to check the types of their operands. As I understand it, HHVM already does such optimisations, and might benefit from this RFC.

In discussions around this RFC, the declare(strict_types=1); syntax has been controversial. It is arguably rather ugly, and it has all the scoping quirks of declare(). I've now arrived at a point where I'm willing to change to a truly per-file syntax, <?php strict (or similar). However, the RFC is currently being voted on, and I'd rather not cancel the vote. So, this will be proposed in a subsequent RFC.

Another issue that came up is PHP's lack of a typehint for numbers (which also came up with the previous Scalar Type Hinting with Cast RFC). I plan to propose a numeric typehint in a future RFC which would take either an integer or a float.

Vote

As this is a language change, this RFC requires a 2/3 majority to pass. Voting for all three votes started on 2015-02-05 and all were to end on 2015-02-19, but the voting was cancelled and the RFC withdrawn on 2015-02-15.

Main

This vote is for the RFC itself.

Accept the Scalar Type Hints RFC and merge patch into master?
Real name Yes No
ab (ab)  
aharvey (aharvey)  
ajf (ajf)  
alan_k (alan_k)  
andi (andi)  
ashnazg (ashnazg)  
bd808 (bd808)  
ben (ben)  
blanchonvincent (blanchonvincent)  
brandon (brandon)  
brianlmoon (brianlmoon)  
bwoebi (bwoebi)  
chregu (chregu)  
chx (chx)  
colder (colder)  
crodas (crodas)  
cyberline (cyberline)  
danack (danack)  
daverandom (daverandom)  
davey (davey)  
demon (demon)  
derick (derick)  
diegopires (diegopires)  
dm (dm)  
dmitry (dmitry)  
dragoonis (dragoonis)  
dsp (dsp)  
duodraco (duodraco)  
Fabien Potencier (fabpot)  
fernandoc (fernandoc)  
fmargaine (fmargaine)  
francois (francois)  
galvao (galvao)  
guilhermeblanco (guilhermeblanco)  
gwynne (gwynne)  
hywan (hywan)  
indeyets (indeyets)  
ircmaxell (ircmaxell)  
irker (irker)  
ivonascimento (ivonascimento)  
jedibc (jedibc)  
jgmdev (jgmdev)  
jmikola (jmikola)  
jpauli (jpauli)  
jwage (jwage)  
kalle (kalle)  
keyur (keyur)  
kinncj (kinncj)  
klaussilveira (klaussilveira)  
krakjoe (krakjoe)  
laruence (laruence)  
lcobucci (lcobucci)  
leigh (leigh)  
levim (levim)  
lstrojny (lstrojny)  
malukenho (malukenho)  
marco (marco)  
mbeccati (mbeccati)  
mfischer (mfischer)  
mfonda (mfonda)  
mike (mike)  
mrook (mrook)  
neeke (neeke)  
nikic (nikic)  
olemarkus (olemarkus)  
pajoye (pajoye)  
patrickallaert (patrickallaert)  
pauloelr (pauloelr)  
peehaa (peehaa)  
philstu (philstu)  
pollita (pollita)  
rafa (rafa)  
ramsey (ramsey)  
rasmus (rasmus)  
rdlowrey (rdlowrey)  
rdohms (rdohms)  
rogeriopradoj (rogeriopradoj)  
royopa (royopa)  
salathe (salathe)  
sebastian (sebastian)  
seld (seld)  
stas (stas)  
stelianm (stelianm)  
stewartlord (stewartlord)  
subjective (subjective)  
tacker (tacker)  
thekid (thekid)  
thorstenr (thorstenr)  
till (till)  
toby (toby)  
tony2001 (tony2001)  
treffynnon (treffynnon)  
tyrael (tyrael)  
weierophinney (weierophinney)  
wiesemann (wiesemann)  
willfitch (willfitch)  
yohgaki (yohgaki)  
yumin1985 (yumin1985)  
yunosh (yunosh)  
zeev (zeev)  
zhangzhenyu (zhangzhenyu)  
Final result: 67 34
This poll has been closed.

Type aliases

This second vote is to solve the open issue regarding the integer and boolean synonyms for int and bool. Whichever option receives the most votes will be implemented.

Type aliases
Real name Allow synonyms Reserve synonyms and produce error message when used Do not reserve
ab (ab)   
aharvey (aharvey)   
ajf (ajf)   
alan_k (alan_k)   
andi (andi)   
ashnazg (ashnazg)   
bd808 (bd808)   
ben (ben)   
blanchonvincent (blanchonvincent)   
brandon (brandon)   
brianlmoon (brianlmoon)   
bwoebi (bwoebi)   
chregu (chregu)   
chx (chx)   
crodas (crodas)   
cyberline (cyberline)   
danack (danack)   
daverandom (daverandom)   
davey (davey)   
diegopires (diegopires)   
dm (dm)   
dmitry (dmitry)   
dragoonis (dragoonis)   
duodraco (duodraco)   
Fabien Potencier (fabpot)   
fernandoc (fernandoc)   
francois (francois)   
galvao (galvao)   
guilhermeblanco (guilhermeblanco)   
gwynne (gwynne)   
hywan (hywan)   
indeyets (indeyets)   
irker (irker)   
ivonascimento (ivonascimento)   
jedibc (jedibc)   
jgmdev (jgmdev)   
jmikola (jmikola)   
jpauli (jpauli)   
kalle (kalle)   
keyur (keyur)   
kinncj (kinncj)   
klaussilveira (klaussilveira)   
krakjoe (krakjoe)   
laruence (laruence)   
lcobucci (lcobucci)   
leigh (leigh)   
lstrojny (lstrojny)   
malukenho (malukenho)   
marco (marco)   
mbeccati (mbeccati)   
mfischer (mfischer)   
mike (mike)   
mrook (mrook)   
neeke (neeke)   
nikic (nikic)   
pajoye (pajoye)   
patrickallaert (patrickallaert)   
pauloelr (pauloelr)   
peehaa (peehaa)   
philstu (philstu)   
pollita (pollita)   
rafa (rafa)   
ramsey (ramsey)   
rasmus (rasmus)   
rdlowrey (rdlowrey)   
rdohms (rdohms)   
rogeriopradoj (rogeriopradoj)   
royopa (royopa)   
salathe (salathe)   
sebastian (sebastian)   
seld (seld)   
stas (stas)   
stelianm (stelianm)   
stewartlord (stewartlord)   
subjective (subjective)   
tacker (tacker)   
thekid (thekid)   
thorstenr (thorstenr)   
till (till)   
toby (toby)   
tony2001 (tony2001)   
treffynnon (treffynnon)   
weierophinney (weierophinney)   
wiesemann (wiesemann)   
willfitch (willfitch)   
yohgaki (yohgaki)   
yunosh (yunosh)   
Final result: 48 19 20
This poll has been closed.

Reserve for future use

This final vote is in case the RFC fails to pass. It's a 2/3 majority-required backwards compatibility-breaking language change, which is to reserve the type hint names proposed by the RFC, so that a future RFC could implement scalar type hints without requiring a backwards compatibility break. The type names reserved include the synonyms integer and boolean. They would not be reserved words, merely prohibited from use as class/interface/trait names, like this RFC's type names.

Reserve type names if RFC does not pass?
Real name Yes No
ab (ab)  
aharvey (aharvey)  
ajf (ajf)  
alan_k (alan_k)  
andi (andi)  
ashnazg (ashnazg)  
bd808 (bd808)  
ben (ben)  
blanchonvincent (blanchonvincent)  
brandon (brandon)  
brianlmoon (brianlmoon)  
bwoebi (bwoebi)  
chregu (chregu)  
chx (chx)  
crodas (crodas)  
cyberline (cyberline)  
danack (danack)  
daverandom (daverandom)  
davey (davey)  
demon (demon)  
diegopires (diegopires)  
dmitry (dmitry)  
dragoonis (dragoonis)  
duodraco (duodraco)  
Fabien Potencier (fabpot)  
fernandoc (fernandoc)  
francois (francois)  
fredemmott (fredemmott)  
galvao (galvao)  
guilhermeblanco (guilhermeblanco)  
gwynne (gwynne)  
hradtke (hradtke)  
hywan (hywan)  
indeyets (indeyets)  
irker (irker)  
ivonascimento (ivonascimento)  
jedibc (jedibc)  
jgmdev (jgmdev)  
jmikola (jmikola)  
jpauli (jpauli)  
jwage (jwage)  
kalle (kalle)  
kinncj (kinncj)  
klaussilveira (klaussilveira)  
krakjoe (krakjoe)  
lcobucci (lcobucci)  
leigh (leigh)  
lstrojny (lstrojny)  
malukenho (malukenho)  
marco (marco)  
mbeccati (mbeccati)  
mfischer (mfischer)  
mfonda (mfonda)  
mike (mike)  
mrook (mrook)  
olemarkus (olemarkus)  
pajoye (pajoye)  
patrickallaert (patrickallaert)  
pauloelr (pauloelr)  
peehaa (peehaa)  
philstu (philstu)  
pollita (pollita)  
rafa (rafa)  
ramsey (ramsey)  
rasmus (rasmus)  
rdlowrey (rdlowrey)  
rdohms (rdohms)  
rogeriopradoj (rogeriopradoj)  
royopa (royopa)  
salathe (salathe)  
sebastian (sebastian)  
seld (seld)  
stas (stas)  
stelianm (stelianm)  
stewartlord (stewartlord)  
subjective (subjective)  
tacker (tacker)  
thekid (thekid)  
thorstenr (thorstenr)  
till (till)  
toby (toby)  
tony2001 (tony2001)  
treffynnon (treffynnon)  
weierophinney (weierophinney)  
wiesemann (wiesemann)  
willfitch (willfitch)  
yohgaki (yohgaki)  
yunosh (yunosh)  
zeev (zeev)  
Final result: 72 17
This poll has been closed.

Patches and Tests

There is a working, but possibly buggy php-src pull request with tests here: https://github.com/php/php-src/pull/998

There is no language specification patch as yet.

Implementation

After the project is implemented, this section should contain

  1. the version(s) it was merged to
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature

References

Changelog