Completed
Push — master ( 383466...f53a3b )
by Garrett
03:21
created

Mediator::realignPriorities()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 3
1
<?php
2
3
namespace Noair;
4
5
/**
6
 * Main event pipeline.
7
 *
8
 * @author  Garrett Whitehorn
9
 * @author  David Tkachuk
10
 *
11
 * @version 1.0
12
 */
13
class Mediator implements Observable
14
{
15
    const PRIORITY_URGENT = 0;
16
    const PRIORITY_HIGHEST = 1;
17
    const PRIORITY_HIGH = 2;
18
    const PRIORITY_NORMAL = 3;
19
    const PRIORITY_LOW = 4;
20
    const PRIORITY_LOWEST = 5;
21
22
    /**
23
     * @api
24
     *
25
     * @var array Holds any published events to which no handler has yet subscribed
26
     *
27
     * @since   1.0
28
     */
29
    public $held = [];
30
31
    /**
32
     * @internal
33
     *
34
     * @var bool Whether we should put published events for which there are no subscribers onto the list.
35
     *
36
     * @since   1.0
37
     */
38
    protected $holdingUnheardEvents = false;
39
40
    /**
41
     * @internal
42
     *
43
     * @var array Contains registered events and their handlers by priority
44
     *
45
     * @since   1.0
46
     */
47
    protected $subscribers = [];
48
49
    /**
50
     * Registers event handler(s) to event name(s).
51
     *
52
     * @api
53
     *
54
     * @throws BadMethodCallException if validation of any handler fails
55
     *
56
     * @param array $eventHandlers Associative array of event names & handlers
57
     *
58
     * @return array The results of firing any held events
59
     *
60
     * @since   1.0
61
     *
62
     * @version 1.0
63
     */
64
    public function subscribe(array $eventHandlers)
65
    {
66
        $results = [];
67
68
        foreach ($eventHandlers as $eventName => $handler) {
69
            if (!self::isValidHandler($handler)) {
70
                throw new \BadMethodCallException('Mediator::subscribe() - invalid handler passed for ' . $eventName);
71
            }
72
73
            // extract interval (in milliseconds) from $eventName
74
            $interval = 0;
75
            if (strpos($eventName, 'timer:') === 0) {
76
                $interval = (int) substr($eventName, 6);
77
                $eventName = 'timer';
78
            }
79
80
            $this->addNewSub($eventName, $interval, $handler);
81
82
            // there will never be held timer events, but otherwise fire matching held events
83
            if ($interval === 0) {
84
                $results[] = $this->fireHeldEvents($eventName);
85
            }
86
        }
87
88
        return $results;
89
    }
90
91
    /**
92
     * Let any relevant subscribers know an event needs to be handled.
93
     *
94
     * Note: The event object can be used to share information to other similar event handlers.
95
     *
96
     * @api
97
     *
98
     * @param Event $event An event object, usually freshly created
99
     *
100
     * @return mixed Result of the event
101
     *
102
     * @since   1.0
103
     *
104
     * @version 1.0
105
     */
106
    public function publish(Event $event)
107
    {
108
        $event->mediator = $this;
109
        $result = null;
110
111
        // Make sure event is fired to any subscribers that listen to all events
112
        // all is greedy, any is not - due to order
113
        foreach (['all', $event->name, 'any'] as $eventName) {
114
            if ($this->hasSubscribers($eventName)) {
115
                $result = $this->fireMatchingSubs($eventName, $event, $result);
116
            }
117
        }
118
119
        if ($result !== null) {
120
            return $result;
121
        }
122
123
        // If no subscribers were listening to this event, try holding it
124
        $this->tryHolding($event);
125
    }
126
127
    /**
128
     * Detach a given handler (or all) from an event name.
129
     *
130
     * @api
131
     *
132
     * @param array $eventHandlers Associative array of event names & handlers
133
     *
134
     * @return self This object
135
     *
136
     * @since   1.0
137
     *
138
     * @version 1.0
139
     */
140
    public function unsubscribe(array $eventHandlers)
141
    {
142
        foreach ($eventHandlers as $eventName => $callback) {
143
            if ($callback == '*') {
144
                // we're unsubscribing all of $eventName
145
                unset($this->subscribers[$eventName]);
146
                continue;
147
            }
148
149
            $callback = $this->formatCallback($eventName, $callback);
150
151
            // if this is a timer subscriber
152
            if (strpos($eventName, 'timer:') === 0) {
153
                // then we'll need to match not only the callback but also the interval
154
                $callback = [
155
                    'interval' => (int) substr($eventName, 6),
156
                    'callback' => $callback,
157
                ];
158
                $eventName = 'timer';
159
            }
160
161
            $this->searchAndDestroy($eventName, $callback);
162
        }
163
164
        return $this;
165
    }
166
167
    /**
168
     * Determine if the event name has any subscribers.
169
     *
170
     * @api
171
     *
172
     * @param string $eventName The desired event's name
173
     *
174
     * @return bool Whether or not the event was published
175
     *
176
     * @since   1.0
177
     *
178
     * @version 1.0
179
     */
180
    public function hasSubscribers($eventName)
181
    {
182
        return (isset($this->subscribers[$eventName])
183
                && count($this->subscribers[$eventName]) > 1);
184
    }
185
186
    /**
187
     * Get or set the value of the holdingUnheardEvents property.
188
     *
189
     * @api
190
     *
191
     * @param bool|null $val true or false to set the value, omit to retrieve
192
     *
193
     * @return bool the value of the property
194
     *
195
     * @since   1.0
196
     *
197
     * @version 1.0
198
     */
199
    public function holdUnheardEvents($val = null)
200
    {
201
        if ($val === null) {
202
            return $this->holdingUnheardEvents;
203
        }
204
205
        $val = (bool) $val;
206
        if ($val === false) {
207
            $this->held = []; // make sure the held list is wiped clean
208
        }
209
210
        return ($this->holdingUnheardEvents = $val);
211
    }
212
213
    /**
214
     * Determine if the described event has been subscribed to or not by the callback.
215
     *
216
     * @api
217
     *
218
     * @param string   $eventName The desired event's name
219
     * @param callable $callback  The specific callback we're looking for
220
     *
221
     * @return int|false Subscriber's array index if found, false otherwise; use ===
222
     *
223
     * @since   1.0
224
     *
225
     * @version 1.0
226
     */
227
    public function isSubscribed($eventName, callable $callback)
228
    {
229
        return ($this->hasSubscribers($eventName))
230
            ? self::arraySearchDeep($callback, $this->subscribers[$eventName])
231
            : false;
232
    }
233
234
    /**
235
     * If any events are held for $eventName, re-publish them now.
236
     *
237
     * @internal
238
     *
239
     * @param string $eventName The event name to check for
240
     *
241
     * @since   1.0
242
     *
243
     * @version 1.0
244
     */
245
    protected function fireHeldEvents($eventName)
246
    {
247
        $results = [];
248
        // loop through any held events
249
        foreach ($this->held as $i => $e) {
250
            // if this held event's name matches our new subscriber
251
            if ($e->getName() == $eventName) {
252
                // re-publish that matching held event
253
                $results[] = $this->publish(array_splice($this->held, $i, 1)[0]);
254
            }
255
        }
256
257
        return $results;
258
    }
259
260
    /**
261
     * Handles inserting the new subscriber into the sorted internal array.
262
     *
263
     * @internal
264
     *
265
     * @param string $eventName The event it will listen for
266
     * @param int    $interval  The timer interval, if it's a timer (0 if not)
267
     * @param array  $handler   Each individual handler coming from the Observer
268
     *
269
     * @since   1.0
270
     *
271
     * @version 1.0
272
     */
273
    protected function addNewSub($eventName, $interval, array $handler)
274
    {
275
        // scaffold if not exist
276
        if (!$this->hasSubscribers($eventName)) {
277
            $this->subscribers[$eventName] = [
278
                [ // insert positions
279
                    self::PRIORITY_URGENT => 1,
280
                    self::PRIORITY_HIGHEST => 1,
281
                    self::PRIORITY_HIGH => 1,
282
                    self::PRIORITY_NORMAL => 1,
283
                    self::PRIORITY_LOW => 1,
284
                    self::PRIORITY_LOWEST => 1,
285
                ]
286
            ];
287
        }
288
289
        switch (count($handler)) {
290
            case 1:
291
                $handler[] = self::PRIORITY_NORMAL;
292
                // fall through
293
            case 2:
294
                $handler[] = false;
295
        }
296
297
        $sub = [
298
            'callback' => $handler[0],
299
            'priority' => $priority = $handler[1],
300
            'force' => $handler[2],
301
            'interval' => $interval,
302
            'nextcalltime' => self::currentTimeMillis() + $interval,
303
        ];
304
305
        $insertpos = $this->subscribers[$eventName][0][$priority];
306
        array_splice($this->subscribers[$eventName], $insertpos, 0, [$sub]);
307
308
        $this->realignPriorities($eventName, $priority);
309
    }
310
311
    /**
312
     * Takes care of actually calling the event handling functions
313
     *
314
     * @internal
315
     *
316
     * @param string $eventName
317
     * @param Event  $event
318
     * @param mixed  $result
319
     *
320
     * @since   1.0
321
     *
322
     * @version 1.0
323
     */
324
    protected function fireMatchingSubs($eventName, Event $event, $result = null)
325
    {
326
        $subs = $this->subscribers[$eventName];
327
        unset($subs[0]);
328
329
        // Loop through the subscribers of this event
330
        foreach ($subs as $i => $subscriber) {
331
332
            // If the event's cancelled and the subscriber isn't forced, skip it
333
            if ($event->cancelled && $subscriber['force'] === false) {
334
                continue;
335
            }
336
337
            // If the subscriber is a timer...
338
            if ($subscriber['interval'] !== 0) {
339
                // Then if the current time is before when the sub needs to be called
340
                if (self::currentTimeMillis() < $subscriber['nextcalltime']) {
341
                    // It's not time yet, so skip it
342
                    continue;
343
                }
344
345
                // Mark down the next call time as another interval away
346
                $this->subscribers[$eventName][$i]['nextcalltime']
347
                    += $subscriber['interval'];
348
            }
349
350
            // Fire it and save the result for passing to any further subscribers
351
            $event->previousResult = $result;
352
            $result = call_user_func($subscriber['callback'], $event);
353
        }
354
355
        return $result;
356
    }
357
358
    /**
359
     *
360
     */
361
    protected function formatCallback($eventName, $callback)
362
    {
363
        if (is_object($callback) && $callback instanceof Observer) {
364
            // assume we're unsubscribing a parsed method name
365
            $callback = [$callback, 'on' . str_replace(':', '', ucfirst($eventName))];
366
        }
367
368
        if (is_array($callback) && !is_callable($callback)) {
369
            // we've probably been given an Observer's handler array
370
            $callback = $callback[0];
371
        }
372
373
        if (!is_callable($callback)) {
374
            // callback is invalid, so halt
375
            throw new \InvalidArgumentException('Cannot unsubscribe a non-callable');
376
        }
377
378
        return $callback;
379
    }
380
381
    /**
382
     *
383
     */
384
    protected function realignPriorities($eventName, $priority, $inc = 1)
385
    {
386
        for ($prio = $priority; $prio <= self::PRIORITY_LOWEST; $prio++) {
387
            $this->subscribers[$eventName][0][$prio] += $inc;
388
        }
389
    }
390
391
    /**
392
     *
393
     * @param callable $callback
394
     */
395
    protected function searchAndDestroy($eventName, $callback)
396
    {
397
        // Loop through the subscribers for the matching event
398
        foreach ($this->subscribers[$eventName] as $key => $subscriber) {
399
400
            // if this subscriber doesn't match what we're looking for, keep looking
401
            if (self::arraySearchDeep($callback, $subscriber) === false) {
402
                continue;
403
            }
404
405
            // otherwise, cut it out and get its priority
406
            $priority = array_splice($this->subscribers[$eventName], $key, 1)[0]['priority'];
407
408
            // shift the insertion points up for equal and lower priorities
409
            $this->realignPriorities($eventName, $priority, -1);
410
        }
411
412
        // If there are no more events, remove the event
413
        if (!$this->hasSubscribers($eventName)) {
414
            unset($this->subscribers[$eventName]);
415
        }
416
    }
417
418
    /**
419
     * Puts an event on the held list if enabled and not a timer.
420
     *
421
     * @internal
422
     *
423
     * @param Event $event The event object to be held
424
     *
425
     * @since   1.0
426
     *
427
     * @version 1.0
428
     */
429
    protected function tryHolding(Event $event)
430
    {
431
        if ($this->holdingUnheardEvents && $event->name != 'timer') {
432
            array_unshift($this->held, $event);
433
        }
434
    }
435
436
    /**
437
     * Searches a multi-dimensional array for a value in any dimension.
438
     *
439
     * @internal
440
     *
441
     * @param mixed $needle   The value to be searched for
442
     * @param array $haystack The array
443
     *
444
     * @return int|bool The top-level key containing the needle if found, false otherwise
445
     *
446
     * @since   1.0
447
     *
448
     * @version 1.0
449
     */
450
    protected static function arraySearchDeep($needle, array $haystack)
451
    {
452
        if (is_array($needle)
453
            && !is_callable($needle)
454
            // and if all key/value pairs in $needle have exact matches in $haystack
455
            && count(array_diff_assoc($needle, $haystack)) == 0
456
        ) {
457
            // we found what we're looking for, so bubble back up with 'true'
458
            return true;
459
        }
460
461
        foreach ($haystack as $key => $value) {
462
            if ($needle === $value
463
                || (is_array($value) && self::arraySearchDeep($needle, $value) !== false)
464
            ) {
465
                // return top-level key of $haystack that contains $needle as a value somewhere
466
                return $key;
467
            }
468
        }
469
        // 404 $needle not found
470
        return false;
471
    }
472
473
    /**
474
     *
475
     */
476
    protected static function isValidHandler($handler)
477
    {
478
        return (is_callable($handler[0])
479
                && (!isset($handler[1]) || is_int($handler[1]))
480
                && (!isset($handler[2]) || is_bool($handler[2]))
481
        );
482
    }
483
484
    /**
485
     * Returns the current timestamp in milliseconds.
486
     * Named for the similar function in Java.
487
     *
488
     * @internal
489
     *
490
     * @return int Current timestamp in milliseconds
491
     *
492
     * @since   1.0
493
     *
494
     * @version 1.0
495
     */
496
    final protected static function currentTimeMillis()
497
    {
498
        // microtime(true) returns a float where there's 4 digits after the
499
        // decimal and if you add 00 on the end, those 6 digits are microseconds.
500
        // But we want milliseconds, so bump that decimal point over 3 places.
501
        return (int) (microtime(true) * 1000);
502
    }
503
}
504