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



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

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

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

Версия 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.

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.

Версия 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

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

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

<?php

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

public function setCity($city): void

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

Далее я пропущу версии 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 объяснит всю суть нововведения лучше любых слов.

<?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. Здесь они  являются особым видом объектов.

Предлагаю реализовать роли пользователя через перечисление, для этого создадим 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';
}

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

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 описан пример создания и использования собственного атрибута. Но также существуют уже созданные, которые можно использовать как для работы кода, так и для улучшения пользовательского опыта при работе с кодом через редактор. К примеру, у IDE PhpStorm есть поддержка ряда существующих атрибутов или созданных нами, помогающих с рефакторингом, подсветкой, автодополнением кода, поиском использований и др.

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

<?php

#[Pure]
public function getAvatar(): string

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

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

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




Комментарии (15):

  1. joffer
    /#23798357 / +11

    отличная подача материала, особенно для не самых опытных разработчиков на слоне (вроде меня)

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

    Ну и, получается, стандартные проверки разделились - если в коде работаем с инстансами классов - использую ??, если более атомарные данные - ?:

  2. FanatPHP
    /#23799221 / +14

    Большое спасибо за очередную качественную статью о современном РНР.

    Какая-то, блин, парадоксальная ситуация - РНР всё так же используется для большинства веб-сайтов, практически весь екоммерс держится на нём - но в публичном поле, и в частности на Хабре, его как будто и нет. Это очень обидно. И при этом интернет всё так же наводнён беспомощными "руководствами" от новичков. Так что тем более важны вот такие статьи от опытных разработчиков.

  3. dimsog
    /#23799253 / +4

    Статья действительно интересная, одно удовольствие ее почитать и вспомнить, что и как было, тем более, когда уже несколько лет пишешь только на PHP 7, а последний год на PHP 7.4, все версии сливаются как бы в одну и удивляешься, как ты раньше без этого вообще жил.

    Но вот этот момент меня смутил:

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

    Да, понятно, что пример синтетический, но очень просто себе выстрелить в ногу таким примером, теперь всегда придется проверять на объект или в сеттере заворачивать строчку в объект или объект приводить к строке. Вообще, еще в PHP 5 появилась такая запись:

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

    Функционал с City|string $city вообще непонятно к чему придумали. Как бы поигрались немного в строгую типизацию и хватит на этом? Если геттер может возвращать объекты разного типа, это здорово может аукнуться на продакшне, когда мы будем ждать объект, а получим строку. Признаюсь, ни разу еще его не использовал и в чужом коде ни разу его не видел. Вот это действительно странный дизайн PHP.

    Я это вижу как: мы всю дорогу, начиная с 5й версии шли в сторону строгой типизации, а с приходом PHP 8.0, ой мы свернули не туда, давайте дадим послабления :)

    • vsh797
      /#23799879 / +2

      При виде City | string сразу вспоминается symfony form component. Где какая-нибудь опция инпута может быть и строкой, и массивом, и объектом. Да и в serializer component без mixed вроде как не обойдешься.

    • zm_llill
      /#23799901 / +1

      Спасибо за отличный, подробный комментарий.

    • marenkov
      /#23807457 / +1

      Пример из статьи действительно не очень. Но можно сделать как-то так:

      public function setCity(City|string $city)
      {
          $this->city = $city instanceof City ? city : new City($city);
      }

  4. ArchDemon
    /#23800129 / +5

    Атрибуты в 8 версии появились.

    Как-то обошли вниманием именованные параметры.

    • zm_llill
      /#23800525

      Как то они выпали из моего поля зрения, хотя это тоже интересное нововведение.

  5. impwx
    /#23800833 / +1

    php продолжает быть серьезно востребованным, например, сейчас “Рексофт” развивает на нем несколько проектов

    Масштаб впечатляет...

  6. testovich-tovich
    /#23803179

    Спасибо за статью! Осталось дождаться, когда concurrency завезут)

  7. Sunchea
    /#23805471 / +1

    Для многих собеседований вплоть до мидла, теперь есть исчерпывающий план разговора и ответов, на тему отличий PHP версии X от Y. ????

    Минимум минут на 30, а то и на час ????????