Completed
Pull Request — master (#7063)
by Will
08:35
created

CoreKernel::getDatabasePrefix()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
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\ThemeManifest;
28
use SilverStripe\View\ThemeResourceLoader;
29
30
/**
31
 * Simple Kernel container
32
 */
33
class CoreKernel implements Kernel
34
{
35
    /**
36
     * @var Kernel
37
     */
38
    protected $nestedFrom = null;
39
40
    /**
41
     * @var Injector
42
     */
43
    protected $container = null;
44
45
    /**
46
     * @var string
47
     */
48
    protected $enviroment = null;
49
50
    /**
51
     * @var ClassLoader
52
     */
53
    protected $classLoader = null;
54
55
    /**
56
     * @var ModuleLoader
57
     */
58
    protected $moduleLoader = null;
59
60
    /**
61
     * @var ConfigLoader
62
     */
63
    protected $configLoader = null;
64
65
    /**
66
     * @var InjectorLoader
67
     */
68
    protected $injectorLoader = null;
69
70
    /**
71
     * @var ThemeResourceLoader
72
     */
73
    protected $themeResourceLoader = null;
74
75
    protected $basePath = null;
76
77
    /**
78
     * Create a new kernel for this application
79
     *
80
     * @param string $basePath Path to base dir for this application
81
     */
82
    public function __construct($basePath)
83
    {
84
        $this->basePath = $basePath;
85
86
        // Initialise the dependency injector as soon as possible, as it is
87
        // subsequently used by some of the following code
88
        $injectorLoader = InjectorLoader::inst();
89
        $injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class));
90
        $injectorLoader->pushManifest($injector);
91
        $this->setInjectorLoader($injectorLoader);
92
93
        // Manifest cache factory
94
        $manifestCacheFactory = $this->buildManifestCacheFactory();
95
96
        // Class loader
97
        $classLoader = ClassLoader::inst();
98
        $classLoader->pushManifest(new ClassManifest($basePath, $manifestCacheFactory));
99
        $this->setClassLoader($classLoader);
100
101
        // Module loader
102
        $moduleLoader = ModuleLoader::inst();
103
        $moduleManifest = new ModuleManifest($basePath, $manifestCacheFactory);
104
        $moduleLoader->pushManifest($moduleManifest);
105
        $this->setModuleLoader($moduleLoader);
106
107
        // Config loader
108
        // @todo refactor CoreConfigFactory
109
        $configFactory = new CoreConfigFactory($manifestCacheFactory);
110
        $configManifest = $configFactory->createRoot();
111
        $configLoader = ConfigLoader::inst();
112
        $configLoader->pushManifest($configManifest);
113
        $this->setConfigLoader($configLoader);
114
115
        // Load template manifest
116
        $themeResourceLoader = ThemeResourceLoader::inst();
117
        $themeResourceLoader->addSet('$default', new ThemeManifest(
118
            $basePath,
119
            project(),
120
            $manifestCacheFactory
121
        ));
122
        $this->setThemeResourceLoader($themeResourceLoader);
123
    }
124
125
    public function getEnvironment()
126
    {
127
        // Check set
128
        if ($this->enviroment) {
129
            return $this->enviroment;
130
        }
131
132
        // Check saved session
133
        $env = $this->sessionEnvironment();
134
        if ($env) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $env of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
135
            return $env;
136
        }
137
138
        // Check getenv
139
        if ($env = getenv('SS_ENVIRONMENT_TYPE')) {
140
            return $env;
141
        }
142
143
        return self::LIVE;
144
    }
145
146
    /**
147
     * Check or update any temporary environment specified in the session.
148
     *
149
     * @return null|string
150
     */
151
    protected function sessionEnvironment()
0 ignored issues
show
Coding Style introduced by
sessionEnvironment uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
sessionEnvironment uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
152
    {
153
        // Check isDev in querystring
154
        if (isset($_GET['isDev'])) {
155
            if (isset($_SESSION)) {
156
                unset($_SESSION['isTest']); // In case we are changing from test mode
157
                $_SESSION['isDev'] = $_GET['isDev'];
158
            }
159
            return self::DEV;
160
        }
161
162
        // Check isTest in querystring
163
        if (isset($_GET['isTest'])) {
164
            if (isset($_SESSION)) {
165
                unset($_SESSION['isDev']); // In case we are changing from dev mode
166
                $_SESSION['isTest'] = $_GET['isTest'];
167
            }
168
            return self::TEST;
169
        }
170
171
        // Check session
172
        if (!empty($_SESSION['isDev'])) {
173
            return self::DEV;
174
        }
175
        if (!empty($_SESSION['isTest'])) {
176
            return self::TEST;
177
        }
178
179
        // no session environment
180
        return null;
181
    }
182
183
    public function boot($flush = false)
184
    {
185
        $this->bootPHP();
186
        $this->bootManifests($flush);
187
        $this->bootErrorHandling();
188
        $this->bootDatabaseEnvVars();
189
        $this->bootConfigs();
190
        $this->bootDatabaseGlobals();
191
        $this->validateDatabase();
192
    }
193
194
    /**
195
     * Include all _config.php files
196
     */
197
    protected function bootConfigs()
198
    {
199
        // After loading all other app manifests, include _config.php files
200
        $this->getModuleLoader()->getManifest()->activateConfig();
201
    }
202
203
    /**
204
     * Load default database configuration from the $database and $databaseConfig globals
205
     */
206
    protected function bootDatabaseGlobals()
207
    {
208
        // Now that configs have been loaded, we can check global for database config
209
        global $databaseConfig;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
210
        global $database;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
211
212
        // Case 1: $databaseConfig global exists. Merge $database in as needed
213
        if (!empty($databaseConfig)) {
214
            if (!empty($database)) {
215
                $databaseConfig['database'] =  $this->getDatabasePrefix() . $database;
216
            }
217
218
            // Only set it if its valid, otherwise ignore $databaseConfig entirely
219
            if (!empty($databaseConfig['database'])) {
220
                DB::setConfig($databaseConfig);
221
222
                return;
223
            }
224
        }
225
226
        // Case 2: $database merged into existing config
227
        if (!empty($database)) {
228
            $existing = DB::getConfig();
229
            $existing['database'] = $this->getDatabasePrefix() . $database;
230
231
            DB::setConfig($existing);
232
        }
233
    }
234
235
    /**
236
     * Load default database configuration from environment variable
237
     */
238
    protected function bootDatabaseEnvVars()
239
    {
240
        // Set default database config
241
        $databaseConfig = $this->getDatabaseConfig();
242
        $databaseConfig['database'] = $this->getDatabaseName();
243
        DB::setConfig($databaseConfig);
244
    }
245
246
    /**
247
     * Check that the database configuration is valid, throwing an HTTPResponse_Exception if it's not
248
     *
249
     * @throws HTTPResponse_Exception
250
     */
251
    protected function validateDatabase()
252
    {
253
        $databaseConfig = DB::getConfig();
254
        // Gracefully fail if no DB is configured
255
        if (empty($databaseConfig['database'])) {
256
            $this->detectLegacyEnvironment();
257
            $this->redirectToInstaller();
258
        }
259
    }
260
261
    /**
262
     * Check if there's a legacy _ss_environment.php file
263
     *
264
     * @throws HTTPResponse_Exception
265
     */
266
    protected function detectLegacyEnvironment()
267
    {
268
        // Is there an _ss_environment.php file?
269
        if (!file_exists($this->basePath . '/_ss_environment.php') &&
270
            !file_exists(dirname($this->basePath) . '/_ss_environment.php')
271
        ) {
272
            return;
273
        }
274
275
        // Build error response
276
        $dv = new DebugView();
277
        $body =
278
            $dv->renderHeader() .
279
            $dv->renderInfo(
280
                "Configuraton Error",
281
                Director::absoluteBaseURL()
0 ignored issues
show
Security Bug introduced by
It seems like \SilverStripe\Control\Director::absoluteBaseURL() targeting SilverStripe\Control\Director::absoluteBaseURL() can also be of type false; however, SilverStripe\Dev\DebugView::renderInfo() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
282
            ) .
283
            $dv->renderParagraph(
284
                'You need to replace your _ss_environment.php file with a .env file, or with environment variables.<br><br>'
285
                . 'See the <a href="https://docs.silverstripe.org/en/4/getting_started/environment_management/">'
286
                . 'Environment Management</a> docs for more information.'
287
            ) .
288
            $dv->renderFooter();
289
290
        // Raise error
291
        $response = new HTTPResponse($body, 500);
292
        throw new HTTPResponse_Exception($response);
293
    }
294
295
    /**
296
     * If missing configuration, redirect to install.php
297
     */
298
    protected function redirectToInstaller()
299
    {
300
        // Error if installer not available
301
        if (!file_exists($this->basePath . '/install.php')) {
302
            throw new HTTPResponse_Exception(
303
                'SilverStripe Framework requires a $databaseConfig defined.',
304
                500
305
            );
306
        }
307
308
        // Redirect to installer
309
        $response = new HTTPResponse();
310
        $response->redirect(Director::absoluteURL('install.php'));
0 ignored issues
show
Security Bug introduced by
It seems like \SilverStripe\Control\Di...oluteURL('install.php') targeting SilverStripe\Control\Director::absoluteURL() can also be of type false; however, SilverStripe\Control\HTTPResponse::redirect() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
311
        throw new HTTPResponse_Exception($response);
312
    }
313
314
    /**
315
     * Load database config from environment
316
     *
317
     * @return array
318
     */
319
    protected function getDatabaseConfig()
320
    {
321
322
        /** @skipUpgrade */
323
        $databaseConfig = [
324
            "type" => getenv('SS_DATABASE_CLASS') ?: 'MySQLDatabase',
325
            "server" => getenv('SS_DATABASE_SERVER') ?: 'localhost',
326
            "username" => getenv('SS_DATABASE_USERNAME') ?: null,
327
            "password" => getenv('SS_DATABASE_PASSWORD') ?: null,
328
        ];
329
330
        // Set the port if called for
331
        $dbPort = getenv('SS_DATABASE_PORT');
332
        if ($dbPort) {
333
            $databaseConfig['port'] = $dbPort;
334
        }
335
336
        // Set the timezone if called for
337
        $dbTZ = getenv('SS_DATABASE_TIMEZONE');
338
        if ($dbTZ) {
339
            $databaseConfig['timezone'] = $dbTZ;
340
        }
341
342
        // For schema enabled drivers:
343
        $dbSchema = getenv('SS_DATABASE_SCHEMA');
344
        if ($dbSchema) {
345
            $databaseConfig["schema"] = $dbSchema;
346
        }
347
348
        // For SQlite3 memory databases (mainly for testing purposes)
349
        $dbMemory = getenv('SS_DATABASE_MEMORY');
350
        if ($dbMemory) {
351
            $databaseConfig["memory"] = $dbMemory;
352
        }
353
354
        // Allow database adapters to handle their own configuration
355
        DatabaseAdapterRegistry::autoconfigure();
356
        return $databaseConfig;
357
    }
358
359
    /**
360
     * @return string
361
     */
362
    protected function getDatabasePrefix()
363
    {
364
        return getenv('SS_DATABASE_PREFIX');
365
    }
366
367
    /**
368
     * Get name of database
369
     *
370
     * @return string
371
     */
372
    protected function getDatabaseName()
373
    {
374
        // Check globals
375
        global $database;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
376
377
        if (!empty($database)) {
378
            return $this->getDatabasePrefix() . $database;
379
        }
380
381
        global $databaseConfig;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
382
383
        if (!empty($databaseConfig['database'])) {
384
            return $databaseConfig['database']; // Note: Already includes prefix
385
        }
386
387
        // Check environment
388
        $database = getenv('SS_DATABASE_NAME');
389
390
        if ($database) {
391
            return $this->getDatabasePrefix() . $database;
392
        }
393
394
        // Auto-detect name
395
        $chooseName = getenv('SS_DATABASE_CHOOSE_NAME');
396
397
        if ($chooseName) {
398
            // Find directory to build name from
399
            $loopCount = (int)$chooseName;
400
            $databaseDir = $this->basePath;
401
            for ($i = 0; $i < $loopCount-1; $i++) {
402
                $databaseDir = dirname($databaseDir);
403
            }
404
405
            // Build name
406
            $database = str_replace('.', '', basename($databaseDir));
407
            $prefix = $this->getDatabasePrefix();
408
409
            if ($prefix === false) {
410
                $prefix = 'SS_';
411
            }
412
413
            return $prefix . $database;
414
        }
415
416
        // no DB name (may be optional for some connectors)
417
        return null;
418
    }
419
420
    /**
421
     * Initialise PHP with default variables
422
     */
423
    protected function bootPHP()
424
    {
425
        if ($this->getEnvironment() === self::LIVE) {
426
            // limited to fatal errors and warnings in live mode
427
            error_reporting(E_ALL & ~(E_DEPRECATED | E_STRICT | E_NOTICE));
428
        } else {
429
            // Report all errors in dev / test mode
430
            error_reporting(E_ALL | E_STRICT);
431
        }
432
433
        /**
434
         * Ensure we have enough memory
435
         */
436
        Environment::increaseMemoryLimitTo('64M');
437
438
        // Ensure we don't run into xdebug's fairly conservative infinite recursion protection limit
439
        if (function_exists('xdebug_enable')) {
440
            $current = ini_get('xdebug.max_nesting_level');
441
            if ((int)$current < 200) {
442
                ini_set('xdebug.max_nesting_level', 200);
443
            }
444
        }
445
446
        /**
447
         * Set default encoding
448
         */
449
        mb_http_output('UTF-8');
450
        mb_internal_encoding('UTF-8');
451
        mb_regex_encoding('UTF-8');
452
453
        /**
454
         * Enable better garbage collection
455
         */
456
        gc_enable();
457
    }
458
459
    /**
460
     * @return ManifestCacheFactory
461
     */
462
    protected function buildManifestCacheFactory()
463
    {
464
        return new ManifestCacheFactory([
465
            'namespace' => 'manifestcache',
466
            'directory' => TempFolder::getTempFolder($this->basePath),
467
        ]);
468
    }
469
470
    /**
471
     * @return bool
472
     */
473
    protected function getIncludeTests()
474
    {
475
        return false;
476
    }
477
478
    /**
479
     * Boot all manifests
480
     *
481
     * @param bool $flush
482
     */
483
    protected function bootManifests($flush)
484
    {
485
        // Setup autoloader
486
        $this->getClassLoader()->init($this->getIncludeTests(), $flush);
487
488
        // Find modules
489
        $this->getModuleLoader()->init($this->getIncludeTests(), $flush);
490
491
        // Flush config
492
        if ($flush) {
493
            $config = $this->getConfigLoader()->getManifest();
494
            if ($config instanceof CachedConfigCollection) {
495
                $config->setFlush(true);
496
            }
497
        }
498
499
        // Find default templates
500
        $defaultSet = $this->getThemeResourceLoader()->getSet('$default');
501
        if ($defaultSet instanceof ThemeManifest) {
502
            $defaultSet->init($this->getIncludeTests(), $flush);
503
        }
504
    }
505
506
    /**
507
     * Turn on error handling
508
     */
509
    protected function bootErrorHandling()
510
    {
511
        // Register error handler
512
        $errorHandler = Injector::inst()->get(ErrorHandler::class);
513
        $errorHandler->start();
514
515
        // Register error log file
516
        $errorLog = getenv('SS_ERROR_LOG');
517
        if ($errorLog) {
518
            $logger = Injector::inst()->get(LoggerInterface::class);
519
            if ($logger instanceof Logger) {
520
                $logger->pushHandler(new StreamHandler($this->basePath . '/' . $errorLog, Logger::WARNING));
521
            } else {
522
                user_error("SS_ERROR_LOG setting only works with Monolog, you are using another logger", E_USER_WARNING);
523
            }
524
        }
525
    }
526
527
    public function shutdown()
528
    {
529
    }
530
531
    public function nest()
532
    {
533
        // Clone this kernel, nesting config / injector manifest containers
534
        $kernel = clone $this;
535
        $kernel->setConfigLoader($this->configLoader->nest());
536
        $kernel->setInjectorLoader($this->injectorLoader->nest());
537
        return $kernel;
538
    }
539
540
    public function activate()
541
    {
542
        $this->configLoader->activate();
543
        $this->injectorLoader->activate();
544
545
        // Self register
546
        $this->getInjectorLoader()
547
            ->getManifest()
548
            ->registerService($this, Kernel::class);
549
        return $this;
550
    }
551
552
    public function getNestedFrom()
553
    {
554
        return $this->nestedFrom;
555
    }
556
557
    public function getContainer()
558
    {
559
        return $this->getInjectorLoader()->getManifest();
560
    }
561
562
    public function setInjectorLoader(InjectorLoader $injectorLoader)
563
    {
564
        $this->injectorLoader = $injectorLoader;
565
        $injectorLoader
566
            ->getManifest()
567
            ->registerService($this, Kernel::class);
568
        return $this;
569
    }
570
571
    public function getInjectorLoader()
572
    {
573
        return $this->injectorLoader;
574
    }
575
576
    public function getClassLoader()
577
    {
578
        return $this->classLoader;
579
    }
580
581
    public function setClassLoader(ClassLoader $classLoader)
582
    {
583
        $this->classLoader = $classLoader;
584
        return $this;
585
    }
586
587
    public function getModuleLoader()
588
    {
589
        return $this->moduleLoader;
590
    }
591
592
    public function setModuleLoader(ModuleLoader $moduleLoader)
593
    {
594
        $this->moduleLoader = $moduleLoader;
595
        return $this;
596
    }
597
598
    public function setEnvironment($environment)
599
    {
600
        if (!in_array($environment, [self::DEV, self::TEST, self::LIVE, null])) {
601
            throw new InvalidArgumentException(
602
                "Director::set_environment_type passed '$environment'.  It should be passed dev, test, or live"
603
            );
604
        }
605
        $this->enviroment = $environment;
606
        return $this;
607
    }
608
609
    public function getConfigLoader()
610
    {
611
        return $this->configLoader;
612
    }
613
614
    public function setConfigLoader($configLoader)
615
    {
616
        $this->configLoader = $configLoader;
617
        return $this;
618
    }
619
620
    public function getThemeResourceLoader()
621
    {
622
        return $this->themeResourceLoader;
623
    }
624
625
    public function setThemeResourceLoader($themeResourceLoader)
626
    {
627
        $this->themeResourceLoader = $themeResourceLoader;
628
        return $this;
629
    }
630
}
631