PHP 7 introduced a feature everyone was waiting for: typing. This solves a lot of bugs/problems caused by wrong types passed to function or returned by functions.
Strict typing vs type coercion
Before I introduce the actual typing of arguments or return values, I must explain PHP’s behaviour when types don’t match.
By default PHP tries to convert the given variable to the defined type. This is called coercion or type juggling:
By default, PHP will coerce values of the wrong type into the expected scalar type if possible. For example, a function that is given an integer for a parameter that expects a string will get a variable of type string.
The example below shows what can go wrong with that (this is also why you need to use the ===
operator when comparing things):
$foo = "0"; // $foo is string (ASCII 48)
$foo += 2; // $foo is now an integer (2)
$foo = $foo + 1.3; // $foo is now a float (3.3)
$foo = 5 + "10 Little Piggies"; // $foo is integer (15)
$foo = 5 + "10 Small Pigs"; // $foo is integer (15)
So if we want actually PHP to throw an exception when types don’t match, we must manually enable strict typing (in each file which calls the function):
It is possible to enable strict mode on a per-file basis. In strict mode, only a variable of exact type of the type declaration will be accepted, or a TypeError will be thrown. The only exception to this rule is that an integer may be given to a function expecting a float.
Strict typing applies to function calls made from within the file with strict typing enabled, not to the functions declared within that file. If a file without strict typing enabled makes a call to a function that was defined in a file with strict typing, the caller’s preference (weak typing) will be respected, and the value will be coerced.
This done by adding the following line to the file:
declare(strict_types=1);
It yields the following TypeError exception when types don’t match:
Fatal error: Uncaught TypeError: Argument 1 passed to factorial() must be of the type integer, string given, called in - on line 9 and defined in -:4
Stack trace:
#0 -(9): factorial("5")
#1 {main}
thrown in - on line 4
You can catch these TypeError exceptions like any other exception:
try {
factorial("5");
} catch (TypeError $e) {
echo 'Error: '.$e->getMessage();
}
Types of function arguments
The first sort of typing I will demonstrate is the type declaration of function arguments. Since PHP 7 it’s possible to give parameters of a function a type, the type of the arguments is then checked on function call, and if the callee defined strict typing, a type error is thrown when they don’t match:
function factorial(int $n) {
return $n * ($n-1);
}
If we check this with both an int
and a string
:
declare(strict_types=1);
echo factorial(5);
echo factorial("5");
We got the following result:
20
PHP Fatal error: Uncaught TypeError: Argument 1 passed to factorial() must be of the type integer, string given, called in /Users/Mathias/Sites/types.php on line 15 and defined in /Users/Mathias/Sites/types.php:5
Stack trace:
#0 /Users/Mathias/Sites/types.php(15): factorial('5')
#1 {main}
thrown in /Users/Mathias/Sites/types.php on line 5
If we don’t specify strict typing, we got two times 20 as result.
This can naturally also be used to check type conformance when passing instances of user-defined classes to functions or implementations of interfaces:
class Fruit {}
class Apple extends Fruit {}
class Car {}
function getTaste(Fruit $f) {
// return something useful :)
}
declare(strict_types=1);
getTaste(new Fruit);
getTaste(new Apple);
getTaste(new Car);
As one would expect, both Fruit
and Apple
are accepted, but Car
is not accepted. Note that a subtype always conforms to the supertype, even PHP properly follows the Liskov substitution principle1. The above code yields the following results:
PHP Fatal error: Uncaught TypeError: Argument 1 passed to getTaste() must be an instance of Fruit, instance of Car given, called in /Users/Mathias/Sites/types.php on line 19 and defined in /Users/Mathias/Sites/types.php:12
Stack trace:
#0 /Users/Mathias/Sites/types.php(19): getTaste(Object(Car))
#1 {main}
thrown in /Users/Mathias/Sites/types.php on line 12
Types of return values
The return type declarations are similar to the argument type declarations:
function factorial($n): int {
return $n * ($n-1);
}
This can again also be done with class instances or interface implementations.
If we now call the factorial function with a float, it will return a float instead of the integer we specified in the function declaration:
declare(strict_types=1);
echo factorial(5.5);
This yields:
PHP Fatal error: Uncaught TypeError: Return value of factorial() must be of the type integer, float returned in /Users/Mathias/Sites/types.php:13
Stack trace:
#0 /Users/Mathias/Sites/types.php(20): factorial(5.5)
#1 {main}
thrown in /Users/Mathias/Sites/types.php on line 13
When should I use type declarations?
Always? The more you use type declarations, the less time you will have to spend debugging your code. The most useful advantage of type checking is that you don’t need to manually check for Null
values being passed to functions or returned by functions. PHP type declarations encode the pre- and postconditions you have to implement manually otherwise. So the more typed functions you have, the quicker you will discover problems, and the faster you can develop!
Especially when developing with others, you want strong preconditions, so the callee is more restricted in what he may pass to your function. Having stronger postcondition restricts what you can return, and makes it easier for the callee.
Notes
- Typing is (as expected) not as advanced as in other languages like C++/Java/Go, and doesn’t make PHP a statically typed language.
- When overriding class methods of a parent, the child should use the same type.
When overriding a parent method, the child’s method must match any return type declaration on the parent. If the parent doesn’t define a return type, then the child method may do so.
Any good programmer should ignore the last sentence in the quote: preconditions (being the type declarations in this case) can be modified in redefined routines, but they may only be weakened.2 So PHP actually doesn’t fully conforms to Liskov substitution principle.
- When you’re working with strict typing, you sometimes want to coerce the types yourself. You can do this using type casting:
$foo = 1; // $foo is an integer $bar = (boolean) $foo; // $bar is a boolean $fst = (string) $foo; // $fst is a string
- Liskov substitution principle: if S is a subtype of T, then objects of type T may be replaced with objects of type S. Feel free to read the original paper by Barbara Liskov: Family Values: A Behavioral Notion of Subtyping
- Meyer, Bertrand, Object-Oriented Software Construction, second edition, Prentice Hall, 1997, p. 570-573