Passed
Push — develop ( 528c82...91706d )
by Peter
03:12 queued 10s
created

EventDispatcherTrait::getHandlers()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 4
nop 1
dl 0
loc 18
rs 9.6111
c 0
b 0
f 0
1
<?php
2
/**
3
 * Webino™ (http://webino.sk)
4
 *
5
 * @link        https://github.com/webino/event-emitter
6
 * @copyright   Copyright (c) 2019 Webino, s.r.o. (http://webino.sk)
7
 * @author      Peter Bačinský <[email protected]>
8
 * @license     BSD-3-Clause
9
 */
10
11
namespace Webino;
12
13
/**
14
 * Trait EventDispatcherTrait
15
 * @package event-emitter
16
 */
17
trait EventDispatcherTrait
18
{
19
    /**
20
     * Subscribed events and their handlers
21
     *
22
     * STRUCTURE:
23
     * [
24
     *     <string name> => [
25
     *         <int priority> => [
26
     *             0 => [<callable handler>, ...]
27
     *         ],
28
     *         ...
29
     *     ],
30
     *     ...
31
     * ]
32
     *
33
     * NOTE:
34
     * This structure helps us to reuse the list of handlers
35
     * instead of first iterating over it and generating a new one
36
     * -> In result it improves performance by up to 25% even if it looks a bit strange
37
     *
38
     * @var array[]
39
     */
40
    protected $events = [];
41
42
    /**
43
     * @var EventInterface|null
44
     */
45
    protected $eventPrototype;
46
47
    /**
48
     * Invoke handlers
49
     *
50
     * @param string|EventInterface $event Event name or object
51
     * @param callable|null $until Invoke handlers until callback return value evaluate to true
52
     * @return EventInterface Event object
53
     * @throws InvalidEventException Invalid event
54
     */
55
    public function emit($event, callable $until = null): EventInterface
56
    {
57
        $event = $this->normalizeEvent($event);
58
        $name = $event->getName();
59
60
        if (empty($name)) {
61
            throw (new InvalidEventException('Cannot emit event %s missing a name'))
62
                ->format($event);
63
        }
64
65
        // get handlers by priority in reverse order
66
        $handlers = $this->getHandlers($name);
67
68
        // set event initial values
69
        $event->setResults([]);
70
        $event->stop(false);
71
72
        if ($this instanceof EventEmitterInterface) {
73
            $event->setTarget($this);
74
        }
75
76
        // invoke handlers
77
        foreach ($handlers as $eventHandlers) {
78
            foreach ($eventHandlers as $subHandlers) {
79
                foreach ($subHandlers as $callback) {
80
                    $result = $callback($event);
81
                    $event->setResult($result);
82
83
                    // stop propagation if the event was asked to
84
                    if ($event->isStopped()) {
85
                        return $event;
86
                    }
87
88
                    // stop propagation if the result causes our validation callback to return true
89
                    if ($until && $until($result)) {
90
                        return $event;
91
                    }
92
                }
93
            }
94
        }
95
96
        return $event;
97
    }
98
99
    /**
100
     * Set event handler
101
     *
102
     * @param string|EventInterface|EventHandlerInterface $event Event name, object or event handler
103
     * @param callable|null $callback Event handler
104
     * @param int $priority Handler invocation priority
105
     * @return void
106
     */
107
    public function on($event, $callback = null, int $priority = 0)
108
    {
109
        if ($event instanceof EventHandlerInterface && $this instanceof EventEmitterInterface) {
110
            $event->attachEventEmitter($this);
111
            return;
112
        }
113
114
        $event = $this->normalizeEvent($event);
115
        $name = $event->getName();
116
117
        isset($this->events[$name]) or $this->events[$name] = [];
118
        isset($this->events[$name][(int)$priority]) or $this->events[$name][(int)$priority] = [];
119
        isset($this->events[$name][(int)$priority][0]) or $this->events[$name][(int)$priority][0] = [];
120
121
        array_unshift($this->events[$name][(int)$priority][0], $callback);
122
    }
123
124
    /**
125
     * Remove event handler
126
     *
127
     * @param callable|EventHandlerInterface|null $callback Event handler
128
     * @param string|EventInterface|null $event Event name or object
129
     * @return void
130
     */
131
    public function off($callback = null, $event = null): void
132
    {
133
        if ($callback instanceof EventHandlerInterface && $this instanceof EventEmitterInterface) {
134
            $callback->detachEventEmitter($this);
135
            return;
136
        }
137
138
        if (!$event) {
139
            // remove listeners from all events
140
            foreach (array_keys($this->events) as $name) {
141
                $this->off($callback, $name);
142
            }
143
            return;
144
        }
145
146
        $event = $this->normalizeEvent($event);
147
        $name = $event->getName();
148
149
        if (!isset($this->events[$name])) {
150
            return;
151
        }
152
153
        foreach ($this->events[$name] as $priority => $handlers) {
154
            foreach ($handlers[0] as $index => $subHandler) {
155
                if ($callback && $subHandler !== $callback) {
156
                    continue;
157
                }
158
159
                // remove founded listener
160
                unset($this->events[$name][$priority][0][$index]);
161
162
                // remove event if the queue for given priority is empty
163
                if (empty($this->events[$name][$priority][0])) {
164
                    unset($this->events[$name][$priority]);
165
                    break;
166
                }
167
            }
168
        }
169
170
        // remove event if the queue given is empty
171
        if (empty($this->events[$name])) {
172
            unset($this->events[$name]);
173
        }
174
    }
175
176
    /**
177
     * Return listeners sort by priority in reverse order
178
     *
179
     * @param string|null $name Event name
180
     * @return array Sorted listeners
181
     */
182
    protected function getHandlers(string $name = null): array
183
    {
184
        if (isset($this->events[$name])) {
185
            $handlers = $this->events[$name];
186
187
            if (isset($this->events['*'])) {
188
                foreach ($this->events['*'] as $priority => $handlers) {
189
                    $handlers[$priority][] = $handlers[0];
190
                }
191
            }
192
        } elseif (isset($this->events['*'])) {
193
            $handlers = $this->events['*'];
194
        } else {
195
            $handlers = [];
196
        }
197
198
        krsort($handlers);
199
        return $handlers;
200
    }
201
202
    /**
203
     * Return event as object, if any
204
     *
205
     * @param string|EventInterface|object $event Event name or object
206
     * @return EventInterface Event object
207
     * @throws InvalidArgumentException Invalid event
208
     */
209
    protected function normalizeEvent($event): EventInterface
210
    {
211
        if (is_string($event)) {
212
            $eventClone = clone $this->getEventPrototype();
213
            $eventClone->setName($event);
214
            return $eventClone;
215
        }
216
217
        if ($event instanceof EventInterface) {
218
            return $event;
219
        }
220
221
        throw (new InvalidArgumentException('Expected event as: %s instead of: %s;'))
222
            ->format('string|EventInterface', $event);
223
    }
224
225
    /**
226
     * @return EventInterface
227
     */
228
    protected function getEventPrototype(): EventInterface
229
    {
230
        if (!$this->eventPrototype) {
231
            if ($this instanceof EventEmitterInterface) {
232
                $this->eventPrototype = new Event(null, $this);
233
            } else {
234
                $this->eventPrototype = new Event;
235
            }
236
        }
237
        return $this->eventPrototype;
238
    }
239
}
240