When PHP 7 came up with strong types, I saw the light. I had the hope not to see anymore bugs and inconsistencies due to weak typing in PHP.
I remember reading some code and having no idea what could be the type of the variables I had in front of me. Can I use the return of this method as an int? A boolean? Will it create silent bugs and unexpected behaviors?
Scalar and return type are very useful. It indicates what exactly is the data you’re manipulating. You don’t have to guess anymore.
Wait. You don’t have to guess anymore?
PHP typing hide a lot of weird behaviors and ambiguity you need to know. Ambiguities mean doubts. Doubts mean bugs and headaches.
I used PHP 7.1.1-dev in the cli for every examples in this article.
PHP type declaration
PHP 7 introduced two possible typing: scalar types and return types.
How great is that? Now we know what is the input of our functions and the output as well! What can go wrong?
PHP 7 typing: surprising behavior
In theory PHP 7 typing sounds great. Then you’re bored at work so you begin to play with it. Then you ask yourself some questions: is it really strongly typed? What is this output? Why?
Skeptical? Here some examples:
<?php
function mySuperFunction(): int
{
return "hello world";
}
mySuperFunction();
No problem with this code. The type declaration indicate that the method should return an int. Instead, it returns a string. It is not astonishing that PHP throw an error:
Fatal error: Uncaught TypeError: Return value of mySuperFunction() must be of the type integer, string returned
.
Let see another example:
<?php
function mySuperFunction():int
{
return "hello world1";
}
mySuperFunction();
The same results as above: a type error. Great! What the problem with return type then? Did I lie to you?
<?php
function mySuperFunction():int
{
return "1hello world";
}
mySuperFunction();
This should throw an exception, isn’t it? The return is still a string, we clearly typed the return as an int.
This function returns 1
. No error. “1Hello world” is a string for PHP. Let’s continue.
<?php
function mySuperFunction():int
{
return 1.1456415;
}
mySuperFunction();
The return type is clearly not an int but a float. However, this code doesn’t throw any exception.
Instead it returns 1
.
<?php
function mySuperFunction():bool
{
return "1hello world";
}
mySuperFunction();
PHP consider “1hello world” as a boolean and it returns… 1
.
<?php
function mySuperFunction():bool
{
return "abcde";
}
mySuperFunction();
In the weird world of PHP, "abcde"
is a boolean. Yes, this method returns true
!
As you can see, with these typing rules you can still mess up things pretty badly. Simply because the code states clearly something which may not be true.
In short: these typing rules can be seriously misleading.
Enter PHP strict types
Types should be as strong as Hulk himself!
PHP like weak typing.
I don’t.
The fact that we can convert silently a string to a boolean to an int at runtime allows you… to mess your code! It adds ambiguity to something which should be simple and straightforward.
Let’s be clear here. We are developers. Therefore we should control the data types in our code.
If we don’t, we let the door open to bugs, weird behaviors and misunderstanding between developers. The code will change. Bugs will appear, your boss will fire you, your wife will let you down and you will finish in hell, sad, alone, burning in the flames.
But wait! Everything is still possible. There is hope. PHP has the solution!
Beware the strict type mode:
<?php declare(strict_types=1);
function mySuperFunction(): bool{
return 'abcde';
}
echo mySuperFunction();
Run this code and you will get Fatal error: Uncaught TypeError: Return value of mySuperFunction() must be of the type boolean, string returned
!
I felt relieved when I saw this error popping on my screen. Of course a string is not a boolean!
My advice: put this strict type declaration everywhere in your code. Everywhere! Create a snippet in your IDE for it. Each time you create a PHP file, you should put this strict type declaration on top.
Unfortunately you can’t set this strict mode globally. You need to implement it in every single PHP files. The reason is simple: it allows whatever bundle and other external resources to work in your application, even if they don’t implement this strict typing mode.
I know some of you won’t agree. I know some of you are ready to burn consistency and clarity for the sake of a supposedly ‘flexibility’. I know you will argue that PHP is not Java. Thanks god it’s not.
I know the arguments: ‘yes but it is easier because my boolean need to be displayed and therefore should be a string at one point’.
I would answer to them: fix your architecture and/or your implementation. If you need this weak typing, something is wrong in your code. If you really need it, please fix the real problem, don’t go around by using a boolean as a string as an int and whatnot.
You are a developer. You are not a hacker. You solve problems. You don’t go around them.
In 5 words: glory to the strict types! Now PHP 7 is finally a strongly typed language!
Nullable type in PHP 7.1
The nullable type will smile at you but careful! He is a beast!
Here the PHP RFC for the new nullable type.
How can you use it wrong?
<?php
declare(strict_types=1);
class User{
//Value object
}
class UserRepository{
private $database;
public function __construct(Database $database){
$this->database = $database;
}
public function getUserById(int $id):?User
{
//If user is not in the database, return null
$user = $this->database->fetchUser($id);
return $user;
}
}
class EmailSender
{
public function sendEmailToUser(int $userId)
{
$userRepository = new UserRepository(new Database());
$user = $userRepository->getUserById($userId);
//Can send email to... null!
$this->sendEmail($user);
}
}
$emailSender = new EmailSender();
$emailSender->sendEmailToUser(12345);
This code will crash if we try to fetch a User
which doesn’t exist in the database. How can we send an email to null
?
Obviously you can fix this with a condition as follow:
<?php
...
class EmailSender
{
public function sendEmailToUser($userId)
{
$userRepository = new UserRepository(new Database());
$user = $userRepository->getUserById($userId);
if ($user !== null) {
$this->sendEmail($user);
}
}
}
However this approach has two problems:
- Using The nullable operator will create this kind of null condition (
if ($methodReturn !== null)
) everywhere. It is useless and noisy. - The code above will fail silently if the user doesn’t exist. “Why this user didn’t receive his email?” will be your nightmare. You need to see that:
- The
User
doesn’t exists (probably a wrong user id passed to getUSerById) - The
User
isnull
because of the nullable type! - Somebody put this null condition and let the application… not doing anything
Here another solution:
...
class UserRepository{
private $database;
public function __construct($database){
$this->database = $database;
}
public function getUserById($id):User
{
$user = $this->database->fetchUser($id);
//If user is not in the database, an error will be thrown!
return $user;
}
}
...
There is no need for the nullable type in this specific case. Your code will throw an exception where it should. If an User
doesn’t exist, the execution of the application should stop.
You just need to handle the error at that point. It is simple, clear, effective and don’t let space to confusion.
PHP nullable type and interface
The nullable type has other… surprises.
<?php
declare(strict_types=1);
interface MySuperInterface
{
public function superFunction():?int;
}
class SuperClass implements mySuperInterface
{
public function superFunction():int
{
return 42;
}
}
$superClass = new SuperClass();
echo $superClass->superFunction();
The implementation SuperClass
of the interface MySuperInterface
doesn’t fullfil his contract. The interface ask for a nullable return type, the implementation states that only int can be returned.
However, this piece of code work in PHP 7.1. No error will be thrown.
Wait… maybe we can anyway return null as stated in the interface?
Let’s try:
<?php
declare(strict_types=1);
interface MySuperInterface
{
public function superFunction():?int;
}
class SuperClass implements mySuperInterface
{
public function superFunction():int
{
return null;
}
}
$superClass = new SuperClass();
echo $superClass->superFunction();
And the result is:
Fatal error: Uncaught TypeError: Return value of SuperClass::superFunction() must be of the type integer, null returned
Now I can understand that in some cases it could be useful. For example it’s not rare to have null values in a database.
I would advise you very strongly to use it with care. I rarely use this type in my code because I always find better solutions.
I would even prefer using a null object instead of null
in most cases. Why? Simply because I hate checking if my variable is null all the time!
In a nutshell: you need to be careful
I like PHP. Especially since it has improved its typing. It’s not perfect, for sure, but it gets better and better.
Nevertheless you need to be careful when manipulating types in PHP. I won’t repeat it enough:
- We need to use the strict mode.
- We need to control the data which flow in our application.
- If you need the weak typing, there is a problem in your implementation. Therefore: fix it!
- We shouldn’t try to guess what variable has what type.
- Avoid the nullable type as much as you can
We need to be consistent for the sake of every developers using our code. For me, it means being simply professional.
Obviously I would be happy to read your opinion on this in the comment below.