Комментарии 26
по моему, попытки внедрять в РНР идеи из других языков внесут больше избыточности, чем пользы от подобных действий.
хорошая архитектура приложения принесет больше профита,
если Ваш метод/функция использует несколько ресурсов (открывает файлы, соединения етс.) — очевидно стоит задуматся разделить на несколько методов/функций,
в случае: один метод/функция делают несколько операций — вносят неоднозначность в задачу метода/функции, а это влечет сложность понимания общей архитектуры и отладки.
$readHandle = fopen($sourceName, «r»);
и почему не проверяется существование файла? file_exists() может избавить от проблем: файла нет, права овнер/группы на файл не разрешают его читать и т.п.
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
});
Насчет того, полагаться или нет — тут и контекст можно использовать как попало, например передать внешний контекст во внутреннюю функцию. Выше уже сказали — отличие в том, что в go это инструмент языка и его невозможно использовать некорректно, а программную реализацию всегда можно исковеркать
Передать контекст во внутреннюю функцию иногда может быть полезно. Например, если в рамках какого-то процесса нужно создать несколько временных файлов, которые будут использоваться в разных местах процесса, а удалить все файлы нужно в конце процесса. В этом смысле Defer мощнее, чем в go.
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) если внутри какого-то отложенного обработчика будет брошено исключение, то вся цепочка вызовов сломается.
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
Defer в Go для реализации RAII имеет неприятную особенность — его можно забыть (при рефакторинге особенно). Я к тому, что в языке с ООП (а php это умеет) defer для ресурсов просто инородный костыль.
А может проще вот так?
function defer(\Closure $callback): void
{
\register_shutdown_function($callback);
}
А может быть просто следить за своим кодом? Улучшать не его читабельность, а его производительность и качество?
Всегда можно наплодить десяток лишних классов вместо того, чтобы просто добавить где нужно закрытие того или иного ресурса. Но в итоге это приведёт только к огромному оверхеду из кучи бесполезных вызовов, от которых можно было бы избавиться с применением головы разработчика и решения не на уровне 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)
В смысле синтаксиса, как говорится, сколько людей, столько и мнений, а для реализации поддержки, как мне кажется, такой вариант может оказаться проще.
Много избыточного кода. Думаю в PHP стоит применять другой подход.
он уже придуман — умирающая модель выполнения ;)
А кто хоронит?
умирающая не в том смысле что устаревшая, а в том что все ваши хэндлеры будут автоматически закрыты по завершении запроса. Но вообще все спокойно хэндлится через finally
и не нужны никакие deferred (try with может быть еще был бы полезен).
все ваши хэндлеры будут автоматически закрыты по завершении запроса.
Теоретически вы правы. Практически, все может быть не так однозначно.
Представим себе старый добрый apache с mod_php и скрипт, который ловит данные от посетителя, открывает соединение с БД, пишет туда, и завершается.
Когда будет закрыто текущее соединение:
- по окончании скрипта
- по истечении таймаута соединения с БД
- при приходе следующего запроса, который создаст новое соединение и текущий дескриптор сможет быть утилизирован (кстати, как я понимаю, сборка мусора в пыхе тоже процесс асинхронный, а значит дескриптор еще некоторое время будет существовать вопреки всему)
- при смерти и перезапуске дочернего процесса апача, после истечения отведенного ему времени жизни/количества запросов
Есть идеи?
Но вообще все спокойно хэндлится через finally
Так же, как и при использовании обычного close, если речь про файлы. Finaly, как и close может далеко отстоять от места, где вы этот файл открыли. Вся прелесть defer или менеджера контекста в стиле python, в том и состоит, что, об автоматическом освобождении ресурсов вы можете позаботится сразу после их создания.
Defer: из Go в PHP