eFusion 13 марта 2020

PHP: текущее положение языка и ожидаемые изменения

В этой статье речь пойдет о светлой стороне развития PHP – теперь вы можете написать современный и чистый код без хаоса, который был 10 лет назад.
PHP: текущее положение языка и ожидаемые изменения

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

Однако многое из этого уже в прошлом. Это достойный язык, на котором вы можете писать современные, управляемые и надежные приложения.

Как все это было

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

В конце эры 5.*, разработчики пытались сохранить последовательный годовой цикл выпуска апдейтов, и успешно справлялись с этим в течение последних четырех лет. Каждый новый релиз активно поддерживался два года, и получал еще один год “security fixes only”.

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

Фреймворки и экосистема

Существует два основных фреймворка для веба и несколько более мелких: Symfony (на нем, например, работает сайт Библиотеки программиста) и Laravel. Конечно, есть и Laminas, Yii, Cake, Code Igniter и т. д. – но если вы хотите знать, как выглядит современная разработка на PHP, то берите один из первых двух.

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

Данные фреймворки предназначены для разработки. Если задача только в чистом управлении контентом, выбирайте WordPress, CraftCMS и Statamic т. к. они постоянно совершенствуются.

Один из способов измерить текущее состояние экосистемы PHP – это посмотреть на основной репозиторий для PHP. Около 25 млн загрузок в день. А вот график с количеством пакетов и версий с течением времени:

PHP: текущее положение языка и ожидаемые изменения

Еще больше PHP-статистики вы найдете на Packagist.

Помимо фреймворков и CMS, в последние годы наблюдается рост асинхронных фреймворков (Swool, Amp и ReactPHP). Этот софт написан на PHP или других языках, позволяющих юзерам запускать асинхронный код.

Производительность

Во времена пятой версии производительность PHP была в лучшем случае средняя. Начиная с версии 7.0, большая часть ядра была переписана, что привело к увеличению производительности в 2-3 раза. Каждый новый релиз оказывает положительное влияние на производительность, и это можно наблюдать на бенчмарках, например, здесь.

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

Когда появится PHP 8, в список ускорялок добавится JIT-компилятор, который также позволит PHP войти в новые области помимо веб-разработки.

Система типов

С тех пор как PHP начали использовать для крупных проектов, стали очевидны недостатки типизации, возникла необходимость в более сильной поддержке типов. Сегодня PHP по-прежнему позволяет писать полностью динамический и слабо типизированный код, но имеет более качественную систему типов. В сочетании с инструментами анализа, такими как Psalm, Phan и PHPStan, можно написать безопасный и строго типизированный код.

Возьмем, к примеру, этот фрагмент кода PHP, использующий современную систему типов:

        <?php

declare(strict_types=1);

final class Foo
{
    public int $intProperty = 2;

    public ?string $nullableString = null;

    private Bar $bar;

    public function __construct(Bar $bar) {
        $this->bar = $bar;
    }
    
    public function withInt(int $value): self
    {
        $clone = clone $this;
    
        $clone->intProperty = $value;

        return $clone;
    }
    
    public function unionTypes(int|float $input): void
    {
        // Union types will be added in PHP 8
    }
}
    

В системе типов PHP все еще отсутствует одна важная фича: generics. Есть надежда, что они в скорости будут добавлены. В случае с типизированными массивами вам придется полагаться на docblocks, чтобы получить надлежащую поддержку IDE:

        /** @var int[] */
public array $arrayOfInts = [];
    

Синтаксис

Эра 7.* сделала много хороших вещей для превращения PHP в более зрелый язык. Ниже представлены некоторые примеры.

Разложение массива:

        [$a, $b] = $array;
    

Null-коалесцентный оператор:

        $value = $object->property ?? 'fallback if null';

$value = $array['foo'] ?? "fallback if key doesn't exists"; 
    

Null-коалесцентный оператор присваивания:

        public function get(string $input): string 
{
    return $this->cache[$input] ??= $this->sanitize($input);
}
    

Распространение массива:

        $a = [/* … */];
$b = [/* … */];

$mergedArray = [...$a, ...$b];
    

Вариативные функции:

        public function get(Foo ...$foos): void
{
    foreach($foos as $foo) {
        // …
    }
}
    

Распаковка аргумента:

        $this->get(...$arrayOfFoo);
    

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

        public int $intProperty;
    

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

        $ids = array_map(fn(Post $post): int => $post->id, $posts);
    

Генераторы:

        function make(array $input): Generator
{
    foreach ($input as $item) {
        yield $this->doSomethingWith($item);
    }
}
    

Мы рассмотрели текущие дела PHP, а теперь перейдем к тому, что нас ждет с выходом восьмерки.

Объединенные типы

Учитывая динамически типизированную природу PHP, есть много случаев, когда бывают полезны объединенные типы – наборы из двух или более типов.

Обратите внимание, что void никогда не может быть частью объединенных типов, так как он указывает на полное отсутствие возвращаемого значения. Кроме того, обнуляемые (nullable) типы могут быть записаны с помощью | null или с помощью такой следующей нотации:

        public function foo(Foo|Bar $input): int|float;
    

JIT

JIT – Just In Time-компилятор обещает значительное улучшение производительности, хотя и не всегда в контексте веб-запросов. На данной теме мы предлагаем остановиться подробнее.

JIT реализован как часть расширения Opcache и нужен для компиляции кода в инструкции процессора в рантайме. Больше не придется интерпретировать код с помощью Zend VM – инструкции будут выполняться непосредственно на уровне процессора. Чтобы все лучше понять, нужно подумать о том, как PHP работает внутри.

Как выполняется PHP-код?

Мы все знаем, что PHP – интерпретируемый язык. Всякий раз, когда он выполняется, нужно пройти через интерпретатор: PHP-FPM или CLI. Их работа очевидна: получить код, интерпретировать, вернуть результат. Детальнее:

  1. PHP-код считывается и преобразуется в набор ключевых слов, известных как токены. Этот процесс позволяет интерпретатору понять, какой фрагмент кода написан в какой части программы. Этот шаг называется токенизацией.
  2. Интерпретатор анализирует коллекцию токенов и пытается извлечь из них смысл. В результате синтаксического анализа генерируется абстрактное синтаксическое дерево (AST), представляющее набор узлов, указывающих, какие операции должны быть выполнены.
  3. Преобразование AST во что-то выполняемое, требует промежуточного представления (IR), которое в PHP называется Opcode, а процесс преобразования – компиляцией.
  4. Zend VM получает список Opcod-ов и выполняет их.
PHP: текущее положение языка и ожидаемые изменения

Здесь есть узкое место: какой смысл токенировать и парсить код каждый раз, если он так часто не изменяется? Нас заботит только Opcode. И тут появляется расширение Opcache.

Расширение Opcache

Данное расширение поставляется вместе с PHP и добавляет в память общий уровень кэша для Opcod-ов. Его задача – с участием preloading взять сгенерированные Opcod-ы из AST и закэшить, чтобы дальнейшее выполнение могло пройти без токенизации и синтаксического анализа.

PHP: текущее положение языка и ожидаемые изменения

Что же делает JIT таким эффективным?

Zend VM – это программа, написанная на языке Си, которая действует как прослойка между Opcode-ми и CPU. JIT генерирует скомпилированный код в рантайме, чтобы PHP мог пропустить передачу в Zend.

В PHP JIT-реализация использует либу DynASM (Dynamic Assembler), которая преобразует Opcod-ы в специфичный для архитектуры машинный код. А еще, PHP слабо типизирован, и он не знает, какой тип имеет переменная, пока Zend VM не выполнит код.

Как ведет себя JIT?

Мы знаем, что выяснить типы заранее мы не можем и что компиляция в рантайме ресурсозатратна. Чтобы уравновесить ситуацию, JIT пытается скомпилировать только несколько Opcod-ов, которые, по его мнению, могут окупиться (опираясь на ваш конфиг).

Когда определенный код операции компилится, JIT делегирует ему выполнение вместо Zend VM:

PHP: текущее положение языка и ожидаемые изменения

Таким образом, в расширении Opcache есть пара инструкций, определяющих, следует ли компилировать определенный код операции или нет. Если да, то компилятор преобразует код операции в машинный код с помощью DynASM и выполняет его.

Интересно то, что поскольку в текущей реализации существует ограничение в мегабайтах (это настраивается), для скомпилированного кода выполнение должно легко переключаться между JIT и интерпретируемым кодом.

Будет ли прирост производительности?

Надеемся, что стало понятнее, почему большинство PHP-приложений не получат больших преимуществ производительности от использования компилятора Just In Time.

Все потому, что JIT оптимизирует операции, связанные с CPU, а большинство PHP-приложений в настоящее время больше связаны с I/O. Не имеет значения, компилируются ли операции обработки, если в процессе работы придется обращаться к диску или сети.

Если вы используете JIT, например, в обработке изображений или машинном обучении, и там нет заточенности под I/O – JIT выиграет у стандартного компилятора.

::class для объектов

Небольшая, но полезная новая функция: теперь можно использовать ::class для объектов, вместо использования get_class(). Он работает так же, как get_class():

        $foo = new Foo();

var_dump($foo::class);
    

Статический возвращаемый тип

Статический возвращаемый тип будет полезен многим разработчикам, особенно при наличии динамической типизации:

        class Foo
{
    public function test(): static
    {
        return new static();
    }
}
    

Weak maps (Слабые карты)

Все начиналось с weakrefs RFC, которая была добавлена в PHP 7.4, но реализация WeakMap добавлена только лишь в PHP 8. Слабые карты содержат ссылки на объекты, которые не препятствуют сбору мусора этих объектов.

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

Большая плюшка слабых карт – возможность использования более ресурсо-дружественных способов работы с этими объектами. Вот как выглядят слабые карты:

        class Foo 
{
    private WeakMap $cache;
 
    public function getSomethingWithCaching(object $obj): object
    {
        return $this->cache[$obj]
           ??= $this->computeSomethingExpensive($obj);
    }
}
    

Stringable интерфейс

Stringtable может использоваться для ввода подсказок на все, что является строкой или реализует __toString(). Кроме того, всякий раз, когда класс реализует __toString(), автоматически реализуется и интерфейс, что исключает необходимость реализовывать его вручную.

        class Foo
{
    public function __toString(): string
    {
        return 'foo';
    }
}

function bar(Stringable $stringable) { /* … */ }

bar(new Foo());
bar('abc');
    

Создание DateTime-объекта из интерфейса

Вы можете создать объект DateTime из объекта DateTimeImmutable, используя DateTime::create From Immutable($immutableDateTime. Путем добавления типа DateTime::createFromInterface() и DatetimeImmutable::createFromInterface() есть обобщенный способ для преобразования DateTime и объектов DateTimeImmutable друг в друга.

        DateTime::createFromInterface(DateTimeInterface $other);

DateTimeImmutable::createFromInterface(DateTimeInterface $other);
    

Функция fdiv

Функция fdiv делает что-то похожее на fmod и intdiv, что позволяет делить на 0. Вместо ошибок вы получите INF, –INF или NAN, в зависимости от ситуации.

Критические изменения

Как уже упоминалось ранее, это крупное обновление, а значит, будут серьезные изменения. Чтобы узнать полный список критических изменений в UPGRADING-файлике.

Многие из этих изменений были отклонены в предыдущих 7.* версиях, однако, если вы регулярно обновлялись, переход на PHP 8 не должен вызвать проблем.

Приоритет конкатенации

Данное изменение устарело в PHP 7.4, но оно снова в строю. Если бы вы написали что-нибудь такое:

        echo "sum: " . $a + $b;
    

Ранее PHP распознал бы это так:

        echo ("sum: " . $a) + $b;
    

А теперь это интерпретируется следующим образом:

        echo "sum: " . ($a + $b);
    

Последовательные ошибки

Пользовательские функции в PHP уже выбрасывают TypeErrors, но внутренние функции этого не делали, они возвращали предупреждения или null. Начиная с PHP 8 поведение внутренних функций стало иным.

Отчет об ошибках по умолчанию

Теперь это E_ALL вместо E_NOTICE и E_DEPRECATED. Это означает, что может проявиться много ошибок, которые ранее тихонько себе сидели и игнорировались, хотя, уже существовали до PHP 8.

Оператор @ больше не промолчит

Это изменение может выявить ошибки, которые были скрыты до PHP 8. Обязательно установите display_errors=off на ваших продакшен-серверах.

Изменение сигнатуры методов

Изменены три сигнатуры методов классов:

        ReflectionClass::newInstance($args);
ReflectionFunction::invoke($args);
ReflectionMethod::invoke($object, $args);
    

Теперь стало так:

        ReflectionClass::newInstance(...$args);
ReflectionFunction::invoke(...$args);
ReflectionMethod::invoke($object, ...$args);
    

В руководстве по обновлению сказано, что если вы расширяете эти классы и хотите использовать как PHP 7, так и PHP 8, то допускаются следующие сигнатуры:

        ReflectionClass::newInstance($arg = null, ...$args);
ReflectionFunction::invoke($arg = null, ...$args);
ReflectionMethod::invoke($object, $arg = null, ...$args);
    

Заключение

Мы рассмотрели несколько изменений PHP примерно за последние десять лет. Все идет к тому, что восьмая версия станет огромным шагом вперед. Библиотека программиста надеется, что материал был полезным и вы так же, как и мы, ожидаете появления этой версии. Напоследок добавим ссылки на несколько полезных статей по PHP 7:

Источники

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ