Nullable Types


In this video we take a look at a new feature in PHP7.1 called Nullable Types.

This is an improvement on the Return Types and Scalar Type Declarations features added in PHP7.0.

So before covering Nullable Types, let's quickly recap Scalar Type Declarations, and Return Types.

If you work with modern PHP, you will likely have used some form of dependency injection. This manifests typically with function signatures that look similar to:

public function someMethod(SomeInterface $someClassImplementingInterface)
{
    // ... implementation here
}

This ensures we can only ever use a concrete implementation of SomeInterface when invoking someMethod. And this is fairly awesome. It stops a lot of bugs slipping into your code.

However, it didn't work for scalar types - string, bool, int, and so on.

Let's say we have a function that accepts a string as a parameter, and returns a string. In PHP5.x we might write:

function ourStringAcceptingFunction($someVar)
{
    return $someVar;
}

This is ok, and will behave as expected in the "happy path" scenario, but has some problems.

Firstly, it doesn't just accept strings. It will quite happily take any type of variable - bool, int, array, etc.

We might think to add in a helpful DocBlock to inform ourselves, and others, that dammit, we only want strings:

/**
 * @param string $someVar
 * @return string
 */
function ourStringAcceptingFunction($someVar)
{
    return $someVar;
}

Ok, that's helpful, but effectively useless:

echo ourStringAcceptingFunction('hello');
// outputs: hello

echo ourStringAcceptingFunction(true);
// outputs: 1

Comments aren't code.

Realising this was a bit of an issue, PHP7 added in Scalar Type Declarations. This fixed the earlier problem. Now we could refactor our code:

/**
 * @param string $someVar
 * @return string
 */
function ourStringAcceptingFunction(string $someVar)
{
    return $someVar;
}

And this fixes the problem, right?

Well... sort of.

PHP will - potentially unexpectedly - coerce some values for you:

echo ourStringAcceptingFunction('hello');
// outputs: hello

echo ourStringAcceptingFunction(123);
// outputs: 123

echo ourStringAcceptingFunction(true);
// outputs: 1

Which seems kinda... odd? It does to me.

Now, there is a potential fix to this, but even that is not quite as cut and dry as you might expect. We can declare(strict_types=1);. More on this shortly.

Getting back on track, scalar type hints quickly became one of my favourite parts of PHP7.

Return Types, on the other hand, seemed initially great, but quickly became troublesome when used with Symfony.

Upon upgrading to PHP7, the first thing I did was to blast through all my entities and add in the various return types you might expect from an entity:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="some_table")
 */
class SomeEntity
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var string
     * @ORM\Column(type="string")
     */
    protected $title;

    /**
     * @var string
     * @ORM\Column(type="string")
     */
    protected $body;

    /**
     * @return mixed
     */
    public function getId() : int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getTitle() : string
    {
        return $this->title;
    }

    /**
     * @param string $title
     * @return SomeEntity
     */
    public function setTitle(string $title) : SomeEntity
    {
        $this->title = $title;

        return $this;
    }

    /**
     * @return string
     */
    public function getBody() : string
    {
        return $this->body;
    }

    /**
     * @param string $body
     * @return SomeEntity
     */
    public function setBody(string $body) : SomeEntity
    {
        $this->body = $body;

        return $this;
    }
}

Awesome, right?

Not quite.

As soon as you new up this entity, and try and use it with the Symfony form component, as the initial values for title, body, etc, are null, this will blow up with fatal errors.

Ok, we all use git, so sad times, but let's roll back the Return Types part of that change and carry on without just the scalar type declarations.

Fast forward to PHP7.1 and we can fix this up:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="some_table")
 */
class SomeEntity
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var string
     * @ORM\Column(type="string")
     */
    protected $title;

    /**
     * @var string
     * @ORM\Column(type="string")
     */
    protected $body;

    /**
     * @return mixed
     */
    public function getId() : ?int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getTitle() : ?string
    {
        return $this->title;
    }

    /**
     * @param string $title
     * @return SomeEntity
     */
    public function setTitle(string $title) : SomeEntity
    {
        $this->title = $title;

        return $this;
    }

    /**
     * @return string
     */
    public function getBody() : ?string
    {
        return $this->body;
    }

    /**
     * @param string $body
     * @return SomeEntity
     */
    public function setBody(string $body) : SomeEntity
    {
        $this->body = $body;

        return $this;
    }
}

By adding in the ? before the expected return type, we can tell PHP that either we expect a string, or null - ?string.

The way in which this is written defies the convention of most other languages. Typically this would be written as string? in TypeScript, or C# and likely many others.

And this goes for function parameters also. Taking the above example, we could just as easily say the call to setBody can either be a string or null:

public function setBody(string $body) : SomeEntity

// becomes

public function setBody(?string $body) : SomeEntity

Ok, so we know we can now have this 'either' / 'or' situation.

Let's quickly revisit our earlier function:

function ourStringAcceptingFunction(string $someVar)
{
    return $someVar;
}

And let's add in a return type here:

function ourStringAcceptingFunction(string $someVar) : int
{
    return $someVar;
}

How might this behave?

echo ourStringAcceptingFunction(123);
// outputs: 123

echo ourStringAcceptingFunction('987');
// outputs: 987

echo ourStringAcceptingFunction(true);
// outputs: 1

echo ourStringAcceptingFunction('hello');
// outputs:

PHP Fatal error:  Uncaught TypeError: Return value of ourStringAcceptingFunction() must be of the type integer, string returned in /Users/chris/Development/php7.1/nullable_types.php:16
Stack trace:
#0 /Users/Shared/Development/php7.1/nullable_types.php(8): ourStringAcceptingFunction('abc')
#1 {main}
  thrown in /Users/chris/Development/php7.1/nullable_types.php on line 5

The return type does impact the returned value. Setting the return type from int to bool changes things up again:

function ourStringAcceptingFunction(string $someVar) : bool
{
    return $someVar;
}

echo ourStringAcceptingFunction(123);
// outputs: 1

echo ourStringAcceptingFunction('987');
// outputs: 1

echo ourStringAcceptingFunction(true);
// outputs: 1

echo ourStringAcceptingFunction(false);
// outputs nothing

echo ourStringAcceptingFunction('hello');
// outputs: 1

Uh-huh.

Now, as mentioned a little earlier we can make PHP behave a little better by using declare(strict_types=1);:

declare(strict_types=1);

function ourStringAcceptingFunction(string $someVar) : bool
{
    return $someVar;
}

echo ourStringAcceptingFunction('987');
// throws

echo ourStringAcceptingFunction(1);
// throws

echo ourStringAcceptingFunction(false);
// throws

echo ourStringAcceptingFunction('hello');
// throws

As now the scalar type hint of string, and the return type of bool are respected as you might expect.

However, even this is not enough to guarantee our code behaves properly.

There is a hidden gem here that this declare(strict_types=1); declaration only applies if the function call is made from within a file with strict typing set. You can read more about this here.

The gist of this is that if we had a class that has declare(strict_types=1) set. And then we call a method on this class from a different file that does not have strict typing set, then strict typing is ignored. It only applies if the calling file also has the strict typing set. Yikes.

Note that strict typing applies to both return types, and scalar type declarations.

Using Homebrew

As a side note, during this video I demonstrate switching from PHP5.6 to PHP7.1 from the command line on my OSX machine.

To do this I make use of Homebrew, the command being:

brew unlink php5.6 && brew link php7.1

This assumes you have the expected versions of PHP installed, e.g.:

brew install php7.1

I also note that I only do this for demonstration purposes.

The reasoning for this is that at any one time your laptop can only be configured for one setup. Let's say I am working on two projects concurrently. One is a legacy project on PHP5.3 (yes, this is still happening :(), and another is one from the 21st century with PHP7.1.

I would advise having two designated environments - vagrant, ansible, whatever - to do this. It completely separates the environments and ensures no globally installed 'thing' or unexpectedly shared library somehow impacts one or the other project.

The problems this sort of thing can introduce cannot be explained - they must be experienced first hand, usually under some horrendous deadline. Don't do this to yourself.

But that said, homebrew is cool, and for demonstration or exploration, it can be really nice to do this.

Episodes

# Title Duration
1 Shorthand Destructuring 06:13
2 Nullable Types 06:37
3 Class Constant Visibility 03:15
4 Void Functions 01:53
5 Multi Catch Exception Handling 05:10