Completed
Push — master ( 09c050...8664f4 )
by Eric
10s
created

Jarvis::broadcast()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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