Typing with PHP 7: what you shouldn't do

PHP 7.0 7.1 typing

When PHP 7 came up with scalar and return typing, I saw the light. I had the hope not to see anymore bugs and inconsistencies due to the way PHP handles types.

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?

I think reliable typing is a big help. You know what is the data you're manipulating. You know what type methods and functions return.
You don't have to guess anymore.

However, PHP 7 wasn't the end of my typing struggle. You can still add a lot of ambiguity even if apparently PHP 7 tried to fix the problem. You still need to follow a couple of rules to keep your code consistently typed.

I used PHP 7.1.1-dev in the cli for every examples in this article.

What? PHP allow some sort of typing?

PHP 7 introduced two typing: scalar types and return types.
I won't explain here the difference and how to use them. The PHP RFC will do it better than me:

Types in PHP 7 or welcome to the land of weird and unexpected behaviors

Let be straightforward: typing in PHP 7 can have unexpected results.

Skeptical? Here some examples:

<?php

function mySuperFunction(): int  
{
    return "hello world";
}

mySuperFunction();

No problem with this code. We expect to return an int, we return 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 type the return as an int.

Wrong. This function returns 1.

<?php

function mySuperFunction():int  
{
    return 1.1456415;
}

mySuperFunction();  

No exception thrown. It returns 1. Yep.

<?php

function mySuperFunction():bool  
{
    return "1hello world";
}

mySuperFunction();  

PHP consider "1hello world" as boolean and it returns... 1.

<?php

function mySuperFunction():bool  
{
     return "abcde";
}

mySuperFunction();  

PHP, how can you tell me that string "abcde" is a boolean? That it returns 1? How dare you?

As you can see, with those typing rules you can still mess up things pretty badly. Simply because the code pustule clearly something which may not be true.

What about a strong typing?

strong guy Type 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 allow you... nothing except messing 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 flowing through our code.
We should know what our variables are.

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. 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 was happy and relieved to see this error when it popup 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 even if they don't implement this strict typing mode.

I know some of you will object. I know some of you are ready to burn consistency and clarity on the stake for the sake of a supposedly 'flexibility'.

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 mode! Now PHP 7 is finally a very and strongly typed language!

It was true. It was the paradise. It was before PHP 7.1.

PHP 7.1 or how to introduce a new beast: the nullable type!

The nullable type: what a beast! The nullable type will smile at you but careful! He is a beast!

Here the PHP RFC for the new nullable type: https://wiki.php.net/rfc/nullable_types.

Thanks to PHP 7.1, we have now the variable-which-is-a-boolean-but-maybe-null-not-sure.

Why is 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:
    1. The User doesn't exists (probably a wrong user id passed to getUSerById)
    2. The User is null because of the nullable type!
    3. 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. 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.

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.

This shouldn't be allowed. An interface should describe exactly what an implementation can and can't do, without any ambiguity.

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

I am speechless on this behavior. Why allowing an implementation not to fulfill entirely the contract of an interface?

What the benefit? Why?

In other word: I advice you to banish the nullable type.
Forever.

In a nutshell

I like PHP. Especially since it has improved its typing. It is 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.
  • Don't use the nullable type.

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.