AbstractConfig   C
last analyzed

Complexity

Total Complexity 78

Size/Duplication

Total Lines 397
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 97.75%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 78
c 1
b 0
f 0
lcom 1
cbo 0
dl 0
loc 397
ccs 174
cts 178
cp 0.9775
rs 5.4563

13 Methods

Rating   Name   Duplication   Size   Complexity  
A isConfigExists() 0 10 4
B bootstrapConfig() 0 22 6
B getConfigClassId() 0 30 6
A initConfig() 0 4 1
C modifyConfig() 0 24 8
C getConfig() 0 50 16
B setConfig() 0 16 6
C configToArray() 0 22 11
A validateConfig() 0 4 1
A onConfigChange() 0 3 1
B resolveLazyConfigInit() 0 20 6
A lazyConfigInit() 0 4 1
C mergeConfig() 0 28 11

How to fix   Complexity   

Complex Class

Complex classes like AbstractConfig often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractConfig, and based on these observations, apply Extract Interface, too.

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
     * @param array $config
141
     * @param array|\ArrayAccess|\Iterator|object|string $modification
142
     * @param mixed $value
143
     * @return array
144
     */
145 10
    public function modifyConfig(array $config, $modification, $value = null)
146
    {
147
        // Call getConfig() for given configuration options, but only if it is necessary
148
        // Without this check it is possible to get infinite recursion loop in a case
149
        // if getConfig() is overridden and calls modifyConfig() by itself
150 10
        if ((!array_key_exists(self::CLASS_ID_KEY, $config)) ||
151 10
            ($config[self::CLASS_ID_KEY] !== $this->getConfigClassId())
152 10
        ) {
153 2
            $config = $this->getConfig($config);
154 2
        }
155 10
        $modification = $this->configToArray($modification, $value, true);
156 10
        if ((!is_array($modification)) || (!count($modification))) {
157 2
            return $config;
158
        }
159 8
        foreach ($modification as $mName => $mValue) {
160 8
            if (!array_key_exists($mName, $this->config)) {
161 4
                continue;
162
            }
163 4
            if ($this->validateConfig($mName, $mValue)) {
164 4
                $config[$mName] = $mValue;
165 4
            }
166 8
        }
167 8
        return $config;
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     * @param array|\ArrayAccess|\Iterator|object|string|null $config
173
     * @param boolean $export
174
     * @return mixed
175
     * @throws \RuntimeException
176
     */
177 54
    public function getConfig($config = null, $export = false)
178
    {
179 54
        if (!is_array($this->config)) {
180 23
            $this->bootstrapConfig();
181 19
        }
182 50
        if (is_string($config)) {
183
            // This is request for configuration option value
184 10
            if (array_key_exists($config, $this->config)) {
185 10
                if (array_key_exists($config, $this->configPendingLazyInit)) {
186 3
                    $this->resolveLazyConfigInit($config);
187 3
                }
188 10
                return $this->config[$config];
189
            }
190 2
            return null;
191
        }
192
193 43
        if ($config === null) {
194
            // This is request for complete configuration options set
195 37
            $this->resolveLazyConfigInit();
196 36
            $config = $this->config;
197 36
            if (!$export) {
198 36
                $config[self::CLASS_ID_KEY] = $this->getConfigClassId();
199 36
            }
200 36
            return $config;
201
        }
202
203 14
        if (is_array($config) &&
204 14
            array_key_exists(self::CLASS_ID_KEY, $config) &&
205 14
            ($config[self::CLASS_ID_KEY] === $this->getConfigClassId())) {
206
            // This is repetitive call to getConfig()
207 2
            return $config;
208
        }
209
210
        // This is request for configuration (with possible merging)
211 12
        $config = $this->configToArray($config);
212 12
        if (!is_array($config)) {
213 2
            $config = [];
214 2
        }
215 12
        $this->resolveLazyConfigInit();
216 12
        $result = $this->config;
217 12
        if (!$export) {
218 12
            $result[self::CLASS_ID_KEY] = $this->getConfigClassId();
219 12
        }
220 12
        foreach ($config as $name => $value) {
221 10
            if ($name !== self::CLASS_ID_KEY && array_key_exists($name, $result) && $this->validateConfig($name, $value)) {
222 10
                $result[$name] = $value;
223 10
            }
224 12
        }
225 12
        return $result;
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     * @param array|\ArrayAccess|\Iterator|object|string $config
231
     * @param mixed $value
232
     * @return void
233
     */
234 35
    public function setConfig($config, $value = null)
235
    {
236 35
        if (!is_array($this->config)) {
237 13
            $this->bootstrapConfig();
238 13
        }
239 35
        $config = $this->configToArray($config, $value, true);
240 35
        if (is_array($config)) {
241 35
            foreach ($config as $ck => $cv) {
242 22
                if (array_key_exists($ck, $this->config) && $this->validateConfig($ck, $cv)) {
243 16
                    $this->config[$ck] = $cv;
244 16
                    unset($this->configPendingLazyInit[$ck]);
245 16
                    $this->onConfigChange($ck, $cv);
246 16
                }
247 33
            }
248 33
        }
249 33
    }
250
251
    /**
252
     * Attempt to convert given configuration information to array
253
     *
254
     * @param array|\ArrayAccess|\Iterator|object|string $config Value to convert to array
255
     * @param mixed $value                                       OPTIONAL Array entry value for inline array entry
256
     * @param boolean $inline                                    OPTIONAL TRUE to allow treating given string values as array entry
257
     * @return mixed
258
     */
259 45
    protected function configToArray($config, $value = null, $inline = false)
260
    {
261 45
        if (is_object($config)) {
262 10
            if (is_callable([$config, 'toArray'])) {
263 4
                $config = $config->toArray();
264 10
            } elseif ($config instanceof \Iterator) {
265 6
                $config = iterator_to_array($config, true);
266 10
            } elseif ($config instanceof \ArrayAccess) {
267 8
                $temp = [];
268 8
                foreach ($this->config as $k => $v) {
269 8
                    if ($k !== ConfigurableInterface::CLASS_ID_KEY && $config->offsetExists($k)) {
270 4
                        $temp[$k] = $config->offsetGet($k);
271 4
                    }
272 8
                }
273 8
                $config = $temp;
274 8
            }
275 10
        }
276 45
        if ($inline && is_string($config) && '' !== $config) {
277 15
            $config = [$config => $value];
278 15
        }
279 45
        return $config;
280
    }
281
282
    /**
283
     * Check that given value of configuration option is valid
284
     *
285
     * This method is mean to be overridden in a case if additional validation
286
     * of configuration option value should be performed before using it
287
     * Method should validate and, if required, normalize given value
288
     * of configuration option and return true if option can be used and false if not
289
     * It is important that this method will be:
290
     * - as simple as possible to optimize performance
291
     * - will not call other methods that attempts to modify or merge object configuration
292
     * to avoid infinite loop
293
     * Normally this method should look like this:
294
     *
295
     * <code>
296
     * switch($name) {
297
     *      case 'option':
298
     *          // $value validation and normalization code
299
     *          break;
300
     *      default:
301
     *          return parent::validateConfig($name, $value);
302
     *          break;
303
     * }
304
     * </code>
305
     *
306
     * @param string $name Configuration option name
307
     * @param mixed $value Option value (passed by reference)
308
     * @return boolean
309
     */
310 2
    protected function validateConfig($name, &$value)
311
    {
312 2
        return true;
313
    }
314
315
    /**
316
     * Perform required operations when configuration option value is changed
317
     *
318
     * This method is mean to be overridden in a case if some kind of additional logic
319
     * is required to be performed upon setting value of configuration option.
320
     *
321
     * @param string $name Configuration option name
322
     * @param mixed $value Configuration option value
323
     * @return void
324
     */
325 10
    protected function onConfigChange($name, $value)
326
    {
327 10
    }
328
329
    /**
330
     * Resolve lazy initialization of configuration options
331
     *
332
     * @param string $name OPTIONAL Configuration option to perform lazy initialization of
333
     * @throws \RuntimeException
334
     * @return void
335
     */
336 45
    protected function resolveLazyConfigInit($name = null)
337
    {
338 45
        if (!count($this->configPendingLazyInit)) {
339 32
            return;
340
        }
341 18
        if ($name !== null) {
342 3
            $options = array_key_exists($name, $this->configPendingLazyInit) ? [$name] : [];
343 3
        } else {
344 16
            $options = array_keys($this->configPendingLazyInit);
345
        }
346 18
        foreach ($options as $oName) {
347 18
            $value = $this->lazyConfigInit($oName);
348 18
            if ($this->validateConfig($oName, $value)) {
349 18
                $this->config[$oName] = $value;
350 18
                unset($this->configPendingLazyInit[$oName]);
351 18
            } else {
352 1
                throw new \RuntimeException('Lazily initialized configuration option "' . $oName . '" is not passed validation check');
353
            }
354 18
        }
355 17
    }
356
357
    /**
358
     * Perform "lazy initialization" of configuration option with given name
359
     *
360
     * @param string $name Configuration option name
361
     * @return mixed
362
     */
363
    protected function lazyConfigInit($name)
364
    {
365
        return null;
366
    }
367
368
    /**
369
     * Merge given configuration options with current configuration options
370
     *
371
     * @param array $config Configuration options to merge
372
     * @throws \RuntimeException
373
     * @throws \InvalidArgumentException
374
     * @return void
375
     */
376 65
    protected function mergeConfig(array $config)
377
    {
378 65
        if (!$this->configInBootstrap) {
379 2
            throw new \RuntimeException('mergeConfig() can only be used for configuration initialization');
380
        }
381 63
        if (is_int(key($config)) && (array_keys($config) === range(0, count($config) - 1))) {
382
            // Configuration is defined as array of keys with lazy initialization
383 26
            if (array_reduce($config, function ($n, $v) {
384 26
                return $n || !is_string($v);
385 26
            }, false)) {
386 2
                throw new \InvalidArgumentException('Lazy configuration should be list of string configuration keys');
387
            }
388 24
            $this->config = array_merge($this->config, array_fill_keys($config, null));
389 24
        } else {
390
            // Configuration is given as normal key->value array
391 37
            foreach ($config as $key => $value) {
392 37
                if ($value !== null) {
393 37
                    if ((!is_scalar($value)) && (!is_array($value))) {
394 2
                        throw new \InvalidArgumentException(sprintf('Non-scalar initial value for configuration option "%s" for class "%s"', $key, get_class($this)));
395
                    }
396 35
                    if (!$this->validateConfig($key, $value)) {
397 2
                        throw new \RuntimeException(sprintf('Invalid initial value for configuration option "%s" for class "%s"', $key, get_class($this)));
398
                    }
399 35
                }
400 35
                $this->config[$key] = $value;
401 35
            }
402
        }
403 59
    }
404
}
405