PHP 8.0 и 8.1: Объяснение современных возможностей

Источник: «Modern PHP features explained - PHP 8.0 and 8.1»
С момента выхода, в конце 2020 года, PHP 8 изменил правила игры. В этом руководстве я рассмотрю все последние нововведения с реальными примерами того, как я могу их использовать.

Я влюбился в язык PHP с самого начала, и с тех пор я защищаю его как язык при каждом удобном случае. Однако начиная с версии PHP 8.*, мне не нужно ничего преувеличивать. Вместо этого я могу полагаться исключительно на факты. Давайте пройдёмся по нескольким выдающимся функциям появившимся с релиза PHP 8.0.

Объявление свойств в конструкторе

Это одна из часто используемых мной функций PHP 8.0, избавившая от лишних нажатий клавиш. Давайте разберём её:

// До PHP 8.0
class Client
{
private string $url;

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

Теперь мы можем задавать свойства объектов непосредственно в конструкторе, в качестве аргумента, вместо того, чтобы назначать их вручную. Я использую это постоянно, это экономит усилия, но сохраняет свойства содержащиеся в конструкторе — поэтому вы узнаёте гораздо больше об объекте не перелистывая код.

Объединение Типов

Ещё одна фантастическая функция, которая была выпущена — Объединение Типов. Это подсказка с указанием типа переменной или возвращаемого значения может быть одного или нескольких типов. Это полезно при статическом анализе, когда у вас может быть условный возврат значения внутри метода. Давайте рассмотрим пример.

// До 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();
}
}

Это новое дополнение позволяет быть конкретными в том, как статический анализ и мы сами понимаем наш код — даже при беглом взгляде. Метод all вернёт либо массив, либо коллекцию, а это значит, что код гораздо более предсказуем, и мы знаем как с ним обращаться.

Именованные аргументы

Ещё одна функция, которой я, возможно, злоупотребляю. Я считаю, что именованные аргументы делают код более декларативным — не нужно гадать, что означает этот третий параметр этой функции по отношению к вашей кодовой базе. Например.

// До PHP 8.0
class ProcessImage
{
public static function handle(string $path, int $height, int $width, string $type, int $quality, int $compression): void
{
// логика обработки изображения
}
}

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
{
// логика обработки изображения
}
}

ProcessImage::handle(
path: '/path/to/image.jpg',
height: 500,
width: 300,
type: 'jpg',
quality: 100,
compression: 5,
);

Как вы можете видеть в примере, неправильная установка высоты и ширины может привести к неожиданным эффектам. Когда класс и реализация находятся радом друг с другом, это относительно просто. Представьте, что этот метод был из установленного вами пакета у которого не лучшая документация — использование именованных аргументов позволяет вам и всем, кто использует вашу кодовую базу, понять порядок этих аргументов для метода. Тем не менее это всё же следует использовать с осторожностью, поскольку авторы библиотек склонны чаще менять имена параметров и не всегда считают это критическими изменениями.

Выражение Match

Улучшение, которое нравится всем, то о чём я говорил, значительное улучшение. Раньше мы использовали громоздкие конструкции switch с несколькими вариантами case, и давайте будем честными — это было не самое приятное занятие. Давайте посмотрим пример.

// До 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.",
),
};

У оператора 'match' более компактный и удобочитаемый синтаксис. Я не могу говорить о каких-либо улучшениях производительности, которые могли быть появиться, но я знаю, что с ним намного проще работать.

Применение ::class на объектах

Раньше, когда вы хотели передать строку класса методу, приходилось использовать что-то вроде get_class, что всегда казалось бессмысленным. Система уже знает о классе, так как вы уже автоматически его загрузили или создали новый экземпляр класса. Давай посмотрим пример.

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

Возможно, этот не что-то впечатляющее на фоне остальных возможностей, но это определённо то, что я использую и всегда буду использовать при необходимости.

Не захватывающие блоки catch

Иногда при создании приложения вам не нужен доступ к переменной исключения, которая может быть сгенерирована. Хотя у меня это редко встречается. Однако ваш стиль может отличаться. Давайте посмотрим примеры.

// До 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.');
}

Нам не нужно перехватывать исключение и сохранять его в переменной, поскольку в данном случае мы его не используем. Если мы захотим включить сообщение из исключения, то возможно, следует убедиться в том, что мы поймали исключение и сохранили его в переменной. Как я уже сказал, это не то, что я использую, так как обычно я хочу использовать брошенное исключение.

Мы все согласны с тем, что PHP 8.0 был фантастическим релизом, которого все ждали. А что на счёт PHP 8.1? Что он нам дал? Конечно, лучше уже быть не может, правда? Если бы вы думали, как и я, вы бы ошибались. Вот почему.

Перечисления / Enums

Прекрасные Перечисления, спаситель от бессмысленных таблиц баз данных и плавающих констант в кодовых базах мира. Перечисления быстро стали одной из моих любимых фич PHP 8.1 — теперь я могу перемещать роли в перечисления вместо того, чтобы хранить их в таблице, которая никогда не меняется. Я могу указать методы HTTP в Перечислениях вместо констант или статичных публичных свойств класса, которые я никогда не хотел использовать. Давайте посмотрим пример.

// До 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';
}

В приведённом выше примере выделены синтаксические различия, которые были улучшены, но как насчёт фактического использования? Давайте рассмотрим быстрый пример трейта, который я обычно использую при интеграции API.

// До 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,
);
}
}

Это позволяет моему методу точно знать, что передаётся с точки зрения типа, и это даёт меньше шансов, что будет выдано Исключение из-за неподдерживаемого типа. Если мы хотим расширить поддерживаемые HTTP-методы, то добавляем новый case в наше Перечисление Method — вместо добавления новой константы и необходимости рефакторинга всех условий, в которых мы могли бы проверять поддерживаемые HTTP-методы.

Распаковка Массивов

Я не был уверен, что буду использовать распаковку массивов, пока не попробовал её. Раньше нам всегда приходилось реплицировать что-то или объединять массивы, чтобы получить то, что нам нужно. Теперь мы можем проста распаковать массив, и поведение будет таким же. Я часто использую DTO в своём коде, и у всех ни есть метод toArray, который позволяет легко преобразовывать DTO во что-то, что Eloquent будет обрабатывать за меня. Давайте посмотрим пример.

// До 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,
],
);
}
}

Как вы видите, это всего лишь небольшое изменение кода. Но оно означает, что мне не нужно беспокоиться о слиянии массива, и я могу просто распаковать его на месте, чтобы построить нужный массив. Он чище и проще в управлении. Я не могу прокомментировать производительность, поскольку это небольшая операция, но мне было бы интересно услышать от любого, кто сравнивал этот и другой подход, чтобы увидеть есть ли какие-либо различия.

Новое в конструкторах

Что я могу рассказать о новых конструкторах, которые вы ещё не представляете? Не много, но попробую. До PHP 8.1 иногда вы могли не передавать новый экземпляр класса конструктору, но по разным причинам, а иногда передавать. Это создало ситуацию, когда вы не были уверены нужно ли передавать экземпляр класса или нет. В такой момент я просто передаю null и смотрю что произойдёт, надеясь на лучшее. Спасибо PHP 8.1 за то, что предоставил нам защиту от дедлайнов и поспешных решений. Я прав? Давайте посмотрим пример.

// До 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(),
) {}
}

Итак, на мой взгляд, главный выигрыш — чистота кода. Использую новую особенность в конструкторе, мы можем перестать беспокоиться о возможной передаче значения null и просто позволить классу справиться с этим. Приведённый выше пример немного упрощён. Честно говоря, это связано с тем, что раньше у меня не было таких проблем. Тем не менее я знаю, многие из вас сделали бы так и надеюсь, увидят преимущества этой особенности.

Свойства только для чтения / Readonly свойства

Я влюблён, и я не обманываю. Для меня это было огромным изменением правил игры. Readonly свойства позволяют задавать иммутабельность без необходимости уменьшения видимости. Ранее мне приходилось изменять свойства, которые я хотел сделать публичными или приватными. Это означало, что мне приходилось добавлять геттеры в класс, что похоже на добавление шаблонного кода, который на самом деле не нужен. Давайте посмотрим на пример.

// До 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,
}
}

Глядя на этот пример кода, улучшения которые можно добавить благодаря новым возможностям языка — впечатляющие. Ясно видно преимущество, которое даёт readonly свойство — ваш код становиться менее многословным, и вы можете ослабить видимость и доступность свойству сохраняя иммутабельность.

Конечно, это не исчерпывающий список — всего лишь несколько ключевых моментов выделяющих релиз. В PHP 8.0 и 8.1 было добавлено много других вещей, о которых я не упомянул. Если вы хотите более детально ознакомиться со всеми нововведениями, я настоятельно рекомендую ознакомиться с сайтом Stitcher Брента Руса, усердно публикующего обновления PHP связанные с языковыми обновлениями.

Я не буду углубляться в PHP 8.2 в этой статье, так как он ещё не выпущен и я ещё не сформулировал своё мнение о новых возможностях, но следите за обновлениями, поскольку скоро оно появится. Не говоря уже об улучшения запланированных в PHP 8.3!

Какая ваша любимая современная фича PHP? Что бы вы хотели видеть добавленным в новых релизах? Дайте нам знать в Твиттере!

Дополнительные материалы

Предыдущая Статья

Laravel: 20 полезных советов

Следующая Статья

PHP 8.1: Readonly-свойства / свойства только для чтения