DEV Community

loading...

PHP 8: before and after

Brent Roose
Originally published at stitcher.io on ・4 min read

It's only a few months before PHP 8 will be released, and honestly there are so many good features. In this post I want to share the real-life impact that PHP 8 will have on my own code.

# Events subscribers with attributes

I'm going to try not to abuse attributes, but I think configuring event listeners is an example of an annotation I'll be using extensively.

You might know that I've been working on event sourced systems lately, and I can tell you: there's lots of event configuration to do. Take this simple projector, for example:

// Before

class CartsProjector implements Projector
{
    use ProjectsEvents;

    protected array $handlesEvents = [
        CartStartedEvent::class => 'onCartStarted',
        CartItemAddedEvent::class => 'onCartItemAdded',
        CartItemRemovedEvent::class => 'onCartItemRemoved',
        CartExpiredEvent::class => 'onCartExpired',
        CartCheckedOutEvent::class => 'onCartCheckedOut',
        CouponAddedToCartItemEvent::class => 'onCouponAddedToCartItem',
    ];

    public function onCartStarted(CartStartedEvent $event): void
    { /* … */ }

    public function onCartItemAdded(CartItemAddedEvent $event): void
    { /* … */ }

    public function onCartItemRemoved(CartItemRemovedEvent $event): void
    { /* … */ }

    public function onCartCheckedOut(CartCheckedOutEvent $event): void
    { /* … */ }

    public function onCartExpired(CartExpiredEvent $event): void
    { /* … */ }

    public function onCouponAddedToCartItem(CouponAddedToCartItemEvent $event): void
    { /* … */ }
}
Enter fullscreen mode Exit fullscreen mode

PHP 7.4

There are two benefits attributes will give me:

  • Event listener configuration and handlers are put together, I don't have to scroll to the top of the file to know whether a listener is configured correctly.
  • I don't have to bother anymore writing and managing method names as strings: your IDE can't autocomplete them, there's no static analysis on typos and method renaming doesn't work.

Luckily, PHP 8 solves these problems:

class CartsProjector implements Projector
{
    use ProjectsEvents;

    @@SubscribesTo(CartStartedEvent::class)
    public function onCartStarted(CartStartedEvent $event): void
    { /* … */ }

    @@SubscribesTo(CartItemAddedEvent::class)
    public function onCartItemAdded(CartItemAddedEvent $event): void
    { /* … */ }

    @@SubscribesTo(CartItemRemovedEvent::class)
    public function onCartItemRemoved(CartItemRemovedEvent $event): void
    { /* … */ }

    @@SubscribesTo(CartCheckedOutEvent::class)
    public function onCartCheckedOut(CartCheckedOutEvent $event): void
    { /* … */ }

    @@SubscribesTo(CartExpiredEvent::class)
    public function onCartExpired(CartExpiredEvent $event): void
    { /* … */ }

    @@SubscribesTo(CouponAddedToCartItemEvent::class)
    public function onCouponAddedToCartItem(CouponAddedToCartItemEvent $event): void
    { /* … */ }
}
Enter fullscreen mode Exit fullscreen mode

PHP 8

# Static instead of doc blocks

A smaller one, but this one will have a day-by-day impact. I often find myself still needing doc blocks because of two things: static return types en generics. The latter one can't be solved yet, but luckily the first one will in PHP 8!

When I'd write this in PHP 7.4:

/**
 * @return static
 */
public static function new()
{
    return new static();
}
Enter fullscreen mode Exit fullscreen mode

PHP 7.4

I'll now be able to write:

public static function new(): static
{
    return new static();
}
Enter fullscreen mode Exit fullscreen mode

PHP 8

# DTO's, property promotion and named arguments

If you read my blog, you know I wrote quite a bit about the use of PHP's type system combined with data transfer objects. Naturally, I use lots of DTOs in my own code, so you can image how happy I am, being able to rewrite this:

class CustomerData extends DataTransferObject
{
    public string $name;

    public string $email;

    public int $age;

    public static function fromRequest(
        CustomerRequest $request
    ): self {
        return new self([
            'name' => $request->get('name'),
            'email' => $request->get('email'),
            'age' => $request->get('age'),
        ]);
    }
}

$data = CustomerData::fromRequest($customerRequest);
Enter fullscreen mode Exit fullscreen mode

PHP 7.4

As this:

class CustomerData
{
    public function __construct(
        public string $name,
        public string $email,
        public int $age,
    ) {}
}

$data = new CustomerData(...$customerRequest->validated());
Enter fullscreen mode Exit fullscreen mode

PHP 8

Note the use of both constructor property promotion, as well as named arguments. Yes, they can be passed using named arrays and the spread operator!

# Enums and the match expression

Do you sometimes find yourself using an enum with some methods on it, that will give a different result based on the enum value?

/**
 * @method static self PENDING()
 * @method static self PAID()
 */
class InvoiceState extends Enum
{
    private const PENDING = 'pending';
    private const PAID = 'paid';

    public function getColour(): string
    {
        return [
            self::PENDING => 'orange',
            self::PAID => 'green',
        ][$this->value] ?? 'gray';   
    }
}
Enter fullscreen mode Exit fullscreen mode

PHP 7.4

I would argue that for complexer conditions, you're better off using the state pattern, yet there are cases where an enum does suffice. This weird array syntax already is a shorthand for a more verbose conditional:

/**
 * @method static self PENDING()
 * @method static self PAID()
 */
class InvoiceState extends Enum
{
    private const PENDING = 'pending';
    private const PAID = 'paid';

    public function getColour(): string
    {
        if ($this->value === self::PENDING) {
            return 'orange';
        }

        if ($this->value === self::PAID) {
            return 'green'
        }

        return 'gray';
    }
}
Enter fullscreen mode Exit fullscreen mode

PHP 7.4 — alternative

But with PHP 8, we can use the match expression instead!

/**
 * @method static self PENDING()
 * @method static self PAID()
 */
class InvoiceState extends Enum
{
    private const PENDING = 'pending';
    private const PAID = 'paid';

    public function getColour(): string
    {
        return match ($this->value) {
            self::PENDING => 'orange',
            self::PAID => 'green',
            default => 'gray',
        };
}
Enter fullscreen mode Exit fullscreen mode

PHP 8

# Union types instead of doc blocks

When I mentioned the static return type before, I forgot another use case where docblock type hints were required: union types. At least, they were required before, because PHP 8 supports them natively!

/**
 * @param string|int $input
 *
 * @return string 
 */
public function sanitize($input): string;
Enter fullscreen mode Exit fullscreen mode

PHP 7.4

public function sanitize(string|int $input): string;
Enter fullscreen mode Exit fullscreen mode

PHP 8

# Throw expressions

Before PHP 8, you couldn't use throw in an expression, meaning you'd have to do explicit checks like so:

public function (array $input): void
{
    if (! isset($input['bar'])) {
        throw BarIsMissing::new();
    }

    $bar = $input['bar'];

    // …
}
Enter fullscreen mode Exit fullscreen mode

PHP 7.4

In PHP 8, throw has become an expression, meaning you can use it like so:

public function (array $input): void
{
    $bar = $input['bar'] ?? throw BarIsMissing::new();

    // …
}
Enter fullscreen mode Exit fullscreen mode

PHP 8


What's your favourite PHP 8 feature?

Discussion (1)

Collapse
dakujem profile image
Andrej Rypo

Thank you for this useful post!

In php 7.x, one can use self as the return type hint to achieve most of the type safety (at least much better than no type hint)

/**
 * @return static
 */
public static function new(): self {
    return new static();
}
Enter fullscreen mode Exit fullscreen mode