10.3 C
Pakistan
Thursday, February 29, 2024

An explanation of modern PHP features in PHP 8.0 and 8.1

PHP 8 has revolutionized the game since its release in late 2020. I’ll go over all the newest features in this tutorial and provide instances from real-world situations where I might use them.

Early in my professional life, I developed a deep love for the PHP language, and ever since, I have pushed for its adoption whenever I have the opportunity. But I haven’t had to exaggerate anything since the 8.* releases. Instead, thanks to the language, I can now only rely on the facts. Let’s go over some of the most notable features included in the PHP 8.0 release.

Promoter of constructor properties

This feature, which has saved me a ton of keystrokes, has to be one of my most used 8.0 features. Let’s dissect it:

// Before PHP 8.0
class Client
{
    private string $url;
 
    public function __construct(string $url)
    {
        $this->url = $url;
    }
}
// PHP 8.0class Client
{    public function __construct(        private string $url,    ) {}}

Rather than assigning properties to our objects by hand, we can now set them directly as an argument in the constructor. I now use this almost exclusively because it saves time and effort while also containing the properties within the constructor, allowing you to learn a great deal more about your objects immediately without having to scroll.

Union Types

Union Types is another amazing feature that was released. Here, a return type or a type-hinted variable may be one or more types. Static analysis has benefited from this as well, as methods may have conditional returns. Let’s examine an illustration.

// Before PHP 8.0
class PostService
{
    public function all(): mixed
    {
        if (! Auth::check()) {
            return [];
        }
 
        return Post::query()->get();
    }
}
// PHP 8.0
class PostService
{
    public function all(): array|Collection
    {
        if (! Auth::check()) {
            return [];
        }
 
        return Post::query()->get();
    }
}

We know that the all method will either return an array or a collection, so our code is much more predictable and we know how to handle it. Named Arguments: Another feature that I may be overusing these days, but I find that using named arguments allows us to be declarative in our code – no more guessing what that third parameter to that function means in relation to your code base. Let’s look at another example. This new addition allows us to be extremely specific in how static analysis and ourselves understand our code – even just from a cursory glance.

// Before PHP 8.0
class ProcessImage
{
    public static function handle(string $path, int $height, int $width, string $type, int $quality, int $compression): void
    {
        // logic for handling image processing
    }
}
 
ProcessImage::handle('/path/to/image.jpg', 500, 300, 'jpg', 100, 5);
// PHP 8.0
class ProcessImage
{
    public static function handle(string $path, int $height, int $width, string $type, int $quality, int $compression): void
    {
        // logic for handling image processing
    }
}
 
ProcessImage::handle(
    path: '/path/to/image.jpg',
    height: 500,
    width: 300,
    type: 'jpg',
    quality: 100,
    compression: 5,
);

As the example above illustrates, if the height and width were oriented incorrectly, the results would be unexpected. It is fairly simple because the implementation and the class are located next to each other. Using named arguments makes it possible for you and any other users of your code base to understand the order of these arguments for the method, even if it comes from an installed package with subpar documentation. This should still be used with caution though, since parameter names are often changed by library authors and aren’t always regarded as breaking changes.

Sync Expressions

A major improvement, acknowledged by everyone I spoke to, that they all adore. We used to have a large switch statement with numerous cases, and let’s face it—it wasn’t the most pleasant to look at or work with. Let’s examine an illustration.

// Before PHP 8.0
switch (string $method) {
    case 'GET':
        $method = 'GET';
        break;
    case 'POST':
        $method = 'POST';
        break;
    default:
        throw new Exception("$method is not supported yet.");
}
// PHP 8.0
match (string $method) {
    'GET' => $method = 'GET',
    'POST' => $method = 'POST',
    default => throw new Exception(
        message: "$method is not supported yet.",
    ),
};

The match statement is much easier to read and allows for a much more simplified syntax. Although I am unable to speak for any potential performance gains, I am aware that it is far simpler to use in the real world.

Applying::class to entities

It always seemed a bit silly to use something like get_class in the past when you wanted to pass a class string to a method. Because you had either autoloaded the class or created a new instance, the system was already aware of it at that point. Let’s examine an illustration.

/ Before PHP 8.0
$commandBus->dispatch(get_class($event), $payload);
// PHP 8.0
$commandBus->dispatch(
    event: $event::class,
    payload: $payload,
);

In terms of features, it might not be a show-stopper, but I use it and will always reach for it when necessary.

Not a single catching catch block

You might not always need access to the exception that might be thrown when developing an application. But for me, this is rarely the case. However, your mileage may differ. Let’s examine an illustration.

// Before PHP 8.0
try {
    $response = $this->sendRequest();
} catch (RequestException $exception) {
    Log::error('API request failed to send.');
}
// PHP 8.0
try {
    $response = $this->sendRequest();
} catch (RequestException) {
    Log::error('API request failed to send.');
}

Since we’re not using the exception in this instance, we don’t need to catch it. Maybe make sure you catch the exception if we wanted to include a message from the exception. As I previously mentioned, I prefer to use the thrown exception, so I don’t usually use this.

It’s safe to say that PHP 8.0 was the wonderful release that we had all been waiting for. What about PHP 8.1, then? What was the outcome for us? It can’t possibly get any better, can it? You would be incorrect if you were thinking this way, as I was. For this reason.

Enums

Floating constants and useless database tables everywhere in codebases have been saved by lovely enums. One of my favorite features of PHP 8.1 so far is Enums; I can now push my roles into Enums rather than having them in a table that is always changing. Rather than using constants or public static properties of a class I never really wanted to use, I can set HTTP methods to Enums. Let’s examine this.

// Before PHP 8.1
class Method
{
    public const GET = 'GET';
    public const POST = 'POST';
    public const PUT = 'PUT';
    public const PATCH = 'PATCH';
    public const DELETE = 'DELETE';
}
// PHP 8.1
enum Method: string
{
    case GET = 'GET';
    case POST = 'POST';
    case PUT = 'PUT';
    case PATCH = 'PATCH';
    case DELETE = 'DELETE';
}

While the syntactic differences are improved in the example above, what about their actual usage? Let me demonstrate a trait I would usually use in an API integration with a brief example.

// Before PHP 8.1
trait SendsRequests
{
    public function send(string $method, string $uri, array $options = []): Response
    {
        if (! in_array($method, ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'])) {
            throw new InvalidArgumentException(
                message: "Method [$method] is not supported.",
            );
        }
 
        return $this->buildRequest()->send(
            method: $method,
            uri: $uri,
            options: $options,
        );
    }
}
// PHP 8.1
trait SendsRequests
{
    public function send(Method $method, string $uri, array $options = []): Response
    {
        return $this->buildRequest()->send(
            method: $method->value,
            uri: $uri,
            options: $options,
        );
    }
}

From a type perspective, it enables my method to know exactly what is being passed in, and it reduces the possibility that an Exception will be thrown because of an unsupported type. Rather than creating a new constant and reworking every scenario in which we might be looking for support, we can now add a new case to our Enum if we wish to extend support.

Unpacking Arrays

It wasn’t until I used this feature that I was certain I would use it. Previously, in order to obtain what we needed, we would constantly need to duplicate items or combine arrays. All we need to do is unpack the array at this point to maintain the same behavior. I use DTOs a lot in my code, and they all have a toArray method that makes it simple for me to turn a DTO into something Eloquent will take care of. Let’s examine an illustration.

// Before PHP 8.1
final class CreateNewClient implements CreateNewClientContract
{
    public function handle(DataObjectContract $client, int $account): Model|Client
    {
        return Client::query()->create(
            attributes: array_merge(
                $client->toArray(),
                [
                    'account_id' => $account,
                ],
            ),
        );
    }
}

// PHP 8.1
final class CreateNewClient implements CreateNewClientContract
{
    public function handle(DataObjectContract $client, int $account): Model|Client
    {
        return Client::query()->create(
            attributes: [
                ...$client->toArray(),
                'account_id' => $account,
            ],
        );
    }
}

It’s just a minor code alteration, as you can see, but it saves me from having to worry about merging arrays—I can just unpack in place and create the array I require. It is simpler to handle and cleaner. Since it’s such a small operation, I am unable to comment on performance; however, I would be curious to know if anyone has benchmarked this alternative method and found any differences.

New in constructors

Regarding new constructors, what can I say that I haven’t already imagined? Not much, but I’ll still try. A new instance of a class may or may not be passed to a constructor prior to PHP 8.1, depending on a number of factors. It led to the predicament where you were never entirely certain whether you required to pass an instance or not. hoping for the best moment and thinking, “I’ll just pass null and see what happens.” We appreciate PHP 8.1 giving us some protection against hurried decisions and deadlines. Am I correct? Let’s examine an illustration.

// Before PHP 8.1
class BuyerWorkflow
{
    public function __construct(
        private null|WorkflowStepContract $step = null
    ) {
        $this->step = new InitialBuyerStep();
    }
}
// PHP 8.1
class BuyerWorkflow
{
    public function __construct(
        private WorkflowStepContract $step = new InitialBuyerStep(),
    ) {}
}

Code cleanliness is therefore, in my opinion, the main victory here. We can let the class handle passing null and stop worrying about it by using the new in constructor feature. The preceding example is a little oversimplified. Since I haven’t really experienced these problems before, that could be the cause, to be completely honest. But I’m sure a lot of you would have already taken advantage of this new feature, and you can hopefully see its advantages.

Read-only Properties

I’ve found love. Not gonna lie. For me, this was a major paradigm shift. It makes it simple for me to program in immutability without sacrificing visibility. In the past, I had to add getters to the class in order to change properties that I wanted to be private or protected from the public. This felt like superfluous boilerplate. Let’s examine an illustration.

// Before PHP 8.1
class Post
{
    public function __construct() {
        protected string $title,
        protected string $content,
    }
 
    public function getTitle(): string
    {
        return $this->title;
    }
 
    public function getContent(): string
    {
        return $this->content;
    }
}
// PHP 8.1
class Post
{
    public function __construct() {
        public readonly string $title,
        public readonly string $content,
    }
}

Observing that code sample, you can see how this new language feature can add impressive improvements. The advantage of readonly properties is obvious: you can reduce the verbosity of your code and maintain immutability while allowing for more visibility and access.

Naturally, these are only a few of the most notable aspects of the releases; this is by no means an exhaustive list. I have not covered all of the additional features that were added to PHP 8.0 and 8.1. For a more comprehensive overview of everything that has been added, I strongly suggest visiting  Stitcher by Brent Roose

who updates the language updates with diligence.

Since PHP 8.2 is still in development and hasn’t been released yet, I won’t go into detail about it in this post. However, keep an eye out for updates as they will be coming. Not to mention the advancements in PHP 8.3 planning that have already occurred!

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles