Completed
Push — master ( 300c6d...8b6eac )
by Mathieu
08:24
created

AbstractConfig::offsetSet()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 27
Code Lines 16

Duplication

Lines 5
Ratio 18.52 %

Importance

Changes 0
Metric Value
dl 5
loc 27
rs 8.439
c 0
b 0
f 0
cc 6
eloc 16
nc 5
nop 2
1
<?php
2
3
namespace Charcoal\Config;
4
5
// Dependencies from `PHP`
6
use ArrayIterator;
7
use InvalidArgumentException;
8
use IteratorAggregate;
9
use Traversable;
10
11
// Dependencies from `symfony/yaml`
12
use Symfony\Component\Yaml\Parser as YamlParser;
13
14
// Dependencies from `container-interop/container-interop`
15
use Interop\Container\ContainerInterface;
16
17
/**
18
 * Configuration container / registry.
19
 *
20
 * An abstract class that fulfills the full ConfigInterface.
21
 *
22
 * This class also implements the `ArrayAccess` interface, so each member can be accessed with `[]`.
23
 */
24
abstract class AbstractConfig extends AbstractEntity implements
25
    ConfigInterface,
26
    ContainerInterface,
27
    IteratorAggregate
28
{
29
    use DelegatesAwareTrait;
30
    use SeparatorAwareTrait;
31
32
    const DEFAULT_SEPARATOR = '.';
33
34
    /**
35
     * Create the configuration.
36
     *
37
     * @param mixed             $data      Optional default data. Either a filename, an array, or a Config object.
38
     * @param ConfigInterface[] $delegates An array of delegates (config) to set.
39
     * @throws InvalidArgumentException If $data is invalid.
40
     */
41
    final public function __construct($data = null, array $delegates = null)
42
    {
43
        $this->setSeparator(self::DEFAULT_SEPARATOR);
44
        // Always set the default data first.
45
        $this->setData($this->defaults());
0 ignored issues
show
Unused Code introduced by
The call to the method Charcoal\Config\AbstractConfig::setData() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
46
47
        // Set the delegates, if necessary.
48
        if (isset($delegates)) {
49
            $this->setDelegates($delegates);
50
        }
51
52
        if ($data === null) {
53
            return;
54
        }
55
56
        if (is_string($data)) {
57
            // Treat the parameter as a filename
58
            $this->addFile($data);
59
        } elseif (is_array($data)) {
60
            $this->merge($data);
61
        } elseif ($data instanceof ConfigInterface) {
62
            $this->merge($data);
63
        } else {
64
            throw new InvalidArgumentException(
65
                'Data must be an array, a file string or a ConfigInterface object.'
66
            );
67
        }
68
    }
69
70
    /**
71
     * Determine if a configuration key exists.
72
     *
73
     * @see ArrayAccess::offsetExists()
74
     * @param string $key The key of the configuration item to look for.
75
     * @throws InvalidArgumentException If the key argument is not a string or is a "numeric" value.
76
     * @return boolean
77
     */
78
    public function offsetExists($key)
79
    {
80
        if (is_numeric($key)) {
81
            throw new InvalidArgumentException(
82
                'Entity array access only supports non-numeric keys.'
83
            );
84
        }
85
86
        if ($this->separator && strstr($key, $this->separator)) {
87
            return $this->hasWithSeparator($key);
88
        }
89
90
        $key = $this->camelize($key);
91 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...
92
            $value = $this->{$key}();
93
        } else {
94
            if (!isset($this->{$key})) {
95
                return $this->hasInDelegates($key);
96
            }
97
            $value = $this->{$key};
98
        }
99
        return ($value !== null);
100
    }
101
102
    /**
103
     * Find an entry of the configuration by its key and retrieve it.
104
     *
105
     * @see ArrayAccess::offsetGet()
106
     * @param string $key The key of the configuration item to look for.
107
     * @throws InvalidArgumentException If the key argument is not a string or is a "numeric" value.
108
     * @return mixed The value (or null)
109
     */
110
    public function offsetGet($key)
111
    {
112
        if (is_numeric($key)) {
113
            throw new InvalidArgumentException(
114
                'Entity array access only supports non-numeric keys.'
115
            );
116
        }
117
        if ($this->separator && strstr($key, $this->separator)) {
118
            return $this->getWithSeparator($key);
119
        }
120
        $key = $this->camelize($key);
121
122 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...
123
            return $this->{$key}();
124
        } else {
125
            if (isset($this->{$key})) {
126
                return $this    ->{$key};
127
            } else {
128
                return $this->getInDelegates($key);
129
            }
130
        }
131
    }
132
133
    /**
134
     * Assign a value to the specified key of the configuration.
135
     *
136
     * Set the value either by:
137
     * - a setter method (`set_{$key}()`)
138
     * - setting (or overriding)
139
     *
140
     * @see ArrayAccess::offsetSet()
141
     * @param string $key   The key to assign $value to.
142
     * @param mixed  $value Value to assign to $key.
143
     * @throws InvalidArgumentException If the key argument is not a string or is a "numeric" value.
144
     * @return void
145
     */
146
    public function offsetSet($key, $value)
147
    {
148
        if (is_numeric($key)) {
149
            throw new InvalidArgumentException(
150
                'Entity array access only supports non-numeric keys.'
151
            );
152
        }
153
154
        if ($this->separator && strstr($key, $this->separator)) {
155
            $this->setWithSeparator($key, $value);
156
        } else {
157
            $key = $this->camelize($key);
158
            $setter = 'set'.ucfirst($key);
159
160
            // Case: url.com?_=something
161
            if ($setter === 'set') {
162
                return;
163
            }
164
165 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...
166
                $this->{$setter}($value);
167
            } else {
168
                $this->{$key} = $value;
169
            }
170
            $this->keys[$key] = true;
171
        }
172
    }
173
174
175
    /**
176
     * Add a configuration file. The file type is determined by its extension.
177
     *
178
     * Supported file types are `ini`, `json`, `php`
179
     *
180
     * @param string $filename A supported configuration file.
181
     * @throws InvalidArgumentException If the file is invalid.
182
     * @return self
183
     */
184
    public function addFile($filename)
185
    {
186
        $content = $this->loadFile($filename);
187
        if (is_array($content)) {
188
            $this->merge($content);
189
        }
190
        return $this;
191
    }
192
193
    /**
194
     * Load a configuration file. The file type is determined by its extension.
195
     *
196
     * Supported file types are `ini`, `json`, `php`
197
     *
198
     * @param string $filename A supported configuration file.
199
     * @throws InvalidArgumentException If the filename is invalid.
200
     * @return mixed
201
     */
202
    public function loadFile($filename)
203
    {
204
        if (!is_string($filename)) {
205
            throw new InvalidArgumentException(
206
                'Configuration file must be a string.'
207
            );
208
        }
209
        if (!file_exists($filename)) {
210
            throw new InvalidArgumentException(
211
                sprintf('Configuration file "%s" does not exist', $filename)
212
            );
213
        }
214
215
        $ext = pathinfo($filename, PATHINFO_EXTENSION);
216
217
        if ($ext == 'php') {
218
            return $this->loadPhpFile($filename);
219
        } elseif ($ext == 'json') {
220
            return $this->loadJsonFile($filename);
221
        } elseif ($ext == 'ini') {
222
            return $this->loadIniFile($filename);
223
        } elseif ($ext == 'yml' || $ext == 'yaml') {
224
            return $this->loadYamlFile($filename);
225
        } else {
226
            throw new InvalidArgumentException(
227
                'Only JSON, INI and PHP files are accepted as a Configuration file.'
228
            );
229
        }
230
    }
231
232
    /**
233
     * For each key, calls `set()`, which calls `offsetSet()`  (from ArrayAccess).
234
     *
235
     * The provided `$data` can be a simple array or an object which implements `Traversable`
236
     * (such as a `ConfigInterface` instance).
237
     *
238
     * @param array|Traversable|ConfigInterface $data The data to set.
239
     * @return self
240
     * @see self::offsetSet()
241
     */
242
    public function merge($data)
243
    {
244
        foreach ($data as $k => $v) {
245
            if (is_array($v) && isset($this[$k]) && is_array($this[$k])) {
246
                $v = array_replace_recursive($this[$k], $v);
247
            }
248
            $this[$k] = $v;
249
        }
250
        return $this;
251
    }
252
253
254
    /**
255
     * A stub for when the default data is empty.
256
     *
257
     * Make sure to reimplement in children ConfigInterface classes if any default data should be set.
258
     *
259
     * @see ConfigInterface::defaults()
260
     * @return array
261
     */
262
    public function defaults()
263
    {
264
        return [];
265
    }
266
267
    /**
268
     * IteratorAggregate > getIterator()
269
     *
270
     * @return ArrayIterator
271
     */
272
    public function getIterator()
273
    {
274
        return new ArrayIterator($this->data());
275
    }
276
277
    /**
278
     * Add a `.ini` file to the configuration.
279
     *
280
     * @param string $filename A INI configuration file.
281
     * @throws InvalidArgumentException If the file or invalid.
282
     * @return mixed
283
     */
284
    private function loadIniFile($filename)
285
    {
286
        $config = parse_ini_file($filename, true);
287
        if ($config === false) {
288
            throw new InvalidArgumentException(
289
                sprintf('Ini file "%s" is empty or invalid.', $filename)
290
            );
291
        }
292
        return $config;
293
    }
294
295
    /**
296
     * Add a `.json` file to the configuration.
297
     *
298
     * @param string $filename A JSON configuration file.
299
     * @throws InvalidArgumentException If the file or invalid.
300
     * @return mixed
301
     */
302
    private function loadJsonFile($filename)
303
    {
304
        $fileContent = file_get_contents($filename);
305
        $config = json_decode($fileContent, true);
306
        $errCode = json_last_error();
307
        if ($errCode == JSON_ERROR_NONE) {
308
            return $config;
309
        }
310
311
        // Handle JSON error
312
        $errMsg = '';
313
        switch ($errCode) {
314
            case JSON_ERROR_NONE:
315
                break;
316
            case JSON_ERROR_DEPTH:
317
                $errMsg = 'Maximum stack depth exceeded';
318
                break;
319
            case JSON_ERROR_STATE_MISMATCH:
320
                $errMsg = 'Underflow or the modes mismatch';
321
                break;
322
            case JSON_ERROR_CTRL_CHAR:
323
                $errMsg = 'Unexpected control character found';
324
                break;
325
            case JSON_ERROR_SYNTAX:
326
                $errMsg = 'Syntax error, malformed JSON';
327
                break;
328
            case JSON_ERROR_UTF8:
329
                $errMsg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
330
                break;
331
            default:
332
                $errMsg = 'Unknown error';
333
                break;
334
        }
335
336
        throw new InvalidArgumentException(
337
            sprintf('JSON file "%s" could not be parsed: "%s"', $filename, $errMsg)
338
        );
339
    }
340
341
    /**
342
     * Add a PHP file to the configuration
343
     *
344
     * @param string $filename A PHP configuration file.
345
     * @return mixed
346
     */
347
    private function loadPhpFile($filename)
348
    {
349
        // `$this` is bound to the current configuration object (Current `$this`)
350
        $config = include $filename;
351
        return $config;
352
    }
353
354
    /**
355
     * Add a YAML file to the configuration
356
     *
357
     * @param string $filename A YAML configuration file.
358
     * @throws InvalidArgumentException If the YAML file can not correctly be parsed into an array.
359
     * @return mixed
360
     */
361
    private function loadYamlFile($filename)
362
    {
363
        $parser = new YamlParser();
364
        $fileContent = file_get_contents($filename);
365
        $config = $parser->parse($fileContent);
366
        if (!is_array($config)) {
367
            throw new InvalidArgumentException(
368
                sprintf('YAML file "%s" could not be parsed (invalid yaml)', $filename)
369
            );
370
        }
371
        return $config;
372
    }
373
}
374