garrettw/noair

View on GitHub
src/Manager.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

namespace Noair;

/**
 * Repository for subscribers.
 *
 * @author  Garrett Whitehorn
 *
 * @version 1.0
 */
class Manager
{
    const PRIORITY_URGENT = 0;
    const PRIORITY_HIGHEST = 1;
    const PRIORITY_HIGH = 2;
    const PRIORITY_NORMAL = 3;
    const PRIORITY_LOW = 4;
    const PRIORITY_LOWEST = 5;

    /**
     * @internal
     *
     * @var array Contains registered events and their handlers by priority
     *
     * @since   1.0
     */
    protected $subscribers = [];

    /**
     * Determine if the event name has any subscribers.
     *
     * @api
     *
     * @param string $eventName The desired event's name
     *
     * @return bool Whether or not the event was published
     *
     * @since   1.0
     *
     * @version 1.0
     */
    public function hasSubscribers($eventName)
    {
        return (isset($this->subscribers[$eventName])
                && count($this->subscribers[$eventName]) > 1);
    }

    /**
     * Determine if the described event has been subscribed to or not by the callback.
     *
     * @api
     *
     * @param string   $eventName The desired event's name
     * @param callable $callback  The specific callback we're looking for
     *
     * @return int|false Subscriber's array index if found, false otherwise; use ===
     *
     * @since   1.0
     *
     * @version 1.0
     */
    public function isSubscribed($eventName, callable $callback)
    {
        return ($this->hasSubscribers($eventName))
            ? self::arraySearchDeep($callback, $this->subscribers[$eventName])
            : false;
    }

    /**
     * Handles inserting the new subscriber into the sorted internal array.
     *
     * @internal
     *
     * @param string $eventName The event it will listen for
     * @param int    $interval  The timer interval, if it's a timer (0 if not)
     * @param array  $handler   Each individual handler coming from the Observer
     *
     * @since   1.0
     *
     * @version 1.0
     */
    public function add($eventName, $interval, array $handler)
    {
        // scaffold if not exist
        if (!$this->hasSubscribers($eventName)) {
            $this->subscribers[$eventName] = [
                [ // insert positions
                    self::PRIORITY_URGENT => 1,
                    self::PRIORITY_HIGHEST => 1,
                    self::PRIORITY_HIGH => 1,
                    self::PRIORITY_NORMAL => 1,
                    self::PRIORITY_LOW => 1,
                    self::PRIORITY_LOWEST => 1,
                ]
            ];
        }

        switch (count($handler)) {
            case 1:
                $handler[] = self::PRIORITY_NORMAL;
                // no break
            case 2:
                $handler[] = false;
        }

        $sub = [
            'callback' => $handler[0],
            'priority' => $priority = $handler[1],
            'force' => $handler[2],
            'interval' => $interval,
            'nextcalltime' => self::currentTimeMillis() + $interval,
        ];

        $insertpos = $this->subscribers[$eventName][0][$priority];
        array_splice($this->subscribers[$eventName], $insertpos, 0, [$sub]);

        $this->realign($eventName, $priority);
    }

    /**
     * Takes care of actually calling the event handling functions
     *
     * @internal
     *
     * @param string $eventName
     * @param Event  $event
     * @param mixed  $result
     *
     * @since   1.0
     *
     * @version 1.0
     */
    public function fire($eventName, Event $event, $result = null)
    {
        $subs = $this->subscribers[$eventName];
        unset($subs[0]);

        // Loop through the subscribers of this event
        foreach ($subs as $i => $subscriber) {

            // If the event's cancelled and the subscriber isn't forced, skip it
            if ($event->cancelled && $subscriber['force'] === false) {
                continue;
            }

            // If the subscriber is a timer...
            if ($subscriber['interval'] !== 0) {
                // Then if the current time is before when the sub needs to be called
                if (self::currentTimeMillis() < $subscriber['nextcalltime']) {
                    // It's not time yet, so skip it
                    continue;
                }

                // Mark down the next call time as another interval away
                $this->subscribers[$eventName][$i]['nextcalltime']
                    += $subscriber['interval'];
            }

            // Fire it and save the result for passing to any further subscribers
            $event->previousResult = $result;
            $result = call_user_func($subscriber['callback'], $event);
        }

        return $result;
    }

    /**
     *
     */
    public function remove($eventName)
    {
        unset($this->subscribers[$eventName]);
    }

    /**
     *
     * @param callable $callback
     */
    public function searchAndDestroy($eventName, $callback)
    {
        // Loop through the subscribers for the matching event
        foreach ($this->subscribers[$eventName] as $key => $subscriber) {

            // if this subscriber doesn't match what we're looking for, keep looking
            if (self::arraySearchDeep($callback, $subscriber) === false) {
                continue;
            }

            // otherwise, cut it out and get its priority
            $priority = array_splice($this->subscribers[$eventName], $key, 1)[0]['priority'];

            // shift the insertion points up for equal and lower priorities
            $this->realign($eventName, $priority, -1);
        }

        // If there are no more events, remove the event
        if (!$this->hasSubscribers($eventName)) {
            unset($this->subscribers[$eventName]);
        }
    }

    /**
     *
     */
    protected function realign($eventName, $priority, $inc = 1)
    {
        for ($prio = $priority; $prio <= self::PRIORITY_LOWEST; $prio++) {
            $this->subscribers[$eventName][0][$prio] += $inc;
        }
    }

    /**
     * Searches a multi-dimensional array for a value in any dimension.
     *
     * @internal
     *
     * @param mixed $needle   The value to be searched for
     * @param array $haystack The array
     *
     * @return int|bool The top-level key containing the needle if found, false otherwise
     *
     * @since   1.0
     *
     * @version 1.0
     */
    protected static function arraySearchDeep($needle, array $haystack)
    {
        if (is_array($needle)
            && !is_callable($needle)
            // and if all key/value pairs in $needle have exact matches in $haystack
            && count(array_diff_assoc($needle, $haystack)) == 0
        ) {
            // we found what we're looking for, so bubble back up with 'true'
            return true;
        }

        foreach ($haystack as $key => $value) {
            if ($needle === $value
                || (is_array($value) && self::arraySearchDeep($needle, $value) !== false)
            ) {
                // return top-level key of $haystack that contains $needle as a value somewhere
                return $key;
            }
        }
        // 404 $needle not found
        return false;
    }

    /**
     * Returns the current timestamp in milliseconds.
     * Named for the similar function in Java.
     *
     * @internal
     *
     * @return int Current timestamp in milliseconds
     *
     * @since   1.0
     *
     * @version 1.0
     */
    final protected static function currentTimeMillis()
    {
        // microtime(true) returns a float where there's 4 digits after the
        // decimal and if you add 00 on the end, those 6 digits are microseconds.
        // But we want milliseconds, so bump that decimal point over 3 places.
        return (int) (microtime(true) * 1000);
    }
}