Completed
Push — master ( f53a3b...615a7f )
by Garrett
01:25
created

Manager::add()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 37
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 37
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 23
nc 6
nop 3
1
<?php
2
3
namespace Noair;
4
5
/**
6
 * Repository for subscribers.
7
 *
8
 * @author  Garrett Whitehorn
9
 *
10
 * @version 1.0
11
 */
12
class Manager
13
{
14
    const PRIORITY_URGENT = 0;
15
    const PRIORITY_HIGHEST = 1;
16
    const PRIORITY_HIGH = 2;
17
    const PRIORITY_NORMAL = 3;
18
    const PRIORITY_LOW = 4;
19
    const PRIORITY_LOWEST = 5;
20
21
    /**
22
     * @internal
23
     *
24
     * @var array Contains registered events and their handlers by priority
25
     *
26
     * @since   1.0
27
     */
28
    protected $subscribers = [];
29
30
    /**
31
     * Determine if the event name has any subscribers.
32
     *
33
     * @api
34
     *
35
     * @param string $eventName The desired event's name
36
     *
37
     * @return bool Whether or not the event was published
38
     *
39
     * @since   1.0
40
     *
41
     * @version 1.0
42
     */
43
    public function hasSubscribers($eventName)
44
    {
45
        return (isset($this->subscribers[$eventName])
46
                && count($this->subscribers[$eventName]) > 1);
47
    }
48
49
    /**
50
     * Determine if the described event has been subscribed to or not by the callback.
51
     *
52
     * @api
53
     *
54
     * @param string   $eventName The desired event's name
55
     * @param callable $callback  The specific callback we're looking for
56
     *
57
     * @return int|false Subscriber's array index if found, false otherwise; use ===
58
     *
59
     * @since   1.0
60
     *
61
     * @version 1.0
62
     */
63
    public function isSubscribed($eventName, callable $callback)
64
    {
65
        return ($this->hasSubscribers($eventName))
66
            ? self::arraySearchDeep($callback, $this->subscribers[$eventName])
67
            : false;
68
    }
69
70
    /**
71
     * Handles inserting the new subscriber into the sorted internal array.
72
     *
73
     * @internal
74
     *
75
     * @param string $eventName The event it will listen for
76
     * @param int    $interval  The timer interval, if it's a timer (0 if not)
77
     * @param array  $handler   Each individual handler coming from the Observer
78
     *
79
     * @since   1.0
80
     *
81
     * @version 1.0
82
     */
83
    public function add($eventName, $interval, array $handler)
84
    {
85
        // scaffold if not exist
86
        if (!$this->hasSubscribers($eventName)) {
87
            $this->subscribers[$eventName] = [
88
                [ // insert positions
89
                    self::PRIORITY_URGENT => 1,
90
                    self::PRIORITY_HIGHEST => 1,
91
                    self::PRIORITY_HIGH => 1,
92
                    self::PRIORITY_NORMAL => 1,
93
                    self::PRIORITY_LOW => 1,
94
                    self::PRIORITY_LOWEST => 1,
95
                ]
96
            ];
97
        }
98
99
        switch (count($handler)) {
100
            case 1:
101
                $handler[] = self::PRIORITY_NORMAL;
102
                // no break
103
            case 2:
104
                $handler[] = false;
105
        }
106
107
        $sub = [
108
            'callback' => $handler[0],
109
            'priority' => $priority = $handler[1],
110
            'force' => $handler[2],
111
            'interval' => $interval,
112
            'nextcalltime' => self::currentTimeMillis() + $interval,
113
        ];
114
115
        $insertpos = $this->subscribers[$eventName][0][$priority];
116
        array_splice($this->subscribers[$eventName], $insertpos, 0, [$sub]);
117
118
        $this->realign($eventName, $priority);
119
    }
120
121
    /**
122
     * Takes care of actually calling the event handling functions
123
     *
124
     * @internal
125
     *
126
     * @param string $eventName
127
     * @param Event  $event
128
     * @param mixed  $result
129
     *
130
     * @since   1.0
131
     *
132
     * @version 1.0
133
     */
134
    public function fire($eventName, Event $event, $result = null)
135
    {
136
        $subs = $this->subscribers[$eventName];
137
        unset($subs[0]);
138
139
        // Loop through the subscribers of this event
140
        foreach ($subs as $i => $subscriber) {
141
142
            // If the event's cancelled and the subscriber isn't forced, skip it
143
            if ($event->cancelled && $subscriber['force'] === false) {
144
                continue;
145
            }
146
147
            // If the subscriber is a timer...
148
            if ($subscriber['interval'] !== 0) {
149
                // Then if the current time is before when the sub needs to be called
150
                if (self::currentTimeMillis() < $subscriber['nextcalltime']) {
151
                    // It's not time yet, so skip it
152
                    continue;
153
                }
154
155
                // Mark down the next call time as another interval away
156
                $this->subscribers[$eventName][$i]['nextcalltime']
157
                    += $subscriber['interval'];
158
            }
159
160
            // Fire it and save the result for passing to any further subscribers
161
            $event->previousResult = $result;
162
            $result = call_user_func($subscriber['callback'], $event);
163
        }
164
165
        return $result;
166
    }
167
168
    /**
169
     *
170
     */
171
    public function remove($eventName)
172
    {
173
        unset($this->subscribers[$eventName]);
174
    }
175
176
    /**
177
     *
178
     * @param callable $callback
179
     */
180
    public function searchAndDestroy($eventName, $callback)
181
    {
182
        // Loop through the subscribers for the matching event
183
        foreach ($this->subscribers[$eventName] as $key => $subscriber) {
184
185
            // if this subscriber doesn't match what we're looking for, keep looking
186
            if (self::arraySearchDeep($callback, $subscriber) === false) {
187
                continue;
188
            }
189
190
            // otherwise, cut it out and get its priority
191
            $priority = array_splice($this->subscribers[$eventName], $key, 1)[0]['priority'];
192
193
            // shift the insertion points up for equal and lower priorities
194
            $this->realign($eventName, $priority, -1);
195
        }
196
197
        // If there are no more events, remove the event
198
        if (!$this->hasSubscribers($eventName)) {
199
            unset($this->subscribers[$eventName]);
200
        }
201
    }
202
203
    /**
204
     *
205
     */
206
    protected function realign($eventName, $priority, $inc = 1)
207
    {
208
        for ($prio = $priority; $prio <= self::PRIORITY_LOWEST; $prio++) {
209
            $this->subscribers[$eventName][0][$prio] += $inc;
210
        }
211
    }
212
213
    /**
214
     * Searches a multi-dimensional array for a value in any dimension.
215
     *
216
     * @internal
217
     *
218
     * @param mixed $needle   The value to be searched for
219
     * @param array $haystack The array
220
     *
221
     * @return int|bool The top-level key containing the needle if found, false otherwise
222
     *
223
     * @since   1.0
224
     *
225
     * @version 1.0
226
     */
227
    protected static function arraySearchDeep($needle, array $haystack)
228
    {
229
        if (is_array($needle)
230
            && !is_callable($needle)
231
            // and if all key/value pairs in $needle have exact matches in $haystack
232
            && count(array_diff_assoc($needle, $haystack)) == 0
233
        ) {
234
            // we found what we're looking for, so bubble back up with 'true'
235
            return true;
236
        }
237
238
        foreach ($haystack as $key => $value) {
239
            if ($needle === $value
240
                || (is_array($value) && self::arraySearchDeep($needle, $value) !== false)
241
            ) {
242
                // return top-level key of $haystack that contains $needle as a value somewhere
243
                return $key;
244
            }
245
        }
246
        // 404 $needle not found
247
        return false;
248
    }
249
250
    /**
251
     * Returns the current timestamp in milliseconds.
252
     * Named for the similar function in Java.
253
     *
254
     * @internal
255
     *
256
     * @return int Current timestamp in milliseconds
257
     *
258
     * @since   1.0
259
     *
260
     * @version 1.0
261
     */
262
    final protected static function currentTimeMillis()
263
    {
264
        // microtime(true) returns a float where there's 4 digits after the
265
        // decimal and if you add 00 on the end, those 6 digits are microseconds.
266
        // But we want milliseconds, so bump that decimal point over 3 places.
267
        return (int) (microtime(true) * 1000);
268
    }
269
}
270