Defer: из Go в PHP
В языке Go есть полезная конструкция defer. Обычно она используется для освобождения ресурсов и работает следующим образом: в качестве аргумента defer передается функция, которая помещается в список функций. Этот список функций выполняется при выходе из объемлющей функции.
У defer есть несколько очевидных и не очень достоинств:
- улучшает понимание кода — при создании ресурса сразу виден код, ответственный за его освобождение. Не нужно искать try {} finally {} или все точки выхода из функции
- позволяет избежать частых ошибок, связанных с освобождением ресурсов, например, с необработанными исключениями, или в случае открытия нескольких ресурсов.
К примеру, такой код:
class Utils
{
public function copyFile(string $sourceName, string $destName): void
{
$readHandle = fopen($sourceName, "r");
if ($readHandle === false) {
throw new \Exception();
}
$writeHandle = fopen($destName, "w");
if ($writeHandle === false) {
fclose($readHandle);
throw new \Exception();
}
while (($buffer = fgets($readHandle)) !== false) {
$wasFailure = fwrite($writeHandle, $buffer);
if ($wasFailure) {
fclose($readHandle);
fclose($writeHandle);
throw new \Exception();
}
}
if (!feof($readHandle)) {
fclose($readHandle);
fclose($writeHandle);
throw new \Exception();
}
fclose($readHandle);
fclose($writeHandle);
}
}
Можно было бы превратить в такой:
class Utils
{
public function copyFile(string $sourceName, string $destName): void
{
$readHandle = fopen($sourceName, "r");
if ($readHandle === false) {
throw new \Exception();
}
defer fclose($readHandle);
$writeHandle = fopen($destName, "w");
if ($writeHandle === false) {
throw new \Exception();
}
defer fclose($writeHandle);
while (($buffer = fgets($readHandle)) !== false) {
$wasFailure = fwrite($writeHandle, $buffer);
if ($wasFailure) {
throw new \Exception();
}
}
if (!feof($readHandle)) {
throw new \Exception();
}
}
}
Во втором случае работать с закрытием файлов гораздо проще — каждый файл нужно закрыть только один раз. Снижается вероятность, что кто-то забудет закрыть файл, особенно, если их будет не 2, а больше.
Но, к сожалению, в PHP нет defer. Зато можно написать свою реализацию. Она выглядит следующим образом:
class DeferredContext
{
protected $deferredActions = [];
public function defer(callable $deferAction)
{
$this->deferredActions[] = $deferAction;
}
public function executeDeferredActions()
{
$actionsCount = count($this->deferredActions);
if ($actionsCount > 0) {
for ($i = $actionsCount - 1; $i >= 0; $i--) {
$action = $this->deferredActions[$i];
try {
$action();
} catch (\Exception $e) {
}
unset($this->deferredActions[$i]);
}
}
$this->deferredActions = [];
}
}
trait DeferredTrait
{
private function deferred(callable $callback) {
$context = new DeferredContext();
try {
$callback($context);
} finally {
$context->executeDeferredActions();
}
}
}
DeferredContext — класс, в котором накапливаются функции-обработчики. При выходе из функции необходимо вызвать метод executeDeferredActions (), который выполнит все обработчики. Для того, чтобы не создавать вручную DeferredContext, можно использовать трейт DeferredTrait, который инкапсулирует в себе логику работы с DeferredContext.
С использованием данного подхода код из примера выше будет выглядеть так:
class Utils
{
use DeferredTrait;
public function copyFile(string $sourceName, string $destName): void
{
$this->deferred(function(DeferredContext $context) use ($destName, $sourceName) {
$readHandle = fopen($sourceName, "r");
if ($readHandle === false) {
throw new \Exception();
}
$context->defer(function() use ($readHandle) { fclose($readHandle); });
$writeHandle = fopen($destName, "w");
if ($writeHandle === false) {
throw new \Exception();
}
$context->defer(function() use ($writeHandle) { fclose($writeHandle); });
while (($buffer = fgets($readHandle)) !== false) {
$wasFailure = fwrite($writeHandle, $buffer);
if ($wasFailure) {
throw new \Exception();
}
}
if (!feof($readHandle)) {
throw new \Exception();
}
});
}
}
Надеюсь, что эта идея поможет вам уменьшить количество багов в коде и создавать более надежные программы.