Config   A
last analyzed

Complexity

Total Complexity 28

Size/Duplication

Total Lines 301
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 28
lcom 1
cbo 5
dl 0
loc 301
rs 10
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A isolated_require() 0 9 2
A __construct() 0 7 1
A offsetSet() 0 4 1
A offsetExists() 0 4 1
A offsetUnset() 0 4 1
A offsetGet() 0 22 3
A get_cache_key() 0 9 2
A revoke() 0 5 1
A add() 0 23 3
A get_fragments() 0 20 3
B synthesize() 0 29 6
A synthesize_for_real() 0 21 4
1
<?php
2
3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ICanBoogie;
13
14
use ICanBoogie\Config\NoFragmentDefined;
15
use ICanBoogie\Config\NoSynthesizerDefined;
16
use ICanBoogie\Storage\Storage;
17
18
/**
19
 * Provides synthesized low-level configurations.
20
 */
21
class Config implements \ArrayAccess
22
{
23
	static private $require_cache = [];
24
25
	static private function isolated_require($__FILE__)
26
	{
27
		if (isset(self::$require_cache[$__FILE__]))
28
		{
29
			return self::$require_cache[$__FILE__];
30
		}
31
32
		return self::$require_cache[$__FILE__] = require $__FILE__;
33
	}
34
35
	/**
36
	 * An array of key/value where _key_ is a path to a config directory and _value_ is its weight.
37
	 * The array is sorted according to the weight of the paths.
38
	 *
39
	 * @var array
40
	 */
41
	private $paths = [];
42
43
	/**
44
	 * Callbacks to synthesize the configurations.
45
	 *
46
	 * @var array
47
	 */
48
	private $synthesizers = [];
49
50
	/**
51
	 * Synthesized configurations.
52
	 *
53
	 * @var array
54
	 */
55
	private $synthesized = [];
56
57
	/**
58
	 * A cache to store and retrieve the synthesized configurations.
59
	 *
60
	 * @var Storage
61
	 */
62
	public $cache;
63
64
	/**
65
	 * Initialize the {@link $paths}, {@link $synthesizers}, and {@link $cache} properties.
66
	 *
67
	 * @param array $paths An array of key/value pairs where _key_ is the path to a config
68
	 * directory and _value_ is the weight of that path.
69
	 * @param array $synthesizers
70
	 * @param Storage $cache A cache for synthesized configurations.
71
	 */
72
	public function __construct(array $paths, array $synthesizers = [], Storage $cache = null)
73
	{
74
		$this->synthesizers = $synthesizers;
75
		$this->cache = $cache;
76
77
		$this->add($paths);
78
	}
79
80
	/**
81
	 * @inheritdoc
82
	 *
83
	 * @throws OffsetNotWritable in attempt to set a configuration.
84
	 */
85
	public function offsetSet($offset, $value)
86
	{
87
		throw new OffsetNotWritable([ $offset, $this ]);
88
	}
89
90
	/**
91
	 * Checks if a config has been synthesized.
92
	 *
93
	 * @param string $id The identifier of the config.
94
	 *
95
	 * @return bool `true` if the config has been synthesized, `false` otherwise.
96
	 */
97
	public function offsetExists($id)
98
	{
99
		return isset($this->synthesized[$id]);
100
	}
101
102
	/**
103
	 * @inheritdoc
104
	 *
105
	 * @throws OffsetNotWritable in attempt to unset an offset.
106
	 */
107
	public function offsetUnset($offset)
108
	{
109
		throw new OffsetNotWritable([ $offset, $this ]);
110
	}
111
112
	/**
113
	 * Returns the specified synthesized configuration.
114
	 *
115
	 * @param string $id The identifier of the config.
116
	 *
117
	 * @return mixed
118
	 *
119
	 * @throws \InvalidArgumentException in attempt to obtain an undefined config.
120
	 */
121
	public function offsetGet($id)
122
	{
123
		if ($this->offsetExists($id))
124
		{
125
			return $this->synthesized[$id];
126
		}
127
128
		if (empty($this->synthesizers[$id]))
129
		{
130
			throw new NoSynthesizerDefined($id);
131
		}
132
133
		list($synthesizer, $from) = $this->synthesizers[$id] + [ 1 => $id ];
134
135
		$started_at = microtime(true);
136
137
		$config = $this->synthesize($id, $synthesizer, $from);
138
139
		ConfigProfiler::add($started_at, $id, $synthesizer);
140
141
		return $config;
142
	}
143
144
	/**
145
	 * @var string
146
	 */
147
	private $cache_key;
148
149
	/**
150
	 * Build a cache key according to the current paths and the config name.
151
	 *
152
	 * @param string $name
153
	 *
154
	 * @return string
155
	 */
156
	private function get_cache_key($name)
157
	{
158
		if (!$this->cache_key)
159
		{
160
			$this->cache_key = substr(sha1(implode('|', array_keys($this->paths))), 0, 8);
161
		}
162
163
		return $this->cache_key . '_' . $name;
164
	}
165
166
	/**
167
	 * Revokes the synthesized configs and the cache key.
168
	 *
169
	 * The method is usually called after the config paths have been modified.
170
	 */
171
	protected function revoke()
172
	{
173
		$this->synthesized = [];
174
		$this->cache_key = null;
175
	}
176
177
	/**
178
	 * Adds a path or several paths to the config.
179
	 *
180
	 * Paths are sorted according to their weight. The order in which they were defined is
181
	 * preserved for paths with the same weight.
182
	 *
183
	 * <pre>
184
	 * <?php
185
	 *
186
	 * $config->add('/path/to/config', 10);
187
	 * $config->add([
188
	 *
189
	 *     '/path1/to/config' => 10,
190
	 *     '/path2/to/config' => 10,
191
	 *     '/path2/to/config' => -10
192
	 *
193
	 * ]);
194
	 * </pre>
195
	 *
196
	 * @param string|array $path
197
	 * @param int $weight Weight of the path. The argument is discarded if `$path` is an array.
198
	 *
199
	 * @throws \InvalidArgumentException if the path is empty.
200
	 */
201
	public function add($path, $weight = 0)
202
	{
203
		if (!$path)
204
		{
205
			throw new \InvalidArgumentException('$path is empty.');
206
		}
207
208
		$paths = $this->paths;
209
210
		if (is_array($path))
211
		{
212
			$paths = array_merge($paths, $path);
213
		}
214
		else
215
		{
216
			$paths[$path] = $weight;
217
		}
218
219
		stable_sort($paths);
220
221
		$this->paths = $paths;
222
		$this->revoke();
223
	}
224
225
	/**
226
	 * Returns the fragments of a configuration.
227
	 *
228
	 * @param string $name Name of the configuration.
229
	 *
230
	 * @return array Where _key_ is the pathname to the fragment file and _value_ the value
231
	 * returned when the file was required.
232
	 */
233
	public function get_fragments($name)
234
	{
235
		$fragments = [];
236
		$filename = $name . '.php';
237
238
		foreach ($this->paths as $path => $weight)
239
		{
240
			$path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
241
			$pathname = $path . $filename;
242
243
			if (!file_exists($pathname))
244
			{
245
				continue;
246
			}
247
248
			$fragments[$path . $filename] = self::isolated_require($pathname);
249
		}
250
251
		return $fragments;
252
	}
253
254
	/**
255
	 * Synthesize a configuration.
256
	 *
257
	 * @param string $name Name of the configuration to synthesize.
258
	 * @param string|array $synthesizer Callback for the synthesis.
259
	 * @param null|string $from If the configuration is a derivative $from is the name
260
	 * of the source configuration.
261
	 *
262
	 * @return mixed
263
	 */
264
	public function synthesize($name, $synthesizer, $from = null)
265
	{
266
		if (array_key_exists($name, $this->synthesized))
267
		{
268
			return $this->synthesized[$name];
269
		}
270
271
		$cache = $this->cache;
272
		$cache_key = $this->get_cache_key($name);
273
274
		if ($cache)
275
		{
276
			$config = $cache->retrieve($cache_key);
277
278
			if ($config !== null)
279
			{
280
				return $this->synthesized[$name] = $config;
281
			}
282
		}
283
284
		$config = $this->synthesize_for_real($from ?: $name, $synthesizer);
285
286
		if ($cache)
287
		{
288
			$cache->store($cache_key, $config);
289
		}
290
291
		return $this->synthesized[$name] = $config;
292
	}
293
294
	/**
295
	 * @param string $name
296
	 * @param callable $synthesizer
297
	 *
298
	 * @return mixed
299
	 */
300
	private function synthesize_for_real($name, $synthesizer)
301
	{
302
		$fragments = $this->get_fragments($name);
303
304
		if (!$fragments)
305
		{
306
			throw new NoFragmentDefined($name);
307
		}
308
309
		if ($synthesizer == 'merge')
310
		{
311
			return call_user_func_array('array_merge', $fragments);
312
		}
313
314
		if ($synthesizer == 'recursive merge')
315
		{
316
			return call_user_func_array('ICanBoogie\array_merge_recursive', $fragments);
317
		}
318
319
		return call_user_func($synthesizer, $fragments);
320
	}
321
}
322