Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Config
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
6 / 6
36
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 appendArray
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
9
 get
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getPath
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
9
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 __set_state
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3namespace Miniframe\Core;
4
5use RuntimeException;
6
7class Config
8{
9    /**
10     * All config data
11     *
12     * @var array<string, array<string, mixed>>
13     */
14    private $data = array();
15
16    /**
17     * Path in which the config files are located (absolute path, ending with directory separator)
18     *
19     * @var string
20     */
21    private $configFolder;
22
23    /**
24     * Root folder of the project (absolute path, ending with directory separator)
25     *
26     * @var string
27     */
28    private $projectFolder;
29
30    /**
31     * Initializes config; reads all .ini files sorted alphabetically in a specific folder
32     *
33     * @param string $configFolder  Folder that contains .ini files.
34     * @param string $projectFolder Root folder of the project (required for getPath()).
35     */
36    public function __construct(string $configFolder, string $projectFolder)
37    {
38        if (!is_dir($projectFolder)) {
39            throw new \RuntimeException('Project folder doesn\'t exist: ' . $projectFolder);
40        }
41        if (!is_dir($configFolder)) {
42            throw new \RuntimeException('Config folder doesn\'t exist: ' . $configFolder);
43        }
44        $this->projectFolder = rtrim((string)realpath($projectFolder), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
45        $this->configFolder = rtrim((string)realpath($configFolder), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
46        $configFiles = glob($this->configFolder . '*.ini');
47        if (!is_array($configFiles)) {
48            // @codeCoverageIgnoreStart
49            // This should not happen since we already checked if the dir exists.
50            throw new \RuntimeException('No config files found in folder: ' . $configFolder);
51            // @codeCoverageIgnoreEnd
52        }
53        foreach ($configFiles as $configFile) {
54            $parsed = parse_ini_file($configFile, true, INI_SCANNER_TYPED);
55            if ($parsed === false) {
56                // @codeCoverageIgnoreStart
57                // There's no case found so far when this returns false, but for strict typing, this is required.
58                throw new \RuntimeException('Can\'t parse config file: ' . $configFile);
59                // @codeCoverageIgnoreEnd
60            }
61            $this->appendArray($parsed);
62        }
63    }
64
65    /**
66     * Appends the array with some more complex logics
67     *
68     * @param array<string, array<string, mixed>> $array The array data to append.
69     *
70     * @return void
71     */
72    private function appendArray(array $array): void
73    {
74        foreach ($array as $sectionName => $sectionData) {
75            if (!isset($this->data[$sectionName])) {
76                $this->data[$sectionName] = array();
77            }
78            // Loop through section data
79            foreach ($sectionData as $valueName => $valueData) {
80                if (!is_array($valueData)) {
81                    $this->data[$sectionName][$valueName] = $valueData;
82                    continue;
83                }
84                // If the value is an array, append that as well
85                foreach ($valueData as $valueKey => $valueValue) {
86                    if (!isset($this->data[$sectionName][$valueName])) {
87                        $this->data[$sectionName][$valueName] = array();
88                    }
89                    if (!is_array($this->data[$sectionName][$valueName])) {
90                        $this->data[$sectionName][$valueName] = array($this->data[$sectionName][$valueName]);
91                    }
92                    if (is_numeric($valueKey)) {
93                        $this->data[$sectionName][$valueName][] = $valueValue;
94                    } else {
95                        $this->data[$sectionName][$valueName][$valueKey] = $valueValue;
96                    }
97                }
98            }
99        }
100    }
101
102    /**
103     * Returns a specific config value
104     *
105     * @param string $section Configuration section.
106     * @param string $key     Configuration key.
107     *
108     * @return mixed
109     */
110    public function get(string $section, string $key)
111    {
112        if (!array_key_exists($section, $this->data)) {
113            throw new \RuntimeException("Config section does not exist: " . $section);
114        }
115        if (!array_key_exists($key, $this->data[$section])) {
116            throw new \RuntimeException("Config key (" . $key . ") does not exist in section " . $section);
117        }
118
119        return $this->data[$section][$key];
120    }
121
122    /**
123     * Returns a specific config value and converts it to a path, relative from the project root
124     *
125     * @param string $section Configuration section.
126     * @param string $key     Configuration key.
127     *
128     * @return mixed
129     */
130    public function getPath(string $section, string $key)
131    {
132        $original = $this->get($section, $key);
133        if (!is_string($original)) {
134            //throw new RuntimeException('Config value ' . $section . '.' . $key . ' is not a string');
135        }
136        $return = array();
137        foreach ((array)$original as $path) {
138            if (!is_string($path)) {
139                throw new RuntimeException('Invalid path value: ' . var_export($path, true));
140            }
141            // Absolute path
142            if (substr($path, 0, 1) == '/' || substr($path, 1, 1) == ':') {
143                $return[] = $path;
144                continue;
145            }
146            // Relative path
147            $path = $this->projectFolder . $path;
148            // When the path already exists, resolve all relative parts making it absolute
149            if (file_exists($path)) {
150                $path = (string)realpath($path);
151            }
152            // When the path is a dir, suffix with a directory separator
153            if (is_dir($path)) {
154                $path .= DIRECTORY_SEPARATOR;
155            }
156            $return[] = $path;
157        }
158        return is_array($original) ? $return : array_shift($return);
159    }
160
161    /**
162     * Validates if a config value exists
163     *
164     * @param string $section Configuration section.
165     * @param string $key     Configuration key.
166     *
167     * @return boolean
168     */
169    public function has(string $section, string $key): bool
170    {
171        // Using array_key_exists instead of isset, since it's value can be null
172        return array_key_exists($section, $this->data) && array_key_exists($key, $this->data[$section]);
173    }
174
175    /**
176     * Magic method; sets a config object based on a specific state
177     *
178     * @param array<string, mixed> $data The actual state.
179     *
180     * @return Config
181     * @see    var_export()
182     */
183    public static function __set_state(array $data): self
184    {
185        if (
186            !isset($data['configFolder']) || !isset($data['projectFolder']) || !isset($data['data'])
187            || !is_string($data['configFolder']) || !is_string($data['projectFolder']) || !is_array($data['data'])
188        ) {
189            throw new \RuntimeException('Required keys not sent (configFolder, projectFolder & data)');
190        }
191
192        $return = new self($data['configFolder'], $data['projectFolder']);
193        $return->data = $data['data'];
194
195        return $return;
196    }
197}