Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
97 / 97
100.00% covered (success)
100.00%
18 / 18
CRAP
100.00% covered (success)
100.00%
1 / 1
Config
100.00% covered (success)
100.00%
97 / 97
100.00% covered (success)
100.00%
18 / 18
53
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createFromData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseData
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
8
 merge
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 mergeFromData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 render
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
9
 writeToFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 toArrayObject
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 toJson
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toYaml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toIni
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toXml
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 changesAllowed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 arrayToXml
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 arrayToYaml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 arrayToIni
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
9
 __set
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 __unset
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Config;
15
16use Pop\Utils\ArrayObject;
17use SimpleXMLElement;
18use DOMDocument;
19
20/**
21 * Config class
22 *
23 * @category   Pop
24 * @package    Pop\Config
25 * @author     Nick Sagona, III <dev@nolainteractive.com>
26 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
27 * @license    http://www.popphp.org/license     New BSD License
28 * @version    4.0.0
29 */
30class Config extends ArrayObject
31{
32
33    /**
34     * Flag for whether changes are allowed after object instantiation
35     * @var bool
36     */
37    protected bool $allowChanges = false;
38
39    /**
40     * Constructor
41     *
42     * Instantiate a config object
43     *
44     * @param mixed $data
45     * @param bool  $changes
46     * @throws Exception
47     */
48    public function __construct(mixed $data = [], bool $changes = false)
49    {
50        $this->allowChanges = $changes;
51        parent::__construct($data);
52    }
53
54    /**
55     * Method to create a config object from parsed data
56     *
57     * @param  mixed $data
58     * @param  bool  $changes
59     * @return self
60     */
61    public static function createFromData(mixed $data = [], bool $changes = false): Config
62    {
63        return new self(self::parseData($data), $changes);
64    }
65
66    /**
67     * Method to parse data and return config values
68     *
69     * @param  mixed $data
70     * @return array
71     */
72    public static function parseData(mixed $data): array
73    {
74        // If PHP
75        if ((strtolower((substr($data, -6)) == '.phtml') ||
76            strtolower((substr($data, -4)) == '.php'))) {
77            $data = include $data;
78        // If JSON
79        } else if (strtolower(substr($data, -5)) == '.json') {
80            $data = json_decode(file_get_contents($data), true);
81        // If YAML - requires YAML ext
82        } else if ((strtolower(substr($data, -5)) == '.yaml') ||
83            (strtolower(substr($data, -4)) == '.yml'))  {
84            $data = yaml_parse(file_get_contents($data));
85        // If INI
86        } else if (strtolower(substr($data, -4)) == '.ini') {
87            $data = parse_ini_file($data, true);
88        // If XML
89        } else if (strtolower(substr($data, -4)) == '.xml') {
90            $data = (array)simplexml_load_file($data);
91        } else {
92            $data = [];
93        }
94
95        return $data;
96    }
97
98    /**
99     * Merge the values of another config object into this one.
100     * By default, existing values are overwritten, unless the
101     * $preserve flag is set to true.
102     *
103     * @param  mixed $data
104     * @param  bool  $preserve
105     * @throws Exception
106     * @return Config
107     */
108    public function merge(mixed $data, bool $preserve = false): Config
109    {
110        if (!$this->allowChanges) {
111            throw new Exception('Real-time configuration changes are not allowed.');
112        }
113
114        if ($data instanceof Config) {
115            $data = $data->toArray();
116        }
117
118        $this->data = ($preserve) ?
119            array_merge_recursive($this->data, $data) : array_replace_recursive($this->data, $data);
120
121        return $this;
122    }
123
124    /**
125     * Merge the values of another config object into this one.
126     * By default, existing values are overwritten, unless the
127     * $preserve flag is set to true.
128     *
129     * @param  mixed $data
130     * @param  bool  $preserve
131     * @throws Exception
132     * @return Config
133     */
134    public function mergeFromData(mixed $data, bool $preserve = false): Config
135    {
136        if (!$this->allowChanges) {
137            throw new Exception('Real-time configuration changes are not allowed.');
138        }
139
140        return $this->merge(self::parseData($data), $preserve);
141    }
142
143    /**
144     * Render data to a string format
145     *
146     * @param  string $format
147     * @throws Exception
148     * @return string
149     */
150    public function render(string $format): string
151    {
152        $config = '';
153
154        switch (strtolower($format)) {
155            case 'php':
156            case 'phtml':
157                $config  = '<?php' . PHP_EOL . PHP_EOL;
158                $config .= 'return ' . var_export($this->toArray(), true) . ';';
159                $config .= PHP_EOL;
160                break;
161            case 'json':
162                $config = $this->toJson();
163                break;
164            case 'yml':
165            case 'yaml':
166                return $this->toYaml();
167            case 'ini':
168                $config = $this->toIni();
169                break;
170            case 'xml':
171                $config =$this->toXml();
172                break;
173            default:
174                throw new Exception(
175                    "Invalid type '" . $format . "'. Supported config file types are PHP, JSON, YAML, INI or XML."
176                );
177        }
178
179        return $config;
180    }
181
182    /**
183     * Write the config data to file
184     *
185     * @param  string $filename
186     * @throws Exception
187     * @return void
188     */
189    public function writeToFile(string $filename): void
190    {
191        if (str_contains($filename, '.')) {
192            $ext = strtolower(substr($filename, (strrpos($filename, '.') + 1)));
193            file_put_contents($filename, $this->render($ext));
194        }
195    }
196
197    /**
198     * Get the config values as an array
199     *
200     * @throws \Pop\Utils\Exception
201     * @return ArrayObject|\ArrayObject
202     */
203    public function toArrayObject($native = false): ArrayObject|\ArrayObject
204    {
205        return ($native) ? new \ArrayObject($this->toArray(), \ArrayObject::ARRAY_AS_PROPS) : new ArrayObject($this->toArray());
206    }
207
208    /**
209     * Get the config values as a JSON string
210     *
211     * @return string
212     */
213    public function toJson(): string
214    {
215        return $this->jsonSerialize(JSON_PRETTY_PRINT);
216    }
217
218    /**
219     * Get the config values as an YAML string
220     *
221     * @return string
222     */
223    public function toYaml(): string
224    {
225        return $this->arrayToYaml($this->toArray());
226    }
227
228    /**
229     * Get the config values as an INI string
230     *
231     * @return string
232     */
233    public function toIni(): string
234    {
235        return $this->arrayToIni($this->toArray());
236    }
237
238    /**
239     * Get the config values as an XML string
240     *
241     * @return string
242     */
243    public function toXml(): string
244    {
245        $config = new SimpleXMLElement('<?xml version="1.0"?><config></config>');
246        $this->arrayToXml($this->toArray(), $config);
247
248        $dom = new DOMDocument('1.0');
249        $dom->preserveWhiteSpace = false;
250        $dom->formatOutput       = true;
251        $dom->loadXML($config->asXML());
252        return $dom->saveXML();
253    }
254
255    /**
256     * Return if changes to the config are allowed.
257     *
258     * @return bool
259     */
260    public function changesAllowed(): bool
261    {
262        return $this->allowChanges;
263    }
264
265    /**
266     * Method to convert array to XML
267     *
268     * @param  array            $array
269     * @param  SimpleXMLElement $config
270     * @return void
271     */
272    protected function arrayToXml(array $array, SimpleXMLElement &$config): void
273    {
274        foreach($array as $key => $value) {
275            if(is_array($value)) {
276                $subNode = (!is_numeric($key)) ? $config->addChild($key) : $config->addChild('item');
277                $this->arrayToXml($value, $subNode);
278            } else {
279                if (!is_numeric($key)) {
280                    $config->addChild($key, htmlspecialchars($value));
281                } else {
282                    $config->addChild('item', htmlspecialchars($value));
283                }
284            }
285        }
286    }
287
288    /**
289     * Method to convert array to Yaml
290     *
291     * @param  array $array
292     * @return string
293     */
294    protected function arrayToYaml(array $array): string
295    {
296        return yaml_emit($array);
297    }
298
299    /**
300     * Method to convert array to INI
301     *
302     * @param  array $array
303     * @return string
304     */
305    protected function arrayToIni(array $array): string
306    {
307        $ini          = '';
308        $lastWasArray = false;
309
310        foreach ($array as $key => $value) {
311            if (is_array($value)) {
312                if (!$lastWasArray) {
313                    $ini .= PHP_EOL;
314                }
315                $ini .= '[' . $key . ']' . PHP_EOL;
316                foreach ($value as $k => $v) {
317                    if (!is_array($v)) {
318                        $ini .= $key .
319                            '[' . ((!is_numeric($k)) ? $k : null) . '] = ' .
320                            ((!is_numeric($v)) ? '"' . $v . '"' : $v) . PHP_EOL;
321                    }
322                }
323                $ini .= PHP_EOL;
324                $lastWasArray = true;
325            } else {
326                $ini .= $key . " = " . ((!is_numeric($value)) ? '"' . $value . '"' : $value) . PHP_EOL;
327                $lastWasArray = false;
328            }
329        }
330
331        return $ini;
332    }
333
334    /**
335     * Set a value
336     *
337     * @param  ?string $name
338     * @param  mixed $value
339     * @return static
340     */
341    public function __set(?string $name = null, mixed $value = null)
342    {
343        if (!$this->allowChanges) {
344            throw new Exception('Real-time configuration changes are not allowed.');
345        }
346        parent::__set($name, $value);
347    }
348
349    /**
350     * Unset a value
351     *
352     * @param  string $name
353     * @throws Exception
354     * @return void
355     */
356    public function __unset(string $name): void
357    {
358        if (!$this->allowChanges) {
359            throw new Exception('Real-time configuration changes are not allowed.');
360        }
361        parent::__unset($name);
362    }
363
364}