Completed
Push — master ( 1be2e7...d38097 )
by Sam
23s
created

ConfigManifest::addModule()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 1
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core\Manifest;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Core\ClassInfo;
7
use SilverStripe\Core\Config\DAG;
8
use SilverStripe\Core\Config\DAG_CyclicException;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Core\Cache;
11
use Symfony\Component\Yaml\Parser;
12
use Traversable;
13
use Zend_Cache_Core;
14
15
/**
16
 * A utility class which builds a manifest of configuration items
17
 */
18
class ConfigManifest
19
{
20
21
    /** @var string - The base path used when building the manifest */
22
    protected $base;
23
24
    /** @var string - A string to prepend to all cache keys to ensure all keys are unique to just this $base */
25
    protected $key;
26
27
    /** @var bool - Whether `test` directories should be searched when searching for configuration */
28
    protected $includeTests;
29
30
    /**
31
     * @var Zend_Cache_Core
32
     */
33
    protected $cache;
34
35
    /**
36
      * All the values needed to be collected to determine the correct combination of fragements for
37
      * the current environment.
38
      * @var array
39
      */
40
    protected $variantKeySpec = false;
41
42
    /**
43
     * All the _config.php files. Need to be included every request & can't be cached. Not variant specific.
44
     * @var array
45
     */
46
    protected $phpConfigSources = array();
47
48
    /**
49
     * All the _config/*.yml fragments pre-parsed and sorted in ascending include order. Not variant specific.
50
     * @var array
51
     */
52
    protected $yamlConfigFragments = array();
53
54
    /**
55
     * The calculated config from _config/*.yml, sorted, filtered and merged. Variant specific.
56
     * @var array
57
     */
58
    public $yamlConfig = array();
59
60
    /**
61
     * The variant key state as when yamlConfig was loaded
62
     * @var string
63
     */
64
    protected $yamlConfigVariantKey = null;
65
66
    /**
67
     * @var [callback] A list of callbacks to be called whenever the content of yamlConfig changes
68
     */
69
    protected $configChangeCallbacks = array();
70
71
    /**
72
     * A side-effect of collecting the _config fragments is the calculation of all module directories, since
73
     * the definition of a module is "a directory that contains either a _config.php file or a _config directory
74
     * @var array
75
     */
76
    public $modules = array();
77
78
    /**
79
     * Adds a path as a module
80
     *
81
     * @param string $path
82
     */
83
    public function addModule($path)
84
    {
85
        $module = basename($path);
86
        if (isset($this->modules[$module]) && $this->modules[$module] != $path) {
87
            user_error("Module ".$module." in two places - ".$path." and ".$this->modules[$module]);
88
        }
89
        $this->modules[$module] = $path;
90
    }
91
92
    /**
93
     * Returns true if the passed module exists
94
     *
95
     * @param string $module
96
     * @return bool
97
     */
98
    public function moduleExists($module)
99
    {
100
        return array_key_exists($module, $this->modules);
101
    }
102
103
    /**
104
     * Constructs and initialises a new configuration object, either loading
105
     * from the cache or re-scanning for classes.
106
     *
107
     * @param string $base The project base path.
108
     * @param bool $includeTests
109
     * @param bool $forceRegen Force the manifest to be regenerated.
110
     */
111
    public function __construct($base, $includeTests = false, $forceRegen = false)
112
    {
113
        $this->base = $base;
114
        $this->key = sha1($base).'_';
115
        $this->includeTests = $includeTests;
116
117
        // Get the Zend Cache to load/store cache into
118
        $this->cache = $this->getCache();
119
120
        // Unless we're forcing regen, try loading from cache
121
        if (!$forceRegen) {
122
            // The PHP config sources are always needed
123
            $this->phpConfigSources = $this->cache->load($this->key.'php_config_sources');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->cache->load($this.... 'php_config_sources') of type * is incompatible with the declared type array of property $phpConfigSources.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
124
            // Get the variant key spec
125
            $this->variantKeySpec = $this->cache->load($this->key.'variant_key_spec');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->cache->load($this...y . 'variant_key_spec') of type * is incompatible with the declared type array of property $variantKeySpec.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
126
        }
127
128
        // If we don't have a variantKeySpec (because we're forcing regen, or it just wasn't in the cache), generate it
129
        if (false === $this->variantKeySpec) {
130
            $this->regenerate($includeTests);
131
        }
132
133
        // At this point $this->variantKeySpec will always contain something valid, so we can build the variant
134
        $this->buildYamlConfigVariant();
135
    }
136
137
    /**
138
     * Provides a hook for mock unit tests despite no DI
139
     * @return Zend_Cache_Core
140
     */
141
    protected function getCache()
142
    {
143
        return Cache::factory('SS_Configuration', 'Core', array(
144
            'automatic_serialization' => true,
145
            'lifetime' => null
146
        ));
147
    }
148
149
    /**
150
     * Register a callback to be called whenever the calculated merged config changes
151
     *
152
     * In some situations the merged config can change - for instance, code in _config.php can cause which Only
153
     * and Except fragments match. Registering a callback with this function allows code to be called when
154
     * this happens.
155
     *
156
     * @param callback $callback
157
     */
158
    public function registerChangeCallback($callback)
159
    {
160
        $this->configChangeCallbacks[] = $callback;
161
    }
162
163
    /**
164
     * Includes all of the php _config.php files found by this manifest. Called by SS_Config when adding this manifest
165
     * @return void
166
     */
167
    public function activateConfig()
168
    {
169
        foreach ($this->phpConfigSources as $config) {
170
            require_once $config;
171
        }
172
173
        if ($this->variantKey() != $this->yamlConfigVariantKey) {
174
            $this->buildYamlConfigVariant();
175
        }
176
    }
177
178
    /**
179
     * Gets the (merged) config value for the given class and config property name
180
     *
181
     * @param string $class - The class to get the config property value for
182
     * @param string $name - The config property to get the value for
183
     * @param mixed $default - What to return if no value was contained in any YAML file for the passed $class and $name
184
     * @return mixed The merged set of all values contained in all the YAML configuration files for the passed
185
     * $class and $name, or $default if there are none
186
     */
187
    public function get($class, $name, $default = null)
188
    {
189
        if (isset($this->yamlConfig[$class][$name])) {
190
            return $this->yamlConfig[$class][$name];
191
        }
192
        return $default;
193
    }
194
195
    /**
196
     * Returns the string that uniquely identifies this variant. The variant is the combination of classes, modules,
197
     * environment, environment variables and constants that selects which yaml fragments actually make it into the
198
     * configuration because of "only"
199
     * and "except" rules.
200
     *
201
     * @return string
202
     */
203
    public function variantKey()
0 ignored issues
show
Coding Style introduced by
variantKey uses the super-global variable $_ENV 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...
204
    {
205
        $key = $this->variantKeySpec; // Copy to fill in actual values
206
207
        if (isset($key['environment'])) {
208
            $key['environment'] = Director::isDev() ? 'dev' : (Director::isTest() ? 'test' : 'live');
209
        }
210
211
        if (isset($key['envvars'])) {
212
            foreach ($key['envvars'] as $variable => $foo) {
0 ignored issues
show
Bug introduced by
The expression $key['envvars'] of type string is not traversable.
Loading history...
213
                $key['envvars'][$variable] = isset($_ENV[$variable]) ? $_ENV[$variable] : null;
214
            }
215
        }
216
217
        if (isset($key['constants'])) {
218
            foreach ($key['constants'] as $variable => $foo) {
0 ignored issues
show
Bug introduced by
The expression $key['constants'] of type string is not traversable.
Loading history...
219
                $key['constants'][$variable] = defined($variable) ? constant($variable) : null;
220
            }
221
        }
222
223
        return sha1(serialize($key));
224
    }
225
226
    /**
227
     * Completely regenerates the manifest file. Scans through finding all php _config.php and yaml _config/*.ya?ml
228
     * files,parses the yaml files into fragments, sorts them and figures out what values need to be checked to pick
229
     * the correct variant.
230
     *
231
     * Does _not_ build the actual variant
232
     *
233
     * @param bool $includeTests
234
     * @param bool $cache Cache the result.
235
     */
236
    public function regenerate($includeTests = false, $cache = true)
237
    {
238
        $this->phpConfigSources = array();
239
        $this->yamlConfigFragments = array();
240
        $this->variantKeySpec = array();
241
242
        $finder = new ManifestFileFinder();
243
        $finder->setOptions(array(
244
            'min_depth' => 0,
245
            'name_regex'    => '/(^|[\/\\\\])_config.php$/',
246
            'ignore_tests'  => !$includeTests,
247
            'file_callback' => array($this, 'addSourceConfigFile')
248
        ));
249
        $finder->find($this->base);
250
251
        $finder = new ManifestFileFinder();
252
        $finder->setOptions(array(
253
            'name_regex'    => '/\.ya?ml$/',
254
            'ignore_tests'  => !$includeTests,
255
            'file_callback' => array($this, 'addYAMLConfigFile')
256
        ));
257
        $finder->find($this->base);
258
259
        $this->prefilterYamlFragments();
260
        $this->sortYamlFragments();
261
        $this->buildVariantKeySpec();
262
263
        if ($cache) {
264
            $this->cache->save($this->phpConfigSources, $this->key.'php_config_sources');
265
            $this->cache->save($this->yamlConfigFragments, $this->key.'yaml_config_fragments');
266
            $this->cache->save($this->variantKeySpec, $this->key.'variant_key_spec');
267
        }
268
    }
269
270
    /**
271
     * Handle finding a php file. We just keep a record of all php files found, we don't include them
272
     * at this stage
273
     *
274
     * Public so that ManifestFileFinder can call it. Not for general use.
275
     *
276
     * @param string $basename
277
     * @param string $pathname
278
     * @param int $depth
279
     */
280
    public function addSourceConfigFile($basename, $pathname, $depth)
0 ignored issues
show
Unused Code introduced by
The parameter $depth is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
281
    {
282
        $this->phpConfigSources[] = $pathname;
283
        // Add this module too
284
        $this->addModule(dirname($pathname));
285
    }
286
287
    /**
288
     * Handle finding a yml file. Parse the file by spliting it into header/fragment pairs,
289
     * and normalising some of the header values (especially: give anonymous name if none assigned,
290
     * splt/complete before and after matchers)
291
     *
292
     * Public so that ManifestFileFinder can call it. Not for general use.
293
     *
294
     * @param string $basename
295
     * @param string $pathname
296
     * @param int $depth
297
     */
298
    public function addYAMLConfigFile($basename, $pathname, $depth)
0 ignored issues
show
Unused Code introduced by
The parameter $depth is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
299
    {
300
        if (!preg_match('{/([^/]+)/_config/}', $pathname, $match)) {
301
            return;
302
        }
303
304
        // Keep track of all the modules we've seen
305
        $this->addModule(dirname(dirname($pathname)));
306
307
        $parser = new Parser();
308
309
        // The base header
310
        $base = array(
311
            'module' => $match[1],
312
            'file' => basename(basename($basename, '.yml'), '.yaml')
313
        );
314
315
        // Make sure the linefeeds are all converted to \n, PCRE '$' will not match anything else.
316
        $fileContents = str_replace(array("\r\n", "\r"), "\n", file_get_contents($pathname));
317
318
        // YAML parsers really should handle this properly themselves, but neither spyc nor symfony-yaml do. So we
319
        // follow in their vein and just do what we need, not what the spec says
320
        $parts = preg_split('/^---$/m', $fileContents, -1, PREG_SPLIT_NO_EMPTY);
321
322
        // If only one document, it's a headerless fragment. So just add it with an anonymous name
323
        if (count($parts) == 1) {
324
            $this->yamlConfigFragments[] = $base + array(
325
                'name' => 'anonymous-1',
326
                'fragment' => $parser->parse($parts[0])
327
            );
328
        } // Otherwise it's a set of header/document pairs
329
        else {
330
            // If we got an odd number of parts the config file doesn't have a header for every document
331
            if (count($parts) % 2 != 0) {
332
                user_error("Configuration file '$pathname' does not have an equal number of headers and config blocks");
333
            }
334
335
            // Step through each pair
336
            for ($i = 0; $i < count($parts); $i+=2) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
337
                // Make all the first-level keys of the header lower case
338
                $header = array_change_key_case($parser->parse($parts[$i]), CASE_LOWER);
339
340
                // Assign a name if non assigned already
341
                if (!isset($header['name'])) {
342
                    $header['name'] = 'anonymous-'.(1+$i/2);
343
                }
344
345
                // Parse & normalise the before and after if present
346
                foreach (array('before', 'after') as $order) {
347
                    if (isset($header[$order])) {
348
                        // First, splice into parts (multiple before or after parts are allowed, comma separated)
349
                        if (is_array($header[$order])) {
350
                            $orderparts = $header[$order];
351
                        } else {
352
                            $orderparts = preg_split('/\s*,\s*/', $header[$order], -1, PREG_SPLIT_NO_EMPTY);
353
                        }
354
355
                        // For each, parse out into module/file#name, and set any missing to "*"
356
                        $header[$order] = array();
357
                        foreach ($orderparts as $part) {
358
                            preg_match(
359
                                '! (?P<module>\*|[^\/#]+)? (\/ (?P<file>\*|\w+))? (\# (?P<fragment>\*|\w+))? !x',
360
                                $part,
361
                                $match
362
                            );
363
364
                            $header[$order][] = array(
365
                                'module' => isset($match['module']) && $match['module'] ? $match['module'] : '*',
366
                                'file' => isset($match['file']) && $match['file'] ? $match['file'] : '*',
367
                                'name' => isset($match['fragment'])  && $match['fragment'] ? $match['fragment'] : '*'
368
                            );
369
                        }
370
                    }
371
                }
372
373
                // And add to the fragments list
374
                $this->yamlConfigFragments[] = $base + $header + array(
375
                    'fragment' => $parser->parse($parts[$i+1])
376
                );
377
            }
378
        }
379
    }
380
381
    /**
382
     * Sorts the YAML fragments so that the "before" and "after" rules are met.
383
     * Throws an error if there's a loop
384
     *
385
     * We can't use regular sorts here - we need a topological sort. Easiest
386
     * way is with a DAG, so build up a DAG based on the before/after rules, then
387
     * sort that.
388
     *
389
     * @return void
390
     */
391
    protected function sortYamlFragments()
0 ignored issues
show
Coding Style introduced by
sortYamlFragments uses the super-global variable $_REQUEST 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...
392
    {
393
        $frags = $this->yamlConfigFragments;
394
395
        // Build a directed graph
396
        $dag = new DAG($frags);
397
398
        foreach ($frags as $i => $frag) {
399
            foreach ($frags as $j => $otherfrag) {
400
                if ($i == $j) {
401
                    continue;
402
                }
403
404
                $order = $this->relativeOrder($frag, $otherfrag);
405
406
                if ($order == 'before') {
407
                    $dag->addedge($i, $j);
408
                } elseif ($order == 'after') {
409
                    $dag->addedge($j, $i);
410
                }
411
            }
412
        }
413
414
        try {
415
            $this->yamlConfigFragments = $dag->sort();
416
        } catch (DAG_CyclicException $e) {
417
            if (!Director::isLive() && isset($_REQUEST['debug'])) {
418
                $res = '<h1>Remaining config fragment graph</h1>';
419
                $res .= '<dl>';
420
421
                foreach ($e->dag as $node) {
422
                    $res .= "<dt>{$node['from']['module']}/{$node['from']['file']}#{$node['from']['name']}"
423
                        . " marked to come after</dt><dd><ul>";
424
                    foreach ($node['to'] as $to) {
425
                        $res .= "<li>{$to['module']}/{$to['file']}#{$to['name']}</li>";
426
                    }
427
                    $res .= "</ul></dd>";
428
                }
429
430
                $res .= '</dl>';
431
                echo $res;
432
            }
433
434
            user_error(
435
                'Based on their before & after rules two fragments both need to be before/after each other',
436
                E_USER_ERROR
437
            );
438
        }
439
    }
440
441
    /**
442
     * Return a string "after", "before" or "undefined" depending on whether the YAML fragment array element passed
443
     * as $a should be positioned after, before, or either compared to the YAML fragment array element passed as $b
444
     *
445
     * @param array $a A YAML config fragment as loaded by addYAMLConfigFile
446
     * @param array $b A YAML config fragment as loaded by addYAMLConfigFile
447
     * @return string "after", "before" or "undefined"
448
     */
449
    protected function relativeOrder($a, $b)
450
    {
451
        $matches = array();
452
453
        // Do the same thing for after and before
454
        foreach (array('before', 'after') as $rulename) {
455
            $matches[$rulename] = array();
456
457
            // Figure out for each rule, which part matches
458
            if (isset($a[$rulename])) {
459
                foreach ($a[$rulename] as $rule) {
460
                    $match = array();
461
462
                    foreach (array('module', 'file', 'name') as $part) {
463
                        // If part is *, we match _unless_ the opposite rule has a non-* matcher than also matches $b
464
                        if ($rule[$part] == '*') {
465
                            $match[$part] = 'wild';
466
                        } else {
467
                            $match[$part] = ($rule[$part] == $b[$part]);
468
                        }
469
                    }
470
471
                    $matches[$rulename][] = $match;
472
                }
473
            }
474
        }
475
476
        // Figure out the specificness of each match. 1 an actual match, 0 for a wildcard match, remove if no match
477
        $matchlevel = array('before' => -1, 'after' => -1);
478
479
        foreach (array('before', 'after') as $rulename) {
480
            foreach ($matches[$rulename] as $i => $rule) {
481
                $level = 0;
482
483
                foreach ($rule as $part => $partmatches) {
484
                    if ($partmatches === false) {
485
                        continue 2;
486
                    }
487
                    if ($partmatches === true) {
488
                        $level += 1;
489
                    }
490
                }
491
492
                if ($matchlevel[$rulename] === false || $level > $matchlevel[$rulename]) {
493
                    $matchlevel[$rulename] = $level;
494
                }
495
            }
496
        }
497
498
        if ($matchlevel['before'] === -1 && $matchlevel['after'] === -1) {
499
            return 'undefined';
500
        } elseif ($matchlevel['before'] === $matchlevel['after']) {
501
            user_error('Config fragment requires itself to be both before _and_ after another fragment', E_USER_ERROR);
502
        } else {
503
            return ($matchlevel['before'] > $matchlevel['after']) ? 'before' : 'after';
504
        }
505
    }
506
507
    /**
508
     * This function filters the loaded yaml fragments, removing any that can't ever have their "only" and "except"
509
     * rules match.
510
     *
511
     * Some tests in "only" and "except" rules need to be checked per request, but some are manifest based -
512
     * these are invariant over requests and only need checking on manifest rebuild. So we can prefilter these before
513
     * saving yamlConfigFragments to speed up the process of checking the per-request variant/
514
     */
515
    public function prefilterYamlFragments()
516
    {
517
        $matchingFragments = array();
518
519
        foreach ($this->yamlConfigFragments as $i => $fragment) {
520
            $matches = true;
521
522
            if (isset($fragment['only'])) {
523
                $matches = $matches && ($this->matchesPrefilterVariantRules($fragment['only']) !== false);
524
            }
525
526
            if (isset($fragment['except'])) {
527
                $matches = $matches && ($this->matchesPrefilterVariantRules($fragment['except']) !== true);
528
            }
529
530
            if ($matches) {
531
                $matchingFragments[] = $fragment;
532
            }
533
        }
534
535
        $this->yamlConfigFragments = $matchingFragments;
536
    }
537
538
    /**
539
     * Returns false if the prefilterable parts of the rule aren't met, and true if they are
540
     *
541
     * @param  $rules array - A hash of rules as allowed in the only or except portion of a config fragment header
542
     * @return bool - True if the rules are met, false if not. (Note that depending on whether we were passed an
543
     *                only or an except rule,
544
     * which values means accept or reject a fragment
545
     */
546
    public function matchesPrefilterVariantRules($rules)
547
    {
548
        $matches = "undefined"; // Needs to be truthy, but not true
549
550
        foreach ($rules as $k => $v) {
551
            switch (strtolower($k)) {
552
                case 'classexists':
553
                    $matches = $matches && ClassInfo::exists($v);
554
                    break;
555
556
                case 'moduleexists':
557
                    $matches = $matches && $this->moduleExists($v);
558
                    break;
559
560
                default:
561
                    // NOP
562
            }
563
564
            if ($matches === false) {
565
                return $matches;
566
            }
567
        }
568
569
        return $matches;
570
    }
571
572
    /**
573
     * Builds the variant key spec - the list of values that need to be build to give a key that uniquely identifies
574
     * this variant.
575
     */
576
    public function buildVariantKeySpec()
577
    {
578
        $this->variantKeySpec = array();
579
580
        foreach ($this->yamlConfigFragments as $fragment) {
581
            if (isset($fragment['only'])) {
582
                $this->addVariantKeySpecRules($fragment['only']);
583
            }
584
            if (isset($fragment['except'])) {
585
                $this->addVariantKeySpecRules($fragment['except']);
586
            }
587
        }
588
    }
589
590
    /**
591
     * Adds any variables referenced in the passed rules to the $this->variantKeySpec array
592
     *
593
     * @param array $rules
594
     */
595
    public function addVariantKeySpecRules($rules)
596
    {
597
        foreach ($rules as $k => $v) {
598
            switch (strtolower($k)) {
599
                case 'classexists':
600
                case 'moduleexists':
601
                    // Classes and modules are a special case - we can pre-filter on config regenerate because we
602
                    // already know if the class or module exists
603
                    break;
604
605
                case 'environment':
606
                    $this->variantKeySpec['environment'] = true;
607
                    break;
608
609
                case 'envvarset':
610
                    if (!isset($this->variantKeySpec['envvars'])) {
611
                        $this->variantKeySpec['envvars'] = array();
612
                    }
613
                    $this->variantKeySpec['envvars'][$k] = $k;
614
                    break;
615
616
                case 'constantdefined':
617
                    if (!isset($this->variantKeySpec['constants'])) {
618
                        $this->variantKeySpec['constants'] = array();
619
                    }
620
                    $this->variantKeySpec['constants'][$k] = $k;
621
                    break;
622
623
                default:
624
                    if (!isset($this->variantKeySpec['envvars'])) {
625
                        $this->variantKeySpec['envvars'] = array();
626
                    }
627
                    if (!isset($this->variantKeySpec['constants'])) {
628
                        $this->variantKeySpec['constants'] = array();
629
                    }
630
                    $this->variantKeySpec['envvars'][$k] = $this->variantKeySpec['constants'][$k] = $k;
631
            }
632
        }
633
    }
634
635
    /**
636
     * Calculates which yaml config fragments are applicable in this variant, and merge those all together into
637
     * the $this->yamlConfig propperty
638
     *
639
     * Checks cache and takes care of loading yamlConfigFragments if they aren't already present, but expects
640
     * $variantKeySpec to already be set
641
     *
642
     * @param bool $cache
643
     */
644
    public function buildYamlConfigVariant($cache = true)
645
    {
646
        // Only try loading from cache if we don't have the fragments already loaded, as there's no way to know if a
647
        // given variant is stale compared to the complete set of fragments
648
        if (!$this->yamlConfigFragments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->yamlConfigFragments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
649
            // First try and just load the exact variant
650
            if ($this->yamlConfig = $this->cache->load($this->key.'yaml_config_'.$this->variantKey())) {
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->cache->load($this... . $this->variantKey()) of type * is incompatible with the declared type array of property $yamlConfig.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
651
                $this->yamlConfigVariantKey = $this->variantKey();
652
                return;
653
            } // Otherwise try and load the fragments so we can build the variant
654
            else {
655
                $this->yamlConfigFragments = $this->cache->load($this->key.'yaml_config_fragments');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->cache->load($this...yaml_config_fragments') of type * is incompatible with the declared type array of property $yamlConfigFragments.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
656
            }
657
        }
658
659
        // If we still don't have any fragments we have to build them
660
        if (!$this->yamlConfigFragments) {
661
            $this->regenerate($this->includeTests, $cache);
662
        }
663
664
        $this->yamlConfig = array();
665
        $this->yamlConfigVariantKey = $this->variantKey();
666
667
        foreach ($this->yamlConfigFragments as $i => $fragment) {
668
            $matches = true;
669
670
            if (isset($fragment['only'])) {
671
                $matches = $matches && ($this->matchesVariantRules($fragment['only']) !== false);
672
            }
673
674
            if (isset($fragment['except'])) {
675
                $matches = $matches && ($this->matchesVariantRules($fragment['except']) !== true);
676
            }
677
678
            if ($matches) {
679
                $this->mergeInYamlFragment($this->yamlConfig, $fragment['fragment']);
680
            }
681
        }
682
683
        if ($cache) {
684
            $this->cache->save($this->yamlConfig, $this->key.'yaml_config_'.$this->variantKey());
685
        }
686
687
        // Since yamlConfig has changed, call any callbacks that are interested
688
        foreach ($this->configChangeCallbacks as $callback) {
689
            call_user_func($callback);
690
        }
691
    }
692
693
    /**
694
     * Returns false if the non-prefilterable parts of the rule aren't met, and true if they are
695
     *
696
     * @param array $rules
697
     * @return bool|string
698
     */
699
    public function matchesVariantRules($rules)
0 ignored issues
show
Coding Style introduced by
matchesVariantRules uses the super-global variable $_ENV 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...
700
    {
701
        $matches = "undefined"; // Needs to be truthy, but not true
702
703
        foreach ($rules as $k => $v) {
704
            switch (strtolower($k)) {
705
                case 'classexists':
706
                case 'moduleexists':
707
                    break;
708
709
                case 'environment':
710
                    switch (strtolower($v)) {
711
                        case 'live':
712
                            $matches = $matches && Director::isLive();
713
                            break;
714
                        case 'test':
715
                            $matches = $matches && Director::isTest();
716
                            break;
717
                        case 'dev':
718
                            $matches = $matches && Director::isDev();
719
                            break;
720
                        default:
721
                            user_error('Unknown environment '.$v.' in config fragment', E_USER_ERROR);
722
                    }
723
                    break;
724
725
                case 'envvarset':
726
                    $matches = $matches && isset($_ENV[$v]);
727
                    break;
728
729
                case 'constantdefined':
730
                    $matches = $matches && defined($v);
731
                    break;
732
733
                default:
734
                    $matches = $matches && (
735
                        (isset($_ENV[$k]) && $_ENV[$k] == $v) ||
736
                        (defined($k) && constant($k) == $v)
737
                    );
738
                    break;
739
            }
740
741
            if ($matches === false) {
742
                return $matches;
743
            }
744
        }
745
746
        return $matches;
747
    }
748
749
    /**
750
     * Recursively merge a yaml fragment's configuration array into the primary merged configuration array.
751
     * @param  $into
752
     * @param  $fragment
753
     * @return void
754
     */
755
    public function mergeInYamlFragment(&$into, $fragment)
756
    {
757
        if (is_array($fragment) || ($fragment instanceof  Traversable)) {
758
            foreach ($fragment as $k => $v) {
759
                Config::merge_high_into_low($into[$k], $v);
760
            }
761
        }
762
    }
763
}
764