eric-chau/jarvis

View on GitHub
src/Jarvis.php

Summary

Maintainability
A
35 mins
Test Coverage
<?php

declare(strict_types=1);

namespace Jarvis;

use Jarvis\Skill\Core\CallbackResolver;
use Jarvis\Skill\DependencyInjection\Container;
use Jarvis\Skill\DependencyInjection\ContainerProvider;
use Jarvis\Skill\DependencyInjection\ContainerProviderInterface;
use Jarvis\Skill\EventBroadcaster\BroadcasterInterface;
use Jarvis\Skill\EventBroadcaster\BroadcasterTrait;
use Jarvis\Skill\EventBroadcaster\ControllerEvent;
use Jarvis\Skill\EventBroadcaster\EventInterface;
use Jarvis\Skill\EventBroadcaster\ExceptionEvent;
use Jarvis\Skill\EventBroadcaster\ResponseEvent;
use Jarvis\Skill\EventBroadcaster\RunEvent;
use Jarvis\Skill\Routing\Router;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Jarvis. Minimalist dependency injection container.
 *
 * @property bool                                              $debug
 * @property Router                                            $router
 * @property Request                                           $request
 * @property \Symfony\Component\HttpFoundation\Session\Session $session
 * @property CallbackResolver                                  $callbackResolver
 *
 * @author Eric Chau <eriic.chau@gmail.com>
 */
class Jarvis extends Container implements BroadcasterInterface
{
    use BroadcasterTrait {
        broadcast as traitBroadcast;
    }

    const DEFAULT_DEBUG = false;
    const CONTAINER_PROVIDER_FQCN = ContainerProvider::class;

    private $masterSetter = false;

    /**
     * Creates an instance of Jarvis. It can take settings as first argument.
     * List of accepted options:
     *   - providers (type: string|array): fqcn of your container provider
     *   - extra
     *
     * @param  array $settings Your own settings to modify Jarvis behavior
     */
    public function __construct(array $settings = [])
    {
        parent::__construct();

        $this['settings'] = $settings;
        $providers = array_merge([static::CONTAINER_PROVIDER_FQCN], (array) ($settings['providers'] ?? []));
        foreach (array_unique($providers) as $classname) {
            $this->hydrate(new $classname());
        }
    }

    public function __destruct()
    {
        $this->masterBroadcast(BroadcasterInterface::TERMINATE_EVENT);
    }

    /**
     * This method is an another way to get a locked value.
     *
     * Example: $this['foo'] is equal to $this->foo, but it ONLY works for locked values.
     *
     * @param  string $key The key of the locked value
     * @return mixed
     * @throws \InvalidArgumentException if the requested key is not associated to a locked service
     */
    public function __get(string $key)
    {
        if (!isset($this->locked[$key])) {
            throw new \InvalidArgumentException(sprintf('"%s" is not a key of a locked value.', $key));
        }

        $this->masterSet($key, $this[$key]);

        return $this->$key;
    }

    /**
     * Sets new attributes to Jarvis. Note that this method is reserved to Jarvis itself only.
     *
     * @param string $key   The key name of the new attribute
     * @param mixed  $value The value to associate to provided key
     * @throws \LogicException if this method is not called by Jarvis itself
     */
    public function __set(string $key, $value)
    {
        if (!$this->masterSetter) {
            throw new \LogicException('You are not allowed to set new attribute into Jarvis.');
        }

        $this->$key = $value;
    }

    /**
     * {@inheritdoc}
     */
    public function offsetSet($id, $v): void
    {
        parent::offsetSet($id, $v);

        if (!($v instanceof \Closure)) {
            return;
        }

        $refMethod = new \ReflectionMethod($v, '__invoke');
        if (null === $returntype = $refMethod->getReturnType()) {
            return;
        }

        $alias = $returntype->getName();
        if (
            $alias === $id
            || (!class_exists($alias) && !interface_exists($alias))
        ) {
            return;
        }

        if (!isset($this[$alias])) {
            $this->alias($alias, $id);
        } else {
            unset($this[$alias]);
        }
    }


    /**
     * @param  ContainerProviderInterface $provider
     */
    public function hydrate(ContainerProviderInterface $provider): void
    {
        $provider->hydrate($this);
    }

    /**
     * @param  Request|null $request
     * @return Response
     */
    public function run(Request $request = null): Response
    {
        $request = $request ?? $this[Request::class];
        $event = new RunEvent($request);

        try {
            $this->masterBroadcast(BroadcasterInterface::RUN_EVENT, $event);
            if ($response = $event->response()) {
                return $response;
            }

            [$callback, $arguments] = $this[Router::class]->match($request->getMethod(), $request->getPathInfo());
            $event = new ControllerEvent($this[CallbackResolver::class]->resolveReference($callback), $arguments);
            $this->masterBroadcast(BroadcasterInterface::CONTROLLER_EVENT, $event);

            $response = call_user_func_array($event->callback(), $event->arguments());
            $event = new ResponseEvent($request, $response);
            $this->masterBroadcast(BroadcasterInterface::RESPONSE_EVENT, $event);
        } catch (\Throwable $throwable) {
            $event = new ExceptionEvent($throwable);
            $this->masterBroadcast(BroadcasterInterface::EXCEPTION_EVENT, $event);
        }

        return $event->response();
    }

    /**
     * {@inheritdoc}
     */
    public function broadcast(string $name, EventInterface $event = null): void
    {
        if (!$this->masterEmitter && in_array($name, BroadcasterInterface::RESERVED_EVENT_NAMES)) {
            throw new \LogicException(sprintf(
                'You\'re trying to broadcast "$name" but "%s" are reserved event names.',
                implode('|', BroadcasterInterface::RESERVED_EVENT_NAMES)
            ));
        }

        $this->traitBroadcast($name, $event);
    }

    /**
     * {@inheritdoc}
     */
    protected function runReceiverCallback($receiver, EventInterface $event)
    {
        $this[CallbackResolver::class]->resolveAndCall($receiver, ['event' => $event]);
    }

    /**
     * Sets new attribute into Jarvis.
     *
     * @param  string $key   The name of the new attribute
     * @param  mixed  $value The value of the new attribute
     */
    private function masterSet(string $key, $value): void
    {
        $this->masterSetter = true;
        $this->$key = $value;
        $this->masterSetter = false;
    }

    /**
     * Enables master emitter mode.
     */
    private function masterBroadcast(string $name, EventInterface $event = null): void
    {
        $this->masterEmitter = true;
        $this->broadcast($name, $event);
        $this->masterEmitter = false;
    }
}