Completed
Push — master ( 14deb8...09c050 )
by Eric
8s
created

Jarvis::run()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 40
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 40
rs 8.439
cc 6
eloc 25
nc 18
nop 1
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace Jarvis;
6
7
use FastRoute\Dispatcher;
8
use Jarvis\Skill\DependencyInjection\Container;
9
use Jarvis\Skill\DependencyInjection\ContainerProvider;
10
use Jarvis\Skill\DependencyInjection\ContainerProviderInterface;
11
use Jarvis\Skill\EventBroadcaster\BroadcasterInterface;
12
use Jarvis\Skill\EventBroadcaster\ControllerEvent;
13
use Jarvis\Skill\EventBroadcaster\EventInterface;
14
use Jarvis\Skill\EventBroadcaster\ExceptionEvent;
15
use Jarvis\Skill\EventBroadcaster\PermanentEventInterface;
16
use Jarvis\Skill\EventBroadcaster\ResponseEvent;
17
use Jarvis\Skill\EventBroadcaster\RunEvent;
18
use Jarvis\Skill\EventBroadcaster\SimpleEvent;
19
use Symfony\Component\HttpFoundation\ParameterBag;
20
use Symfony\Component\HttpFoundation\Request;
21
use Symfony\Component\HttpFoundation\Response;
22
23
/**
24
 * Jarvis. Minimalist dependency injection container.
25
 *
26
 * @property boolean $debug
27
 * @property \Jarvis\Skill\Routing\Router $router
28
 * @property \Symfony\Component\HttpFoundation\Request $request
29
 * @property \Symfony\Component\HttpFoundation\Session\Session $session
30
 * @property \Jarvis\Skill\Core\CallbackResolver $callbackResolver
31
 * @property \Symfony\Component\HttpFoundation\ParameterBag $settings
32
 *
33
 * @author Eric Chau <[email protected]>
34
 */
35
class Jarvis extends Container implements BroadcasterInterface
36
{
37
    const DEFAULT_DEBUG = false;
38
    const CONTAINER_PROVIDER_FQCN = ContainerProvider::class;
39
40
    private $receivers = [];
41
    private $permanentEvents = [];
42
    private $computedReceivers = [];
43
    private $masterEmitter = false;
44
    private $masterSet = false;
45
46
    /**
47
     * Creates an instance of Jarvis. It can take settings as first argument.
48
     * List of accepted options:
49
     *   - container_provider (type: string|array): fqcn of your container provider
50
     *
51
     * @param  array $settings Your own settings to modify Jarvis behavior
52
     */
53
    public function __construct(array $settings = [])
54
    {
55
        parent::__construct();
56
57
        $this['settings'] = new ParameterBag($settings);
58
        $this->lock('settings');
59
60
        $this['debug'] = $this->settings->getBoolean('debug', static::DEFAULT_DEBUG);
61
        $this->lock('debug');
62
63
        if (!$this->settings->has('container_provider')) {
64
            $this->settings->set('container_provider', [static::CONTAINER_PROVIDER_FQCN]);
65
        } else {
66
            $containerProvider = (array) $this->settings->get('container_provider');
67
            array_unshift($containerProvider, static::CONTAINER_PROVIDER_FQCN);
68
            $this->settings->set('container_provider', $containerProvider);
69
        }
70
71
        foreach ($this->settings->get('container_provider') as $classname) {
72
            $this->hydrate(new $classname());
73
        }
74
    }
75
76
    public function __destruct()
77
    {
78
        $this->masterBroadcast(BroadcasterInterface::TERMINATE_EVENT);
79
    }
80
81
    /**
82
     * This method is an another way to get a locked value.
83
     *
84
     * Example: $this['foo'] is equal to $this->foo, but it ONLY works for locked values.
85
     *
86
     * @param  string $key The key of the locked value
87
     * @return mixed
88
     * @throws \InvalidArgumentException if the requested key is not associated to a locked service
89
     */
90
    public function __get(string $key)
91
    {
92
        if (!isset($this->locked[$key])) {
93
            throw new \InvalidArgumentException(sprintf('"%s" is not a key of a locked value.', $key));
94
        }
95
96
        $this->masterSetter($key, $this[$key]);
97
98
        return $this->$key;
99
    }
100
101
    /**
102
     * Sets new attributes to Jarvis. Note that this method is reserved to Jarvis itself only.
103
     *
104
     * @param string $key   The key name of the new attribute
105
     * @param mixed  $value The value to associate to provided key
106
     * @throws \LogicException if this method is not called by Jarvis itself
107
     */
108
    public function __set(string $key, $value)
109
    {
110
        if (!$this->masterSet) {
111
            throw new \LogicException('You are not allowed to set new attribute into Jarvis.');
112
        }
113
114
        $this->$key = $value;
115
    }
116
117
    /**
118
     * @param  Request|null $request
119
     * @return Response
120
     */
121
    public function run(Request $request = null): Response
122
    {
123
        $request = $request ?? $this->request;
124
        $response = null;
125
126
        try {
127
            $this->masterBroadcast(BroadcasterInterface::RUN_EVENT, $runEvent = new RunEvent($request));
128
129
            if ($response = $runEvent->response()) {
130
                return $response;
131
            }
132
133
            $routeInfo = $this->router->match($request->getMethod(), $request->getPathInfo());
134
            if (Dispatcher::FOUND === $routeInfo[0]) {
135
                $callback = $this->callbackResolver->resolve($routeInfo[1]);
136
137
                $event = new ControllerEvent($callback, $routeInfo[2]);
138
                $this->masterBroadcast(BroadcasterInterface::CONTROLLER_EVENT, $event);
139
140
                $response = call_user_func_array($event->callback(), $event->arguments());
141
142
                if (is_scalar($response)) {
143
                    $response = new Response((string) $response);
144
                }
145
            } else {
146
                $response = new Response(null, Dispatcher::NOT_FOUND === $routeInfo[0]
147
                    ? Response::HTTP_NOT_FOUND
148
                    : Response::HTTP_METHOD_NOT_ALLOWED
149
                );
150
            }
151
152
            $this->masterBroadcast(BroadcasterInterface::RESPONSE_EVENT, new ResponseEvent($request, $response));
153
        } catch (\Throwable $throwable) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
154
            $exceptionEvent = new ExceptionEvent($throwable);
155
            $this->masterBroadcast(BroadcasterInterface::EXCEPTION_EVENT, $exceptionEvent);
156
            $response = $exceptionEvent->response();
157
        }
158
159
        return $response;
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function on(string $eventName, $receiver, int $priority = BroadcasterInterface::RECEIVER_NORMAL_PRIORITY)
166
    {
167
        if (!isset($this->receivers[$eventName])) {
168
            $this->receivers[$eventName] = [
169
                BroadcasterInterface::RECEIVER_LOW_PRIORITY    => [],
170
                BroadcasterInterface::RECEIVER_NORMAL_PRIORITY => [],
171
                BroadcasterInterface::RECEIVER_HIGH_PRIORITY   => [],
172
            ];
173
        }
174
175
        $this->receivers[$eventName][$priority][] = $receiver;
176
        $this->computedReceivers[$eventName] = null;
177
178
        if (isset($this->permanentEvents[$eventName])) {
179
            $event = $this->permanentEvents[$eventName];
180
181
            call_user_func_array($this->callbackResolver->resolve($receiver), [$event]);
182
        }
183
184
        return $this;
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190
    public function broadcast(string $eventName, EventInterface $event = null)
191
    {
192
        if (!$this->masterEmitter && in_array($eventName, BroadcasterInterface::RESERVED_EVENT_NAMES)) {
193
            throw new \LogicException(sprintf(
194
                'You\'re trying to broadcast "$eventName" but "%s" are reserved event names.',
195
                implode('|', BroadcasterInterface::RESERVED_EVENT_NAMES)
196
            ));
197
        }
198
199
        if (isset($this->receivers[$eventName])) {
200
            $event = $event ?? new SimpleEvent();
201
            if ($event instanceof PermanentEventInterface && $event->isPermanent()) {
202
                $this->permanentEvents[$eventName] = $event;
203
            }
204
205
            foreach ($this->buildEventReceivers($eventName) as $receiver) {
206
                call_user_func_array($this->callbackResolver->resolve($receiver), [$event]);
207
208
                if ($event->isPropagationStopped()) {
209
                    break;
210
                }
211
            }
212
        }
213
214
        return $this;
215
    }
216
217
    /**
218
     * @param  ContainerProviderInterface $provider
219
     * @return self
220
     */
221
    public function hydrate(ContainerProviderInterface $provider): Jarvis
222
    {
223
        $provider->hydrate($this);
224
225
        return $this;
226
    }
227
228
    /**
229
     * Enables master emitter mode.
230
     *
231
     * @return self
232
     */
233
    private function masterBroadcast(string $eventName, EventInterface $event = null): Jarvis
234
    {
235
        $this->masterEmitter = true;
236
        $this->broadcast($eventName, $event);
237
        $this->masterEmitter = false;
238
239
        return $this;
240
    }
241
242
    /**
243
     * Sets new attribute into Jarvis.
244
     *
245
     * @param  string $key   The name of the new attribute
246
     * @param  mixed  $value The value of the new attribute
247
     * @return self
248
     */
249
    private function masterSetter(string $key, $value): Jarvis
250
    {
251
        $this->masterSet = true;
252
        $this->$key = $value;
253
        $this->masterSet = false;
254
255
        return $this;
256
    }
257
258
    /**
259
     * Builds and returns well ordered receivers collection that match with provided event name.
260
     *
261
     * @param  string $eventName The event name we want to get its receivers
262
     * @return array
263
     */
264
    private function buildEventReceivers(string $eventName): array
265
    {
266
        return $this->computedReceivers[$eventName] = $this->computedReceivers[$eventName] ?? array_merge(
267
            $this->receivers[$eventName][BroadcasterInterface::RECEIVER_HIGH_PRIORITY],
268
            $this->receivers[$eventName][BroadcasterInterface::RECEIVER_NORMAL_PRIORITY],
269
            $this->receivers[$eventName][BroadcasterInterface::RECEIVER_LOW_PRIORITY]
270
        );
271
    }
272
}
273