PHP 8.0 feature focus: type improvements

Larry Garfield
Larry Garfield
Director of Developer Experience
17 Sep 2020

PHP 8 is almost here, and with it a host of improvements, new functionality, and overall polish to make the web’s favorite server-side language even better. In this weekly series, leading up to the final release by the end of the year, we’ll cover what you need to know about PHP 8. It’s an exciting release, and we’re not even going to be covering all of it! There’s just that much going on.

First up, we’ll cover the various improvements to the type system.

Union types

The biggest type system change is the introduction of union types. Union types are a virtual type that are the “union” (a logical “OR”) of two other types. For example:

<?php

function mangleUsers(string|array $users): array
{
    If (is_string($users)) {
        $users = [$users];
    }
    // ...
}

Previously, if you wanted to support the “one or many” parameter style like that you couldn’t type the parameter. Now, you can specify string|array and either a string or array variable is acceptable. Integers, objects, and so on will still be rejected.

Union types work on parameters, return types, and property types, and they support any type PHP supports: primitives, objects, user-defined classes, etc. While they can be used to produce all kinds of grody, inconsistent APIs, those were already possible. At least now they can be documented. More importantly, it opens up some interesting use cases that were previously impossible to document in the type system itself.

For example, a function that requires a number but can work on both ints or floats can now say exactly that:

<?php

function doMath(int|float): int|float
{
    // ...
}

It’s also possible to explicitly say that a function or method may return objects of different types. For example, a function that takes a PSR-7 request object and either returns a new request or a corresponding response would look like this:

<?php

function handleRequest(RequestInterface $req): RequestInterface|ResponseInterface
{
    // ...
}

In inheritance, union types follow the same covariant/contravariant rules as any other type. That is, a subclass’s method parameters are allowed to accept a broader type definition than its parent (so adding a |Foo is OK, but not removing it), while a return value is allowed to specify a narrower type definition than its parent (so removing |Foo is OK, but not adding it).

The reflection API has also been updated to support inspecting union types on functions and properties.

More details are available in the original RFC. Thanks to Nikita Popov for this one.

Specialty unions

PHP already has a number of one-off union types defined in the language. iterable, for instance, is effectively a union type for array|\Traversable. Nullable types, such as ?Product, are essentially a union type of null|Product.

In fact, it is now possible to specify null as a type in a union, but only in a union. So null|RequestInterface|ResponseInterface is a legal type definition. The ? nullable flag is now a shorthand for null| when there’s only a single other value allowed.

Another union-type-only option is false. That is mainly to support existing PHP internal functions that, due to errors of judgement made back in the 1990s, return “a value, or false on error.” Those functions can now be typed as:

<?php

function base64_decode(string $str, bool $strict = false): string|false {}

That capability is only to support legacy code that can’t change its API to be less bad. Please don’t use it for any new code. Like null, false cannot be used as a stand-alone type declaration. Also, the void type declaration cannot be combined with anything, as it literally means “nothing at all is returned, period,” so it makes little sense to combine with other types.

PHP 8 also introduces a new built-in union type. The new mixed type is equivalent to array|bool|callable|int|float|null|object|resource|string. That’s not quite the same as omitting the type entirely. Omitting the type could mean that the developer just forgot, or that the type system is not robust enough to describe the possible allowed values. Using the mixed type explicitly says to the engine and other developers, “I accept anything here, and I mean it.” There are now extremely few cases where it’s not possible to type a parameter, return, or property in PHP 8.

Thanks to Máté Kocsis and Dan Ackroid for this RFC.

The Stringable interface

PHP objects have long been able to define a way that they can be cast to a string, using the __toString() magic method. However, the string type hint doesn’t accept string-castable objects in strict mode, rendering that less useful since PHP 7.0 introduced scalar types.

PHP 8 now introduces a Stringable interface that corresponds to an object having the __toString() magic method. It also does so in a clever, backward-compatible way.

As of 8.0, a class may implement a Stringable interface that defines a method public function __toString(): string. If it doesn’t, but still implements that method, the engine will add the method and return type automatically. That means any stringable object may now be type-checked, including as part of a union type, like so:

<?php

function show_message(string|Stringable $message): void
{
    // ...
}

Boom! __toString is useful again.

Because the new interface is, technically, slightly more strict than the original __toString() method (since it enforces a string return rather than just assuming it), the Symfony team has included a polyfill for it in their symfony/polyfill-php80 package. That allows developers to start using it now to ensure their types are correct and will be forward-compatible for PHP 8 immediately.

Mind you, just because __toString() is more usable now doesn’t mean you should go overboard. Most objects should not have a __toString() method and should have meaningful methods that return strings instead. Where __toString() is useful is for value objects that have one and only one possible meaningful string representation, because the object is essentially just a fancy string. A good example is an IPv4Address object, which would only make sense to cast to a string as 1.2.3.4 type strings.

Thanks to Nicolas Grekas for the Stringable RFC.

Odds and ends

Finally, there’s two other small improvements to help everyday code.

First, objects now have a magic constant that specifies their class, just as class names do. $object::class is a string containing the class name, such as App\Form\FormDef. It’s the same as get_class(), but easier to use. Credit for this feature goes again to Nikita Popov.

Finally, a personal favorite, methods can now have a return type of static. That’s mainly useful for interfaces with chained methods. That means the following is now possible:

<?php

Interface TaskBuilder
{
    public function addStep(Step $s): static;
}

class  ImportantTask implements TaskBuilder 
{
    public function addStep(Step $s): static
    {
        $this->steps[] = $s;
        return $this;
    }
}

And now ImportantTask::addStep() is typed to return an instance of ImportantTask. Previously with a return type of self it would only indicate that it’s returning TaskBuilder.

Credit for this improvement goes once again to Nikita Popov. (We’ll be seeing his name a lot in this series.)

You can try out pre-release copies of PHP 8.0 today on Platform.sh, with just a one line change. Give it a whirl, and let us know what your favorite features are.