Completed
Push — master ( f5d71d...bfd9cb )
by Sam
12:52
created

ConfigManifest::regenerate()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 31
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 23
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 31
rs 8.8571
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
	/** @var string - The base path used when building the manifest */
21
	protected $base;
22
23
	/** @var string - A string to prepend to all cache keys to ensure all keys are unique to just this $base */
24
	protected $key;
25
26
	/** @var bool - Whether `test` directories should be searched when searching for configuration */
27
	protected $includeTests;
28
29
	/**
30
	 * @var Zend_Cache_Core
31
	 */
32
	protected $cache;
33
34
	/**
35
	  * All the values needed to be collected to determine the correct combination of fragements for
36
	  * the current environment.
37
	  * @var array
38
	  */
39
	protected $variantKeySpec = false;
40
41
	/**
42
	 * All the _config.php files. Need to be included every request & can't be cached. Not variant specific.
43
	 * @var array
44
	 */
45
	protected $phpConfigSources = array();
46
47
	/**
48
	 * All the _config/*.yml fragments pre-parsed and sorted in ascending include order. Not variant specific.
49
	 * @var array
50
	 */
51
	protected $yamlConfigFragments = array();
52
53
	/**
54
	 * The calculated config from _config/*.yml, sorted, filtered and merged. Variant specific.
55
	 * @var array
56
	 */
57
	public $yamlConfig = array();
58
59
	/**
60
	 * The variant key state as when yamlConfig was loaded
61
	 * @var string
62
	 */
63
	protected $yamlConfigVariantKey = null;
64
65
	/**
66
	 * @var [callback] A list of callbacks to be called whenever the content of yamlConfig changes
67
	 */
68
	protected $configChangeCallbacks = array();
69
70
	/**
71
	 * A side-effect of collecting the _config fragments is the calculation of all module directories, since
72
	 * the definition of a module is "a directory that contains either a _config.php file or a _config directory
73
	 * @var array
74
	 */
75
	public $modules = array();
76
77
	/**
78
	 * Adds a path as a module
79
	 *
80
	 * @param string $path
81
	 */
82
	public function addModule($path) {
83
		$module = basename($path);
84
		if (isset($this->modules[$module]) && $this->modules[$module] != $path) {
85
			user_error("Module ".$module." in two places - ".$path." and ".$this->modules[$module]);
86
		}
87
		$this->modules[$module] = $path;
88
	}
89
90
	/**
91
	 * Returns true if the passed module exists
92
	 *
93
	 * @param string $module
94
	 * @return bool
95
	 */
96
	public function moduleExists($module) {
97
		return array_key_exists($module, $this->modules);
98
	}
99
100
	/**
101
	 * Constructs and initialises a new configuration object, either loading
102
	 * from the cache or re-scanning for classes.
103
	 *
104
	 * @param string $base The project base path.
105
	 * @param bool $includeTests
106
	 * @param bool $forceRegen Force the manifest to be regenerated.
107
	 */
108
	public function __construct($base, $includeTests = false, $forceRegen = false ) {
109
		$this->base = $base;
110
		$this->key = sha1($base).'_';
111
		$this->includeTests = $includeTests;
112
113
		// Get the Zend Cache to load/store cache into
114
		$this->cache = $this->getCache();
115
116
		// Unless we're forcing regen, try loading from cache
117
		if (!$forceRegen) {
118
			// The PHP config sources are always needed
119
			$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...
120
			// Get the variant key spec
121
			$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...
122
		}
123
124
		// If we don't have a variantKeySpec (because we're forcing regen, or it just wasn't in the cache), generate it
125
		if (false === $this->variantKeySpec) {
126
			$this->regenerate($includeTests);
127
		}
128
129
		// At this point $this->variantKeySpec will always contain something valid, so we can build the variant
130
		$this->buildYamlConfigVariant();
131
	}
132
133
	/**
134
	 * Provides a hook for mock unit tests despite no DI
135
	 * @return Zend_Cache_Core
136
	 */
137
	protected function getCache()
138
	{
139
		return Cache::factory('SS_Configuration', 'Core', array(
140
			'automatic_serialization' => true,
141
			'lifetime' => null
142
		));
143
	}
144
145
	/**
146
	 * Register a callback to be called whenever the calculated merged config changes
147
	 *
148
	 * In some situations the merged config can change - for instance, code in _config.php can cause which Only
149
	 * and Except fragments match. Registering a callback with this function allows code to be called when
150
	 * this happens.
151
	 *
152
	 * @param callback $callback
153
	 */
154
	public function registerChangeCallback($callback) {
155
		$this->configChangeCallbacks[] = $callback;
156
	}
157
158
	/**
159
	 * Includes all of the php _config.php files found by this manifest. Called by SS_Config when adding this manifest
160
	 * @return void
161
	 */
162
	public function activateConfig() {
163
		foreach ($this->phpConfigSources as $config) {
164
			require_once $config;
165
		}
166
167
		if ($this->variantKey() != $this->yamlConfigVariantKey) $this->buildYamlConfigVariant();
168
	}
169
170
	/**
171
	 * Gets the (merged) config value for the given class and config property name
172
	 *
173
	 * @param string $class - The class to get the config property value for
174
	 * @param string $name - The config property to get the value for
175
	 * @param mixed $default - What to return if no value was contained in any YAML file for the passed $class and $name
176
	 * @return mixed The merged set of all values contained in all the YAML configuration files for the passed
177
	 * $class and $name, or $default if there are none
178
	 */
179
	public function get($class, $name, $default=null) {
180
		if (isset($this->yamlConfig[$class][$name])) {
181
			return $this->yamlConfig[$class][$name];
182
		}
183
		return $default;
184
	}
185
186
	/**
187
	 * Returns the string that uniquely identifies this variant. The variant is the combination of classes, modules,
188
	 * environment, environment variables and constants that selects which yaml fragments actually make it into the
189
	 * configuration because of "only"
190
	 * and "except" rules.
191
	 *
192
	 * @return string
193
	 */
194
	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...
195
		$key = $this->variantKeySpec; // Copy to fill in actual values
196
197
		if (isset($key['environment'])) {
198
			$key['environment'] = Director::isDev() ? 'dev' : (Director::isTest() ? 'test' : 'live');
199
		}
200
201
		if (isset($key['envvars'])) 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...
202
			$key['envvars'][$variable] = isset($_ENV[$variable]) ? $_ENV[$variable] : null;
203
		}
204
205
		if (isset($key['constants'])) 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...
206
			$key['constants'][$variable] = defined($variable) ? constant($variable) : null;
207
		}
208
209
		return sha1(serialize($key));
210
	}
211
212
	/**
213
	 * Completely regenerates the manifest file. Scans through finding all php _config.php and yaml _config/*.ya?ml
214
	 * files,parses the yaml files into fragments, sorts them and figures out what values need to be checked to pick
215
	 * the correct variant.
216
	 *
217
	 * Does _not_ build the actual variant
218
	 *
219
	 * @param bool $includeTests
220
	 * @param bool $cache Cache the result.
221
	 */
222
	public function regenerate($includeTests = false, $cache = true) {
223
		$this->phpConfigSources = array();
224
		$this->yamlConfigFragments = array();
225
		$this->variantKeySpec = array();
226
227
		$finder = new ManifestFileFinder();
228
		$finder->setOptions(array(
229
			'name_regex'    => '/(^|[\/\\\\])_config.php$/',
230
			'ignore_tests'  => !$includeTests,
231
			'file_callback' => array($this, 'addSourceConfigFile')
232
		));
233
		$finder->find($this->base);
234
235
		$finder = new ManifestFileFinder();
236
		$finder->setOptions(array(
237
			'name_regex'    => '/\.ya?ml$/',
238
			'ignore_tests'  => !$includeTests,
239
			'file_callback' => array($this, 'addYAMLConfigFile')
240
		));
241
		$finder->find($this->base);
242
243
		$this->prefilterYamlFragments();
244
		$this->sortYamlFragments();
245
		$this->buildVariantKeySpec();
246
247
		if ($cache) {
248
			$this->cache->save($this->phpConfigSources, $this->key.'php_config_sources');
249
			$this->cache->save($this->yamlConfigFragments, $this->key.'yaml_config_fragments');
250
			$this->cache->save($this->variantKeySpec, $this->key.'variant_key_spec');
251
		}
252
	}
253
254
	/**
255
	 * Handle finding a php file. We just keep a record of all php files found, we don't include them
256
	 * at this stage
257
	 *
258
	 * Public so that ManifestFileFinder can call it. Not for general use.
259
	 *
260
	 * @param string $basename
261
	 * @param string $pathname
262
	 * @param int $depth
263
	 */
264
	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...
265
		$this->phpConfigSources[] = $pathname;
266
		// Add this module too
267
		$this->addModule(dirname($pathname));
268
	}
269
270
	/**
271
	 * Handle finding a yml file. Parse the file by spliting it into header/fragment pairs,
272
	 * and normalising some of the header values (especially: give anonymous name if none assigned,
273
	 * splt/complete before and after matchers)
274
	 *
275
	 * Public so that ManifestFileFinder can call it. Not for general use.
276
	 *
277
	 * @param string $basename
278
	 * @param string $pathname
279
	 * @param int $depth
280
	 */
281
	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...
282
		if (!preg_match('{/([^/]+)/_config/}', $pathname, $match)) return;
283
284
		// Keep track of all the modules we've seen
285
		$this->addModule(dirname(dirname($pathname)));
286
287
		$parser = new Parser();
288
289
		// The base header
290
		$base = array(
291
			'module' => $match[1],
292
			'file' => basename(basename($basename, '.yml'), '.yaml')
293
		);
294
295
		// Make sure the linefeeds are all converted to \n, PCRE '$' will not match anything else.
296
		$fileContents = str_replace(array("\r\n", "\r"), "\n", file_get_contents($pathname));
297
298
		// YAML parsers really should handle this properly themselves, but neither spyc nor symfony-yaml do. So we
299
		// follow in their vein and just do what we need, not what the spec says
300
		$parts = preg_split('/^---$/m', $fileContents, -1, PREG_SPLIT_NO_EMPTY);
301
302
		// If only one document, it's a headerless fragment. So just add it with an anonymous name
303
		if (count($parts) == 1) {
304
			$this->yamlConfigFragments[] = $base + array(
305
				'name' => 'anonymous-1',
306
				'fragment' => $parser->parse($parts[0])
307
			);
308
		}
309
		// Otherwise it's a set of header/document pairs
310
		else {
311
			// If we got an odd number of parts the config file doesn't have a header for every document
312
			if (count($parts) % 2 != 0) {
313
				user_error("Configuration file '$pathname' does not have an equal number of headers and config blocks");
314
			}
315
316
			// Step through each pair
317
			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...
318
				// Make all the first-level keys of the header lower case
319
				$header = array_change_key_case($parser->parse($parts[$i]), CASE_LOWER);
320
321
				// Assign a name if non assigned already
322
				if (!isset($header['name'])) $header['name'] = 'anonymous-'.(1+$i/2);
323
324
				// Parse & normalise the before and after if present
325
				foreach (array('before', 'after') as $order) {
326
					if (isset($header[$order])) {
327
						// First, splice into parts (multiple before or after parts are allowed, comma separated)
328
						if (is_array($header[$order])) $orderparts = $header[$order];
329
						else $orderparts = preg_split('/\s*,\s*/', $header[$order], -1, PREG_SPLIT_NO_EMPTY);
330
331
						// For each, parse out into module/file#name, and set any missing to "*"
332
						$header[$order] = array();
333
						foreach($orderparts as $part) {
334
							preg_match('! (?P<module>\*|[^\/#]+)? (\/ (?P<file>\*|\w+))? (\# (?P<fragment>\*|\w+))? !x',
335
								$part, $match);
336
337
							$header[$order][] = array(
338
								'module' => isset($match['module']) && $match['module'] ? $match['module'] : '*',
339
								'file' => isset($match['file']) && $match['file'] ? $match['file'] : '*',
340
								'name' => isset($match['fragment'])  && $match['fragment'] ? $match['fragment'] : '*'
341
							);
342
						}
343
					}
344
				}
345
346
				// And add to the fragments list
347
				$this->yamlConfigFragments[] = $base + $header + array(
348
					'fragment' => $parser->parse($parts[$i+1])
349
				);
350
			}
351
		}
352
	}
353
354
	/**
355
	 * Sorts the YAML fragments so that the "before" and "after" rules are met.
356
	 * Throws an error if there's a loop
357
	 *
358
	 * We can't use regular sorts here - we need a topological sort. Easiest
359
	 * way is with a DAG, so build up a DAG based on the before/after rules, then
360
	 * sort that.
361
	 *
362
	 * @return void
363
	 */
364
	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...
365
		$frags = $this->yamlConfigFragments;
366
367
		// Build a directed graph
368
		$dag = new DAG($frags);
369
370
		foreach ($frags as $i => $frag) {
371
			foreach ($frags as $j => $otherfrag) {
372
				if ($i == $j) continue;
373
374
				$order = $this->relativeOrder($frag, $otherfrag);
375
376
				if ($order == 'before') $dag->addedge($i, $j);
377
				elseif ($order == 'after') $dag->addedge($j, $i);
378
			}
379
		}
380
381
		try {
382
			$this->yamlConfigFragments = $dag->sort();
383
		}
384
		catch (DAG_CyclicException $e) {
385
386
			if (!Director::isLive() && isset($_REQUEST['debug'])) {
387
				$res = '<h1>Remaining config fragment graph</h1>';
388
				$res .= '<dl>';
389
390
				foreach ($e->dag as $node) {
391
					$res .= "<dt>{$node['from']['module']}/{$node['from']['file']}#{$node['from']['name']}"
392
						. " marked to come after</dt><dd><ul>";
393
					foreach ($node['to'] as $to) {
394
						$res .= "<li>{$to['module']}/{$to['file']}#{$to['name']}</li>";
395
					}
396
					$res .= "</ul></dd>";
397
				}
398
399
				$res .= '</dl>';
400
				echo $res;
401
			}
402
403
			user_error('Based on their before & after rules two fragments both need to be before/after each other',
404
				E_USER_ERROR);
405
		}
406
407
	}
408
409
	/**
410
	 * Return a string "after", "before" or "undefined" depending on whether the YAML fragment array element passed
411
	 * as $a should be positioned after, before, or either compared to the YAML fragment array element passed as $b
412
	 *
413
	 * @param array $a A YAML config fragment as loaded by addYAMLConfigFile
414
	 * @param array $b A YAML config fragment as loaded by addYAMLConfigFile
415
	 * @return string "after", "before" or "undefined"
416
	 */
417
	protected function relativeOrder($a, $b) {
418
		$matches = array();
419
420
		// Do the same thing for after and before
421
		foreach (array('before', 'after') as $rulename) {
422
			$matches[$rulename] = array();
423
424
			// Figure out for each rule, which part matches
425
			if (isset($a[$rulename])) foreach ($a[$rulename] as $rule) {
426
				$match = array();
427
428
				foreach(array('module', 'file', 'name') as $part) {
429
					// If part is *, we match _unless_ the opposite rule has a non-* matcher than also matches $b
430
					if ($rule[$part] == '*') {
431
						$match[$part] = 'wild';
432
					}
433
					else {
434
						$match[$part] = ($rule[$part] == $b[$part]);
435
					}
436
				}
437
438
				$matches[$rulename][] = $match;
439
			}
440
		}
441
442
		// Figure out the specificness of each match. 1 an actual match, 0 for a wildcard match, remove if no match
443
		$matchlevel = array('before' => -1, 'after' => -1);
444
445
		foreach (array('before', 'after') as $rulename) {
446
			foreach ($matches[$rulename] as $i => $rule) {
447
				$level = 0;
448
449
				foreach ($rule as $part => $partmatches) {
450
					if ($partmatches === false) continue 2;
451
					if ($partmatches === true) $level += 1;
452
				}
453
454
				if ($matchlevel[$rulename] === false || $level > $matchlevel[$rulename]) {
455
					$matchlevel[$rulename] = $level;
456
				}
457
			}
458
		}
459
460
		if ($matchlevel['before'] === -1 && $matchlevel['after'] === -1) {
461
			return 'undefined';
462
		}
463
		else if ($matchlevel['before'] === $matchlevel['after']) {
464
			user_error('Config fragment requires itself to be both before _and_ after another fragment', E_USER_ERROR);
465
		}
466
		else {
467
			return ($matchlevel['before'] > $matchlevel['after']) ? 'before' : 'after';
468
		}
469
	}
470
471
	/**
472
	 * This function filters the loaded yaml fragments, removing any that can't ever have their "only" and "except"
473
	 * rules match.
474
	 *
475
	 * Some tests in "only" and "except" rules need to be checked per request, but some are manifest based -
476
	 * these are invariant over requests and only need checking on manifest rebuild. So we can prefilter these before
477
	 * saving yamlConfigFragments to speed up the process of checking the per-request variant/
478
	 */
479
	public function prefilterYamlFragments() {
480
		$matchingFragments = array();
481
482
		foreach ($this->yamlConfigFragments as $i => $fragment) {
483
			$matches = true;
484
485
			if (isset($fragment['only'])) {
486
				$matches = $matches && ($this->matchesPrefilterVariantRules($fragment['only']) !== false);
487
			}
488
489
			if (isset($fragment['except'])) {
490
				$matches = $matches && ($this->matchesPrefilterVariantRules($fragment['except']) !== true);
491
			}
492
493
			if ($matches) $matchingFragments[] = $fragment;
494
		}
495
496
		$this->yamlConfigFragments = $matchingFragments;
497
	}
498
499
	/**
500
	 * Returns false if the prefilterable parts of the rule aren't met, and true if they are
501
	 *
502
	 * @param  $rules array - A hash of rules as allowed in the only or except portion of a config fragment header
503
	 * @return bool - True if the rules are met, false if not. (Note that depending on whether we were passed an
504
	 *                only or an except rule,
505
	 * which values means accept or reject a fragment
506
	 */
507
	public function matchesPrefilterVariantRules($rules) {
508
		$matches = "undefined"; // Needs to be truthy, but not true
509
510
		foreach ($rules as $k => $v) {
511
			switch (strtolower($k)) {
512
				case 'classexists':
513
					$matches = $matches && ClassInfo::exists($v);
514
					break;
515
516
				case 'moduleexists':
517
					$matches = $matches && $this->moduleExists($v);
518
					break;
519
520
				default:
521
					// NOP
522
			}
523
524
			if ($matches === false) return $matches;
525
		}
526
527
		return $matches;
528
	}
529
530
	/**
531
	 * Builds the variant key spec - the list of values that need to be build to give a key that uniquely identifies
532
	 * this variant.
533
	 */
534
	public function buildVariantKeySpec() {
535
		$this->variantKeySpec = array();
536
537
		foreach ($this->yamlConfigFragments as $fragment) {
538
			if (isset($fragment['only'])) $this->addVariantKeySpecRules($fragment['only']);
539
			if (isset($fragment['except'])) $this->addVariantKeySpecRules($fragment['except']);
540
		}
541
	}
542
543
	/**
544
	 * Adds any variables referenced in the passed rules to the $this->variantKeySpec array
545
	 *
546
	 * @param array $rules
547
	 */
548
	public function addVariantKeySpecRules($rules) {
549
		foreach ($rules as $k => $v) {
550
			switch (strtolower($k)) {
551
				case 'classexists':
552
				case 'moduleexists':
553
					// Classes and modules are a special case - we can pre-filter on config regenerate because we
554
					// already know if the class or module exists
555
					break;
556
557
				case 'environment':
558
					$this->variantKeySpec['environment'] = true;
559
					break;
560
561
				case 'envvarset':
562
					if (!isset($this->variantKeySpec['envvars'])) $this->variantKeySpec['envvars'] = array();
563
					$this->variantKeySpec['envvars'][$k] = $k;
564
					break;
565
566
				case 'constantdefined':
567
					if (!isset($this->variantKeySpec['constants'])) $this->variantKeySpec['constants'] = array();
568
					$this->variantKeySpec['constants'][$k] = $k;
569
					break;
570
571
				default:
572
					if (!isset($this->variantKeySpec['envvars'])) $this->variantKeySpec['envvars'] = array();
573
					if (!isset($this->variantKeySpec['constants'])) $this->variantKeySpec['constants'] = array();
574
					$this->variantKeySpec['envvars'][$k] = $this->variantKeySpec['constants'][$k] = $k;
575
			}
576
		}
577
	}
578
579
	/**
580
	 * Calculates which yaml config fragments are applicable in this variant, and merge those all together into
581
	 * the $this->yamlConfig propperty
582
	 *
583
	 * Checks cache and takes care of loading yamlConfigFragments if they aren't already present, but expects
584
	 * $variantKeySpec to already be set
585
	 *
586
	 * @param bool $cache
587
	 */
588
	public function buildYamlConfigVariant($cache = true) {
589
		// Only try loading from cache if we don't have the fragments already loaded, as there's no way to know if a
590
		// given variant is stale compared to the complete set of fragments
591
		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...
592
			// First try and just load the exact variant
593
			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...
594
				$this->yamlConfigVariantKey = $this->variantKey();
595
				return;
596
			}
597
			// Otherwise try and load the fragments so we can build the variant
598
			else {
599
				$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...
600
			}
601
		}
602
603
		// If we still don't have any fragments we have to build them
604
		if (!$this->yamlConfigFragments) {
605
			$this->regenerate($this->includeTests, $cache);
606
		}
607
608
		$this->yamlConfig = array();
609
		$this->yamlConfigVariantKey = $this->variantKey();
610
611
		foreach ($this->yamlConfigFragments as $i => $fragment) {
612
			$matches = true;
613
614
			if (isset($fragment['only'])) {
615
				$matches = $matches && ($this->matchesVariantRules($fragment['only']) !== false);
616
			}
617
618
			if (isset($fragment['except'])) {
619
				$matches = $matches && ($this->matchesVariantRules($fragment['except']) !== true);
620
			}
621
622
			if ($matches) $this->mergeInYamlFragment($this->yamlConfig, $fragment['fragment']);
623
		}
624
625
		if ($cache) {
626
			$this->cache->save($this->yamlConfig, $this->key.'yaml_config_'.$this->variantKey());
627
		}
628
629
		// Since yamlConfig has changed, call any callbacks that are interested
630
		foreach ($this->configChangeCallbacks as $callback) call_user_func($callback);
631
	}
632
633
	/**
634
	 * Returns false if the non-prefilterable parts of the rule aren't met, and true if they are
635
	 *
636
	 * @param array $rules
637
	 * @return bool|string
638
	 */
639
	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...
640
		$matches = "undefined"; // Needs to be truthy, but not true
641
642
		foreach ($rules as $k => $v) {
643
			switch (strtolower($k)) {
644
				case 'classexists':
645
				case 'moduleexists':
646
					break;
647
648
				case 'environment':
649
					switch (strtolower($v)) {
650
						case 'live':
651
							$matches = $matches && Director::isLive();
652
							break;
653
						case 'test':
654
							$matches = $matches && Director::isTest();
655
							break;
656
						case 'dev':
657
							$matches = $matches && Director::isDev();
658
							break;
659
						default:
660
							user_error('Unknown environment '.$v.' in config fragment', E_USER_ERROR);
661
					}
662
					break;
663
664
				case 'envvarset':
665
					$matches = $matches && isset($_ENV[$v]);
666
					break;
667
668
				case 'constantdefined':
669
					$matches = $matches && defined($v);
670
					break;
671
672
				default:
673
					$matches = $matches && (
674
						(isset($_ENV[$k]) && $_ENV[$k] == $v) ||
675
						(defined($k) && constant($k) == $v)
676
					);
677
					break;
678
			}
679
680
			if ($matches === false) return $matches;
681
		}
682
683
		return $matches;
684
	}
685
686
	/**
687
	 * Recursively merge a yaml fragment's configuration array into the primary merged configuration array.
688
	 * @param  $into
689
	 * @param  $fragment
690
	 * @return void
691
	 */
692
	public function mergeInYamlFragment(&$into, $fragment) {
693
		if (is_array($fragment) || ($fragment instanceof  Traversable)) {
694
			foreach ($fragment as $k => $v) {
695
				Config::merge_high_into_low($into[$k], $v);
696
			}
697
		}
698
	}
699
700
}
701