Completed
Push — master ( 3cd323...2318d3 )
by Chauncey
07:31
created

AbstractConfig::offsetReplace()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 24
Code Lines 12

Duplication

Lines 6
Ratio 25 %

Importance

Changes 0
Metric Value
dl 6
loc 24
rs 8.5125
c 0
b 0
f 0
cc 6
eloc 12
nc 5
nop 2
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 View Code Duplication
        if (is_callable([ $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...
155
            $value = $this->{$key}();
156
        } else {
157
            if (!isset($this->{$key})) {
158
                return $this->hasInDelegates($key);
159
            }
160
            $value = $this->{$key};
161
        }
162
163
        return ($value !== null);
164
    }
165
166
    /**
167
     * Returns the value from the specified key on this entity.
168
     *
169
     * Routine:
170
     * - If the data key is {@see SeparatorAwareTrait::$separator nested},
171
     *   the data-tree is traversed until the endpoint to return its value, if any;
172
     * - If the data key does NOT exist on the store, a lookup is performed
173
     *   on each delegate store until a value is found, if any.
174
     *
175
     * @see    \ArrayAccess
176
     * @uses   SeparatorAwareTrait::getWithSeparator()
177
     * @uses   DelegatesAwareTrait::getInDelegates()
178
     * @param  string $key The data key to retrieve.
179
     * @throws InvalidArgumentException If the $key is not a string or is a numeric value.
180
     * @return mixed Value of the requested $key on success, NULL if the $key is not set.
181
     */
182
    public function offsetGet($key)
183
    {
184
        if (is_numeric($key)) {
185
            throw new InvalidArgumentException(
186
                'Entity array access only supports non-numeric keys'
187
            );
188
        }
189
190
        if ($this->separator && strstr($key, $this->separator)) {
191
            return $this->getWithSeparator($key);
192
        }
193
194
        $key = $this->camelize($key);
195
196
        /** @internal Edge Case: "_" → "" */
197
        if ($key === '') {
198
            return null;
199
        }
200
201 View Code Duplication
        if (is_callable([ $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...
202
            return $this->{$key}();
203
        } else {
204
            if (isset($this->{$key})) {
205
                return $this->{$key};
206
            } else {
207
                return $this->getInDelegates($key);
208
            }
209
        }
210
    }
211
212
    /**
213
     * Assigns the value to the specified key on this entity.
214
     *
215
     * Routine:
216
     * - If the data key is {@see SeparatorAwareTrait::$separator nested},
217
     *   the data-tree is traversed until the endpoint to assign its value;
218
     *
219
     * @see    \ArrayAccess
220
     * @uses   SeparatorAwareTrait::setWithSeparator()
221
     * @param  string $key   The data key to assign $value to.
222
     * @param  mixed  $value The data value to assign to $key.
223
     * @throws InvalidArgumentException If the $key is not a string or is a numeric value.
224
     * @return void
225
     */
226
    public function offsetSet($key, $value)
227
    {
228
        if (is_numeric($key)) {
229
            throw new InvalidArgumentException(
230
                'Entity array access only supports non-numeric keys'
231
            );
232
        }
233
234
        if ($this->separator && strstr($key, $this->separator)) {
235
            $this->setWithSeparator($key, $value);
236
            return;
237
        }
238
239
        $key = $this->camelize($key);
240
241
        /** @internal Edge Case: "_" → "" */
242
        if ($key === '') {
243
            return;
244
        }
245
246
        $setter = 'set'.ucfirst($key);
247 View Code Duplication
        if (is_callable([ $this, $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...
248
            $this->{$setter}($value);
249
        } else {
250
            $this->{$key} = $value;
251
        }
252
253
        $this->keys[$key] = true;
254
    }
255
256
    /**
257
     * Replaces the value from the specified key.
258
     *
259
     * Routine:
260
     * - When the value in the Config and the new value are both arrays,
261
     *   the method will replace their respective value recursively.
262
     * - Then or otherwise, the new value is {@see self::offsetSet() assigned} to the Config.
263
     *
264
     * @uses   self::offsetSet()
265
     * @uses   array_replace_recursive()
266
     * @param  string $key   The data key to assign or merge $value to.
267
     * @param  mixed  $value The data value to assign to or merge with $key.
268
     * @throws InvalidArgumentException If the $key is not a string or is a numeric value.
269
     * @return void
270
     */
271
    public function offsetReplace($key, $value)
272
    {
273
        if (is_numeric($key)) {
274
            throw new InvalidArgumentException(
275
                'Entity array access only supports non-numeric keys'
276
            );
277
        }
278
279
        $key = $this->camelize($key);
280
281
        /** @internal Edge Case: "_" → "" */
282
        if ($key === '') {
283
            return;
284
        }
285
286 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...
287
            $data = $this[$key];
288
            if (is_array($data)) {
289
                $value = array_replace_recursive($data, $value);
290
            }
291
        }
292
293
        $this[$key] = $value;
294
    }
295
296
    /**
297
     * Adds a configuration file to the configset.
298
     *
299
     * Natively supported file formats: INI, JSON, PHP.
300
     *
301
     * @uses   FileAwareTrait::loadFile()
302
     * @param  string $path The file to load and add.
303
     * @return self
304
     */
305
    public function addFile($path)
306
    {
307
        $config = $this->loadFile($path);
308
        if (is_array($config)) {
309
            $this->merge($config);
310
        }
311
        return $this;
312
    }
313
}
314