Completed
Pull Request — master (#48)
by Eric
02:23
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\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 ParameterBag $settings
29
 * @property \Jarvis\Skill\Routing\Router $router
30
 * @property \Symfony\Component\HttpFoundation\Session\Session $session
31
 * @property \Jarvis\Skill\Core\CallbackResolver $callbackResolver
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
            if ($response = $runEvent->response()) {
129
                return $response;
130
            }
131
132
            [$callback, $arguments] = $this->router->match($request->getMethod(), $request->getPathInfo());
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

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