13 заметок с тегом

php

Скорость выполнения call_user_func_array() на разных версиях PHP

Решил сравнить скорость выполнения call_user_func_array() с обычным вызовом функций на разных версиях PHP и последним HHVM. Весь код измерения времени я запускал на 3v4l.org и фиксировал результаты в сводные таблицы. За основу взял функцию array_merge() с массивом MIME-типов и 1 млн раз вызывал её в цикле разными способами.

Код выше сравнивает скорость выполнения функции array_merge() с её вызовом через call_user_func_array(). Как видно, в седьмых версиях PHP нет никакой разницы по времени выполнения, а вот HHVM 3.22.0 остался на уровне PHP 5.6.30, хотя немного быстрее:

+--------------------------------------------------+----------+----------+----------+-----------+ | |PHP 5.6.30|PHP 7.0.24|PHP 7.1.10|HHVM 3.22.0| +--------------------------------------------------+----------+----------+----------+-----------+ |array_merge(...$args) |0.9381 |0.2910 |0.2945 |0.7795 | |call_user_func_array('array_merge', $args) |1.4194 |0.3073 |0.2938 |1.0372 | +--------------------------------------------------+----------+----------+----------+-----------+

В PHP 7.*.* провели хорошую оптимизацию и при вызове функций можно не задумываться об производительности call_user_func_array(). Это касается и вызова самописных функций и методов вызываемых из класса.

Замеры скорости получились следующие:

+--------------------------------------------------+----------+----------+----------+-----------+ | |PHP 5.6.30|PHP 7.0.24|PHP 7.1.10|HHVM 3.22.0| +--------------------------------------------------+----------+----------+----------+-----------+ |fn($args) |1.0934 |0.6369 |0.4660 |1.2059 | |$fn($args) |1.4507 |0.5399 |0.6090 |1.2501 | |$class->fn($args) |1.5088 |0.5622 |0.5282 |1.4568 | |call_user_func_array('fn', [$args]) |1.5022 |0.4198 |0.3750 |1.2159 | |call_user_func_array([new A, '__invoke'], [$args])|1.9369 |0.5629 |0.6538 |1.3701 | |call_user_func_array([new B, 'fn'], [$args]) |2.1243 |0.7088 |0.6625 |1.9792 | +--------------------------------------------------+----------+----------+----------+-----------+

В PHP 5.6.30 и HHVM 3.22.0 видно большую разницу при разных способах вызова функций, а в PHP 7.* как бы вы функцию не вызывали, её время выполнения будет примерно на одном уровне. Проще говоря, в последних версиях PHP вызов функций через call_user_func_array() и её исходных вариантах не имеет разницы.

10 октября   benchmark   call_user_func_array()   php

Обработчики запросов вместо контроллеров

Вашему внимание представляю перевод статьи Goodbye controllers, hello request handlers от Jens Segers, который опубликовал её в своём блоге. Если переводить буквально, то статья в русском переводе должна называться «Прощайте контроллеры, да здравствуют обработчики запросов», но я решил назвать её проще — «Обработчики запросов вместо контроллеров». Вся статья сводится к тому, что все действия контроллеров нужно выносить в отдельные исполняемые классы. Как, зачем и почему читайте ниже.

За последние годы в ландшафте PHP многое изменилось. Мы стали использовать больше паттернов программирования и таких принципов, как DRY и SOLID. Но почему мы всё ещё используем контроллеры?

Если вам доводилось работать над большими приложениями, возможно, вы заметили, что рано или поздно контроллеры становятся толстыми. Даже при внедрении репозиториев или сервисов в контроллеры, со временем количество зависимостей, методов и строк кода неизбежно растёт.

Позвольте представить вам обработчики запросов (request handlers). Концепция очень проста, но малоизвестна среди PHP-разработчиков. Обработчик запроса является контроллером, но ограничен одним действием. Эта концепция очень похожа на шаблон Action-Domain-Responder, предложенная Paul M. Jones, альтернативой шаблону MVC, которая фокусируется на более «чистых» запросах и ответах (request-response) для веб-приложений.

Модель Action-Domain-Responder (ADR), которая является объектно ориентированной надстройкой над MVC. Оперирует понятиями близкими к доменной области приложения.

Хороший способ создания обработчиков запросов — использование вызываемых классов. Вызываемые классы — это классы, которые используют магический метод __invoke() в PHP, превращая их в исполняемые, что позволяет из класса сделать функцию. Вот простой пример вызываемого класса:

<?php

class Greeting
{
    public function __invoke($name)
    {
        echo 'Hello ' . $name;
    }
}

$welcome = new Greeting();

// Hello John Doe
$welcome('John Doe');

На данный момент вы, скорее всего, спрашиваете себя: «Зачем мне это нужно?». Это имеет смысл при ожидании маршрутизатором callback-функций или конкретных зависимостей. Проще говоря, не нужно парсить строку и разбивать её на контроллер и метод. Вы можете сразу указать свою зависимость из http-слоя. Посмотрите наглядный пример, как обработчики запросов уже сейчас можно регистрировать в Laravel или Slim:

<?php

Route::get('/{name}', Greeting::class);

А теперь сравните это с тем, что обычно происходит в ваших проектах:

<?php

Route::get('/{name}', 'SomeController@greeting');

Помимо удобочитаемости, здесь гораздо больше преимуществ. Позвольте мне продолжить и сравнить ключевые особенности обработчиков запросов и контроллеров.

Принцип единственной ответственности

На мой взгляд, контроллеры со многими видами действий нарушают принцип единственной ответственности (SRP) из SOLID. Обработчики запросов, наоборот, решают эту проблему через разделение методов контроллеров на свои собственные независимые классы, что упрощает их обслуживание, рефакторинг и тестирование.

Ниже приведен пример двух обработчиков запросов, которые вынесены из двух методов, вшитых в UserController (редактирование и обновление данных пользователя):

<?php

class EditUserHandler
{
    public function __construct(
        UserRepository $repository,
        Twig $twig
    ) {
        /* ... */
    }

    public function __invoke(Request $request, Response $response)
    {
        /* ... */
    }
}

class UpdateUserHandler
{
    public function __construct(
        UserRepository $repository,
        UpdateUserValidator $validator,
        ImageManager $resizer,
        Filesystem $storage
    ) {
        /* ... */
    }

    public function __invoke(Request $request, Response $response)
    {
        /* ... */
    }
}

Это подводит нас к следующему преимуществу.

Тестируемость

Как давно вы писали модульные тесты для своих контроллеров? Вероятно, вы закончили созданием смешанных зависимостей, которые даже не были связаны с вашим тестом. Поскольку обработчики запросов разделяют различные действия контроллера на отдельные классы, вам нужно только вводить или связывать mock-объекты для конкретного действия.

Недавно Jeffrey Way в своём Твиттере дал совет по тестированию: сделайте свои классы тестов как можно более конкретными, а затем используйте каждый тест для описания важного правила/способности (rule/ability).

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

Рефакторинг

В современных IDE есть мощные варианты рефакторинга. Но если вы привязываете методы контроллеров к маршрутам в конфигурационных файлах (например, Laravel или Slim), то вы сталкнётесь с проблемой замены старых вызовов (строчных) на название объектов. Ничего не поделать. Вот так должно выглядеть:

<?php

Route::get('/{name}', Greeting::class);

И это намного проще, чем такой вариант:

<?php

Route::get('/{name}', 'SomeController@greeting');

Вывод

Обработчики запросов предлагают отличную альтернативу для контроллеров. Действия контроллера разбиваются на отдельные классы обработчика запросов, ответственные за одно действие. Это приводит к созданию кода, который проще в обслуживании, рефакторинге и тестировании.

Должны ли вы пойти и заменить все контроллеры обработчиками запросов? Возможно нет. Для небольших приложений может иметь смысл группировать действия вместе для простоты. Я только недавно открыл для себя обработчики запросов, когда начал работать в Teamleader, и я не чувствую необходимости возвращаться к контроллерам в ближайшее время.

Ссылки по теме
1) Porto (Software Architectural Pattern)
   https://github.com/Mahmoudz/Porto

2) Action-Domain-Responder
   https://github.com/pmjones/adr

3) Request-Driven Development
   http://martinbean.co.uk/blog/2017/01/05/request-driven-development/

4) Classes vs. Namespaces
   http://antonkril.github.io/classes-vs-namespaces
3 октября   ADR   mvc   php   SOLID   SRP   паттерны   перевод

Способы хранения временных данных в PHP

PHP позволяет хранить временные данные в ОЗУ или файлах. Это реализовано с помощью потоков чтения-записи php://memory и php://temp, которые можно открыть через fopen() и управлять данными в файлоподобной обёртке до завершения скрипта. После окончания работы PHP уничтожит все временные данные, если их намеренно не сохранить. Куда записать временные данные:

Способ                 Описание
————————————————————————————————————————————————————————————————————————————————————
php://memory           Записывает и всегда хранит данные в ОЗУ. С помощью fopen()
                       можно работать как с обычным файлом. Сохранить данные можно
                       копированием в другой поток stream_copy_to_stream() или
                       передачей контента stream_get_contents() в другой обработчик.

php://temp             Создаёт файл во временной папке, когда размер данных
                       превышает 2 Мбайт. До этого все записанные данные
                       хранятся в ОЗУ. Временная папка определяется аналогично
                       функции sys_get_temp_dir().

php://temp/maxmemory:0 Обнуляет максимальный размер данных в байтах для хранения
                       в памяти и с первым байтом создаёт файл во временной папке.
                       Можно установить свой лимит стартового хранения данных в
                       ОЗУ путем добавления /maxmemory:NN, где NN — это
                       максимальный размер данных в байтах для хранения в памяти
                       перед использованием временного файла.

tmpfile()              Создаёт временный файл с уникальным именем в режиме
                       чтения и записи r+, а затем возвращает файловый указатель.
                       Ничем не отличается от php://temp/maxmemory:0, кроме
                       способа вызова. URI временного файла можно получить
                       с помощью функции stream_get_meta_data() и сохранить данные
                       через копирование файла copy() по URI.

Все временные файлы автоматически удаляются, а ОЗУ очищается после завершения скрипта. php://memory и php://temp нельзя будет переиспользовать, т. к. после закрытия потоков невозможно сослаться на них. Как правило, сохранность временных данных, по необходимости, достигается путём копирования. Ниже привожу два примера, которые демонстрируют работу со временными данными:

<?php

///////
// 1 //
///////

// Откроем поток php://memory
$temp_data = fopen('php://memory', 'w+');

// Запишем "Hello, world!"
fwrite($temp_data, 'Hello, world!');

// Извлечём метаданные из потока
stream_get_meta_data($temp_data);

// Array
// (
//     [timed_out]    => false
//     [blocked]      => true
//     [eof]          => false
//     [wrapper_type] => PHP
//     [stream_type]  => MEMORY
//     [mode]         => w+b
//     [unread_bytes] => 0
//     [seekable]     => true
//     [uri]          => php://memory
// )

// Сбросим курсор
rewind($temp_data);

// Получим "Hello, world!"
stream_get_contents($temp_data);

// Создадим новый файл data.txt
$fh = fopen('data.txt', 'w+');

// Сбросим курсор ещё раз
rewind($temp_data);

// Сохраним временные данные в новый файл
stream_copy_to_stream($temp_data, $fh);

// Закроем ресурс файла data.txt
fclose($fh);

// Закрывать поток php://memory необязательно, т. к. все данные
// будут автоматически уничтожены по завершению скрипта.
// fclose($temp_data);


///////
// 2 //
///////

// Создадим временный файл
$tmpfile = tmpfile();

// Запишем "Hello, world!"
fwrite($tmpfile, 'Hello, world!');

// Извлечём метаданные из потока
stream_get_meta_data($tmpfile);

// Array
// (
//     [timed_out]    => false
//     [blocked]      => true
//     [eof]          => false
//     [wrapper_type] => plainfile
//     [stream_type]  => STDIO
//     [mode]         => r+b
//     [unread_bytes] => 0
//     [seekable]     => true
//     [uri]          => home\user\temp\phpDC08.tmp
// )

// Получаем URI временного файла
$uri = stream_get_meta_data($tmpfile)['uri'];

// Сохраним "Hello, world!" в файл data.txt
copy($uri, 'data.txt');

// Временный файл будет автоматически удалён по завершению
// скрипта. Закрывать открытый дескриптор необязательно.
// fclose($tmpfile);
2017   php   tmpfile   данные   файл

Уменьшаем итерабельную сложность массива в PHP

Задача. Объедините массив, приведённый ниже, таким образом, чтобы по значениям результирующего массива работал поиск array_search() и in_array(). Массив должен быть проходимым за один цикл. Ключи сохранять не обязательно.
$data = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

На первый взгляд кажется, что можно скормить массив в array_merge() и получить желаемый результат, но array_merge() принимает как минимум два аргумента с массивами, а у нас один. Заметьте, вложенные массивы с данными обёрнуты в ещё один массив. Это усложняет доступ к этим данным. Можно, конечно, обойти весь массив двумя foreach() и извлечь все данные, но такой подход довольно-таки топорный:

<?php

$data = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

$flattened = [];

foreach ($data as $array) {
    foreach ($array as $key => $value) {
        $flattened[] = $value;
    }
}

var_dump($flattened);

// вернёт [1, 2, 3, 4, 5, 6, 7, 8, 9]

Существует более изящный способ объединить все массивы в один — скормить массив с данными в array_merge(), но через call_user_func_array(). Данная функция вызывает другую функцию с массивом параметров, где первым аргументом идёт название функции, а вторым — параметры в виде индексированного массива. Получается, что массив-обёртку съест call_user_func_array(), а остальные пойдут как аргументы в array_merge() и в итоге мы получим склеенный массив:

<?php

$data = [ // <- съест call_user_func_array()
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]; // <- съест call_user_func_array()

call_user_func_array('array_merge', $data);

// вернёт [1, 2, 3, 4, 5, 6, 7, 8, 9]

Начиная с версии 5.6 мы можем распаковать массив-обёртку, если перед передаваемым аргументом с массивом данных при вызове функции array_merge() подставим многоточие. Читайте подробнее про списки аргументов переменной длины. В нашем случае массив будет распакован и вложенные в него массивы объединятся:

<?php

$data = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

array_merge(...$data);

// вернёт [1, 2, 3, 4, 5, 6, 7, 8, 9]

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

<?php

///////
// 1 //
///////
if ( ! function_exists('array_flatten') ) {

    /**
     * Выравнивает массив без сохранения ключей
     *
     * @link https://gist.github.com/intedinmamma/246665
     *
     * @param array $data
     * @param array $flattened
     *
     * @return array
     */
    function array_flatten(array $data, array $flattened = [])
    {
        foreach($data as $value) {
            is_array($value)                               ?
            $flattened = array_flatten($value, $flattened) :
            $flattened[] = $value;
        }

        return $flattened;
    }
}

///////
// 2 //
///////
if ( ! function_exists('array_flatten') ) {

    /**
     * Выравнивает массив с сохранением ключей
     *
     * @link http://brandonwamboldt.github.io/utilphp/#array_flatten
     *
     * @param array $data
     * @param bool  $preserve_keys
     * @param array $flattened
     *
     * @return array
     */
    function array_flatten(
        array $data,
        bool $preserve_keys = true,
        array $flattened = []
    ) {
        array_walk_recursive($data, function ($value, $key) use (&$flattened, $preserve_keys) {
            $preserve_keys && !is_int($key) ?
            $flattened[$key] = $value       :
            $flattened[] = $value;
        });

        return $flattened;
    }
}

///////
// 3 //
///////
if ( ! function_exists('array_flatten') ) {

    /**
     * Выравнивает массив с помощью итератора с сохранением ключей или без
     *
     * @link http://stackoverflow.com/a/12205381
     *
     * @param array $data
     * @param bool  $use_keys
     *
     * @return array
     */
    function array_flatten(array $data, bool $use_keys = false)
    {
        $flattened = new \RecursiveIteratorIterator(
            new \RecursiveArrayIterator($data)
        );

        return iterator_to_array($flattened, $use_keys);
    }
}

///////
// 4 //
///////
if ( ! function_exists('array_flatten') ) {

    /**
     * Выравнивает массив по уровню вложенности
     *
     * @link https://github.com/spatie/array-functions#array_flatten
     *
     * @param array $array
     * @param int   $levels
     *
     * @return array
     */
    function array_flatten(array $array, int $levels = -1)
    {
        if ($levels === 0) {
            return $array;
        }

        $flattened = [];

        if ($levels !== -1) {
            --$levels;
        }

        foreach ($array as $value) {
            $flattened = array_merge(
                $flattened,
                is_array($value) ? array_flatten($value, $levels) : [$value]
            );
        }

        return $flattened;
    }
}


Пример для любой из функций array_flatten():

<?php

$data = [
    [1, 2, 3],
    [4, [5, 6, 7], 8],
    [9, [10, 11], 12],
];

array_flatten($data);

// вернёт [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
2017   php   массив

Файл .htaccess и RewriteRule

В последнее время мне стало интересно изучать низкоуровневые подходы минуя тот функционал, который предлагают нам фреймворки. Таким образом, я открываю для себя новые знания в программировании. Когда-то я проходил все стадии написания «велосипедов», но чтобы быть нормальным проггером, нужно знать как там всё устроено, это нормально. Сегодня рассмотрим перенаправления с помощью файла .htaccess для веб-сервера Apache на небольшом примере.

Чтобы не томиться ожиданием, давайте сразу перейдём к задаче: нужно сделать перенаправление со страницы /pages/about.php на /about, т. е. убрать в адресе папку /pages и расширение .php в названии файла. Ещё нужно позаботиться о том, чтобы в конце URL не было закрывающего слеша. Вот такую файловую структуру содержит наш пример:

┌public_html/
├──pages/
│  ├──about.php
│  ├──contact.php
│  └──subscribe.php
├──index.php
└──.htaccess

В Apache для преобразования URL’ов есть специальный модуль mod_rewrite. Он отвечает за создание ЧПУ — создаёт красивые URL для PHP-скриптов на уровне веб-сервера. Его мы и задействуем для решения нашей задачи. Для начала создадим конфигурационный файл .htaccess в корне сайта со следующим содержимым, а затем я объясню как он отработает:

# Включаем mod_rewrite
RewriteEngine On

# Убираем последний слеш
RewriteRule ^(.*)/$ /$1 [L,R=301]

# Преобразуем /pages/about.php в /about
RewriteRule ^(\w+)$ /pages/$1.php [L,NC]

Командой RewriteEngine On мы включаем модуль mod_rewrite, а с помощью RewriteRule задаём правила преобразования адресов. RewriteRule просто преобразует входной URL по порядку в соответствии с регулярными выражениями. В конце каждого правила можно задать флаги для указания поведения mod_rewrite при обработке URL. Синтаксис прост как три копейки:

RewriteRule новый реальный [флаг1,флаг2,флаг3]

Давайте разберём первое правило из примера: ^(.*)/$ /$1 [L,R=301] — между маркерами ^ и $ мы указали начало строки с любого символа и любой длины (.*), где конец строки должен заканчиваться на слеш /. Всё, что заключено в круглые скобки (.*) образует группу символов, которую можно использовать через макрос $1.

Теперь все URL’ы, которые заканчиваются на слеш, будут переброшены на адрес без последнего слеша, где /$1 — все входные символы без последнего слеша. Флаг [L] останавливает чтение .htaccess, а [R=301] генерирует HTTP-код ответа сервера HTTP/1.1 301 Moved Permanently. После того, как правило отработало и Apache обрезал закрывающий слеш, файл .htaccess выполнится ещё раз и первое правило больше не отработает.

Второе правило: ^(\w+)$ /pages/$1.php [L,NC] — пользователь запросит адрес страницы, который имеет буквенное или цифровое содержимое от одного и больше символов (\w+). Это образует группу символов, которые мы подставим в адрес скрипта /pages/$1.php. По факту выполниться скрипт под другим URL. Флаг [L] останавливает .htaccess, а [NC] отменяет проверку регистра символов.

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

Флаг  Описание
———————————————————————————————————————————————————————————————————————————————
[C]   Chain — объединяет несколько правил в цепочку. Если первое правило
      цепочки не срабатывает, то вся цепочка игнорируется.

[F]   Forbidden — возвращает ошибку 403 Forbidden (запрещено).

[G]   Gone — возвращает ошибку 410 Gone (удалён).

[L]   Last — останавливает процесс преобразования, и текущая ссылка
      считается окончательной.

[N]   Next — запускает процесс преобразования с первого по порядку правила.

[NS]  NoSubreq — разрешает срабатывание правила только для настоящих
      запросов, игнорируя подзапросы.

[NC]  NoCase — отключает проверку регистра символов.

[P]   Proxy — даёт команду Apache выполнить подзапрос к указанной странице
      с использованием программного модуля mod_proxy, при этом пользователь
      ничего не узнает об этом подзапросе. Если модуль mod_proxy отсутствует,
      то произойдет ошибка.

[PT]  PassThrough — останавливает процесс преобразования и передает
      полученную новую ссылку дальше по цепочке.
      
[QSA] Qsappend — добавляет исходные параметры запроса (Query String)
      к замене. Если замена не включает в себя новые параметры запроса,
      то исходные параметры запроса добавляются автоматически. Если же
      включает, то без флага QSA исходные параметры запроса будут утеряны.

[R]   Redirect — останавливает процесс преобразования и возвращает
      результат браузеру клиента как редирект на новую страницу.
      По умолчанию передаётся HTTP-код 302 Moved Temporarily (перемещенно
      временно), но его можно изменить путём присвоения нового статуса
      через знак равенства [R=301]. В этом случае будет передан HTTP-код
      301 Moved Permanently (перемещено навсегда).

[S]   Skip — пропускает следующее правило, если текущее правило сработало.
      Можно указать количество последующих игнорируемых правил [S=2].

Полезные ссылки:

    Остальные флаги в документации Apache
    Как на самом деле работает mod_rewrite
    Описание основных флагов mod_rewrite
    Волшебный файл .htaccess
    Коллекция сниппетов .htaccess

Ctrl + ↓ Ранее