1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* spiral |
4
|
|
|
* |
5
|
|
|
* @author Wolfy-J |
6
|
|
|
*/ |
7
|
|
|
namespace Spiral\Core; |
8
|
|
|
|
9
|
|
|
use Interop\Container\ContainerInterface as InteropContainer; |
10
|
|
|
use Spiral\Console\ConsoleDispatcher; |
11
|
|
|
use Spiral\Core\Containers\SpiralContainer; |
12
|
|
|
use Spiral\Core\Exceptions\CoreException; |
13
|
|
|
use Spiral\Core\Exceptions\DirectoryException; |
14
|
|
|
use Spiral\Core\Exceptions\FatalException; |
15
|
|
|
use Spiral\Core\Exceptions\ScopeException; |
16
|
|
|
use Spiral\Core\HMVC\CoreInterface; |
17
|
|
|
use Spiral\Core\Traits\SharedTrait; |
18
|
|
|
use Spiral\Debug\SnapshotInterface; |
19
|
|
|
use Spiral\Files\FilesInterface; |
20
|
|
|
use Spiral\Http\HttpDispatcher; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Spiral core responsible for application timezone, memory, represents spiral container (can be |
24
|
|
|
* overwritten with custom instance). |
25
|
|
|
* |
26
|
|
|
* Btw, you can design your architecture any way you want: MVC, MMVC, HMVC, ADR, anything which can |
27
|
|
|
* be invoked and/or routed. Technically you can even invent your own, application specific, |
28
|
|
|
* architecture. |
29
|
|
|
*/ |
30
|
|
|
abstract class Core extends AbstractCore implements DirectoriesInterface |
31
|
|
|
{ |
32
|
|
|
use SharedTrait; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* I need this constant for Symfony Console. :/ |
36
|
|
|
*/ |
37
|
|
|
const VERSION = '0.9.0-rc'; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Memory section for bootloaders cache. |
41
|
|
|
*/ |
42
|
|
|
const BOOT_MEMORY = 'app'; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Components to be autoloader while application initialization. This property can be redefined |
46
|
|
|
* on application level. |
47
|
|
|
*/ |
48
|
|
|
const LOAD = []; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Every application should have defined timezone. |
52
|
|
|
* |
53
|
|
|
* @see setTimezone() |
54
|
|
|
* @see getTimezone() |
55
|
|
|
* @var string |
56
|
|
|
*/ |
57
|
|
|
private $timezone = 'UTC'; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Set of primary application directories. |
61
|
|
|
* |
62
|
|
|
* @see setDirectory() |
63
|
|
|
* @see directory() |
64
|
|
|
* @see getDirectories() |
65
|
|
|
* @var array |
66
|
|
|
*/ |
67
|
|
|
private $directories = [ |
68
|
|
|
'root' => null, |
69
|
|
|
'public' => null, |
70
|
|
|
'libraries' => null, |
71
|
|
|
'framework' => null, |
72
|
|
|
'application' => null, |
73
|
|
|
'locales' => null, |
74
|
|
|
'runtime' => null, |
75
|
|
|
'config' => null, |
76
|
|
|
'cache' => null |
77
|
|
|
]; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* @var BootloadManager |
81
|
|
|
*/ |
82
|
|
|
protected $bootloader; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @var EnvironmentInterface |
86
|
|
|
*/ |
87
|
|
|
protected $environment; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Not set until start method. Can be set manually in bootload. |
91
|
|
|
* |
92
|
|
|
* @var DispatcherInterface |
93
|
|
|
*/ |
94
|
|
|
protected $dispatcher; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Application memory. |
98
|
|
|
* |
99
|
|
|
* @invisible |
100
|
|
|
* @var MemoryInterface |
101
|
|
|
*/ |
102
|
|
|
protected $memory; |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Components to be autoloader while application initialization. This property can be redefined |
106
|
|
|
* on application level. |
107
|
|
|
* |
108
|
|
|
* @deprecated use LOAD constant instead |
109
|
|
|
* @invisible |
110
|
|
|
*/ |
111
|
|
|
protected $load = []; |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Core class will extend default spiral container and initiate set of directories. You must |
115
|
|
|
* provide application, libraries and root directories to constructor. |
116
|
|
|
* |
117
|
|
|
* @param array $directories Core directories list. Every directory must have / |
118
|
|
|
* at the end. |
119
|
|
|
* @param ContainerInterface $container |
120
|
|
|
* @param MemoryInterface $memory |
121
|
|
|
*/ |
122
|
|
|
public function __construct( |
123
|
|
|
array $directories, |
124
|
|
|
ContainerInterface $container, |
125
|
|
|
MemoryInterface $memory = null |
126
|
|
|
) { |
127
|
|
|
$this->container = $container; |
128
|
|
|
|
129
|
|
|
/* |
130
|
|
|
* Default directories pattern, you can overwrite any directory you want in index file. |
131
|
|
|
*/ |
132
|
|
|
$this->directories = $directories + [ |
133
|
|
|
'framework' => dirname(__DIR__) . '/', |
134
|
|
|
'public' => $directories['root'] . 'webroot/', |
135
|
|
|
'config' => $directories['application'] . 'config/', |
136
|
|
|
'views' => $directories['application'] . 'views/', |
137
|
|
|
'runtime' => $directories['application'] . 'runtime/', |
138
|
|
|
'cache' => $directories['application'] . 'runtime/cache/', |
139
|
|
|
'resources' => $directories['application'] . 'resources/', |
140
|
|
|
'locales' => $directories['application'] . 'resources/locales/' |
141
|
|
|
]; |
142
|
|
|
|
143
|
|
|
//Every application needs timezone to be set, by default we are using UTC |
144
|
|
|
date_default_timezone_set($this->timezone); |
145
|
|
|
|
146
|
|
|
//Default memory implementation as fallback |
147
|
|
|
$this->memory = $memory ?? new Memory( |
148
|
|
|
$this->directory('cache'), |
149
|
|
|
$container->get(FilesInterface::class) |
150
|
|
|
); |
151
|
|
|
|
152
|
|
|
$this->bootloader = new BootloadManager($this->container, $this->memory); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Change application timezone. |
157
|
|
|
* |
158
|
|
|
* @param string $timezone |
159
|
|
|
* |
160
|
|
|
* @return $this|self |
161
|
|
|
* @throws CoreException |
162
|
|
|
*/ |
163
|
|
|
public function setTimezone(string $timezone): Core |
164
|
|
|
{ |
165
|
|
|
try { |
166
|
|
|
date_default_timezone_set($timezone); |
167
|
|
|
} catch (\Exception $e) { |
168
|
|
|
throw new CoreException($e->getMessage(), $e->getCode(), $e); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
$this->timezone = $timezone; |
172
|
|
|
|
173
|
|
|
return $this; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Get active application timezone. |
178
|
|
|
* |
179
|
|
|
* @return \DateTimeZone |
180
|
|
|
*/ |
181
|
|
|
public function getTimezone(): \DateTimeZone |
182
|
|
|
{ |
183
|
|
|
return new \DateTimeZone($this->timezone); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* {@inheritdoc} |
188
|
|
|
*/ |
189
|
|
|
public function hasDirectory(string $alias): bool |
190
|
|
|
{ |
191
|
|
|
return isset($this->directories[$alias]); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* {@inheritdoc} |
196
|
|
|
*/ |
197
|
|
|
public function setDirectory(string $alias, string $path): DirectoriesInterface |
198
|
|
|
{ |
199
|
|
|
$this->directories[$alias] = rtrim($path, '/\\') . '/'; |
200
|
|
|
|
201
|
|
|
return $this; |
|
|
|
|
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* {@inheritdoc} |
206
|
|
|
*/ |
207
|
|
|
public function directory(string $alias): string |
208
|
|
|
{ |
209
|
|
|
if (!$this->hasDirectory($alias)) { |
210
|
|
|
throw new DirectoryException("Undefined directory alias '{$alias}'"); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
return $this->directories[$alias]; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* {@inheritdoc} |
218
|
|
|
*/ |
219
|
|
|
public function getDirectories(): array |
220
|
|
|
{ |
221
|
|
|
return $this->directories; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Change application environment. Attention, already loaded configs would not be altered! |
226
|
|
|
* |
227
|
|
|
* @param EnvironmentInterface $environment |
228
|
|
|
*/ |
229
|
|
|
public function setEnvironment(EnvironmentInterface $environment) |
230
|
|
|
{ |
231
|
|
|
$this->environment = $environment; |
232
|
|
|
|
233
|
|
|
//Making sure environment is available in container scope |
234
|
|
|
$this->container->bindSingleton(EnvironmentInterface::class, $this->environment); |
|
|
|
|
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* @return EnvironmentInterface |
239
|
|
|
* |
240
|
|
|
* @throws CoreException |
241
|
|
|
*/ |
242
|
|
|
public function getEnvironment() |
243
|
|
|
{ |
244
|
|
|
if (empty($this->environment)) { |
245
|
|
|
throw new CoreException("Application environment not set"); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
return $this->environment; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* BootloadManager responsible for initiation of your application. |
253
|
|
|
* |
254
|
|
|
* @return BootloadManager |
255
|
|
|
*/ |
256
|
|
|
public function getBootloader() |
257
|
|
|
{ |
258
|
|
|
return $this->bootloader; |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* Handle php shutdown and search for fatal errors. |
263
|
|
|
*/ |
264
|
|
|
public function handleShutdown() |
265
|
|
|
{ |
266
|
|
|
if (!$this->container->has(SnapshotInterface::class)) { |
267
|
|
|
//We are unable to handle exception without proper snaphotter |
268
|
|
|
return; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
if (!empty($error = error_get_last())) { |
272
|
|
|
$this->handleException(new FatalException( |
273
|
|
|
$error['message'], $error['type'], 0, $error['file'], $error['line'] |
274
|
|
|
)); |
275
|
|
|
} |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Convert application error into exception. |
280
|
|
|
* |
281
|
|
|
* @param int $code |
282
|
|
|
* @param string $message |
283
|
|
|
* @param string $filename |
284
|
|
|
* @param int $line |
285
|
|
|
* |
286
|
|
|
* @throws \ErrorException |
287
|
|
|
*/ |
288
|
|
|
public function handleError($code, $message, $filename = '', $line = 0) |
289
|
|
|
{ |
290
|
|
|
throw new \ErrorException($message, $code, 0, $filename, $line); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Handle exception using associated application dispatcher and snapshot class. |
295
|
|
|
* |
296
|
|
|
* @param \Throwable $exception |
297
|
|
|
* |
298
|
|
|
* @throws \Throwable |
299
|
|
|
*/ |
300
|
|
|
public function handleException(\Throwable $exception) |
301
|
|
|
{ |
302
|
|
|
restore_error_handler(); |
303
|
|
|
restore_exception_handler(); |
304
|
|
|
|
305
|
|
|
$snapshot = $this->makeSnapshot($exception); |
306
|
|
|
|
307
|
|
|
if (empty($snapshot)) { |
308
|
|
|
//No action is required |
309
|
|
|
throw $exception; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
//Let's allow snapshot to report about itself |
313
|
|
|
$snapshot->report(); |
314
|
|
|
|
315
|
|
|
if (!empty($this->dispatcher)) { |
316
|
|
|
//Now dispatcher can handle snapshot it's own way |
317
|
|
|
$this->dispatcher->handleSnapshot($snapshot); |
318
|
|
|
} else { |
319
|
|
|
echo $snapshot->render(); |
320
|
|
|
} |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* Create appropriate snapshot for given exception. By default SnapshotInterface binding will be |
325
|
|
|
* used. |
326
|
|
|
* |
327
|
|
|
* Method can return null, in this case exception will be ignored and handled default way. |
328
|
|
|
* |
329
|
|
|
* @param \Throwable $exception |
330
|
|
|
* |
331
|
|
|
* @return SnapshotInterface|null |
332
|
|
|
*/ |
333
|
|
|
public function makeSnapshot(\Throwable $exception) |
334
|
|
|
{ |
335
|
|
|
if (!$this->container->has(SnapshotInterface::class)) { |
336
|
|
|
return null; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
return $this->container->make(SnapshotInterface::class, compact('exception')); |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* Start application using custom or default dispatcher. |
344
|
|
|
* |
345
|
|
|
* @param DispatcherInterface $dispatcher Custom dispatcher. |
346
|
|
|
*/ |
347
|
|
|
public function start(DispatcherInterface $dispatcher = null) |
348
|
|
|
{ |
349
|
|
|
$this->dispatcher = $dispatcher ?? $this->createDispatcher(); |
350
|
|
|
$this->dispatcher->start(); |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
/** |
354
|
|
|
* Bootstrap application. Must be executed before start method. |
355
|
|
|
*/ |
356
|
|
|
abstract protected function bootstrap(); |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Create default application dispatcher based on environment value. |
360
|
|
|
* |
361
|
|
|
* @return DispatcherInterface|ConsoleDispatcher|HttpDispatcher |
362
|
|
|
*/ |
363
|
|
|
protected function createDispatcher() |
364
|
|
|
{ |
365
|
|
|
if (php_sapi_name() === 'cli') { |
366
|
|
|
return $this->container->make(ConsoleDispatcher::class); |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
return $this->container->make(HttpDispatcher::class); |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
/** |
373
|
|
|
* Bootload all registered classes using BootloadManager. |
374
|
|
|
* |
375
|
|
|
* @return $this |
376
|
|
|
*/ |
377
|
|
|
private function bootload() |
378
|
|
|
{ |
379
|
|
|
$this->bootloader->bootload( |
380
|
|
|
$this->load + static::LOAD, |
|
|
|
|
381
|
|
|
$this->environment->get('CACHE_BOOTLOADERS', false) ? static::BOOT_MEMORY : null |
382
|
|
|
); |
383
|
|
|
|
384
|
|
|
return $this; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* Shared container instance (needed for helpers and etc). Attention, method will fail if no |
389
|
|
|
* global container is set. |
390
|
|
|
* |
391
|
|
|
* @return InteropContainer |
392
|
|
|
* |
393
|
|
|
* @throws ScopeException |
394
|
|
|
*/ |
395
|
|
|
public static function sharedContainer() |
396
|
|
|
{ |
397
|
|
|
$container = self::staticContainer(); |
398
|
|
|
if (empty($container)) { |
399
|
|
|
throw new ScopeException("No shared/global container scope are set"); |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
return $container; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Initiate application core. Method will set global container if none exists. |
407
|
|
|
* |
408
|
|
|
* @param array $directories Spiral directories should include root, libraries |
409
|
|
|
* and application directories. |
410
|
|
|
* @param EnvironmentInterface $environment Application specific environment if any. |
411
|
|
|
* @param ContainerInterface $container Initial container instance. |
412
|
|
|
* @param bool $handleErrors |
413
|
|
|
* |
414
|
|
|
* @return self |
415
|
|
|
*/ |
416
|
|
|
public static function init( |
417
|
|
|
array $directories, |
418
|
|
|
EnvironmentInterface $environment = null, |
419
|
|
|
ContainerInterface $container = null, |
420
|
|
|
bool $handleErrors = true |
421
|
|
|
): self { |
422
|
|
|
//Default spiral container |
423
|
|
|
$container = $container ?? new SpiralContainer(); |
424
|
|
|
|
425
|
|
|
//Spiral core interface, @see SpiralContainer |
426
|
|
|
$container->bindSingleton(ContainerInterface::class, $container); |
|
|
|
|
427
|
|
|
|
428
|
|
|
/** |
429
|
|
|
* @var Core $core |
430
|
|
|
*/ |
431
|
|
|
$core = new static($directories, $container); |
432
|
|
|
|
433
|
|
|
//Core binding |
434
|
|
|
$container->bindSingleton(self::class, $core); |
|
|
|
|
435
|
|
|
$container->bindSingleton(static::class, $core); |
|
|
|
|
436
|
|
|
|
437
|
|
|
//Core shared interfaces |
438
|
|
|
$container->bindSingleton(CoreInterface::class, $core); |
|
|
|
|
439
|
|
|
$container->bindSingleton(DirectoriesInterface::class, $core); |
|
|
|
|
440
|
|
|
|
441
|
|
|
//Core shared components |
442
|
|
|
$container->bindSingleton(BootloadManager::class, $core->bootloader); |
|
|
|
|
443
|
|
|
$container->bindSingleton(MemoryInterface::class, $core->memory); |
|
|
|
|
444
|
|
|
|
445
|
|
|
//Setting environment (by default - dotenv extension) |
446
|
|
|
if (empty($environment)) { |
447
|
|
|
/* |
448
|
|
|
* Default spiral environment is based on .env file. |
449
|
|
|
*/ |
450
|
|
|
$environment = new Environment( |
451
|
|
|
$core->directory('root') . '.env', |
452
|
|
|
$container->get(FilesInterface::class), |
453
|
|
|
$core->memory |
454
|
|
|
); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
//Mounting environment to be available for other components |
458
|
|
|
$core->setEnvironment($environment); |
459
|
|
|
|
460
|
|
|
//Initiating config loader |
461
|
|
|
$container->bindSingleton( |
462
|
|
|
ConfiguratorInterface::class, |
463
|
|
|
$container->make(ConfigFactory::class, ['directory' => $core->directory('config')]) |
|
|
|
|
464
|
|
|
); |
465
|
|
|
|
466
|
|
|
if ($handleErrors) { |
467
|
|
|
//Error and exception handlers |
468
|
|
|
register_shutdown_function([$core, 'handleShutdown']); |
469
|
|
|
set_error_handler([$core, 'handleError']); |
470
|
|
|
set_exception_handler([$core, 'handleException']); |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
$scope = self::staticContainer($container); |
474
|
|
|
try { |
475
|
|
|
//Bootloading our application in a defined GLOBAL container scope |
476
|
|
|
$core->bootload()->bootstrap(); |
477
|
|
|
} finally { |
478
|
|
|
self::staticContainer($scope); |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
return $core; |
482
|
|
|
} |
483
|
|
|
} |
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.