Passed
Push — master ( 2fcb04...472abe )
by Alexander
03:20
created

AbstractConfig::setConfig()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 17
cts 17
cp 1
rs 7.551
c 0
b 0
f 0
cc 7
eloc 14
nc 10
nop 2
crap 7
1
<?php
2
3
namespace Flying\Config;
4
5
/**
6
 * Base implementation of configurable class
7
 */
8
abstract class AbstractConfig implements ConfigurableInterface
9
{
10
    /**
11
     * Mapping table between class name and its configuration options set
12
     *
13
     * @var array
14
     */
15
    private static $configCache = [
16
        'classes_map' => [],
17
        'config'      => [],
18
        'lazy_init'   => [],
19
    ];
20
    /**
21
     * Configuration options
22
     *
23
     * @var array
24
     */
25
    private $config;
26
    /**
27
     * List of configuration options that are not yet initialized
28
     *
29
     * @var array
30
     */
31
    private $configPendingLazyInit = [];
32
    /**
33
     * TRUE if configuration options bootstrap is being performed, FALSE otherwise
34
     *
35
     * @var boolean
36
     */
37
    private $configInBootstrap = false;
38
39
    /**
40
     * {@inheritdoc}
41
     */
42 2
    public function isConfigExists($name)
43
    {
44 2
        if (!is_array($this->config)) {
45 1
            $this->bootstrapConfig();
46 1
        }
47 2
        if (is_string($name) && ($name !== self::CLASS_ID_KEY)) {
48 2
            return array_key_exists($name, $this->config);
49
        }
50 2
        return false;
51
    }
52
53
    /**
54
     * Bootstrap object configuration options
55
     *
56
     * @return void
57
     */
58 63
    protected function bootstrapConfig()
59
    {
60 63
        if (is_array($this->config) || $this->configInBootstrap) {
61
            return;
62
        }
63 63
        $this->configInBootstrap = true;
64 63
        $id = $this->getConfigClassId();
65 63
        if (!array_key_exists($id, self::$configCache['config'])) {
66 63
            $this->initConfig();
67 57
            $lazy = [];
68 57
            foreach ($this->config as $name => $value) {
69 57
                if ($value === null) {
70 26
                    $lazy[$name] = true;
71 26
                }
72 57
            }
73 57
            self::$configCache['config'][$id] = $this->config;
74 57
            self::$configCache['lazy_init'][$id] = $lazy;
75 57
        }
76 57
        $this->config = self::$configCache['config'][$id];
77 57
        $this->configPendingLazyInit = self::$configCache['lazy_init'][$id];
78 57
        $this->configInBootstrap = false;
79 57
    }
80
81
    /**
82
     * Get Id of configuration class that is used for given class
83
     *
84
     * @return string
85
     */
86 43
    protected function getConfigClassId()
87
    {
88 43
        $class = get_class($this);
89 43
        if (!array_key_exists($class, self::$configCache['classes_map'])) {
90
            // Determine which class actually defines configuration for given class
91
            // It is highly uncommon, but still possible situation when class
92
            // have no initConfig() method, so its configuration is completely inherited from parent,
93
            // but has validateConfig() method, so initial state of configuration can be different
94
            // from its parent.
95
            // To handle this properly we need to find earliest parent class that have either initConfig()
96
            // ot validateConfig() method and use is as a mapping target for current class
97
            try {
98 43
                $reflection = new \ReflectionClass($class);
99 43
            } catch (\ReflectionException $e) {
100
                return \stdClass::class;
101
            }
102 43
            $c = $class;
103
            do {
104 43
                if (($reflection->getMethod('initConfig')->getDeclaringClass()->getName() === $c) ||
105 3
                    ($reflection->getMethod('validateConfig')->getDeclaringClass()->getName() === $c)
106 43
                ) {
107 43
                    break;
108
                }
109 1
                $reflection = $reflection->getParentClass();
110 1
                $c = $reflection->getName();
111 1
            } while ($reflection);
112 43
            self::$configCache['classes_map'][$class] = $reflection->getName();
113 43
        }
114 43
        return self::$configCache['classes_map'][$class];
115
    }
116
117
    /**
118
     * Initialize list of configuration options
119
     *
120
     * This method is mean to be overridden to provide configuration options set.
121
     * To allow inheritance of configuration options sets across several levels
122
     * of inherited classes - this method in nested classes should look like this:
123
     *
124
     * <code>
125
     * parent::initConfig();
126
     * $this->mergeConfig([
127
     *     'option' => 'default value',
128
     * ]);
129
     * </code>
130
     *
131
     * @return void
132
     */
133 63
    protected function initConfig()
134
    {
135 63
        $this->config = [];
136 63
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141 10
    public function modifyConfig(array $config, $modification, $value = null)
142
    {
143
        // Call getConfig() for given configuration options, but only if it is necessary
144
        // Without this check it is possible to get infinite recursion loop in a case
145
        // if getConfig() is overridden and calls modifyConfig() by itself
146 10
        if ((!array_key_exists(self::CLASS_ID_KEY, $config)) ||
147 10
            ($config[self::CLASS_ID_KEY] !== $this->getConfigClassId())
148 10
        ) {
149 2
            $config = $this->getConfig($config);
150 2
        }
151 10
        $modification = $this->configToArray($modification, $value, true);
152 10
        if ((!is_array($modification)) || (!count($modification))) {
153 2
            return $config;
154
        }
155 8
        foreach ($modification as $mName => $mValue) {
156 8
            if (!array_key_exists($mName, $this->config)) {
157 4
                continue;
158
            }
159 4
            if ($this->validateConfig($mName, $mValue)) {
160 4
                $config[$mName] = $mValue;
161 4
            }
162 8
        }
163 8
        return $config;
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     * @param string|array|null $config
169
     * @param boolean $export
170
     * @return mixed
171
     * @throws \RuntimeException
172
     */
173 54
    public function getConfig($config = null, $export = false)
174
    {
175 54
        if (!is_array($this->config)) {
176 23
            $this->bootstrapConfig();
177 19
        }
178 50
        if (is_string($config)) {
179
            // This is request for configuration option value
180 10
            if (array_key_exists($config, $this->config)) {
181 10
                if (array_key_exists($config, $this->configPendingLazyInit)) {
182 3
                    $this->resolveLazyConfigInit($config);
183 3
                }
184 10
                return $this->config[$config];
185
            }
186 2
            return null;
187
        }
188
189 43
        if ($config === null) {
190
            // This is request for complete configuration options set
191 37
            $this->resolveLazyConfigInit();
192 36
            $config = $this->config;
193 36
            if (!$export) {
194 36
                $config[self::CLASS_ID_KEY] = $this->getConfigClassId();
195 36
            }
196 36
            return $config;
197
        }
198
199 14
        if (is_array($config) &&
200 14
            array_key_exists(self::CLASS_ID_KEY, $config) &&
201 14
            ($config[self::CLASS_ID_KEY] === $this->getConfigClassId())) {
202
            // This is repetitive call to getConfig()
203 2
            return $config;
204
        }
205
206
        // This is request for configuration (with possible merging)
207 12
        $config = $this->configToArray($config);
208 12
        if (!is_array($config)) {
209 2
            $config = [];
210 2
        }
211 12
        $this->resolveLazyConfigInit();
212 12
        $result = $this->config;
213 12
        if (!$export) {
214 12
            $result[self::CLASS_ID_KEY] = $this->getConfigClassId();
215 12
        }
216 12
        foreach ($config as $name => $value) {
217 10
            if ((!array_key_exists($name, $result)) || ($name === self::CLASS_ID_KEY)) {
218 4
                continue;
219
            }
220 10
            if ($this->validateConfig($name, $value)) {
221 10
                $result[$name] = $value;
222 10
            }
223 12
        }
224 12
        return $result;
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230 35
    public function setConfig($config, $value = null)
231
    {
232 35
        if (!is_array($this->config)) {
233 13
            $this->bootstrapConfig();
234 13
        }
235 35
        $config = $this->configToArray($config, $value, true);
236 35
        if ((!is_array($config)) || (!count($config))) {
237 20
            return;
238
        }
239 22
        foreach ($config as $ck => $cv) {
240 22
            if (!array_key_exists($ck, $this->config)) {
241 5
                continue;
242
            }
243 20
            if (!$this->validateConfig($ck, $cv)) {
244 2
                continue;
245
            }
246 16
            $this->config[$ck] = $cv;
247 16
            unset($this->configPendingLazyInit[$ck]);
248 16
            $this->onConfigChange($ck, $cv);
249 20
        }
250 20
    }
251
252
    /**
253
     * Attempt to convert given configuration information to array
254
     *
255
     * @param mixed $config   Value to convert to array
256
     * @param mixed $value    OPTIONAL Array entry value for inline array entry
257
     * @param boolean $inline OPTIONAL TRUE to allow treating given string values as array entry
258
     * @return mixed
259
     */
260 45
    protected function configToArray($config, $value = null, $inline = false)
261
    {
262 45
        if (is_object($config)) {
263 10
            if (is_callable([$config, 'toArray'])) {
264 4
                $config = $config->toArray();
265 10
            } elseif ($config instanceof \Iterator) {
266 6
                $config = iterator_to_array($config, true);
267 10
            } elseif ($config instanceof \ArrayAccess) {
268 8
                $temp = [];
269 8
                foreach ($this->config as $k => $v) {
270 8
                    if (($k === ConfigurableInterface::CLASS_ID_KEY) || (!$config->offsetExists($k))) {
271 4
                        continue;
272
                    }
273 4
                    $temp[$k] = $config->offsetGet($k);
274 8
                }
275 8
                $config = $temp;
276 8
            }
277 10
        }
278 45
        if ($inline && is_string($config) && '' !== $config) {
279 15
            $config = [$config => $value];
280 15
        }
281 45
        return $config;
282
    }
283
284
    /**
285
     * Check that given value of configuration option is valid
286
     *
287
     * This method is mean to be overridden in a case if additional validation
288
     * of configuration option value should be performed before using it
289
     * Method should validate and, if required, normalize given value
290
     * of configuration option and return true if option can be used and false if not
291
     * It is important that this method will be:
292
     * - as simple as possible to optimize performance
293
     * - will not call other methods that attempts to modify or merge object configuration
294
     * to avoid infinite loop
295
     * Normally this method should look like this:
296
     *
297
     * <code>
298
     * switch($name) {
299
     *      case 'option':
300
     *          // $value validation and normalization code
301
     *          break;
302
     *      default:
303
     *          return parent::validateConfig($name, $value);
304
     *          break;
305
     * }
306
     * </code>
307
     *
308
     * @param string $name Configuration option name
309
     * @param mixed $value Option value (passed by reference)
310
     * @return boolean
311
     */
312 2
    protected function validateConfig($name, &$value)
313
    {
314 2
        return true;
315
    }
316
317
    /**
318
     * Perform required operations when configuration option value is changed
319
     *
320
     * This method is mean to be overridden in a case if some kind of additional logic
321
     * is required to be performed upon setting value of configuration option.
322
     *
323
     * @param string $name Configuration option name
324
     * @param mixed $value Configuration option value
325
     * @return void
326
     */
327 10
    protected function onConfigChange($name, $value)
328
    {
329 10
    }
330
331
    /**
332
     * Resolve lazy initialization of configuration options
333
     *
334
     * @param string $name OPTIONAL Configuration option to perform lazy initialization of
335
     * @throws \RuntimeException
336
     * @return void
337
     */
338 45
    protected function resolveLazyConfigInit($name = null)
339
    {
340 45
        if (!count($this->configPendingLazyInit)) {
341 32
            return;
342
        }
343 18
        if ($name !== null) {
344 3
            $options = array_key_exists($name, $this->configPendingLazyInit) ? [$name] : [];
345 3
        } else {
346 16
            $options = array_keys($this->configPendingLazyInit);
347
        }
348 18
        foreach ($options as $oName) {
349 18
            $value = $this->lazyConfigInit($oName);
350 18
            if ($this->validateConfig($oName, $value)) {
351 18
                $this->config[$oName] = $value;
352 18
                unset($this->configPendingLazyInit[$oName]);
353 18
            } else {
354 1
                throw new \RuntimeException('Lazily initialized configuration option "' . $oName . '" is not passed validation check');
355
            }
356 18
        }
357 17
    }
358
359
    /**
360
     * Perform "lazy initialization" of configuration option with given name
361
     *
362
     * @param string $name Configuration option name
363
     * @return mixed
364
     */
365
    protected function lazyConfigInit($name)
366
    {
367
        return null;
368
    }
369
370
    /**
371
     * Merge given configuration options with current configuration options
372
     *
373
     * @param array $config Configuration options to merge
374
     * @throws \RuntimeException
375
     * @throws \InvalidArgumentException
376
     * @return void
377
     */
378 65
    protected function mergeConfig(array $config)
379
    {
380 65
        if (!$this->configInBootstrap) {
381 2
            throw new \RuntimeException('mergeConfig() can only be used for configuration initialization');
382
        }
383 63
        if (is_int(key($config)) && (array_keys($config) === range(0, count($config) - 1))) {
384
            // Configuration is defined as array of keys with lazy initialization
385 26
            $temp = [];
386 26
            foreach ($config as $key) {
387 26
                if (!is_string($key)) {
388 2
                    throw new \InvalidArgumentException('Configuration option name must be a string');
389
                }
390 24
                $temp[$key] = null;
391 24
            }
392 24
            $config = $temp;
393 24
        }
394 61
        foreach ($config as $key => $value) {
395 61
            if ($value !== null) {
396 37
                if ((!is_scalar($value)) && (!is_array($value))) {
397 2
                    throw new \InvalidArgumentException(sprintf('Non-scalar initial value for configuration option "%s" for class "%s"', $key, get_class($this)));
398
                }
399 35
                if (!$this->validateConfig($key, $value)) {
400 2
                    throw new \RuntimeException(sprintf('Invalid initial value for configuration option "%s" for class "%s"', $key, get_class($this)));
401
                }
402 35
            }
403 59
            $this->config[$key] = $value;
404 59
        }
405 59
    }
406
}
407