Passed
Push — master ( 8638e9...a09c87 )
by Alain
02:55
created

Config::__construct()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 49
Code Lines 28

Duplication

Lines 8
Ratio 16.33 %

Code Coverage

Tests 17
CRAP Score 6.2488

Importance

Changes 6
Bugs 0 Features 2
Metric Value
c 6
b 0
f 2
dl 8
loc 49
ccs 17
cts 21
cp 0.8095
rs 8.5906
cc 6
eloc 28
nc 7
nop 4
crap 6.2488
1
<?php
2
/**
3
 * Generic Config contract implementation
4
 *
5
 * @package   BrightNucleus\Config
6
 * @author    Alain Schlesser <[email protected]>
7
 * @license   GPL-2.0+
8
 * @link      http://www.brightnucleus.com/
9
 * @copyright 2016 Alain Schlesser, Bright Nucleus
10
 */
11
12
namespace BrightNucleus\Config;
13
14
use BrightNucleus\Config\ConfigSchemaInterface as Schema;
15
use BrightNucleus\Config\ConfigValidatorInterface as Validator;
16
use Exception;
17
use InvalidArgumentException;
18
use RuntimeException;
19
use Symfony\Component\OptionsResolver\OptionsResolver;
20
use UnexpectedValueException;
21
22
/**
23
 * Class Config
24
 *
25
 * @since   0.1.0
26
 *
27
 * @package BrightNucleus\Config
28
 * @author  Alain Schlesser <[email protected]>
29
 */
30
class Config extends AbstractConfig
31
{
32
33
    /**
34
     * The schema of the Config file.
35
     *
36
     * @var Schema
37
     */
38
    protected $schema;
39
40
    /**
41
     * The Validator class that gets asked to do the validation of the config.
42
     *
43
     * @since 0.1.0
44
     *
45
     * @var Validator
46
     */
47
    protected $validator;
48
49
    /**
50
     * Instantiate the Config object.
51
     *
52
     * It accepts either an array with the configuration settings, or a
53
     * filename pointing to a PHP file it can include.
54
     *
55
     * @since 0.1.0
56
     * @since 0.1.6 Accepts a delimiter to parse configuration keys.
57
     *
58
     * @param array|string         $config    Array with settings or filename for the
59
     *                                        settings file.
60
     * @param Schema|null          $schema    Optional. Config that contains default
61
     *                                        values that can get overwritten.
62
     * @param Validator|null       $validator Optional. Validator class that does the
63
     *                                        actual validation.
64
     * @param string[]|string|null $delimiter A string or array of strings that are used as delimiters to parse
65
     *                                        configuration keys. Defaults to "\", "/" & ".".
66
     * @throws InvalidArgumentException If the config source is not a string or
67
     *                                        array.
68
     * @throws RuntimeException         If loading of the config source failed.
69
     * @throws UnexpectedValueException If the config file is not valid.
70
     */
71 8
    public function __construct(
72
        $config,
73
        Schema $schema = null,
74
        Validator $validator = null,
75
        $delimiter = null
76
    ) {
77 8
        $this->schema    = $schema;
78 8
        $this->validator = $validator;
79
80
        // Make sure $config is either a string or array.
81 8 View Code Duplication
        if (! (is_string($config) || is_array($config))) {
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...
82 1
            throw new InvalidArgumentException(
83
                sprintf(
84 1
                    _('Invalid configuration source: %1$s'),
85 1
                    print_r($config, true)
86
                )
87
            );
88
        }
89
90 7
        if (is_string($config)) {
91 5
            $config = $this->fetchArrayData($config);
92
        }
93
94
        // Run the $config through the OptionsResolver.
95 5
        \Assert\that($config)->isArray();
96 5
        $config = $this->resolveOptions($config);
97
98
        // Instantiate the parent class.
99
        try {
100 5
            parent::__construct($config, $delimiter);
101
        } catch (Exception $exception) {
102
            throw new RuntimeException(
103
                sprintf(
104
                    _('Could not instantiate the configuration through its parent. Reason: %1$s'),
105
                    $exception->getMessage()
106
                )
107
            );
108
        }
109
110
        // Finally, validate the resulting config.
111 5
        if (! $this->isValid()) {
112 1
            throw new UnexpectedValueException(
113
                sprintf(
114 1
                    _('ConfigInterface file is not valid: %1$s'),
115 1
                    print_r($config, true)
116
                )
117
            );
118
        }
119 5
    }
120
121
    /**
122
     * Validate the Config file.
123
     *
124
     * @since  0.1.0
125
     *
126
     * @return boolean
127
     */
128 1
    public function isValid()
129
    {
130 1
        if ($this->validator) {
131 1
            return $this->validator->isValid($this);
132
        }
133
134 1
        return true;
135
    }
136
137
    /**
138
     * Fetch array data from a string pointing to a file.
139
     *
140
     * @since 0.1.0
141
     *
142
     * @param  string $filename         Filename for the settings file.
143
     * @return array                    Array with configuration settings.
144
     * @throws RuntimeException         If the config source is a non-existing
145
     *                                  file.
146
     * @throws RuntimeException         If loading of the config source failed.
147
     */
148 5
    protected function fetchArrayData($filename)
149
    {
150
        try {
151
            // Assert that $filename is a readable file.
152 5
            \Assert\that($filename)
153 5
                ->notEmpty()
154 5
                ->file()
155 4
                ->readable();
156
157
            // Try to load the file through PHP's include().
158
            // Make sure we don't accidentally create output.
159 4
            ob_get_contents();
160 4
            $config = include($filename);
161 4
            ob_clean();
162
163
            // The included should return an array.
164 4
            \Assert\that($config)->isArray();
165 2
        } catch (Exception $exception) {
166 2
            throw new RuntimeException(
167
                sprintf(
168 2
                    _('Loading from configuration source %1$s failed. Reason: %2$s'),
169 2
                    (string)$filename,
170 2
                    (string)$exception->getMessage()
171
                )
172
            );
173
        }
174
175 3
        return $config;
176
    }
177
178
    /**
179
     * Process the passed-in defaults and merge them with the new values, while
180
     * checking that all required options are set.
181
     *
182
     * @since 0.1.0
183
     *
184
     * @param array $config             Configuration settings to resolve.
185
     * @return array                    Resolved configuration settings.
186
     * @throws UnexpectedValueException If there are errors while resolving the
187
     *                                  options.
188
     */
189 3
    protected function resolveOptions($config)
190
    {
191 3
        if (! $this->schema) {
192 3
            return $config;
193
        }
194
195
        try {
196 2
            $resolver = new OptionsResolver();
197 2
            if ($this->configureOptions($resolver)) {
198 2
                $config = $resolver->resolve($config);
199
            }
200 1
        } catch (Exception $exception) {
201 1
            throw new UnexpectedValueException(
202
                sprintf(
203 1
                    _('Error while resolving config options: %1$s'),
204 1
                    $exception->getMessage()
205
                )
206
            );
207
        }
208
209 1
        return $config;
210
    }
211
212
    /**
213
     * Configure the possible and required options for the Config.
214
     *
215
     * This should return a bool to let the resolve_options() know whether the
216
     * actual resolving needs to be done or not.
217
     *
218
     * @since 0.1.0
219
     *
220
     * @param OptionsResolver $resolver Reference to the OptionsResolver
221
     *                                  instance.
222
     * @return bool Whether to do the resolving.
223
     * @throws UnexpectedValueException If there are errors while processing.
224
     */
225 2
    protected function configureOptions(OptionsResolver $resolver)
226
    {
227 2
        $defined  = $this->schema->getDefinedOptions();
228 2
        $defaults = $this->schema->getDefaultOptions();
229 2
        $required = $this->schema->getRequiredOptions();
230
231 2
        if (! $defined && ! $defaults && ! $required) {
232
            return false;
233
        }
234
235
        try {
236 2
            if ($defined) {
237 2
                $resolver->setDefined($defined);
238
            }
239 2
            if ($defaults) {
240 2
                $resolver->setDefaults($defaults);
241
            }
242 2
            if ($required) {
243 2
                $resolver->setRequired($required);
244
            }
245
        } catch (Exception $exception) {
246
            throw new UnexpectedValueException(
247
                sprintf(
248
                    _('Error while processing config options: %1$s'),
249
                    $exception->getMessage()
250
                )
251
            );
252
        }
253
254 2
        return true;
255
    }
256
}
257