Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.33% covered (success)
93.33%
14 / 15
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Defer
93.33% covered (success)
93.33%
14 / 15
88.89% covered (warning)
88.89%
8 / 9
12.04
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 __destruct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 defer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setErrorReporter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getErrorReporter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 flush
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of fast-forward/defer.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/defer
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\Defer;
20
21use Throwable;
22use FastForward\Defer\ErrorReporter\ErrorLogErrorReporter;
23
24/**
25 * This class MUST be used to manage deferred callbacks that SHALL be executed at the end of a scope.
26 * It provides mechanisms to register, execute, and report errors for callbacks in a LIFO order.
27 */
28final class Defer implements DeferInterface
29{
30    /**
31     * Stack of deferred callbacks and their arguments.
32     *
33     * @var array<int, array{0: callable, 1: array<int, mixed>}> the stack of callbacks
34     */
35    private array $stack = [];
36
37    /**
38     * The global error reporter instance. This property MAY be set to customize error reporting.
39     */
40    private static ?ErrorReporterInterface $errorReporter = null;
41
42    /**
43     * Constructs a new Defer instance and optionally registers an initial callback.
44     *
45     * @param callable|null $callback the initial callback to register (optional)
46     * @param mixed ...$arguments Arguments for the callback.
47     */
48    public function __construct(?callable $callback = null, mixed ...$arguments)
49    {
50        if (null !== $callback) {
51            $this->defer($callback, ...$arguments);
52        }
53    }
54
55    /**
56     * Executes all registered callbacks when the object is destroyed.
57     *
58     * @return mixed
59     */
60    public function __destruct()
61    {
62        $this->flush();
63    }
64
65    /**
66     * Registers a callback to be executed later.
67     *
68     * This method MUST add the callback to the stack. It MAY be called multiple times.
69     *
70     * @param callable $callback the callback to register
71     * @param mixed ...$arguments Arguments for the callback.
72     *
73     * @return void
74     */
75    public function __invoke(callable $callback, mixed ...$arguments): void
76    {
77        $this->defer($callback, ...$arguments);
78    }
79
80    /**
81     * Returns the number of registered callbacks.
82     *
83     * @return int the number of callbacks
84     */
85    public function count(): int
86    {
87        return \count($this->stack);
88    }
89
90    /**
91     * Registers a callback to be executed later.
92     *
93     * This method MUST add the callback to the stack. It MAY be called multiple times.
94     *
95     * @param callable $callback the callback to register
96     * @param mixed ...$args Arguments for the callback.
97     *
98     * @return void
99     */
100    public function defer(callable $callback, mixed ...$args): void
101    {
102        $this->stack[] = [$callback, $args];
103    }
104
105    /**
106     * Determines if there are no registered callbacks.
107     *
108     * @return bool true if no callbacks are registered; otherwise, false
109     */
110    public function isEmpty(): bool
111    {
112        return [] === $this->stack;
113    }
114
115    /**
116     * Sets the global error reporter for all Defer instances.
117     *
118     * @param ErrorReporterInterface|null $reporter the error reporter to use
119     *
120     * @return void
121     */
122    public static function setErrorReporter(?ErrorReporterInterface $reporter): void
123    {
124        self::$errorReporter = $reporter;
125    }
126
127    /**
128     * Gets the configured error reporter or instantiates a default one.
129     *
130     * @return ErrorReporterInterface the error reporter instance
131     */
132    private function getErrorReporter(): ErrorReporterInterface
133    {
134        return self::$errorReporter ??= new ErrorLogErrorReporter();
135    }
136
137    /**
138     * Executes all registered callbacks, reporting exceptions as needed.
139     *
140     * This method MUST execute callbacks in LIFO order. If a callback throws, the error MUST be reported.
141     *
142     * @return void
143     */
144    private function flush(): void
145    {
146        while (($item = array_pop($this->stack)) !== null) {
147            [$callback, $args] = $item;
148
149            try {
150                $callback(...$args);
151            } catch (Throwable $throwable) {
152                $this->getErrorReporter()
153                    ->report($throwable, $callback, $args);
154            }
155        }
156    }
157}