Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.00% |
97 / 100 |
|
92.31% |
12 / 13 |
CRAP | |
0.00% |
0 / 1 |
EventManager | |
97.00% |
97 / 100 |
|
92.31% |
12 / 13 |
54 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
importFromFile | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
clear | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
attach | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
triggerSimilar | |
91.18% |
31 / 34 |
|
0.00% |
0 / 1 |
20.27 | |||
trigger | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
8 | |||
reset | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
hasOutstanding | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
detach | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
detachCallback | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
detachGroup | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
addGroup | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
count | |
100.00% |
1 / 1 |
|
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 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\Event; |
16 | |
17 | use phpOMS\Dispatcher\Dispatcher; |
18 | use 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 | */ |
34 | final 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 | } |