Как стать автором
Обновить

Комментарии 26

Попробуйте finally.
defer в Go — часть языка,

по моему, попытки внедрять в РНР идеи из других языков внесут больше избыточности, чем пользы от подобных действий.

хорошая архитектура приложения принесет больше профита,

если Ваш метод/функция использует несколько ресурсов (открывает файлы, соединения етс.) — очевидно стоит задуматся разделить на несколько методов/функций,

в случае: один метод/функция делают несколько операций — вносят неоднозначность в задачу метода/функции, а это влечет сложность понимания общей архитектуры и отладки.

$readHandle = fopen($sourceName, «r»);


и почему не проверяется существование файла? file_exists() может избавить от проблем: файла нет, права овнер/группы на файл не разрешают его читать и т.п.
Между проверкой на file_exists и fopen может вклиниться другой процесс, который файл удалит и всё равно будет ошибка. Но проверить всё равно можно, если только это не супер критичный к производительности кусок, потому что можно тогда использовать свой обработчик ошибок вместо того, чтобы ошибку обрабатывал сам PHP. Впрочем, его не стоило бы писать на PHP, если лишний вызов stat() жалко.
Неудобно, что defer можно применить только внутри метода класса и с использованием обертки. Я бы предложил какую то подобную структуру. Работает автомагически, не требует особых оберток

final class Defer
{
    private $actions = [];

    public static function create(): self
    {
        return new self();
    }

    public function attach(callable $action): self
    {
        $this->actions[] = $action;

        return $this;
    }

    public function __destruct()
    {
        $this->release();
    }

    private function release(): void
    {
        foreach ($this->actions as $action) {
            $action();
        }
    }
}


function testDefer()
{
    $defer = Defer::create()->attach(
        function () {
            echo 'Deferred start'.PHP_EOL;
        }
    );


    echo 'Sync action'.PHP_EOL;
    $defer->attach(
        function () {
            echo 'async action'.PHP_EOL;
        }
    );

    if (true) {
        // Не стал изголяться с fopen и проверками, поэтому if true
        throw new \Exception('Exception!');
    }
}

try {
    testDefer();
} catch (\Exception $exception) {
    echo 'Exception caught: '.$exception->getMessage().PHP_EOL;
}


Sync action
Deferred start
async action
Exception caught: Exception!
Ваш вариант интересный. Но есть несколько моментов:
1) Я бы не стал полагаться на, что деструктор будет вызван когда нужно. Например, я могу передать $defer куда-нибудь еще, или сохранить в какой-то глобальной переменной, и тогда логика работы функций-обработчиков может нарушиться. Хотя для локального использования так даже проще
2) Обработчики нужно выполнять в порядке, обратном их добавлению
3) Нужно ловить исключения при вызове обработчика

В качестве замены трейту можно использовать что-то такое:
function deferred(callable $callback) {
    $context = new DeferredContext();
    try {
        $callback($context);
    } finally {
        $context->executeDeferredActions();
    }
}


И использовать:
deferred(function(DeferredContext $context){
   // Some actions 
});
2 и 3 согласен, просто накидал идею.

Насчет того, полагаться или нет — тут и контекст можно использовать как попало, например передать внешний контекст во внутреннюю функцию. Выше уже сказали — отличие в том, что в go это инструмент языка и его невозможно использовать некорректно, а программную реализацию всегда можно исковеркать
Да, всегда найдется способ использовать неправильно. Но использование замыкания в deferred() гарантирует, что обработчики в любом случае выполнятся после завершения замыкания.
Передать контекст во внутреннюю функцию иногда может быть полезно. Например, если в рамках какого-то процесса нужно создать несколько временных файлов, которые будут использоваться в разных местах процесса, а удалить все файлы нужно в конце процесса. В этом смысле Defer мощнее, чем в go.
1. Для fopen и многих других ресурсов не требуется явное закрытие, потому что эти ресурсы сами закрываются, как только выходят из зоны видимости (поведение аналогично деструктору).
2. Для реализации defer можно сделать ещё более простой синтаксис:

class autoClose {
    private $func;
    private $args;
    public function __construct($func, $args) { $this->func = $func; $this->args = $args; }
    public function __destruct() { call_user_func_array($this->func, $this->args); }
}

function defer(&$arr, $func, ...$args) {
    if ($arr === null) $arr = [];
    array_unshift($arr, new autoClose($func, $args));
}


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

function main() {
    $str = 'hello habr';
    defer($_, 'var_dump', $str);
}

main();


Практическая ценность небольшая, потому что нужно такое редко, на самом деле, из-за пункта 1. Поэтому для всяких временных файлов лучше просто сделать отдельный класс с удалением временного файла в деструкторе. Деструкторы гарантированно вызываются при сразу при потере последней ссылки на соответствующий объект. Можно на этом сделать таймеры, например как тут: github.com/YuriyNasretdinov/gitphp/blob/bd7c529f52c9fad6b083508e1de816be17dae0c1/include/DebugAutoLog.class.php

Деструкторы не вызовутся, если перед ними будет брошено какое-нибудь исключение или выполнится функция die(). Надёжнее сразу в конструкторе определить register_shutdown_function() с нужными условиями. Взгляните на мою глобальную реализацию:


<?php

if (!function_exists('defer')) {

    /**
     * @var callable[]
     */
    $_ENV['deferred_handlers'] = [];

    /**
     * Добавит коллбек в массив $_ENV['deferred_handlers']
     *
     * @param callable $callback
     * @return void
     */
    function defer(callable $callback) {
        array_unshift($_ENV['deferred_handlers'], $callback);
    }

    /** 
     * Выполняет коллбеки после завершения скрипта
     *
     * @return void
     */
    register_shutdown_function(function () {
        foreach ($_ENV['deferred_handlers'] as $callback) {
            $callback();
        }
    });
}

$fh = fopen('data.txt', 'w');

defer(function () use ($fh) {
    fclose($fh);
});

throw new Exception;

// Fatal error: Uncaught Exception in \localhost\index.php:38
// and run deferred handlers from env variable $_ENV['deferred_handlers']

Здесь есть минусы: 1) переменную окружения $_ENV['deferred_handlers'] можно грохнуть или вся $_ENV каким-то магическим образом обнулится; 2) если внутри какого-то отложенного обработчика будет брошено исключение, то вся цепочка вызовов сломается.

При die() правда не вызовутся, но при бросании исключения деструкторы отрабатывают:

function main() {
    $str = 'hello habr';
    defer($_, 'var_dump', $str);
    throw new \Exception("test");
}

main();


Этот код выводит следующее:

string(10) "hello habr"

Fatal error: Uncaught exception 'Exception' with message 'test' in /Users/yuriy/tmp/defer.php:17
Stack trace:
#0 /Users/yuriy/tmp/defer.php(20): main()
#1 {main}
  thrown in /Users/yuriy/tmp/defer.php on line 17

Да, всё верно. Деструктор не вызовется, если исключение будет брошено внутри класса. В вашем случае класса autoClose.

Вызовется. Можно обернуть код внутри __destruct в try/catch, чтобы не бросать исключения внутри деструктора. Но лучше в деструктор не пихать код, который бросает исключения :).
А может в Go просто не завезли RAII? А в PHP вроде с этим неплохо.
Defer в Go для реализации RAII имеет неприятную особенность — его можно забыть (при рефакторинге особенно). Я к тому, что в языке с ООП (а php это умеет) defer для ресурсов просто инородный костыль.
Тогда это будет вызвано при завершении скрипта, а не при выходе из функции. В то время, как для веб-скриптов разница не очень большая, в CLI или в демонах такой способ неприемлем.

А может быть просто следить за своим кодом? Улучшать не его читабельность, а его производительность и качество?
Всегда можно наплодить десяток лишних классов вместо того, чтобы просто добавить где нужно закрытие того или иного ресурса. Но в итоге это приведёт только к огромному оверхеду из кучи бесполезных вызовов, от которых можно было бы избавиться с применением головы разработчика и решения не на уровне defer-костылей, а на уровне архитектуры (тот же стандартный __destructor или finally).
Коллеги, из-за такого вот украшения кода все и называют наш любимый PHP поганеньким и тормозным языком. А если копнуть поглубже, так большая часть проблем оказывается не в языке, а в нашей с вами лени. Не надо так.

(тот же стандартный __destructor или finally).

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


А вот finally и вынос тела try/catch в отдельный приватный метод намного более разумный вариант, который полностью заменяет необходимость в deferred и при этом мы все еще будем практиковать защитное программирование.


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

Ну во первых лень двигатель прогресса. Во вторых, почему вы с вашими требованиями вообще пишите на PHP? Писали бы на Си, оно и быстро, и к дисциплине приучает (там то немного опаснее не закрыть хэнделр на файл).


Почему в PHP сборщик мусора на ref-count? Это ж безумная трата ресурсов вычислительных! Всего-то надо руками отчищать память, что ж мы ленимся?

В go defer является частью языка, значит и в php, в первую очередь, уместно говорить о дополнении в сам язык. Я, как вариант, посмотрел бы еще и в сторону питона и его менеджера контекста with. На питоне это выглядит как — то так:


with open('newfile.txt', 'w', encoding='utf-8') as g:
    d = int(input())
    print('1 / {} = {}'.format(d, 1 / d), file=g)

В смысле синтаксиса, как говорится, сколько людей, столько и мнений, а для реализации поддержки, как мне кажется, такой вариант может оказаться проще.

Такой подход есть во многих языках, try-with-resources. Он хорошо подходит в случае одного ресурса. Но в случае одновременной работы с несколькими ресурсами получается довольно громоздкие вложенные конструкции. Например, вам надо открыть какое-то (заранее неизвестное) число ssh-прокси соединений, и в конце обязательно закрыть их. Как в этом случае использовать with?
Я посмотрел бы в сторону пулера. Если соединений много, и особенно, если за ними надо присматривать, то пусть для этого будет специальный код, или даже отдельный процесс (эдакий аналог pgbouncer — а, в случае с БД).
В сложном случае да, но в простом случае, если надо только гарантированно закрыть соединения, вполне подходит вариант с набором deferred-обработчиков.

Много избыточного кода. Думаю в PHP стоит применять другой подход.

он уже придуман — умирающая модель выполнения ;)

А кто хоронит?

умирающая не в том смысле что устаревшая, а в том что все ваши хэндлеры будут автоматически закрыты по завершении запроса. Но вообще все спокойно хэндлится через finally и не нужны никакие deferred (try with может быть еще был бы полезен).

все ваши хэндлеры будут автоматически закрыты по завершении запроса.

Теоретически вы правы. Практически, все может быть не так однозначно.
Представим себе старый добрый apache с mod_php и скрипт, который ловит данные от посетителя, открывает соединение с БД, пишет туда, и завершается.
Когда будет закрыто текущее соединение:


  • по окончании скрипта
  • по истечении таймаута соединения с БД
  • при приходе следующего запроса, который создаст новое соединение и текущий дескриптор сможет быть утилизирован (кстати, как я понимаю, сборка мусора в пыхе тоже процесс асинхронный, а значит дескриптор еще некоторое время будет существовать вопреки всему)
  • при смерти и перезапуске дочернего процесса апача, после истечения отведенного ему времени жизни/количества запросов

Есть идеи?


Но вообще все спокойно хэндлится через finally

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

Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории