Completed
Push — master ( 8d6a53...3eaa43 )
by Eric
03:21 queued 55s
created

Jarvis   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
wmc 31
lcom 1
cbo 9
dl 0
loc 240
rs 9.8
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 2
A __destruct() 0 4 1
A __get() 0 10 2
A __set() 0 8 2
B offsetSet() 0 24 5
B run() 0 25 3
A on() 0 19 3
D broadcast() 0 29 9
A hydrate() 0 6 1
A masterBroadcast() 0 8 1
A masterSetter() 0 8 1
A buildEventReceivers() 0 8 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 Request $request
28
 * @property \Jarvis\Skill\Routing\Router $router
29
 * @property \Symfony\Component\HttpFoundation\Session\Session $session
30
 * @property \Jarvis\Skill\Core\CallbackResolver $callbackResolver
31
 *
32
 * @author Eric Chau <[email protected]>
33
 */
34
class Jarvis extends Container implements BroadcasterInterface
35
{
36
    const DEFAULT_DEBUG = false;
37
    const CONTAINER_PROVIDER_FQCN = ContainerProvider::class;
38
39
    private $receivers = [];
40
    private $permanentEvents = [];
41
    private $computedReceivers = [];
42
    private $masterEmitter = false;
43
    private $masterSet = false;
44
45
    /**
46
     * Creates an instance of Jarvis. It can take settings as first argument.
47
     * List of accepted options:
48
     *   - container_provider (type: string|array): fqcn of your container provider
49
     *
50
     * @param  array $settings Your own settings to modify Jarvis behavior
51
     */
52
    public function __construct(array $settings = [])
53
    {
54
        parent::__construct();
55
56
        $this['settings'] = $settings;
57
        $providers = array_merge([static::CONTAINER_PROVIDER_FQCN], (array) ($settings['providers'] ?? []));
58
        foreach ($providers as $classname) {
59
            $this->hydrate(new $classname());
60
        }
61
    }
62
63
    public function __destruct()
64
    {
65
        $this->masterBroadcast(BroadcasterInterface::TERMINATE_EVENT);
66
    }
67
68
    /**
69
     * This method is an another way to get a locked value.
70
     *
71
     * Example: $this['foo'] is equal to $this->foo, but it ONLY works for locked values.
72
     *
73
     * @param  string $key The key of the locked value
74
     * @return mixed
75
     * @throws \InvalidArgumentException if the requested key is not associated to a locked service
76
     */
77
    public function __get(string $key)
78
    {
79
        if (!isset($this->locked[$key])) {
80
            throw new \InvalidArgumentException(sprintf('"%s" is not a key of a locked value.', $key));
81
        }
82
83
        $this->masterSetter($key, $this[$key]);
84
85
        return $this->$key;
86
    }
87
88
    /**
89
     * Sets new attributes to Jarvis. Note that this method is reserved to Jarvis itself only.
90
     *
91
     * @param string $key   The key name of the new attribute
92
     * @param mixed  $value The value to associate to provided key
93
     * @throws \LogicException if this method is not called by Jarvis itself
94
     */
95
    public function __set(string $key, $value)
96
    {
97
        if (!$this->masterSet) {
98
            throw new \LogicException('You are not allowed to set new attribute into Jarvis.');
99
        }
100
101
        $this->$key = $value;
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function offsetSet($id, $v): void
108
    {
109
        parent::offsetSet($id, $v);
110
111
        if (!($v instanceof \Closure)) {
112
            return;
113
        }
114
115
        $refMethod = new \ReflectionMethod($v, '__invoke');
116
        if (null === $returntype = $refMethod->getReturnType()) {
117
            return;
118
        }
119
120
        $alias = $returntype->getName();
121
        if ($alias === $id) {
122
            return;
123
        }
124
125
        if (!isset($this[$alias])) {
126
            $this->alias($alias, $id);
127
        } else {
128
            unset($this[$alias]);
129
        }
130
    }
131
132
    /**
133
     * @param  Request|null $request
134
     * @return Response
135
     */
136
    public function run(Request $request = null): Response
137
    {
138
        $request = $request ?? $this['request'];
139
        $event = null;
140
141
        try {
142
            $this->masterBroadcast(BroadcasterInterface::RUN_EVENT, $event = new RunEvent($request));
143
            if ($response = $event->response()) {
144
                return $response;
145
            }
146
147
            [$callback, $arguments] = $this['router']->match($request->getMethod(), $request->getPathInfo());
0 ignored issues
show
Bug introduced by
The variable $callback does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $arguments does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

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