Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.00% covered (success)
97.00%
97 / 100
92.31% covered (success)
92.31%
12 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventManager
97.00% covered (success)
97.00%
97 / 100
92.31% covered (success)
92.31%
12 / 13
54
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 importFromFile
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 clear
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 attach
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 triggerSimilar
91.18% covered (success)
91.18%
31 / 34
0.00% covered (danger)
0.00%
0 / 1
20.27
 trigger
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
8
 reset
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 hasOutstanding
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 detach
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 detachCallback
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 detachGroup
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addGroup
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Event
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Event;
16
17use phpOMS\Dispatcher\Dispatcher;
18use phpOMS\Dispatcher\DispatcherInterface;
19
20/**
21 * EventManager class.
22 *
23 * The event manager allows to define events which can be triggered/executed in an application.
24 * This implementation allows to create sub-conditions which need to be met (triggered in advance) bevore the actual
25 * callback is getting executed.
26 *
27 * What happens after triggering an event (removing the callback, resetting the sub-conditions etc.) depends on the setup.
28 *
29 * @package phpOMS\Event
30 * @license OMS License 2.0
31 * @link    https://jingga.app
32 * @since   1.0.0
33 */
34final class EventManager implements \Countable
35{
36    /**
37     * Events.
38     *
39     * @var array<string, array<string, bool>>
40     * @since 1.0.0
41     */
42    private array $groups = [];
43
44    /**
45     * Callbacks.
46     *
47     * @var array<string, array{remove:bool, reset:bool, callbacks:array}>
48     * @since 1.0.0
49     */
50    private array $callbacks = [];
51
52    /**
53     * Dispatcher.
54     *
55     * @var DispatcherInterface
56     * @since 1.0.0
57     */
58    private DispatcherInterface $dispatcher;
59
60    /**
61     * Constructor.
62     *
63     * @param Dispatcher $dispatcher Dispatcher. If no dispatcher is provided a simple general purpose dispatcher is used.
64     *
65     * @since 1.0.0
66     */
67    public function __construct(Dispatcher $dispatcher = null)
68    {
69        $this->dispatcher = $dispatcher ?? new class() implements DispatcherInterface {
70            /**
71             * {@inheritdoc}
72             */
73            public function dispatch(array | string | callable $func, mixed ...$data) : array
74            {
75                if (!\is_callable($func)) {
76                    return [];
77                }
78
79                $func(...$data);
80
81                return [];
82            }
83        };
84    }
85
86    /**
87     * Add events from file.
88     *
89     * Files need to return a php array of the following structure:
90     * return [
91     *      '{EVENT_ID}' => [
92     *          'callback' => [
93     *              '{DESTINATION_NAMESPACE:method}', // can also be static by using :: between namespace and functio name
94     *              // more callbacks here
95     *          ],
96     *      ],
97     * ];
98     *
99     * @param string $path Hook file path
100     *
101     * @return bool
102     *
103     * @since 1.0.0
104     */
105    public function importFromFile(string $path) : bool
106    {
107        if (!\is_file($path)) {
108            return false;
109        }
110
111        /** @noinspection PhpIncludeInspection */
112        $hooks = include $path;
113
114        foreach ($hooks as $group => $hook) {
115            foreach ($hook['callback'] as $callback) {
116                $this->attach($group, $callback, $hook['remove'] ?? false, $hook['reset'] ?? true);
117            }
118        }
119
120        return true;
121    }
122
123    /**
124     * Clear all events
125     *
126     * @return void
127     * @since 1.0.0
128     */
129    public function clear() : void
130    {
131        $this->groups    = [];
132        $this->callbacks = [];
133    }
134
135    /**
136     * Attach new event
137     *
138     * @param string          $group    Name of the event (unique)
139     * @param string|Callable $callback Callback or route for the event
140     * @param bool            $remove   Remove event after triggering it?
141     * @param bool            $reset    Reset event after triggering it? Remove must be false!
142     *
143     * @return bool
144     *
145     * @since 1.0.0
146     */
147    public function attach(string $group, string | callable $callback, bool $remove = false, bool $reset = false) : bool
148    {
149        if (!isset($this->callbacks[$group])) {
150            $this->callbacks[$group] = ['remove' => $remove, 'reset' => $reset, 'callbacks' => []];
151        }
152
153        $this->callbacks[$group]['callbacks'][] = $callback;
154        $this->addGroup($group, '');
155
156        return true;
157    }
158
159    /**
160     * Trigger event based on regex for group and/or id.
161     *
162     * This tigger function allows the group to be a regex in either this function call or in the definition of the group.
163     *
164     * @param string $group Name of the event (can be regex)
165     * @param string $id    Sub-requirement for event (can be regex)
166     * @param mixed  $data  Data to pass to the callback
167     *
168     * @return bool returns true on successfully triggering ANY event, false if NO event could be triggered which also includes sub-requirements missing
169     *
170     * @since 1.0.0
171     */
172    public function triggerSimilar(string $group, string $id = '', mixed $data = null) : bool
173    {
174        if (empty($this->callbacks)) {
175            return false;
176        }
177
178        $groupIsRegex = \str_starts_with($group, '/');
179        $idIsRegex    = \str_starts_with($id, '/');
180
181        $groups = [];
182        foreach ($this->groups as $groupName => $_) {
183            $groupNameIsRegex = \str_starts_with($groupName, '/');
184
185            if ($groupIsRegex) {
186                if (\preg_match($group, $groupName) === 1) {
187                    $groups[$groupName] = [];
188                }
189            } elseif ($groupNameIsRegex && \preg_match($groupName, $group) === 1) {
190                $groups[$groupName] = [];
191            } elseif ($groupName === $group) {
192                $groups[$groupName] = [];
193            }
194        }
195
196        foreach ($groups as $groupName => $_) {
197            foreach ($this->groups[$groupName] as $idName => $_2) {
198                $idNameIsRegex = \str_starts_with($idName, '/');
199
200                if ($idIsRegex) {
201                    if (\preg_match($id, $idName) === 1) {
202                        $groups[$groupName][] = $idName;
203                    }
204                } elseif ($idNameIsRegex && \preg_match($idName, $id) === 1) {
205                    $groups[$groupName][] = $id;
206                } elseif ($idName === $id) {
207                    $groups[$groupName] = [];
208                }
209            }
210
211            if (empty($groups[$groupName])) {
212                $groups[$groupName][] = $id;
213            }
214        }
215
216        if (!\is_array($data)) {
217            $data = [$data];
218        }
219
220        $data['@triggerGroup'] ??= $group;
221
222        $triggerValue = false;
223        foreach ($groups as $groupName => $ids) {
224            foreach ($ids as $id) {
225                $triggerValue = $this->trigger($groupName, $id, $data) || $triggerValue;
226            }
227        }
228
229        return $triggerValue;
230    }
231
232    /**
233     * Trigger event
234     *
235     * @param string $group Name of the event
236     * @param string $id    Sub-requirement for event
237     * @param mixed  $data  Data to pass to the callback
238     *
239     * @return bool returns true on successfully triggering the event, false if the event couldn't be triggered which also includes sub-requirements missing
240     *
241     * @since 1.0.0
242     */
243    public function trigger(string $group, string $id = '', mixed $data = null) : bool
244    {
245        if (!isset($this->callbacks[$group])) {
246            return false;
247        }
248
249        if (isset($this->groups[$group])) {
250            $this->groups[$group][$id] = true;
251        }
252
253        if ($this->hasOutstanding($group)) {
254            return false;
255        }
256
257        foreach ($this->callbacks[$group]['callbacks'] as $func) {
258            if (\is_array($data)) {
259                $data['@triggerGroup'] ??= $group;
260                $data['@triggerId']      = $id;
261            } else {
262                $data = [
263                    $data,
264                ];
265
266                $data['@triggerGroup'] = $group;
267                $data['@triggerId']    = $id;
268            }
269
270            $this->dispatcher->dispatch($func, ...\array_values($data));
271        }
272
273        if ($this->callbacks[$group]['remove']) {
274            $this->detach($group);
275        } elseif ($this->callbacks[$group]['reset']) {
276            $this->reset($group);
277        }
278
279        return true;
280    }
281
282    /**
283     * Reset group
284     *
285     * @param string $group Name of the event
286     *
287     * @return void
288     *
289     * @since 1.0.0
290     */
291    private function reset(string $group) : void
292    {
293        if (!isset($this->groups[$group])) {
294            return; // @codeCoverageIgnore
295        }
296
297        foreach ($this->groups[$group] as $id => $ok) {
298            $this->groups[$group][$id] = false;
299        }
300    }
301
302    /**
303     * Check if a group has missing sub-requirements
304     *
305     * @param string $group Name of the event
306     *
307     * @return bool
308     *
309     * @since 1.0.0
310     */
311    private function hasOutstanding(string $group) : bool
312    {
313        if (!isset($this->groups[$group])) {
314            return false; // @codeCoverageIgnore
315        }
316
317        foreach ($this->groups[$group] as $ok) {
318            if (!$ok) {
319                return true;
320            }
321        }
322
323        return false;
324    }
325
326    /**
327     * Detach an event
328     *
329     * @param string $group Name of the event
330     *
331     * @return bool
332     *
333     * @since 1.0.0
334     */
335    public function detach(string $group) : bool
336    {
337        $result1 = $this->detachCallback($group);
338        $result2 = $this->detachGroup($group);
339
340        return $result1 || $result2;
341    }
342
343    /**
344     * Detach an event
345     *
346     * @param string $group Name of the event
347     *
348     * @return bool
349     *
350     * @since 1.0.0
351     */
352    private function detachCallback(string $group) : bool
353    {
354        if (isset($this->callbacks[$group])) {
355            unset($this->callbacks[$group]);
356
357            return true;
358        }
359
360        return false;
361    }
362
363    /**
364     * Detach an event
365     *
366     * @param string $group Name of the event
367     *
368     * @return bool
369     *
370     * @since 1.0.0
371     */
372    private function detachGroup(string $group) : bool
373    {
374        if (isset($this->groups[$group])) {
375            unset($this->groups[$group]);
376
377            return true;
378        }
379
380        return false;
381    }
382
383    /**
384     * Add sub-requirement for event
385     *
386     * @param string $group Name of the event
387     * @param string $id    ID of the sub-requirement
388     *
389     * @return void
390     *
391     * @since 1.0.0
392     */
393    public function addGroup(string $group, string $id) : void
394    {
395        if (!isset($this->groups[$group])) {
396            $this->groups[$group] = [];
397        }
398
399        if (isset($this->groups[$group][''])) {
400            unset($this->groups[$group]['']);
401        }
402
403        $this->groups[$group][$id] = false;
404    }
405
406    /**
407     * {@inheritdoc}
408     */
409    public function count() : int
410    {
411        return \count($this->callbacks);
412    }
413}