PHP 8: before and after
Brent Roose
Posted on October 23, 2020
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
{ /* … */ }
}
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
{ /* … */ }
}
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();
}
PHP 7.4
I'll now be able to write:
public static function new(): static
{
return new static();
}
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);
PHP 7.4
As this:
class CustomerData
{
public function __construct(
public string $name,
public string $email,
public int $age,
) {}
}
$data = new CustomerData(...$customerRequest->validated());
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';
}
}
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';
}
}
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',
};
}
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;
PHP 7.4
public function sanitize(string|int $input): string;
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'];
// …
}
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();
// …
}
PHP 8
What's your favourite PHP 8 feature?
Posted on October 23, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.