bkdotcom/PubSub

View on GitHub
src/ManagerHelperTrait.php

Summary

Maintainability
A
2 hrs
Test Coverage
A
100%
<?php

/**
 * Manage event subscriptions
 *
 * @package   bdk\PubSub
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2023 Brad Kent
 * @version   v3.1
 * @link      http://www.github.com/bkdotcom/PubSub
 */

namespace bdk\PubSub;

use bdk\PubSub\SubscriberInterface;
use Closure;
use InvalidArgumentException;
use RuntimeException;

/**
 * Support methods for manager
 */
trait ManagerHelperTrait
{
    /**
     * Test if value is a callable or "closure factory"
     *
     * @param mixed $val Value to test
     *
     * @return void
     *
     * @throws InvalidArgumentException
     */
    private static function assertCallable($val)
    {
        if (\is_callable($val, true)) {
            return;
        }
        if (self::isClosureFactory($val)) {
            return;
        }
        throw new InvalidArgumentException(\sprintf(
            'Expected callable or "closure factory", but %s provided',
            self::getDebugType($val)
        ));
    }

    /**
     * As a convenience, we'll attach subscriber's return value to event['return']
     *
     *     * Event must already have 'return' value defined and must be `null` or ""
     *
     * @param mixed $return return value
     * @param Event $event  Event instance
     *
     * @return void
     */
    private function attachReturnToEvent($return, Event $event)
    {
        if ($return === null) {
            return;
        }
        if (\in_array($event['return'], array(null, ''), true) === false) {
            // event already has non-null return value
            return;
        }
        if (\array_key_exists('return', $event->getValues()) === false) {
            // return value not defined / not expected to be set
            return;
        }
        $event['return'] = $return;
    }

    /**
     * Instantiate the object wrapped in the closure factory
     * closure factory may be
     *    [Closure, 'methodName'] - closure returns object
     *    [Closure] - closure returns object that is callable (ie has __invoke)
     *
     * @param array $closureFactory "closure factory" lazy loads an object / subscriber
     *
     * @return callable
     */
    private function doClosureFactory($closureFactory = array())
    {
        $closureFactory[0] = $closureFactory[0]($this);
        return \count($closureFactory) === 1
            ? $closureFactory[0]    // invokeable object
            : $closureFactory;      // [obj, 'method']
    }

    /**
     * Gets the type name of a variable in a way that is suitable for debugging
     *
     * @param mixed $value Value to inspect
     *
     * @return string
     */
    private static function getDebugType($value)
    {
        return \is_object($value)
            ? \get_class($value)
            : \gettype($value);
    }

    /**
     * Calls the passed object's getSubscriptions() method and returns a normalized list of subscriptions
     *
     * @param SubscriberInterface $interface SubscriberInterface instance
     *
     * @return array
     *
     * @throws RuntimeException
     */
    private static function getInterfaceSubscribers(SubscriberInterface $interface)
    {
        $subscriptions = $interface->getSubscriptions();
        if (\is_array($subscriptions) === false) {
            throw new RuntimeException(\sprintf(
                'Expected array from %s::getSubscriptions().  Got %s',
                \get_class($interface),
                self::getDebugType($subscriptions)
            ));
        }
        foreach ($subscriptions as $eventName => $mixed) {
            $eventSubscribers = self::normalizeInterfaceSubscribers($interface, $mixed);
            if ($eventSubscribers === false) {
                throw new RuntimeException(\sprintf(
                    '%s::getSubscriptions():  Unexpected subscriber(s) defined for %s',
                    \get_class($interface),
                    $eventName
                ));
            }
            $subscriptions[$eventName] = $eventSubscribers;
        }
        return $subscriptions;
    }

    /**
     * Does val appear to be a "closure factory"?
     * array & array[0] instanceof Closure
     *
     * @param mixed $val value to check
     *
     * @return bool
     *
     * @psalm-assert-if-true array $val
     */
    private static function isClosureFactory($val)
    {
        return \is_array($val) && isset($val[0]) && $val[0] instanceof Closure;
    }

    /**
     * Normalize event subscribers
     *
     * @param SubscriberInterface $interface SubscriberInterface instance
     * @param string|array        $mixed     method(s) with optional priority/onlyOnce
     *
     * @return array|false list of array(methodName, priority)
     */
    private static function normalizeInterfaceSubscribers(SubscriberInterface $interface, $mixed)
    {
        // test if single subscriber
        $subscriberInfo = self::normalizeInterfaceSubscriber($interface, $mixed);
        if ($subscriberInfo) {
            return array($subscriberInfo);
        }
        if (\is_array($mixed) === false) {
            return false;
        }
        // multiple subscribers
        $eventSubscribers = array();
        foreach ($mixed as $mixed2) {
            $subscriberInfo = self::normalizeInterfaceSubscriber($interface, $mixed2);
            if ($subscriberInfo) {
                $eventSubscribers[] = $subscriberInfo;
                continue;
            }
            return false;
        }
        return $eventSubscribers;
    }

    /**
     * Test if value defines method/priority/onlyOnce
     *
     * @param SubscriberInterface  $interface SubscriberInterface instance
     * @param array|string|Closure $mixed     method/priority/onlyOnce info
     *
     * @return array|false
     */
    private static function normalizeInterfaceSubscriber(SubscriberInterface $interface, $mixed)
    {
        $subscriberInfo = array(
            'callable' => null,
            'onlyOnce' => false,
            'priority' => self::DEFAULT_PRIORITY,
        );
        if (\is_string($mixed)) {
            $subscriberInfo['callable'] = array($interface, $mixed);
            return $subscriberInfo;
        }
        if ($mixed instanceof Closure) {
            $subscriberInfo['callable'] = $mixed;
            return $subscriberInfo;
        }
        if (\is_array($mixed)) {
            $subscriberInfo = self::normalizeInterfaceSubscriberArray($interface, $mixed, $subscriberInfo);
        }
        return $subscriberInfo['callable'] !== null
            ? $subscriberInfo
            : false;
    }

    /**
     * Test if given array defines method/priority/onlyOnce
     *
     * @param SubscriberInterface $interface      SubscriberInterface instance
     * @param array               $values         array values
     * @param array               $subscriberInfo default subscriberInfo values
     *
     * @return array updated subscriberInfo
     */
    private static function normalizeInterfaceSubscriberArray(SubscriberInterface $interface, array $values, array $subscriberInfo)
    {
        $tests = array(
            'callable' => static function ($val) {
                return \is_string($val) || ($val instanceof Closure);
            },
            'onlyOnce' => 'is_bool',
            'priority' => 'is_int',
        );
        while ($values && $tests) {
            $val = \array_shift($values);
            foreach ($tests as $key => $test) {
                if ($test($val)) {
                    $subscriberInfo[$key] = $val;
                    unset($tests[$key]);
                    continue 2;
                }
            }
            // all tests failed for current value
            $subscriberInfo['callable'] = null;
            break;
        }
        if (\is_string($subscriberInfo['callable'])) {
            $subscriberInfo['callable'] = array($interface, $subscriberInfo['callable']);
        }
        return $subscriberInfo;
    }

    /**
     * Sorts the internal list of subscribers for the given event by priority.
     * Any closure factories for eventName are invoked
     *
     * @param string $eventName The name of the event
     *
     * @return void
     */
    private function prepSubscribers($eventName)
    {
        if (!isset($this->subscribers[$eventName])) {
            $this->subscribers[$eventName] = array();
        }
        \krsort($this->subscribers[$eventName]);
        $this->sorted[$eventName] = array();
        $priorities = \array_keys($this->subscribers[$eventName]);
        \array_map(function ($priority) use ($eventName) {
            $eventSubscribers = $this->subscribers[$eventName][$priority];
            foreach ($eventSubscribers as $k => $subscriberInfo) {
                if ($this->isClosureFactory($subscriberInfo['callable'])) {
                    $subscriberInfo['callable'] = $this->doClosureFactory($subscriberInfo['callable']);
                    $this->subscribers[$eventName][$priority][$k] = $subscriberInfo;
                }
                $this->sorted[$eventName][] = $subscriberInfo;
            }
        }, $priorities);
    }

    /**
     * Prep and sort subscribers for the specified event name
     *
     * @param string $eventName The name of the event
     *
     * @return void
     */
    private function setSorted($eventName)
    {
        if (!isset($this->sorted[$eventName])) {
            $this->prepSubscribers($eventName);
        }
    }

    /**
     * Add subscriber to subscriber stack
     *
     * @param int   $stackIndex        subscriberStack index to add to
     * @param array $subscriberInfoNew subscriber info (callable, priority, onlyOnce)
     *
     * @return void
     */
    private function subscribeActive($stackIndex, array $subscriberInfoNew)
    {
        $eventSubscribers = $this->subscriberStack[$stackIndex]['subscribers'];
        $priority = $subscriberInfoNew['priority'];
        foreach ($eventSubscribers as $i => $subscriberInfo) {
            if ($priority > $subscriberInfo['priority']) {
                \array_splice($this->subscriberStack[$stackIndex]['subscribers'], $i, 0, array($subscriberInfoNew));
                return;
            }
        }
        $this->subscriberStack[$stackIndex]['subscribers'][] = $subscriberInfoNew;
    }

    /**
     * Remove callable from active event subscribers
     *
     * @param int      $stackIndex subscriberStack index to add to
     * @param callable $callable   callable
     * @param int      $priority   The priority
     *
     * @return void
     */
    private function unsubscribeActive($stackIndex, $callable, $priority)
    {
        $search = \array_filter(array(
            'callable' => $callable,
            'priority' => $priority,
        ));
        $eventSubscribers = $this->subscriberStack[$stackIndex]['subscribers'];
        foreach ($eventSubscribers as $i => $subscriberInfo) {
            if (\array_intersect_key($subscriberInfo, $search) === $search) {
                \array_splice($this->subscriberStack[$stackIndex]['subscribers'], $i, 1);
            }
        }
    }

    /**
     * Find callable in eventName/priority array and remove it
     *
     * @param string   $eventName The event we're unsubscribing from
     * @param callable $callable  callable
     * @param int      $priority  The priority
     * @param bool     $onlyOnce  Only unsubscribe "onlyOnce" subscribers
     *
     * @return void
     */
    private function unsubscribeFromPriority($eventName, $callable, $priority, $onlyOnce)
    {
        $search = \array_filter(array(
            'callable' => $callable,
            'onlyOnce' => $onlyOnce,
        ));
        foreach ($this->subscribers[$eventName][$priority] as $k => $subscriberInfo) {
            if (\array_intersect_key($subscriberInfo, $search) !== $search) {
                continue;
            }
            unset($this->subscribers[$eventName][$priority][$k], $this->sorted[$eventName]);
            if ($onlyOnce) {
                break;
            }
        }
        if (empty($this->subscribers[$eventName][$priority])) {
            unset($this->subscribers[$eventName][$priority]);
        }
    }
}