Completed
Push — master ( d2c583...a726c1 )
by Eric
8s
created

Jarvis::on()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 21
rs 9.3142
cc 3
eloc 12
nc 4
nop 3
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\AnalyzeEvent;
12
use Jarvis\Skill\EventBroadcaster\ControllerEvent;
13
use Jarvis\Skill\EventBroadcaster\BroadcasterInterface;
14
use Jarvis\Skill\EventBroadcaster\EventInterface;
15
use Jarvis\Skill\EventBroadcaster\ExceptionEvent;
16
use Jarvis\Skill\EventBroadcaster\PermanentEventInterface;
17
use Jarvis\Skill\EventBroadcaster\ResponseEvent;
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 \Jarvis\Skill\Core\ScopeManager $scopeManager
32
 * @property \Symfony\Component\HttpFoundation\ParameterBag $settings
33
 *
34
 * @author Eric Chau <[email protected]>
35
 */
36
class Jarvis extends Container implements BroadcasterInterface
37
{
38
    const DEFAULT_DEBUG = false;
39
    const CONTAINER_PROVIDER_FQCN = ContainerProvider::class;
40
    const DEFAULT_SCOPE = 'default';
41
42
    private $receivers = [];
43
    private $permanentEvents = [];
44
    private $computedReceivers = [];
45
    private $masterEmitter = false;
46
    private $masterSet = false;
47
48
    /**
49
     * Creates an instance of Jarvis. It can take settings as first argument.
50
     * List of accepted options:
51
     *   - container_provider (type: string|array): fqcn of your container provider
52
     *
53
     * @param  array $settings Your own settings to modify Jarvis behavior
54
     */
55
    public function __construct(array $settings = [])
56
    {
57
        parent::__construct();
58
59
        $this['settings'] = new ParameterBag($settings);
60
        $this->lock('settings');
61
62
        $this['debug'] = $this->settings->getBoolean('debug', static::DEFAULT_DEBUG);
63
        $this->lock('debug');
64
65
        if (!$this->settings->has('container_provider')) {
66
            $this->settings->set('container_provider', [static::CONTAINER_PROVIDER_FQCN]);
67
        } else {
68
            $containerProvider = (array) $this->settings->get('container_provider');
69
            array_unshift($containerProvider, static::CONTAINER_PROVIDER_FQCN);
70
            $this->settings->set('container_provider', $containerProvider);
71
        }
72
73
        foreach ($this->settings->get('container_provider') as $classname) {
74
            $this->hydrate(new $classname());
75
        }
76
    }
77
78
    public function __destruct()
79
    {
80
        $this->masterBroadcast(BroadcasterInterface::TERMINATE_EVENT);
81
    }
82
83
    /**
84
     * This method is an another way to get a locked value.
85
     *
86
     * Example: $this['foo'] is equal to $this->foo, but it ONLY works for locked values.
87
     *
88
     * @param  string $key The key of the locked value
89
     * @return mixed
90
     * @throws \InvalidArgumentException if the requested key is not associated to a locked service
91
     */
92
    public function __get(string $key)
93
    {
94
        if (!isset($this->locked[$key])) {
95
            throw new \InvalidArgumentException(sprintf('"%s" is not a key of a locked value.', $key));
96
        }
97
98
        $this->masterSetter($key, $this[$key]);
99
100
        return $this->$key;
101
    }
102
103
    /**
104
     * Sets new attributes to Jarvis. Note that this method is reserved to Jarvis itself only.
105
     *
106
     * @param string $key   The key name of the new attribute
107
     * @param mixed  $value The value to associate to provided key
108
     * @throws \LogicException if this method is not called by Jarvis itself
109
     */
110
    public function __set(string $key, $value)
111
    {
112
        if (!$this->masterSet) {
113
            throw new \LogicException('You are not allowed to set new attribute into Jarvis.');
114
        }
115
116
        $this->$key = $value;
117
    }
118
119
    /**
120
     * @param  Request|null $request
121
     * @return Response
122
     */
123
    public function analyze(Request $request = null): Response
124
    {
125
        $request = $request ?? $this->request;
126
        $response = null;
127
128
        try {
129
            $this->masterBroadcast(BroadcasterInterface::ANALYZE_EVENT, $analyzeEvent = new AnalyzeEvent($request));
130
131
            if ($response = $analyzeEvent->response()) {
132
                return $response;
133
            }
134
135
            $routeInfo = $this->router->match($request->getMethod(), $request->getPathInfo());
136
            if (Dispatcher::FOUND === $routeInfo[0]) {
137
                $callback = $this->callbackResolver->resolve($routeInfo[1]);
138
139
                $event = new ControllerEvent($callback, $routeInfo[2]);
140
                $this->masterBroadcast(BroadcasterInterface::CONTROLLER_EVENT, $event);
141
142
                $response = call_user_func_array($event->callback(), $event->arguments());
143
144
                if (is_scalar($response)) {
145
                    $response = new Response((string) $response);
146
                }
147
            } elseif (Dispatcher::NOT_FOUND === $routeInfo[0] || Dispatcher::METHOD_NOT_ALLOWED === $routeInfo[0]) {
148
                $response = new Response(null, Dispatcher::NOT_FOUND === $routeInfo[0]
149
                    ? Response::HTTP_NOT_FOUND
150
                    : Response::HTTP_METHOD_NOT_ALLOWED
151
                );
152
            }
153
154
            $this->masterBroadcast(BroadcasterInterface::RESPONSE_EVENT, new ResponseEvent($request, $response));
155
        } 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...
156
            $exceptionEvent = new ExceptionEvent($throwable);
157
            $this->masterBroadcast(BroadcasterInterface::EXCEPTION_EVENT, $exceptionEvent);
158
            $response = $exceptionEvent->response();
159
        }
160
161
        return $response;
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167
    public function on(string $eventName, $receiver, int $priority = BroadcasterInterface::RECEIVER_NORMAL_PRIORITY)
168
    {
169
        if (!isset($this->receivers[$eventName])) {
170
            $this->receivers[$eventName] = [
171
                BroadcasterInterface::RECEIVER_LOW_PRIORITY    => [],
172
                BroadcasterInterface::RECEIVER_NORMAL_PRIORITY => [],
173
                BroadcasterInterface::RECEIVER_HIGH_PRIORITY   => [],
174
            ];
175
        }
176
177
        $this->receivers[$eventName][$priority][] = $receiver;
178
        $this->computedReceivers[$eventName] = null;
179
180
        if (isset($this->permanentEvents[$eventName])) {
181
            $event = $this->permanentEvents[$eventName];
182
183
            call_user_func_array($this->callbackResolver->resolve($receiver), [$event]);
184
        }
185
186
        return $this;
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192
    public function broadcast(string $eventName, EventInterface $event = null)
193
    {
194
        if (!$this->masterEmitter && in_array($eventName, BroadcasterInterface::RESERVED_EVENT_NAMES)) {
195
            throw new \LogicException(sprintf(
196
                'You\'re trying to broadcast "$eventName" but "%s" are reserved event names.',
197
                implode('|', BroadcasterInterface::RESERVED_EVENT_NAMES)
198
            ));
199
        }
200
201
        if (isset($this->receivers[$eventName])) {
202
            $event = $event ?? new SimpleEvent();
203
            if ($event instanceof PermanentEventInterface && $event->isPermanent()) {
204
                $this->permanentEvents[$eventName] = $event;
205
            }
206
207
            foreach ($this->buildEventReceivers($eventName) as $receiver) {
208
                call_user_func_array($this->callbackResolver->resolve($receiver), [$event]);
209
210
                if ($event->isPropagationStopped()) {
211
                    break;
212
                }
213
            }
214
        }
215
216
        return $this;
217
    }
218
219
    /**
220
     * @param  ContainerProviderInterface $provider
221
     * @return self
222
     */
223
    public function hydrate(ContainerProviderInterface $provider): Jarvis
224
    {
225
        $provider->hydrate($this);
226
227
        return $this;
228
    }
229
230
    /**
231
     * Enables master emitter mode.
232
     *
233
     * @return self
234
     */
235
    private function masterBroadcast(string $eventName, EventInterface $event = null): Jarvis
236
    {
237
        $this->masterEmitter = true;
238
        $this->broadcast($eventName, $event);
239
        $this->masterEmitter = false;
240
241
        return $this;
242
    }
243
244
    /**
245
     * Sets new attribute into Jarvis.
246
     *
247
     * @param  string $key   The name of the new attribute
248
     * @param  mixed  $value The value of the new attribute
249
     * @return self
250
     */
251
    private function masterSetter(string $key, $value): Jarvis
252
    {
253
        $this->masterSet = true;
254
        $this->$key = $value;
255
        $this->masterSet = false;
256
257
        return $this;
258
    }
259
260
    /**
261
     * Builds and returns well ordered receivers collection that match with provided event name.
262
     *
263
     * @param  string $eventName The event name we want to get its receivers
264
     * @return array
265
     */
266
    private function buildEventReceivers(string $eventName): array
267
    {
268
        return $this->computedReceivers[$eventName] = $this->computedReceivers[$eventName] ?? array_merge(
269
            $this->receivers[$eventName][BroadcasterInterface::RECEIVER_HIGH_PRIORITY],
270
            $this->receivers[$eventName][BroadcasterInterface::RECEIVER_NORMAL_PRIORITY],
271
            $this->receivers[$eventName][BroadcasterInterface::RECEIVER_LOW_PRIORITY]
272
        );
273
    }
274
}
275