Configuration::get()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 3
c 3
b 0
f 0
dl 0
loc 8
rs 10
ccs 1
cts 1
cp 1
cc 2
nc 2
nop 2
crap 2
1
<?php
2
3
/**
4
 * This file is part of slick/configuration
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace Slick\Configuration;
11
12
use ReflectionClass;
13
use ReflectionException;
14
use Slick\Configuration\Driver\Environment;
15
use Slick\Configuration\Driver\Ini;
16
use Slick\Configuration\Driver\Php;
17
use Slick\Configuration\Exception\InvalidArgumentException;
18
19
/**
20
 * Configuration
21
 *
22
 * @package Slick\Configuration
23
*/
24
final class Configuration
25
{
26
    /**@#+
27
     * Known configuration drivers
28
     * @var class-string
29
     */
30
    const DRIVER_INI = Ini::class;
31
    const DRIVER_PHP = Php::class;
32
    const DRIVER_ENV = Environment::class;
33
    /**@#- */
34
35
    /** @var array<class-string>|string[]  */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string>|string[] at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string>|string[].
Loading history...
36
    private array $extensionToDriver = [
37
        'ini' => self::DRIVER_INI,
38
        'php' => self::DRIVER_PHP,
39
    ];
40
41
    /**
42
     * @var string|array<int|string, mixed>|null
43
     */
44
    private string|array|null $file;
45
46
    /**
47
     * @var null|class-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment null|class-string at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in null|class-string.
Loading history...
48
     */
49
    private ?string $driverClass;
50
51
    /** @var array<string>|string[]  */
52
    private static array $paths = [
53
        './'
54
    ];
55
56
    /**
57
     * @var null|ConfigurationInterface
58
     */
59
    private static ?ConfigurationInterface $instance = null;
60
61
    /**
62
     * Creates a configuration factory
63
     *
64
     * @param array<int|string, mixed>|string|null $options
65
     * @param null|class-string                    $driverClass
0 ignored issues
show
Documentation Bug introduced by
The doc comment null|class-string at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in null|class-string.
Loading history...
66 18
     */
67
    public function __construct(array|string $options = null, ?string $driverClass = null)
68 18
    {
69 12
        $this->file = $options;
70
        $this->driverClass = $driverClass;
71
        $path = getcwd();
72
        self::addPath(is_string($path) ? $path : './');
0 ignored issues
show
introduced by
The condition is_string($path) is always true.
Loading history...
73
    }
74
75
    /**
76
     * Returns the last ConfigurationInterface
77 3
     *
78
     * If there is no configuration created it will use passed arguments to create one
79 3
     *
80 3
     * @param array<int|string, mixed>|string $fileName
81 3
     * @param null|class-string $driverClass
0 ignored issues
show
Documentation Bug introduced by
The doc comment null|class-string at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in null|class-string.
Loading history...
82 3
     *
83 3
     * @return PriorityConfigurationChain|ConfigurationInterface|null
84
     * @throws ReflectionException
85
     */
86
    public static function get(
87
        array|string $fileName,
88
        ?string $driverClass = null
89
    ): PriorityConfigurationChain|ConfigurationInterface|null {
90
        if (self::$instance === null) {
91
            self::$instance = self::create($fileName, $driverClass);
92
        }
93 9
        return self::$instance;
94
    }
95 9
96
    /**
97 9
     * Creates a ConfigurationInterface with passed arguments
98 9
     *
99 9
     * @param array<int|string, mixed>|string $fileName
100 9
     * @param null|class-string $driverClass
0 ignored issues
show
Documentation Bug introduced by
The doc comment null|class-string at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in null|class-string.
Loading history...
101 9
     *
102
     * @return ConfigurationInterface|PriorityConfigurationChain
103
     * @throws ReflectionException
104
     */
105
    public static function create(
106
        array|string $fileName,
107
        ?string $driverClass = null
108
    ): PriorityConfigurationChain|ConfigurationInterface {
109
        $configuration = new Configuration($fileName, $driverClass);
110
        return $configuration->initialize();
111
    }
112 9
113
    /**
114 9
     * @return PriorityConfigurationChain|ConfigurationInterface
115 9
     * @throws ReflectionException
116 9
     */
117
    public function initialize(): PriorityConfigurationChain|ConfigurationInterface
118 9
    {
119 9
        $chain = new PriorityConfigurationChain();
120 9
121 3
        $options = $this->fixOptions();
122 3
123
        foreach ($options as $option) {
124 9
            $priority = $this->setProperties($option);
125 9
            $chain->add($this->createConfigurationDriver(), $priority);
126
        }
127
128
        return $chain;
129
    }
130
131 18
    /**
132
     * Prepends a searchable path to available paths list.
133 18
     *
134 3
     * @param string $path
135 3
     */
136 3
    public static function addPath(string $path): void
137
    {
138
        if (!in_array($path, self::$paths)) {
139 15
            array_unshift(self::$paths, $path);
140 15
        }
141 3
    }
142 3
143
    /**
144 3
     * Returns the driver class to be initialized
145
     *
146 12
     * @return class-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
147
     */
148
    private function driverClass(): string
149
    {
150
        if (null == $this->driverClass) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $this->driverClass of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
151
            $fromArray = is_array($this->file) && isset($this->file[1]) ? $this->file[1] : null;
152
            $name = is_array($this->file) ? $fromArray : $this->file;
153
            $this->driverClass = $this->determineDriver($name);
154
        }
155
        return $this->driverClass;
156
    }
157
158
    /**
159
     * Tries to determine the driver class based on given file
160
     *
161
     * @param string|null $file
162
     * @return mixed
163
     */
164
    private function determineDriver(?string $file): mixed
165
    {
166
        $exception = new InvalidArgumentException(
167
            "Cannot initialize the configuration driver. I could not determine ".
168
            "the correct driver class."
169
        );
170
171
        if (is_null($file)) {
172
            throw $exception;
173
        }
174
175
        $nameDivision = explode('.', $file);
176
        $extension = strtolower(end($nameDivision));
177
178
        if (!array_key_exists($extension, $this->extensionToDriver)) {
179
            throw $exception;
180
        }
181
182
        return $this->extensionToDriver[$extension];
183
    }
184
185
    /**
186
     * Compose the filename with existing paths and return when match
187
     *
188
     * If not matched the $name is returned as is;
189
     * If no extension provided it will add it from driver class map;
190
     * By default it will try to find <$name>.php file
191
     *
192
     * @param null|string $name
193
     *
194
     * @return string|null
195
     */
196
    private function composeFileName(?string $name): ?string
197
    {
198
        if (is_null($name)) {
199
            return null;
200
        }
201
202
        $ext = $this->determineExtension();
203
        $withExtension = $this->createName($name, $ext);
204
205
        list($found, $fileName) = $this->searchFor($name, $withExtension);
206
207
        return $found ? $fileName : $name;
208
    }
209
210
    /**
211
     * Determine the extension based on the driver class
212
     *
213
     * If there is no driver class given it will default to .php
214
     *
215
     * @return string
216
     */
217
    private function determineExtension(): string
218
    {
219
        $ext = 'php';
220
        if (in_array($this->driverClass, $this->extensionToDriver)) {
221
            $map = array_flip($this->extensionToDriver);
222
            $ext = $map[$this->driverClass];
223
        }
224
        return $ext;
225
    }
226
227
    /**
228
     * Creates the name with the extension for known names
229
     *
230
     * @param string $name
231
     * @param string $ext
232
     *
233
     * @return string
234
     */
235
    private function createName(string $name, string $ext): string
236
    {
237
        $withExtension = $name;
238
        if (!preg_match('/.*\.(ini|php)/i', $name)) {
239
            $withExtension = "$name.$ext";
240
        }
241
        return $withExtension;
242
    }
243
244
    /**
245
     * Search for name in the list of paths
246
     *
247
     * @param string $name
248
     * @param string $withExtension
249
     *
250
     * @return array{bool, string}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{bool, string} at position 2 could not be parsed: Expected ':' at position 2, but found 'bool'.
Loading history...
251
     */
252
    private function searchFor(string $name, string $withExtension): array
253
    {
254
        $found = false;
255
        $fileName = $name;
256
257
        foreach (self::$paths as $path) {
258
            $fileName = "$path/$withExtension";
259
            if (is_file($fileName)) {
260
                $found = true;
261
                break;
262
            }
263
        }
264
265
        return [$found, $fileName];
266
    }
267
268
    /**
269
     * Creates the configuration driver from current properties
270
     *
271
     * @return ConfigurationInterface
272
     * @throws ReflectionException
273
     */
274
    private function createConfigurationDriver(): ConfigurationInterface
275
    {
276
        $reflection = new ReflectionClass($this->driverClass());
277
278
        /** @var ConfigurationInterface $config */
279
        $config = $reflection->hasMethod('__construct')
280
            ? $reflection->newInstanceArgs([$this->file])
281
            : $reflection->newInstance();
282
        return $config;
283
    }
284
285
    /**
286
     * Sets the file and driver class
287
     *
288
     * @param array{?string, ?class-string, ?int} $option
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{?string, ?class-string, ?int} at position 2 could not be parsed: Expected ':' at position 2, but found '?string'.
Loading history...
289
     *
290
     * @return int
291
     */
292
    private function setProperties(array $option): int
293
    {
294
        $priority = $option[2] ?? 0;
295
        $this->driverClass = $option[1] ?? null;
296
        $this->file = isset($option[0]) ? $this->composeFileName($option[0]) : null;
297
        return $priority;
298
    }
299
300
    /**
301
     * Fixes the file for initialization
302
     *
303
     * @return array<int|string, mixed>
304
     */
305
    private function fixOptions(): array
306
    {
307
        return (is_array($this->file))
308
            ? $this->file
309
            : [[$this->file]];
310
    }
311
}
312