Five Exciting Features in PHP 8

June 30, 2020
Written by

Five Exciting Features in PHP 8

It's getting close. PHP 8 will release on November 26th 2020, and I, for one, couldn't be more excited. Equally exciting was that June marked the first alpha release of PHP 8.

I've been playing with PHP 8 since Derick Rethans introduced me to the features on a live stream and I'm hooked on the shiny new features we're getting. So here, months in advance are the 5 things I'm most excited about in PHP 8.

Disclaimer: only features actually implemented are in this list, I'll likely make another list later with the things that aren't created yet.

Constructor Property Promotion

Let's be clear, these are in no particular order, but long time readers/viewers will know that I have a certain unique level of disdain for boilerplate. One thing we do as PHP developers is create value object classes that only contain data values. Still, even when we're creating intricate services, they often have multiple dependencies. Whatever we’re building, we end up writing constructor initialisation over and over again.  

https://3v4l.org/37e6N

<?php

declare(strict_types=1);

class Catalogue
{
        private int $id;
        private string $name;
        private int $type;
        private string $description;
        private Cost $cost;
        private bool $visible;

        public function __construct(
            int $id,
            string $name,
            int $type,
            string $description,
            Cost $cost,
            bool $visible
        ) {
            $this->id = $id;
            $this->name = $name;
            $this->type = $type;
            $this->description = $description;
            $this->cost = $cost;
            $this->visible = $visible;
        }
}

Constructor property promotion removes this tedium by allowing us to declare these properties and set them in one place instead of three; in the constructor parameters.

https://3v4l.org/kKtYs

<?php

declare(strict_types=1);

class Catalogue
{
        private function __construct(
            private int $id,
            private string $name,
            private int $type,
            private string $description,
            private Cost $cost,
            private bool $visible,
        ) {}
}

We can preface the type declaration with property visibility if we want a property created and initialised for us with the value passed in, automatically. In this case, the `name` property will be available privately and set to whatever string we pass in as the second argument.

There are some caveats and rules around how and when you can use promotion, which is  explained well in this blog post. I'm so happy to remove more boilerplate from my code, now we just need to find a way to get rid of those annoying getters and setters.

Union Types

Type declarations are the best. Since we got proper scalar type declarations and return types in PHP 7, through to PHP 7.4's property types, PHP's type system gets stricter and stricter. For those of us who love that kind of thing, it's a joy. PHP 8 takes 2 more steps forward and maybe one step back with the introduction of union types.

I've been doing a lot of open source work modernising the Service Manager from the Laminas (previously Zend Framework) project. Part of my work has been adding type declarations across the board, and it's made a remarkable difference. Not only have we removed a ton of unit tests, but the code has become self-documenting too, which is a great win.

One annoyance while doing this is because of past breaking changes and the lengthy history of this project, in some cases, single types are just not possible to add.

https://3v4l.org/7MX9H 

<?php

declare(strict_types=1);

class ServiceManager
{
        /**
         * Specify a factory for a given service name.
         *
         * @param string|callable|Factory\FactoryInterface $factory
         */
        public function setFactory(string $serviceName, $factory): void
        {}

        /**
         * Create a new instance with an already resolved name
         *
         * @return mixed
         */
        private function doCreate(string $resolvedName, ?array $options = null)
        {}
}

Here we can see that the setFactory method can take several types for a $factory and that's fine. In an ideal world, we'd refactor this to create new methods each taking a single type that calls this generic method under the hood, then make this method private, but that introduces a backwards compatibility (BC) break. It's also a very time-consuming amount of work to clear with management when you're under pressure to release the next impressive feature.

Union types can give us a measure of type safety in these exact situations where legacy code has deemed it impossible to receive or return a single type.

https://3v4l.org/O0aXb 

<?php

declare(strict_types=1);

class ServiceManager
{
        /**
         * Specify a factory for a given service name.
         *
         */
        public function setFactory(string $serviceName, string|callable|Factory\FactoryInterface $factory): void
        {}

        /**
         * Create a new instance with an already resolved name
         */
        private function doCreate(string $resolvedName, ?array $options = null): string|bool|object|array
        {}
}

This isn't ideal, we should be cautious when adding union types to new code as they can be a decent indicator of bad API design. Still, for legacy code improvements this is a useful improvement.

Attributes

DocBlock annotations have become an everyday part of PHP development, and they are great. Lots of annotations have become de-facto standards that improve functionality in our IDEs, like @var and @deprecated. Libraries like Doctrine Annotations allow us to parse these docblocks at run-time to add all kinds of swanky new magic functionality based on their values.

https://3v4l.org/6Btml 

<?php

declare(strict_types=1);

/**
 * @deprecated
 */
class Catalogue
{
        /**
         * @cacheable
         * @linksTo $item
         * @var Item[]
         */
        public array $items = [];

        /**
         * @return Item[]
         */
        public function getItems(): array
        {
            return $this->items;
        }
}

But it's a little like the wild-west out there. Annotations starting with an @ are by convention only, and your docblocks are comments and therefore not subject to parse errors or other type safety.

Say <<Hello>> to attributes.

Attributes are a formalised standard way to add metadata to your PHP code, parsed by the PHP engine and available in reflection. They are currently surrounded with << and >> and work very similarly to how annotations do, but are a first-class construct of the language.

https://3v4l.org/i7aRc 

<?php

declare(strict_types=1);

<<deprecated>>
class Catalogue
{
        <<cacheable>>
        <<linksTo(Item::class)>>
        <<type('Item[]')>>
        public array $items = [];

        <<returns('Item[]')>>
        public function getItems(): array
        {
                return $this->items;
        }
}

It's difficult to say just how these will change the landscape of PHP in the coming years. Without using these in anger in real projects, it's challenging to comprehend where this will take us. As a community, we'll still need to agree on standards and conventions for using attributes just like we did with docblocks, and tools like IDEs and static analysis libraries will need to start supporting them. At least the caching and parsing of metadata can now be handled by the PHP engine. Let's wait and see.

NOTE: As of 1st July 2020 the attribute format has changed from `<<>>` to `@@` thanks to this RFC

String Functions

If you haven't implemented your own stringContains function you're either very new at PHP, using a framework, or a liar. I've also written my own stringStartsWith and stringEndsWith functions more than once over the years.

https://3v4l.org/QWFZ3 

<?php

declare(strict_types=1);

function stringContains(string $needle, string $haystack): bool
{
return strpos($haystack, $needle) !== false;
}
assert(stringContains('Ahoy', 'Ahoy World!') === true);
assert(stringContains('Hello', 'Ahoy World!') === false);

function stringStartsWith(string $needle, string $haystack): bool
{
        return strpos($haystack, $needle) === 0;
}
assert(stringStartsWith('Ahoy', 'Ahoy World!') === true);
assert(stringStartsWith('Hello', 'AhoyWorld') === false);

function stringEndsWith(string $needle, string $haystack): bool
{
        return strpos($haystack, $needle) === strlen($haystack) - strlen($needle);
}
assert(stringEndsWith('World!', 'Ahoy World!') === true);
assert(stringEndsWith('Ahoy', 'Ahoy World!') === false);

PHP 8 brings us these functions as part of the language. Hardly exciting, but a solid upgrade nevertheless.

https://3v4l.org/cgeR4

<?php

declare(strict_types=1);

assert(str_contains('Ahoy World!', 'Ahoy') === true);
assert(str_contains('Ahoy World!', 'Hello') === false);

assert(str_starts_with('Ahoy World!', 'Ahoy') === true);
assert(str_starts_with('AhoyWorld', 'Hello') === false);

assert(str_ends_with('Ahoy World!', 'World!') === true);
assert(str_ends_with('Ahoy World!', 'Ahoy') === false);

One annoying thing for me at least is that the argument order is $haystack then $needle to stay in line with strpos. It's a small thing, but I guess we should rejoice in this being one of those cases where consistency is better than logic.

Stringable Interface

Another relatively minor but useful addition, the Stringable interface allows you to type on something that is castable to a string via the __toString magic method. Previously it wasn't possible to declare a type that was a string or Stringable, you'd have to do some shenanigans with is_callable or reflection.

https://3v4l.org/RRKkp 

<?php

declare(strict_types=1);

class CanBeStrung
{
        public function __toString(): string
        {
            return 'I\'m highly strung';
        }
}

function isItStringy($possibleStringType): bool
{
        if (is_string($possibleStringType)) {
            return true;
        }

        if(is_callable([$possibleStringType, '__toString'])) {
            return true;
        }

        return false;
}

assert(isItStringy('a string') === true);
assert(isItStringy(new CanBeStrung()) === true);
assert(isItStringy(42) === false);
assert(isItStringy(new stdClass()) === false);

PHP 8's Stringable interface allows you to type hint on something that implements __toString and is applied automatically to any class that implements that method.

https://3v4l.org/DlBh4 

<?php

declare(strict_types=1);

class CanBeStrung
{
        public function __toString(): string
        {
            return 'I\'m highly strung';
        }
}

function isItStringy(string|Stringable $possibleStringType): bool
{
        return true;
}

assert(isItStringy('a string') === true);
assert(isItStringy(new CanBeStrung()) === true);

try {
        assert(isItStringy(42) === false);
} catch (TypeError $e) {
        echo '42 isnt a string' . PHP_EOL;
}

try {
        assert(isItStringy(new stdClass()) === false);
} catch (TypeError $e) {
        echo 'stdClass cant be strung' . PHP_EOL;
}

Bonus Round

Did you notice anything slightly different in the first PHP 8 constructor property promotion example? Look carefully on line 13, you'll see we've left in the trailing comma for the last parameter in the list. This isn't a typo, trailing parameters are now allowed which brings parameter lists in line with arrays in this regard. Hoorah!

What features are you most excited about for PHP 8? I haven't mentioned the JIT compiler just yet because I'm unsure how much gain it will give in real-world codebases. Let me know what you think on Twitter or in the comments below. We'll be sure to re-address PHP 8 in the future as the release date gets closer.

I can't wait to see what you build.

  • Email: ghockin@twilio.com
  • Twitter: @GeeH