1
|
|
|
<?php |
|
|
|
|
2
|
|
|
|
3
|
|
|
declare(strict_types = 1); |
4
|
|
|
|
5
|
|
|
namespace Jarvis; |
6
|
|
|
|
7
|
|
|
use FastRoute\Dispatcher; |
8
|
|
|
use Jarvis\Skill\DependencyInjection\{ |
9
|
|
|
Container, |
10
|
|
|
ContainerProvider, |
11
|
|
|
ContainerProviderInterface |
12
|
|
|
}; |
13
|
|
|
use Jarvis\Skill\EventBroadcaster\{ |
14
|
|
|
AnalyzeEvent, |
15
|
|
|
ControllerEvent, |
16
|
|
|
EventInterface, |
17
|
|
|
ExceptionEvent, |
18
|
|
|
JarvisEvents, |
19
|
|
|
PermanentEventInterface, |
20
|
|
|
ResponseEvent, |
21
|
|
|
SimpleEvent |
22
|
|
|
}; |
23
|
|
|
use Symfony\Component\HttpFoundation\{ParameterBag, Request, Response}; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Jarvis. Minimalist dependency injection container. |
27
|
|
|
* |
28
|
|
|
* @author Eric Chau <[email protected]> |
29
|
|
|
*/ |
30
|
|
|
class Jarvis extends Container |
31
|
|
|
{ |
32
|
|
|
const DEFAULT_DEBUG = false; |
33
|
|
|
const CONTAINER_PROVIDER_FQCN = ContainerProvider::class; |
34
|
|
|
const DEFAULT_SCOPE = 'default'; |
35
|
|
|
|
36
|
|
|
const RECEIVER_HIGH_PRIORITY = 2; |
37
|
|
|
const RECEIVER_NORMAL_PRIORITY = 1; |
38
|
|
|
const RECEIVER_LOW_PRIORITY = 0; |
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(JarvisEvents::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 (false === $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 analyze(Request $request = null) : Response |
122
|
|
|
{ |
123
|
|
|
$request = $request ?? $this->request; |
|
|
|
|
124
|
|
|
$response = null; |
125
|
|
|
|
126
|
|
|
try { |
127
|
|
|
$this->masterBroadcast(JarvisEvents::ANALYZE_EVENT, $analyzeEvent = new AnalyzeEvent($request)); |
128
|
|
|
|
129
|
|
|
if ($response = $analyzeEvent->response()) { |
130
|
|
|
return $response; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
$routeInfo = $this->router->match($request->getMethod(), $request->getPathInfo()); |
|
|
|
|
134
|
|
|
if (Dispatcher::FOUND === $routeInfo[0]) { |
135
|
|
|
$callback = $this->callback_resolver->resolve($routeInfo[1]); |
|
|
|
|
136
|
|
|
|
137
|
|
|
$event = new ControllerEvent($callback, $routeInfo[2]); |
138
|
|
|
$this->masterBroadcast(JarvisEvents::CONTROLLER_EVENT, $event); |
139
|
|
|
|
140
|
|
|
$response = call_user_func_array($event->callback(), $event->arguments()); |
141
|
|
|
|
142
|
|
|
if (is_string($response)) { |
143
|
|
|
$response = new Response($response); |
144
|
|
|
} |
145
|
|
|
} elseif (Dispatcher::NOT_FOUND === $routeInfo[0] || Dispatcher::METHOD_NOT_ALLOWED === $routeInfo[0]) { |
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(JarvisEvents::RESPONSE_EVENT, new ResponseEvent($request, $response)); |
153
|
|
|
} catch (\Exception $exception) { |
154
|
|
|
$this->masterBroadcast(JarvisEvents::EXCEPTION_EVENT, $exceptionEvent = new ExceptionEvent($exception)); |
155
|
|
|
$response = $exceptionEvent->response(); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
return $response; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* @param string $eventName |
163
|
|
|
* @param mixed $receiver |
164
|
|
|
* @param integer $priority |
165
|
|
|
* @return self |
166
|
|
|
*/ |
167
|
|
|
public function addReceiver(string $eventName, $receiver, int $priority = self::RECEIVER_NORMAL_PRIORITY) : Jarvis |
168
|
|
|
{ |
169
|
|
|
if (!isset($this->receivers[$eventName])) { |
170
|
|
|
$this->receivers[$eventName] = [ |
171
|
|
|
self::RECEIVER_LOW_PRIORITY => [], |
172
|
|
|
self::RECEIVER_NORMAL_PRIORITY => [], |
173
|
|
|
self::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->callback_resolver->resolve($receiver), [$event]); |
|
|
|
|
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
return $this; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* @param string $eventName |
191
|
|
|
* @param EventInterface|null $event |
192
|
|
|
* @return self |
193
|
|
|
*/ |
194
|
|
|
public function broadcast(string $eventName, EventInterface $event = null) : Jarvis |
195
|
|
|
{ |
196
|
|
|
if (!$this->masterEmitter && in_array($eventName, JarvisEvents::RESERVED_EVENT_NAMES)) { |
197
|
|
|
throw new \LogicException(sprintf( |
198
|
|
|
'You\'re trying to broadcast "$eventName" but "%s" are reserved event names.', |
199
|
|
|
implode('|', JarvisEvents::RESERVED_EVENT_NAMES) |
200
|
|
|
)); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
if (isset($this->receivers[$eventName])) { |
204
|
|
|
$event = $event ?? new SimpleEvent(); |
205
|
|
|
if ($event instanceof PermanentEventInterface && $event->isPermanent()) { |
206
|
|
|
$this->permanentEvents[$eventName] = $event; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
foreach ($this->buildEventReceivers($eventName) as $receiver) { |
210
|
|
|
call_user_func_array($this->callback_resolver->resolve($receiver), [$event]); |
|
|
|
|
211
|
|
|
|
212
|
|
|
if ($event->isPropagationStopped()) { |
213
|
|
|
break; |
214
|
|
|
} |
215
|
|
|
} |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
return $this; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* @param ContainerProviderInterface $provider |
223
|
|
|
* @return self |
224
|
|
|
*/ |
225
|
|
|
public function hydrate(ContainerProviderInterface $provider) : Jarvis |
226
|
|
|
{ |
227
|
|
|
$provider->hydrate($this); |
228
|
|
|
|
229
|
|
|
return $this; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Enables master emitter mode. |
234
|
|
|
* |
235
|
|
|
* @return self |
236
|
|
|
*/ |
237
|
|
|
private function masterBroadcast(string $eventName, EventInterface $event = null) : Jarvis |
238
|
|
|
{ |
239
|
|
|
$this->masterEmitter = true; |
240
|
|
|
$this->broadcast($eventName, $event); |
241
|
|
|
$this->masterEmitter = false; |
242
|
|
|
|
243
|
|
|
return $this; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Sets new attribute into Jarvis. |
248
|
|
|
* |
249
|
|
|
* @param string $key The name of the new attribute |
250
|
|
|
* @param mixed $value The value of the new attribute |
251
|
|
|
* @return self |
252
|
|
|
*/ |
253
|
|
|
private function masterSetter(string $key, $value) : Jarvis |
254
|
|
|
{ |
255
|
|
|
$this->masterSet = true; |
256
|
|
|
$this->$key = $value; |
257
|
|
|
$this->masterSet = false; |
258
|
|
|
|
259
|
|
|
return $this; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Builds and returns well ordered receivers collection that match with provided event name. |
264
|
|
|
* |
265
|
|
|
* @param string $eventName The event name we want to get its receivers |
266
|
|
|
* @return array |
267
|
|
|
*/ |
268
|
|
|
private function buildEventReceivers(string $eventName) : array |
269
|
|
|
{ |
270
|
|
|
return $this->computedReceivers[$eventName] = $this->computedReceivers[$eventName] ?? array_merge( |
271
|
|
|
$this->receivers[$eventName][self::RECEIVER_HIGH_PRIORITY], |
272
|
|
|
$this->receivers[$eventName][self::RECEIVER_NORMAL_PRIORITY], |
273
|
|
|
$this->receivers[$eventName][self::RECEIVER_LOW_PRIORITY] |
274
|
|
|
); |
275
|
|
|
} |
276
|
|
|
} |
277
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.