- PVSM.RU - https://www.pvsm.ru -

Самые заметные изменения языка php за последние годы

Я начинал работать с php, когда еще не потерял популярность его 4 выпуск, с тех пор произошли огромные изменения. На мой взгляд, последние несколько лет преобразили разработку на нем.  Кстати php продолжает быть серьезно востребованным, например, сейчас “Рексофт” развивает на нем несколько проектов. В одном из них работаю и я.  Но перейдем к делу. Делюсь с вами самыми интересными изменениями в php, прошедшими с 5 до версии 8.1.

Одним из самых распространенных классов в веб-разработке я бы назвал сущность User, предназначенную для работы с пользователями. В парадигме MVC ее можно назвать моделью пользователя, которая хранит данные (свойства) и предоставляет доступ к ряду методов работы с ними, а также бизнес-логику, связанную с областью применения класса. На примере такого класса я и хочу посмотреть сам и показать тебе, читатель, как изменился язык php за последнии годы.

Для удобного изменения кода, и чтобы в дальнейшем проще было отслеживать, что в каком порядке менялось, я создал репозиторий на гитхаб. Ссылка на репозиторий: https://github.com/ZhukMax/php-evo [1]. Отразил в коммитах каждый шаг, который ниже постараюсь подробно описать.

Версия 5.6 (Август 2014)

Изначальную версию кода пишем на 5.6, последней стабильной версии php5. Можно было бы рассмотреть и более ранние версии, но это уже скорее история, чем практическая необходимость. Весь код, написанный на php до 5.5, который я встречал в своей практике, скорее требовал серьезного рефакторинга, чем обновления интерпретатора. А порой и полной замены.

Итак, первым делом создаем класс пользователя и для начала добавляем всего пару свойств: “имя” и “день рождения”, делаем это с обозначением типа. Поля будут приватными, назначаем значение им только в конструкторе, а получить  можно через методы-геттеры. Имя и день рождения обычно у человека не изменяется в течение жизни, или, по крайней мере, такое происходит довольно редко, поэтому пойдем по пути, когда их назначить можно только однажды. К тому же, практика использования геттеров и сеттеров и до 2014 считалась хорошим решением.

<?php

class User
{
    /** @var string */
    private $name;

    /** @var DateTimeImmutable|null */
    private $birthday;

    /**
     * @param string $name
     * @param DateTimeImmutable|null $birthday
     */
    public function __construct($name, $birthday = null)
    {
        $this->name = isset($name) ? $name : '';
        $this->birthday = $birthday;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return DateTimeImmutable|null
     */
    public function getBirthday()
    {
        return $this->birthday;
    }
}

Пользователь где-то живет, поэтому добавим ему еще свойство “город”, но прежде создаем класс City и затем добавим атрибут в основной класс пользователя, который будет хранить объект нового класса.

<?php

class City
{
    /** @var string */
    public $title;

    /**
     * @param string $title
     */
    public function __construct($title)
    {
      $this->title = $title;
    }
}

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

<?php

/** @var City|string */
private $city;

/**
 * @return City|string
 */
public function getCity()
{
    return $this->city;
}

/**
 * @param City|string $city
 */
public function setCity($city)
{
    $this->city = $city;
}

Версия 7.0 (Декабрь 2015)

Scalar types and return types

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

Теперь можно не указывать эти данных в комментариях к методах, потому что они и так обозначены.

* @param string $name - больше не обязательно писать, из кода и так отлично виден тип данных параметра

<?php

/**
 * @param City|string $city
 * @param DateTimeImmutable|null $birthday
 */
public function __construct(string $name, $city, $birthday = null)
{
  	$this->name = isset($name) ? $name : '';
  	$this->city = $city;
  	$this->birthday = $birthday;
}

Тоже сделаем и с методами, возвращающими данные. Перемещаем тип возвращаемых данных из комментария.

* @return string

<?php

public function getName(): string

Все изменения, связанные с типами, можно увидеть в коммите 7.0: Use scalar types and return types [2].

Null coalescing operator

Также в версии 7.0 удобным и приятным стал "синтаксический сахар" проверки переменной на пустоту через двойной вопросительный знак. Теперь мы можем первую строку ниже заменить на вторую:

<?php

$this->name = isset($name) ? $name : 'Неизвестно';
$this->name = $name ?? 'Неизвестно';

Anonymous classes

Также появилась возможность создания и использования анонимных классов. Это позволяет создать объект, реализующий нужный нам интерфейс, без создания отдельного файла класса. В примере ниже я покажу, как это можно использовать в User.

Изначально создадим метод, который принимает на вход только объект с интерфейсом LoggerInterface и без возможности использования какого-то класса по умолчанию.

<?php

public function setLogger(LoggerInterface $logger)
{
  	$this->logger = $logger;
}

Исправляем это и позволяем вызывать метод setLogger без передачи в него параметров. В таком случае пусть свойству $this->logger присваивается объект дефолного класса, имплементирующего нужный нам интерфейс LoggerInterface.

<?php

/**
 * @param LoggerInterface|null $logger
 */
public function setLogger($logger = null)
{
  $this->logger = $logger ?? new class implements LoggerInterface {
      public function log($message)
      {
          echo $message;
      }
  };
}

Все изменения, связанные со скалярными типами, анонимными классами и типом возвращаемого значения, можно посмотреть в коммите 7.0: Null coalescing operator and Anonymous classes [3].

Версия 7.1 (Декабрь 2016)

Обнуляемые типы

Я удалил комментарии с описание типа параметра не у всех методов, так как в некоторых случаях метод может иметь необязательное поле или даже поле, имеющее несколько допустимых типов. Так, например, в метод setLogger свойство $logger может передаваться как реализация интерфейса LoggerInterface, а может не передаваться вообще.

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

 * @return DateTimeImmutable|null

<?php

public function getBirthday(): ?DateTimeImmutable
{
  	return $this->birthday;
}

Метод setLogger теперь не требует блока комментария с информацией о типе, его можно  удалить:

<?php

public function setLogger(?LoggerInterface $logger = null)

Остальные правки с этим нововведением в коммите 7.1: Nullable types [4]

Отсутствие возврата и видимость констант класса

Теперь можно ограничивать видимость констант класса. Лично я являюсь сторонником указания видимости в том числе для публичных констант, это улучшает читаемость кода. В общем-то, как и явное указание, что метод не возвращает данных с помощью типа void.

<?php

public const CUSTOMER = 'customer';
public const ADMIN = 'admin';

public function setCity($city): void

Коммит 7.1: Void functions and Class constant visibility [5].

Далее я пропущу версии 7.2 и 7.3, потому что не вижу в них достаточно интересных изменений синтаксиса языка, которые стоило бы упомянуть.

Версия 7.4 (Ноябрь 2019)

Типизированные свойства

Тут все просто, но крайне удобно, начиная с версии 7.4, можно указывать тип свойства класса, и тем самым ограничивать динамическую типизацию. Функция уже даже напоминает старших братьев по цеху типа Java с их строгостью типа переменной. Теперь совершенно не обязательно указывать тип в комментарии, а можно оставить его (комментарий) для прямого назначения - описания свойства класса.

/** @var string */

<?php

private string $name;

Стрелочные функции

Это сокращенная форма записи функции с одной строкой кода, результат которой возвращается. Мне они напоминают javascript'овые лямбда функции. Интересным их свойством является видимость внутри тела стрелочной функции переменных из родительской.

В коде класса пользователей я не использовал их, и поэтому ниже покажу пример, которого нет в репозитории. Сначала код на php до 7.4.

<?php

$y = 1;
$fn = function ($x) use ($y) {
    return $x + $y;
};

Используя стрелочную функцию перепишем код.

<?php

$y = 1;
$fn = fn($x) => $x + $y;

Присваивающий оператор объединения с null

Тут все довольно просто, теперь можно писать проверку на NULL и присваивание еще короче.

<?php

$this->role = $this->role ?? $role;

Заменим на:

<?php

$this->role ??= $role;

Версия 8.0 (Ноябрь 2020)

Вариация типов

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

* @param City|string $city

<?php

public function setCity(City|string $city): void

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

Ранее мы создали класс города, напомню его:

<?php

class City
{
    /** @var string */
    public $title;

    /**
     * @param string $title
     */
    public function __construct($title)
    {
        $this->title = $title;
    }
}

В версии языка 8.0 мы можем сократить этот код, перенеся объявление свойства класса в конструктор.

<?php

class City
{
    public function __construct(public string $title) {}
}

Важное замечание: именно обозначение видимости является для конструктора ключевым фактором создавать ли передаваемое значение свойством класса или локальной для метода переменной. Пример выше создает свойство класса, которое будет доступно через $this->title. Ниже я покажу, как передать в конструктор данные доступные только внутри него.

<?php

class City
{
    public function __construct(string $title)
    {
        echo $title; // Вывод строки
    }

    public function test()
    {
      	// Вызовет ошибку интерпретатора, свойство не объявлено
        echo $this->title;
    }
}

Оператор Nullsafe

Очень классная штука, позволяющая сильно упрощать написание цепочки обращений к свойствам и методам класса. Пример из коммита 8.0: Union Types and Match Expression [6] объяснит всю суть нововведения лучше любых слов.

<?php

if ($this->logger) {
    $this->logger->log('Someone get my name');
}

Так мы должны были проверять не пустое ли свойство, прежде чем вызывать один из методов его интерфейса. Теперь эта конструкция заменяется одним символом вопроса.

<?php

$this->logger?->log('Someone get my name');

Выражение Match

У нас есть метод ​getAvatar, который занимается тем, что отдает определенную строку в зависимости от роли пользователя. Ранее я реализовал его с помощью switch, но начиная с 8.0 у нас есть более подходящая конструкция для этого - выражение match.

Так метод выглядит сейчас:

<?php

public function getAvatar(): string
{
  switch ($this->role) {
    case self::CUSTOMER:
      return '/images/customer.png';

    case self::ADMIN:
      return '/images/admin.png';

    default:
      return '/images/avatar.png';
  }
}

Это можно заменить на:

<?php

public function getAvatar(): string
{
  return match ($this->role) {
    self::CUSTOMER => '/images/customer.png',
    self::ADMIN => '/images/admin.png',
    default => '/images/avatar.png',
  };
}

Версия 8.1 (Ноябрь 2021)

Совсем недавно произошло важное событие -  8.1 перешла в статус стабильной версии. Я отметил для себя пару довольно интересных новшеств.

Enums (Перечисления)

Во многих языках программирования существуют перечисления. Мы можем их использовать в С++, Golang, Java и других. Наконец, они появились и в php [7]. Здесь они  являются особым видом объектов.

Предлагаю реализовать роли пользователя через перечисление, для этого создадим enum Role с дополнительным методом определения аватарки в зависимости от роли. И будем его использовать в классе пользователя.

<?php

enum Role: string
{
    case CUSTOMER = 'customer';
    case ADMIN = 'admin';

    public function getAvatar(): string
    {
        return match ($this) {
            self::CUSTOMER => '/images/customer.png',
            self::ADMIN => '/images/admin.png'
        };
    }
}

Теперь можно заменить код в методе getAvatar на использование только что созданного перечисления, отдающего путь к аватарке.

<?php

public function getAvatar(): string
{
    return $this->role->getAvatar() ?? '/images/avatar.png';
}

Это очень интересная тема, и тут можно приводить много примеров, но совсем недавно на Хабре Сергей Пантелеев уже публиковал об этом отличную статью [8].

Readonly свойства класса

Использование модификатора только для чтения (readonly) позволяет добавлять свойства класса, которые недоступны для изменения после инициализации.

<?php

public function __construct(
        private string $name,
        private City|string $city,
        private ?DateTimeImmutable $birthday = null
    ) {}

Если изменить свойство $birthday на публичное предназначенное только для чтения, тогда нам больше не понадобится геттер. Свойство будет доступно на установку значения только в конструкторе, другими словами - только во время создания объекта.

<?php

public function __construct(
        private string $name,
        private City|string $city,
        public readonly ?DateTimeImmutable $birthday = null
    ) {}

Атрибуты

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

В документации на php.net [9] описан пример создания и использования собственного атрибута. Но также существуют уже созданные, которые можно использовать как для работы кода, так и для улучшения пользовательского опыта при работе с кодом через редактор. К примеру, у IDE PhpStorm есть поддержка ряда существующих атрибутов или созданных нами, помогающих с рефакторингом, подсветкой, автодополнением кода, поиском использований и др.

Я решил, а точнее IDE подсказал, что к одному из методов в User можно добавить атрибут чистой функции #[Pure]. Это значит, что ее можно безопасно удалять, если она нигде не используется. Также редактор будет следить, чтобы мы не нарушали ее чистоты.

<?php

#[Pure]
public function getAvatar(): string

Правки, относящиеся к версии языка 8.1 закоммитил в 8.1: Readonly properties and Enums, attributes [10].

Пару слов в заключение

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

Автор: Жук Макс

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/php-2/370337

Ссылки в тексте:

[1] https://github.com/ZhukMax/php-evo: https://github.com/ZhukMax/php-evo

[2] 7.0: Use scalar types and return types: https://github.com/ZhukMax/php-evo/commit/ab882059f93c5af88d651add55c8115477f04446

[3] 7.0: Null coalescing operator and Anonymous classes: https://github.com/ZhukMax/php-evo/commit/1f3bd4f8913033243c75affc02b07283b9da5306

[4] 7.1: Nullable types: https://github.com/ZhukMax/php-evo/commit/8d58b48d8c730fece17f4206efc9612792ad81e3

[5] 7.1: Void functions and Class constant visibility: https://github.com/ZhukMax/php-evo/commit/7b446cf7cf03aeef2509323880ff5d4872f6e0ae

[6] 8.0: Union Types and Match Expression: https://github.com/ZhukMax/php-evo/commit/4e9be230e13aad3998a005ddf4cd8c3adf10c530

[7] появились и в php: https://www.php.net/manual/ru/language.enumerations.overview.php

[8] отличную статью: https://habr.com/ru/post/570508/

[9] документации на php.net: https://www.php.net/manual/ru/language.attributes.overview.php

[10] 8.1: Readonly properties and Enums, attributes: https://github.com/ZhukMax/php-evo/commit/b7d64253f3c6a59f2b1ea2fdf2b0d9910289d6b7

[11] Источник: https://habr.com/ru/post/593731/?utm_source=habrahabr&utm_medium=rss&utm_campaign=593731