EventDispatcherTrait   A
last analyzed

Complexity

Total Complexity 39

Size/Duplication

Total Lines 226
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 80
c 1
b 0
f 0
dl 0
loc 226
rs 9.28
wmc 39

6 Methods

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