PHP 8.0 brings it all together on Platform.sh

Larry Garfield
Larry Garfield
Director of Developer Experience
26 Nov 2020

A year in the making, PHP 8.0 has been released to the world! Sporting a plethora of new functionality to offer more power with less code, it’s the most anticipated release of PHP since … well since PHP 7.4 last year. (PHP is exciting, what can I say?) And you can run it today on Platform.sh.

Stronger together

As we’ve been covering throughout our series, PHP 8.0 is chock full of new functionality. From a more expressive typing system to match() expressions to the potentially huge improvement in persistent services from the new JIT extension, PHP 8 has something to offer everyone.

There are three features I want to call out in particular, though, as the most important. These features are all long-standing requests on their own, but in tandem with each other create something greater than the sum of their parts.

Constructor Property Promotion

The first is constructor property promotion. Constructor promotion is “just” syntactic sugar to make writing constructors and object properties less repetitive. For example, this class:

<?php

class MailMessage
{
    private string $to;
    private string $subject;
    private string $from;
    private string $body;
    private array $cc;
    private array $bcc;
    private string $attachmentPath;


    public function __construct(
        string $to;
        string $subject;
        string $from;
        string $body;
        array $cc = [];
        array $bcc = [];
        string ?$attachmentPath = null;
    ) {
        $this->to = $to;
        $this->subject = $subject;
        $this->from = $from;
        $this->body = $body;
        $this->cc = $cc;
        $this->bcc = $bcc;
        $this->attachmentPath = $attachmentPath;
    }
}

Can now be condensed into this:

<?php

Class MailMessage
{
    public function __construct(
        private string $to,
        private string $subject,
        private string $from,
        private string $body,
        private array $cc = [],
        private array $bcc = [],
        private ?string $attachmentPath = null,
    ) {}
}

Exact same result, one quarter the typing. And of course you can still use non-promoted properties and parameters, still fill in a constructor body for additional behavior, etc.

While that may seem like a small change, it’s actually quite huge. Not just because it reduces needless verbosity (which is great), but because it makes certain approaches easier. In particular, it makes defining a struct-like object (one that contains only public properties mapped from the constructor, or little more than that) 4x easier to write.

Today, many developers will default to using associative arrays for quick-n-dirty structs. That is less self-documenting, uses more memory, is harder to use, and is all around worse in every way than defining a proper class—except a lot of developers find creating a class for that purpose too much work. Well, now defining a class is not too much work and can vastly improve the quality and efficiency of a code base.

Attributes

The second cornerstone feature is attributes. Attributes provide a way to annotate a class, function, property, or argument with metadata that can be examined at runtime, with full native language support. Such functionality was available before via third-party libraries and custom parsing of comment blocks, but now it’s available as a first-class part of the language.

Attributes can be either bare strings or map to classes, with the latter usually being preferable (for all the same reasons why classes are preferable to unstructured arrays above). Although attributes don’t intrinsically have any particular functionality, that is easy enough to add in user-space. For example, to require that the subject line of an email message contain only plain text and be limited to 120 characters, you could define an attribute:

<?php

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class PlainText
{
    public function __construct(private ?int $length = null) {}

    public function valid(string $str): bool
    {
        return (strlen($str) <= $this->length) && strip_tags($str) === $str;
    }
}

Note the constructor promotion at work. Attributes are a perfect example of struct classes, and so constructor promotion is a natural fit. We also flag the attribute as only being valid on properties and parameters, as it wouldn’t make any logical sense on a class or function.

Now the subject line property can be tagged:

<?php

Class MailMessage
{
    public function __construct(
        private string $to,
        #[PlainText(120)]
        private string $subject,
        private string $from,
        private string $body,
        private array $cc = [],
        private array $bcc = [],
        private ?string $attachmentPath = null,
    ) {}
}

And then at any time we can validate a MailMessage object:

<?php

$attribs = new \ReflectionObject($addr)
    ->getProperty('subject')
    ->getAttributes(PlainText::class);
if (! $attribs[0]->valid($mail->subject)) {
  throw new SomethingIsVeryWrongException();
}

(In practice you’d want something a bit more robust, but let’s keep it simple for now.)

All of that is with native PHP code; no third-party libraries needed. That means the attribute syntax is also available to linters for your IDE to color-code, type check, syntax lint, autocomplete, etc.

And that’s just the tip of the iceberg of what meta-programming is now available natively in PHP.

Named arguments

The third and final part of the PHP 8.0 trifecta is named arguments. Named arguments are what they say on the tin. Rather than calling a function with positional arguments, any function or method may now be called with named parameters, in arbitrary order:

<?php

$found = in_array(needle: 'a', haystack: $arr);

While named parameters can be used in almost any situation to improve readability (especially where the parameters are not immediately obvious from context), one of their most important use cases is … calling constructors.

<?php

$mail = new MailMessage(
    subject: 'Write more blog posts about PHP!',
    body: 'PHP is awesome and we should have more articles about it',
    to: 'Larry Garfield',
    from: 'Robert Douglas',
    attachmentPath: '/path/to/some/file',
);

Note in particular that the arguments are not in their declared order, and that’s fine. We’re also skipping some optional arguments entirely as we don’t need them.

Bringing it all together

Constructor property promotion, attributes, and named arguments are all valuable additions in their own right. The combination of them, however, is where PHP 8.0 really shines and where it’s going to change how we write PHP code for years to come.

As an example, let’s look at real code from my PSR-14 Event Dispatcher implementation, Tukio. It defines its own attributes, like so:

<?php

namespace Crell\Tukio;

use \Attribute;

#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
class Listener implements ListenerAttribute
{
   public function __construct(
       public ?string $id = null,
       public ?string $type = null,
   ) {}
}

#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
class ListenerBefore implements ListenerAttribute
{
   public function __construct(
       public string $before,
       public ?string $id = null,
       public ?string $type = null,
   ) {}
}

#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
class ListenerPriority implements ListenerAttribute
{
   public function __construct(
       public ?int $priority,
       public ?string $id = null,
       public ?string $type = null,
   ) {}
}

(And one or two others I’m skipping for space.)

All of them are trivially defined using constructor promotion. Those attributes can then decorate listeners, either functions or methods, in order to define how they get registered. In most cases to specify a custom ID you can do:

<?php

#[Listener('mine')]
function my_listener(MyEvent $event): void
{
    // ...
}

However, especially when using subscriber classes (a class that contains a bunch of listeners), you can mix and match any of them. And many of them are easier to use as named arguments:

<?php

class AttributedSubscriber
{
   #[Listener('a')]
   public function onA(CollectingEvent $event) : void { ... }

   #[ListenerPriority(priority: 5, id: 'this_listener')]
   public function onB(CollectingEvent $event) : void { ... }

   #[ListenerBefore(id: 'listener_c', before: 'a')]
   public function onC(CollectingEvent $event) : void { ... }

   #[ListenerAfter(after: 'a', id: 'extra_listener')]
   public function onD(CollectingEvent $event) : void { ... }

   #[Listener]
   public function onG(RareEvent $event) : void { ... }
}

The entire API is driven by attributes, using named arguments, which themselves were defined with constructor promotion: Highly readable, compact code with a major functionality boost.

That’s the power of designing functionality to play well together: They end up playing well together.

Try it out today

Of course, PHP 8 does have some changes that may trip up older code. As with any PHP version, you want to test your code before you just update. And testing PHP 8.0 on Platform.sh couldn’t be easier.

First, make a new branch in your repository. Then update your .platform.app.yaml file in that branch and change the type key to:

type: php:8.0

Git commit and deploy. That branch is now running on PHP 8.0. Try it out, run your integration tests, spot check it, and otherwise see if anything needs to be updated.

Once you’ve made whatever changes are needed, merge that branch back to master. Congratulations, you’re now running PHP 8.0 on production.

Our PHP 8.0 containers also have two other new features. For one, they bundle the new Composer 2.0 by default. Composer 2.0 offers considerable performance improvements locating and downloading dependencies, but does require some Composer extensions to be updated to support it. Make sure your project is updated accordingly. We’ve also updated the version of Node.js that ships in the PHP 8.0 container to the latest LTS version, Node 12.

Even if you’re not ready to update to PHP 8.0 today, you should still make a plan for it. Security support for PHP 7.2 expires 30 November, and 7.3 goes into security-only mode for another year. If you’re still on older PHP versions, not only are you missing out on the performance improvements and new functionality, but you may be running insecure PHP versions, too. Make version upgrades a regular part of your maintenance cycle and it won’t really be a burden at all, but a pleasure to look forward to. After each upgrade, you get all the new fun toys to play with.

Not quite the end of the journey

Thank you for taking this journey with us through the new features of PHP 8.0. It’s a major release of a major language, and this has been our longest and most detailed blog series to date.

It’s not quite over, however. PHP 8.1 is already in development, and some of the features that are already in progress … for now let’s just say that, if they actually happen, 8.1 may earn an even bigger party than 8.0. Stay tuned.

And there’s a few smaller features of PHP 8.0 that didn’t quite make it into this series, so we’re turning this blog series into an ebook and fleshing it out with the stuff that didn’t make the cut. Expect that to be released here on Platform.sh before the end of the year.

Happy Deploying, PHP!