Completed
Push — master ( eeee3e...05680b )
by Chauncey
02:32 queued 01:18
created

AbstractConfig   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 321
Duplicated Lines 6.54 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 45
lcom 1
cbo 4
dl 21
loc 321
rs 8.8
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 31 6
A defaults() 0 4 1
A merge() 0 7 2
A getIterator() 0 4 1
B offsetExists() 6 44 10
B offsetGet() 6 44 10
B offsetSet() 3 33 7
B offsetReplace() 6 24 6
A addFile() 0 8 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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 Charcoal\Config;
4
5
use ArrayIterator;
6
use IteratorAggregate;
7
use Traversable;
8
use InvalidArgumentException;
9
10
// From PSR-11
11
use Psr\Container\ContainerInterface;
12
13
/**
14
 * Default configuration container / registry.
15
 *
16
 * ### Notes on {@see SeparatorAwareTrait}:
17
 *
18
 * - Provides the ability for a store to fetch data that is nested in a tree-like structure,
19
 *   often referred to as "dot" notation.
20
 *
21
 * ### Notes on {@see DelegatesAwareTrait}:
22
 *
23
 * - Provides the ability for a store to fetch data in another store.
24
 * - Provides this store with a way to register one or more delegate stores.
25
 */
26
abstract class AbstractConfig extends AbstractEntity implements
27
    ConfigInterface,
28
    ContainerInterface,
29
    IteratorAggregate
30
{
31
    use DelegatesAwareTrait;
32
    use FileAwareTrait;
33
    use SeparatorAwareTrait;
34
35
    const DEFAULT_SEPARATOR = '.';
36
37
    /**
38
     * Create the configuration.
39
     *
40
     * @param  mixed             $data      Initial data. Either a filepath,
41
     *     an associative array, or an {@see Traversable iterable object}.
42
     * @param  EntityInterface[] $delegates An array of delegates (config) to set.
43
     * @throws InvalidArgumentException If $data is invalid.
44
     */
45
    final public function __construct($data = null, array $delegates = null)
46
    {
47
        // Always set the default chaining notation
48
        $this->setSeparator(self::DEFAULT_SEPARATOR);
49
50
        // Always set the default data first.
51
        $this->setData($this->defaults());
52
53
        // Set the delegates, if necessary.
54
        if (isset($delegates)) {
55
            $this->setDelegates($delegates);
56
        }
57
58
        if ($data === null) {
59
            return;
60
        }
61
62
        if (is_string($data)) {
63
            // Treat the parameter as a filepath
64
            $this->addFile($data);
65
        } elseif (is_array($data)) {
66
            $this->merge($data);
67
        } elseif ($data instanceof Traversable) {
68
            $this->merge($data);
69
        } else {
70
            throw new InvalidArgumentException(sprintf(
71
                'Data must be a config file, an associative array, or an object implementing %s',
72
                Traversable::class
73
            ));
74
        }
75
    }
76
77
    /**
78
     * Gets all default data from this store.
79
     *
80
     * Pre-populates new stores.
81
     *
82
     * May be reimplemented in inherited classes if any default values should be defined.
83
     *
84
     * @return array Key-value array of data
85
     */
86
    public function defaults()
87
    {
88
        return [];
89
    }
90
91
    /**
92
     * Adds new data, replacing / merging existing data with the same key.
93
     *
94
     * @uses   self::offsetReplace()
95
     * @param  array|Traversable $data Key-value dataset to merge.
96
     *     Either an associative array or an {@see Traversable iterable object}
97
     *     (such as {@see ConfigInterface}).
98
     * @return self
99
     */
100
    public function merge($data)
101
    {
102
        foreach ($data as $key => $value) {
103
            $this->offsetReplace($key, $value);
104
        }
105
        return $this;
106
    }
107
108
    /**
109
     * Create a new iterator from the configuration instance.
110
     *
111
     * @see    IteratorAggregate
112
     * @return ArrayIterator
113
     */
114
    public function getIterator()
115
    {
116
        return new ArrayIterator($this->data());
117
    }
118
119
    /**
120
     * Determines if this store contains the specified key and if its value is not NULL.
121
     *
122
     * Routine:
123
     * - If the data key is {@see SeparatorAwareTrait::$separator nested},
124
     *   the data-tree is traversed until the endpoint is found, if any;
125
     * - If the data key does NOT exist on the store, a lookup is performed
126
     *   on each delegate store until a key is found, if any.
127
     *
128
     * @see    \ArrayAccess
129
     * @uses   SeparatorAwareTrait::hasWithSeparator()
130
     * @uses   DelegatesAwareTrait::hasInDelegates()
131
     * @param  string $key The data key to check.
132
     * @throws InvalidArgumentException If the $key is not a string or is a numeric value.
133
     * @return boolean TRUE if $key exists and has a value other than NULL, FALSE otherwise.
134
     */
135
    public function offsetExists($key)
136
    {
137
        if (is_numeric($key)) {
138
            throw new InvalidArgumentException(
139
                'Entity array access only supports non-numeric keys'
140
            );
141
        }
142
143
        if ($this->separator && strstr($key, $this->separator)) {
144
            return $this->hasWithSeparator($key);
145
        }
146
147
        $key = $this->camelize($key);
148
149
        /** @internal Edge Case: "_" → "" */
150
        if ($key === '') {
151
            return false;
152
        }
153
154
        $getter = 'get'.ucfirst($key);
155 View Code Duplication
        if (!isset($this->mutatorCache[$getter])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
156
            $this->mutatorCache[$getter] = is_callable([ $this, $getter ]);
157
        }
158
159
        if ($this->mutatorCache[$getter]) {
160
            return ($this->{$getter}() !== null);
161
        }
162
163
        // -- START DEPRECATED
164 View Code Duplication
        if (!isset($this->mutatorCache[$key])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
165
            $this->mutatorCache[$key] = is_callable([ $this, $key ]);
166
        }
167
168
        if ($this->mutatorCache[$key]) {
169
            return ($this->{$key}() !== null);
170
        }
171
        // -- END DEPRECATED
172
173
        if (isset($this->{$key})) {
174
            return true;
175
        }
176
177
        return $this->hasInDelegates($key);
178
    }
179
180
    /**
181
     * Returns the value from the specified key on this entity.
182
     *
183
     * Routine:
184
     * - If the data key is {@see SeparatorAwareTrait::$separator nested},
185
     *   the data-tree is traversed until the endpoint to return its value, if any;
186
     * - If the data key does NOT exist on the store, a lookup is performed
187
     *   on each delegate store until a value is found, if any.
188
     *
189
     * @see    \ArrayAccess
190
     * @uses   SeparatorAwareTrait::getWithSeparator()
191
     * @uses   DelegatesAwareTrait::getInDelegates()
192
     * @param  string $key The data key to retrieve.
193
     * @throws InvalidArgumentException If the $key is not a string or is a numeric value.
194
     * @return mixed Value of the requested $key on success, NULL if the $key is not set.
195
     */
196
    public function offsetGet($key)
197
    {
198
        if (is_numeric($key)) {
199
            throw new InvalidArgumentException(
200
                'Entity array access only supports non-numeric keys'
201
            );
202
        }
203
204
        if ($this->separator && strstr($key, $this->separator)) {
205
            return $this->getWithSeparator($key);
206
        }
207
208
        $key = $this->camelize($key);
209
210
        /** @internal Edge Case: "_" → "" */
211
        if ($key === '') {
212
            return null;
213
        }
214
215
        $getter = 'get'.ucfirst($key);
216 View Code Duplication
        if (!isset($this->mutatorCache[$getter])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
217
            $this->mutatorCache[$getter] = is_callable([ $this, $getter ]);
218
        }
219
220
        if ($this->mutatorCache[$getter]) {
221
            return $this->{$getter}();
222
        }
223
224
        // -- START DEPRECATED
225 View Code Duplication
        if (!isset($this->mutatorCache[$key])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
226
            $this->mutatorCache[$key] = is_callable([ $this, $key ]);
227
        }
228
229
        if ($this->mutatorCache[$key]) {
230
            return $this->{$key}();
231
        }
232
        // -- END DEPRECATED
233
234
        if (isset($this->{$key})) {
235
            return $this->{$key};
236
        }
237
238
        return $this->getInDelegates($key);
239
    }
240
241
    /**
242
     * Assigns the value to the specified key on this entity.
243
     *
244
     * Routine:
245
     * - If the data key is {@see SeparatorAwareTrait::$separator nested},
246
     *   the data-tree is traversed until the endpoint to assign its value;
247
     *
248
     * @see    \ArrayAccess
249
     * @uses   SeparatorAwareTrait::setWithSeparator()
250
     * @param  string $key   The data key to assign $value to.
251
     * @param  mixed  $value The data value to assign to $key.
252
     * @throws InvalidArgumentException If the $key is not a string or is a numeric value.
253
     * @return void
254
     */
255
    public function offsetSet($key, $value)
256
    {
257
        if (is_numeric($key)) {
258
            throw new InvalidArgumentException(
259
                'Entity array access only supports non-numeric keys'
260
            );
261
        }
262
263
        if ($this->separator && strstr($key, $this->separator)) {
264
            $this->setWithSeparator($key, $value);
265
            return;
266
        }
267
268
        $key = $this->camelize($key);
269
270
        /** @internal Edge Case: "_" → "" */
271
        if ($key === '') {
272
            return;
273
        }
274
275
        $setter = 'set'.ucfirst($key);
276 View Code Duplication
        if (!isset($this->mutatorCache[$setter])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
277
            $this->mutatorCache[$setter] = is_callable([ $this, $setter ]);
278
        }
279
280
        if ($this->mutatorCache[$setter]) {
281
            $this->{$setter}($value);
282
        } else {
283
            $this->{$key} = $value;
284
        }
285
286
        $this->keyCache[$key] = true;
287
    }
288
289
    /**
290
     * Replaces the value from the specified key.
291
     *
292
     * Routine:
293
     * - When the value in the Config and the new value are both arrays,
294
     *   the method will replace their respective value recursively.
295
     * - Then or otherwise, the new value is {@see self::offsetSet() assigned} to the Config.
296
     *
297
     * @uses   self::offsetSet()
298
     * @uses   array_replace_recursive()
299
     * @param  string $key   The data key to assign or merge $value to.
300
     * @param  mixed  $value The data value to assign to or merge with $key.
301
     * @throws InvalidArgumentException If the $key is not a string or is a numeric value.
302
     * @return void
303
     */
304
    public function offsetReplace($key, $value)
305
    {
306
        if (is_numeric($key)) {
307
            throw new InvalidArgumentException(
308
                'Entity array access only supports non-numeric keys'
309
            );
310
        }
311
312
        $key = $this->camelize($key);
313
314
        /** @internal Edge Case: "_" → "" */
315
        if ($key === '') {
316
            return;
317
        }
318
319 View Code Duplication
        if (is_array($value) && isset($this[$key])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
320
            $data = $this[$key];
321
            if (is_array($data)) {
322
                $value = array_replace_recursive($data, $value);
323
            }
324
        }
325
326
        $this[$key] = $value;
327
    }
328
329
    /**
330
     * Adds a configuration file to the configset.
331
     *
332
     * Natively supported file formats: INI, JSON, PHP.
333
     *
334
     * @uses   FileAwareTrait::loadFile()
335
     * @param  string $path The file to load and add.
336
     * @return self
337
     */
338
    public function addFile($path)
339
    {
340
        $config = $this->loadFile($path);
341
        if (is_array($config)) {
342
            $this->merge($config);
343
        }
344
        return $this;
345
    }
346
}
347