Passed
Push — less-original ( 3c06b4...67fe41 )
by Sam
07:41 queued 24s
created

CoreKernel::shutdown()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 0
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core;
4
5
use InvalidArgumentException;
6
use Monolog\Handler\StreamHandler;
7
use Monolog\Logger;
8
use Psr\Log\LoggerInterface;
9
use SilverStripe\Config\Collections\CachedConfigCollection;
10
use SilverStripe\Control\Director;
11
use SilverStripe\Control\HTTPResponse;
12
use SilverStripe\Control\HTTPResponse_Exception;
13
use SilverStripe\Core\Cache\ManifestCacheFactory;
14
use SilverStripe\Core\Config\ConfigLoader;
15
use SilverStripe\Core\Config\CoreConfigFactory;
16
use SilverStripe\Core\Injector\Injector;
17
use SilverStripe\Core\Injector\InjectorLoader;
18
use SilverStripe\Core\Injector\SilverStripeServiceConfigurationLocator;
19
use SilverStripe\Core\Manifest\ClassLoader;
20
use SilverStripe\Core\Manifest\ClassManifest;
21
use SilverStripe\Core\Manifest\ModuleLoader;
22
use SilverStripe\Core\Manifest\ModuleManifest;
23
use SilverStripe\Dev\DebugView;
24
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
25
use SilverStripe\Logging\ErrorHandler;
26
use SilverStripe\ORM\DB;
27
use SilverStripe\View\PublicThemes;
28
use SilverStripe\View\SSViewer;
29
use SilverStripe\View\ThemeManifest;
30
use SilverStripe\View\ThemeResourceLoader;
31
use SilverStripe\Dev\Deprecation;
32
33
/**
34
 * Simple Kernel container
35
 */
36
class CoreKernel implements Kernel
37
{
38
    /**
39
     * @var Kernel
40
     */
41
    protected $nestedFrom = null;
42
43
    /**
44
     * @var Injector
45
     */
46
    protected $container = null;
47
48
    /**
49
     * @var string
50
     */
51
    protected $enviroment = null;
52
53
    /**
54
     * @var ClassLoader
55
     */
56
    protected $classLoader = null;
57
58
    /**
59
     * @var ModuleLoader
60
     */
61
    protected $moduleLoader = null;
62
63
    /**
64
     * @var ConfigLoader
65
     */
66
    protected $configLoader = null;
67
68
    /**
69
     * @var InjectorLoader
70
     */
71
    protected $injectorLoader = null;
72
73
    /**
74
     * @var ThemeResourceLoader
75
     */
76
    protected $themeResourceLoader = null;
77
78
    protected $basePath = null;
79
80
    /**
81
     * Create a new kernel for this application
82
     *
83
     * @param string $basePath Path to base dir for this application
84
     */
85
    public function __construct($basePath)
86
    {
87
        $this->basePath = $basePath;
88
89
        // Initialise the dependency injector as soon as possible, as it is
90
        // subsequently used by some of the following code
91
        $injectorLoader = InjectorLoader::inst();
92
        $injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class));
93
        $injectorLoader->pushManifest($injector);
94
        $this->setInjectorLoader($injectorLoader);
95
96
        // Manifest cache factory
97
        $manifestCacheFactory = $this->buildManifestCacheFactory();
98
99
        // Class loader
100
        $classLoader = ClassLoader::inst();
101
        $classLoader->pushManifest(new ClassManifest($basePath, $manifestCacheFactory));
102
        $this->setClassLoader($classLoader);
103
104
        // Module loader
105
        $moduleLoader = ModuleLoader::inst();
106
        $moduleManifest = new ModuleManifest($basePath, $manifestCacheFactory);
107
        $moduleLoader->pushManifest($moduleManifest);
108
        $this->setModuleLoader($moduleLoader);
109
110
        // Config loader
111
        // @todo refactor CoreConfigFactory
112
        $configFactory = new CoreConfigFactory($manifestCacheFactory);
113
        $configManifest = $configFactory->createRoot();
114
        $configLoader = ConfigLoader::inst();
115
        $configLoader->pushManifest($configManifest);
116
        $this->setConfigLoader($configLoader);
117
118
        // Load template manifest
119
        $themeResourceLoader = ThemeResourceLoader::inst();
120
        $themeResourceLoader->addSet(SSViewer::PUBLIC_THEME, new PublicThemes());
121
        $themeResourceLoader->addSet(SSViewer::DEFAULT_THEME, new ThemeManifest(
122
            $basePath,
123
            null, // project is defined in config, and this argument is deprecated
124
            $manifestCacheFactory
125
        ));
126
        $this->setThemeResourceLoader($themeResourceLoader);
127
    }
128
129
    public function getEnvironment()
130
    {
131
        // Check set
132
        if ($this->enviroment) {
133
            return $this->enviroment;
134
        }
135
136
        // Check saved session
137
        $env = $this->sessionEnvironment();
138
        if ($env) {
139
            return $env;
140
        }
141
142
        // Check getenv
143
        if ($env = Environment::getEnv('SS_ENVIRONMENT_TYPE')) {
144
            return $env;
145
        }
146
147
        return self::LIVE;
148
    }
149
150
    /**
151
     * Check or update any temporary environment specified in the session.
152
     *
153
     * @return null|string
154
     */
155
    protected function sessionEnvironment()
156
    {
157
        // Check isDev in querystring
158
        if (isset($_GET['isDev'])) {
159
            if (isset($_SESSION)) {
160
                unset($_SESSION['isTest']); // In case we are changing from test mode
161
                $_SESSION['isDev'] = $_GET['isDev'];
162
            }
163
            return self::DEV;
164
        }
165
166
        // Check isTest in querystring
167
        if (isset($_GET['isTest'])) {
168
            if (isset($_SESSION)) {
169
                unset($_SESSION['isDev']); // In case we are changing from dev mode
170
                $_SESSION['isTest'] = $_GET['isTest'];
171
            }
172
            return self::TEST;
173
        }
174
175
        // Check session
176
        if (!empty($_SESSION['isDev'])) {
177
            return self::DEV;
178
        }
179
        if (!empty($_SESSION['isTest'])) {
180
            return self::TEST;
181
        }
182
183
        // no session environment
184
        return null;
185
    }
186
187
    public function boot($flush = false)
188
    {
189
        $this->bootPHP();
190
        $this->bootManifests($flush);
191
        $this->bootErrorHandling();
192
        $this->bootDatabaseEnvVars();
193
        $this->bootConfigs();
194
        $this->bootDatabaseGlobals();
195
        $this->validateDatabase();
196
    }
197
198
    /**
199
     * Include all _config.php files
200
     */
201
    protected function bootConfigs()
202
    {
203
        global $project;
204
        $projectBefore = $project;
205
        $config = ModuleManifest::config();
206
        // After loading all other app manifests, include _config.php files
207
        $this->getModuleLoader()->getManifest()->activateConfig();
208
        if ($project && $project !== $projectBefore) {
209
            Deprecation::notice('5.0', '$project global is deprecated');
210
            $config->set('project', $project);
211
        }
212
    }
213
214
    /**
215
     * Load default database configuration from the $database and $databaseConfig globals
216
     */
217
    protected function bootDatabaseGlobals()
218
    {
219
        // Now that configs have been loaded, we can check global for database config
220
        global $databaseConfig;
221
        global $database;
222
223
        // Case 1: $databaseConfig global exists. Merge $database in as needed
224
        if (!empty($databaseConfig)) {
225
            if (!empty($database)) {
226
                $databaseConfig['database'] =  $this->getDatabasePrefix() . $database . $this->getDatabaseSuffix();
227
            }
228
229
            // Only set it if its valid, otherwise ignore $databaseConfig entirely
230
            if (!empty($databaseConfig['database'])) {
231
                DB::setConfig($databaseConfig);
232
233
                return;
234
            }
235
        }
236
237
        // Case 2: $database merged into existing config
238
        if (!empty($database)) {
239
            $existing = DB::getConfig();
240
            $existing['database'] = $this->getDatabasePrefix() . $database . $this->getDatabaseSuffix();
241
242
            DB::setConfig($existing);
243
        }
244
    }
245
246
    /**
247
     * Load default database configuration from environment variable
248
     */
249
    protected function bootDatabaseEnvVars()
250
    {
251
        // Set default database config
252
        $databaseConfig = $this->getDatabaseConfig();
253
        $databaseConfig['database'] = $this->getDatabaseName();
254
        DB::setConfig($databaseConfig);
255
    }
256
257
    /**
258
     * Check that the database configuration is valid, throwing an HTTPResponse_Exception if it's not
259
     *
260
     * @throws HTTPResponse_Exception
261
     */
262
    protected function validateDatabase()
263
    {
264
        $databaseConfig = DB::getConfig();
265
        // Gracefully fail if no DB is configured
266
        if (empty($databaseConfig['database'])) {
267
            $this->detectLegacyEnvironment();
268
            $this->redirectToInstaller();
269
        }
270
    }
271
272
    /**
273
     * Check if there's a legacy _ss_environment.php file
274
     *
275
     * @throws HTTPResponse_Exception
276
     */
277
    protected function detectLegacyEnvironment()
278
    {
279
        // Is there an _ss_environment.php file?
280
        if (!file_exists($this->basePath . '/_ss_environment.php') &&
281
            !file_exists(dirname($this->basePath) . '/_ss_environment.php')
282
        ) {
283
            return;
284
        }
285
286
        // Build error response
287
        $dv = new DebugView();
288
        $body =
289
            $dv->renderHeader() .
290
            $dv->renderInfo(
291
                "Configuration Error",
292
                Director::absoluteBaseURL()
293
            ) .
294
            $dv->renderParagraph(
295
                'You need to replace your _ss_environment.php file with a .env file, or with environment variables.<br><br>'
296
                . 'See the <a href="https://docs.silverstripe.org/en/4/getting_started/environment_management/">'
297
                . 'Environment Management</a> docs for more information.'
298
            ) .
299
            $dv->renderFooter();
300
301
        // Raise error
302
        $response = new HTTPResponse($body, 500);
303
        throw new HTTPResponse_Exception($response);
304
    }
305
306
    /**
307
     * If missing configuration, redirect to install.php
308
     */
309
    protected function redirectToInstaller()
310
    {
311
        // Error if installer not available
312
        if (!file_exists(Director::publicFolder() . '/install.php')) {
313
            throw new HTTPResponse_Exception(
314
                'SilverStripe Framework requires database configuration defined via .env',
315
                500
316
            );
317
        }
318
319
        // Redirect to installer
320
        $response = new HTTPResponse();
321
        $response->redirect(Director::absoluteURL('install.php'));
322
        throw new HTTPResponse_Exception($response);
323
    }
324
325
    /**
326
     * Load database config from environment
327
     *
328
     * @return array
329
     */
330
    protected function getDatabaseConfig()
331
    {
332
        /** @skipUpgrade */
333
        $databaseConfig = [
334
            "type" => Environment::getEnv('SS_DATABASE_CLASS') ?: 'MySQLDatabase',
335
            "server" => Environment::getEnv('SS_DATABASE_SERVER') ?: 'localhost',
336
            "username" => Environment::getEnv('SS_DATABASE_USERNAME') ?: null,
337
            "password" => Environment::getEnv('SS_DATABASE_PASSWORD') ?: null,
338
        ];
339
340
        // Set the port if called for
341
        $dbPort = Environment::getEnv('SS_DATABASE_PORT');
342
        if ($dbPort) {
343
            $databaseConfig['port'] = $dbPort;
344
        }
345
346
        // Set the timezone if called for
347
        $dbTZ = Environment::getEnv('SS_DATABASE_TIMEZONE');
348
        if ($dbTZ) {
349
            $databaseConfig['timezone'] = $dbTZ;
350
        }
351
352
        // For schema enabled drivers:
353
        $dbSchema = Environment::getEnv('SS_DATABASE_SCHEMA');
354
        if ($dbSchema) {
355
            $databaseConfig["schema"] = $dbSchema;
356
        }
357
358
        // For SQlite3 memory databases (mainly for testing purposes)
359
        $dbMemory = Environment::getEnv('SS_DATABASE_MEMORY');
360
        if ($dbMemory) {
361
            $databaseConfig["memory"] = $dbMemory;
362
        }
363
364
        // Allow database adapters to handle their own configuration
365
        DatabaseAdapterRegistry::autoconfigure($databaseConfig);
366
        return $databaseConfig;
367
    }
368
369
    /**
370
     * @return string
371
     */
372
    protected function getDatabasePrefix()
373
    {
374
        return Environment::getEnv('SS_DATABASE_PREFIX') ?: '';
375
    }
376
377
    /**
378
     * @return string
379
     */
380
    protected function getDatabaseSuffix()
381
    {
382
        return Environment::getEnv('SS_DATABASE_SUFFIX') ?: '';
383
    }
384
385
    /**
386
     * Get name of database
387
     *
388
     * @return string
389
     */
390
    protected function getDatabaseName()
391
    {
392
        // Check globals
393
        global $database;
394
395
        if (!empty($database)) {
396
            return $this->getDatabasePrefix() . $database . $this->getDatabaseSuffix();
397
        }
398
399
        global $databaseConfig;
400
401
        if (!empty($databaseConfig['database'])) {
402
            return $databaseConfig['database']; // Note: Already includes prefix
403
        }
404
405
        // Check environment
406
        $database = Environment::getEnv('SS_DATABASE_NAME');
407
408
        if ($database) {
409
            return $this->getDatabasePrefix() . $database . $this->getDatabaseSuffix();
410
        }
411
412
        // Auto-detect name
413
        $chooseName = Environment::getEnv('SS_DATABASE_CHOOSE_NAME');
414
415
        if ($chooseName) {
416
            // Find directory to build name from
417
            $loopCount = (int)$chooseName;
418
            $databaseDir = $this->basePath;
419
            for ($i = 0; $i < $loopCount-1; $i++) {
420
                $databaseDir = dirname($databaseDir);
421
            }
422
423
            // Build name
424
            $database = str_replace('.', '', basename($databaseDir));
425
            $prefix = $this->getDatabasePrefix();
426
427
            if ($prefix) {
428
                $prefix = 'SS_';
429
            } else {
430
                // If no prefix, hard-code prefix into database global
431
                $prefix = '';
432
                $database = 'SS_' . $database;
433
            }
434
435
            return $prefix . $database;
436
        }
437
438
        // no DB name (may be optional for some connectors)
439
        return null;
440
    }
441
442
    /**
443
     * Initialise PHP with default variables
444
     */
445
    protected function bootPHP()
446
    {
447
        if ($this->getEnvironment() === self::LIVE) {
448
            // limited to fatal errors and warnings in live mode
449
            error_reporting(E_ALL & ~(E_DEPRECATED | E_STRICT | E_NOTICE));
450
        } else {
451
            // Report all errors in dev / test mode
452
            error_reporting(E_ALL | E_STRICT);
453
        }
454
455
        /**
456
         * Ensure we have enough memory
457
         */
458
        Environment::increaseMemoryLimitTo('64M');
459
460
        // Ensure we don't run into xdebug's fairly conservative infinite recursion protection limit
461
        if (function_exists('xdebug_enable')) {
462
            $current = ini_get('xdebug.max_nesting_level');
463
            if ((int)$current < 200) {
464
                ini_set('xdebug.max_nesting_level', 200);
465
            }
466
        }
467
468
        /**
469
         * Set default encoding
470
         */
471
        mb_http_output('UTF-8');
472
        mb_internal_encoding('UTF-8');
473
        mb_regex_encoding('UTF-8');
474
475
        /**
476
         * Enable better garbage collection
477
         */
478
        gc_enable();
479
    }
480
481
    /**
482
     * @return ManifestCacheFactory
483
     */
484
    protected function buildManifestCacheFactory()
485
    {
486
        return new ManifestCacheFactory([
487
            'namespace' => 'manifestcache',
488
            'directory' => TempFolder::getTempFolder($this->basePath),
489
        ]);
490
    }
491
492
    /**
493
     * @return bool
494
     */
495
    protected function getIncludeTests()
496
    {
497
        return false;
498
    }
499
500
    /**
501
     * Boot all manifests
502
     *
503
     * @param bool $flush
504
     */
505
    protected function bootManifests($flush)
506
    {
507
        // Setup autoloader
508
        $this->getClassLoader()->init($this->getIncludeTests(), $flush);
509
510
        // Find modules
511
        $this->getModuleLoader()->init($this->getIncludeTests(), $flush);
512
513
        // Flush config
514
        if ($flush) {
515
            $config = $this->getConfigLoader()->getManifest();
516
            if ($config instanceof CachedConfigCollection) {
517
                $config->setFlush(true);
518
            }
519
        }
520
        // tell modules to sort, now that config is available
521
        $this->getModuleLoader()->getManifest()->sort();
522
523
        // Find default templates
524
        $defaultSet = $this->getThemeResourceLoader()->getSet('$default');
525
        if ($defaultSet instanceof ThemeManifest) {
526
            $defaultSet->setProject(
527
                ModuleManifest::config()->get('project')
528
            );
529
            $defaultSet->init($this->getIncludeTests(), $flush);
530
        }
531
    }
532
533
    /**
534
     * Turn on error handling
535
     */
536
    protected function bootErrorHandling()
537
    {
538
        // Register error handler
539
        $errorHandler = Injector::inst()->get(ErrorHandler::class);
540
        $errorHandler->start();
541
542
        // Register error log file
543
        $errorLog = Environment::getEnv('SS_ERROR_LOG');
544
        if ($errorLog) {
545
            $logger = Injector::inst()->get(LoggerInterface::class);
546
            if ($logger instanceof Logger) {
547
                $logger->pushHandler(new StreamHandler($this->basePath . '/' . $errorLog, Logger::WARNING));
548
            } else {
549
                user_error("SS_ERROR_LOG setting only works with Monolog, you are using another logger", E_USER_WARNING);
550
            }
551
        }
552
    }
553
554
    public function shutdown()
555
    {
556
    }
557
558
    public function nest()
559
    {
560
        // Clone this kernel, nesting config / injector manifest containers
561
        $kernel = clone $this;
562
        $kernel->setConfigLoader($this->configLoader->nest());
563
        $kernel->setInjectorLoader($this->injectorLoader->nest());
564
        $kernel->nestedFrom = $this;
565
        return $kernel;
566
    }
567
568
    public function activate()
569
    {
570
        $this->configLoader->activate();
571
        $this->injectorLoader->activate();
572
573
        // Self register
574
        $this->getInjectorLoader()
575
            ->getManifest()
576
            ->registerService($this, Kernel::class);
577
        return $this;
578
    }
579
580
    public function getNestedFrom()
581
    {
582
        return $this->nestedFrom;
583
    }
584
585
    public function getContainer()
586
    {
587
        return $this->getInjectorLoader()->getManifest();
588
    }
589
590
    public function setInjectorLoader(InjectorLoader $injectorLoader)
591
    {
592
        $this->injectorLoader = $injectorLoader;
593
        $injectorLoader
594
            ->getManifest()
595
            ->registerService($this, Kernel::class);
596
        return $this;
597
    }
598
599
    public function getInjectorLoader()
600
    {
601
        return $this->injectorLoader;
602
    }
603
604
    public function getClassLoader()
605
    {
606
        return $this->classLoader;
607
    }
608
609
    public function setClassLoader(ClassLoader $classLoader)
610
    {
611
        $this->classLoader = $classLoader;
612
        return $this;
613
    }
614
615
    public function getModuleLoader()
616
    {
617
        return $this->moduleLoader;
618
    }
619
620
    public function setModuleLoader(ModuleLoader $moduleLoader)
621
    {
622
        $this->moduleLoader = $moduleLoader;
623
        return $this;
624
    }
625
626
    public function setEnvironment($environment)
627
    {
628
        if (!in_array($environment, [self::DEV, self::TEST, self::LIVE, null])) {
629
            throw new InvalidArgumentException(
630
                "Director::set_environment_type passed '$environment'.  It should be passed dev, test, or live"
631
            );
632
        }
633
        $this->enviroment = $environment;
634
        return $this;
635
    }
636
637
    public function getConfigLoader()
638
    {
639
        return $this->configLoader;
640
    }
641
642
    public function setConfigLoader($configLoader)
643
    {
644
        $this->configLoader = $configLoader;
645
        return $this;
646
    }
647
648
    public function getThemeResourceLoader()
649
    {
650
        return $this->themeResourceLoader;
651
    }
652
653
    public function setThemeResourceLoader($themeResourceLoader)
654
    {
655
        $this->themeResourceLoader = $themeResourceLoader;
656
        return $this;
657
    }
658
}
659