Defer: from Go to PHP

    Go has a useful defer construct . It is usually used to free resources and works as follows: as a defer argument, a function is passed that is placed in the list of functions. This list of functions is executed upon exiting the enclosing function.


    Defer has several obvious and not very good points:


    • improves understanding of the code - when creating a resource, the code responsible for releasing it is immediately visible. No need to look for try {} finally {} or all exit points for a function
    • avoids frequent errors related to the release of resources, for example, with unhandled exceptions, or in the case of opening multiple resources.

    For example, a code like this:


    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);
        }
    }

    It could be turned into this:


    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();
            }
        }
    }

    In the second case, working with closing files is much simpler - each file needs to be closed only once. It reduces the likelihood that someone will forget to close the file, especially if there are not 2, but more.


    But unfortunately there is no defer in PHP. But you can write your own implementation. It looks like this:


    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 - a class in which handler functions accumulate. When you exit the function, you must call the executeDeferredActions () method, which will execute all the handlers. In order not to manually create a DeferredContext, you can use the DeferredTrait trait, which encapsulates the logic of working with DeferredContext.


    Using this approach, the code from the example above will look like this:


    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();
                }
            });
        }
    }

    I hope that this idea will help you reduce the number of bugs in the code and create more reliable programs.


    Also popular now: