Config::build()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 1
c 2
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php declare(strict_types=1);
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 */
11
12
namespace Koded\Stdlib;
13
14
use Exception;
15
use function call_user_func;
16
use function class_exists;
17
use function current;
18
use function error_get_last;
19
use function error_log;
20
use function file_get_contents;
21
use function getcwd;
22
use function getenv;
23
use function is_a;
24
use function is_string;
25
use function iterator_to_array;
26
use function join;
27
use function json_decode;
28
use function parse_ini_file;
29
use function parse_ini_string;
30
use function pathinfo;
31
use function strtr;
32
use function ucfirst;
33
34
/**
35
 * Class Config works as a parameter bag that provides ways to fill it
36
 * from files or other Config instances. There are 2 common patterns
37
 * to populate the config,
38
 *
39
 * either you can fill the Config instance from config files:
40
 *
41
 *     $app->config()->fromPhpFile('myconfig.php');
42
 *     $app->config()->fromJsonFile('myconfig.json');
43
 *     $app->config()->fromEnvFile('.env');
44
 *     $app->config()->fromIniFile('myconfig.ini');
45
 *
46
 * or alternatively you can define the configuration options in the instance
47
 * that calls `fromObject`,
48
 *
49
 *     $app->config()->fromObject(MyConfig::class);
50
 *     $app->config()->fromObject($myconfig); // $myconfig is instance of Config
51
 *
52
 * Other interesting way to load configuration is from an environment variable
53
 * that points to a file
54
 *
55
 *     $app->config()->fromEnvVariable('MY_APP_SETTINGS');
56
 *
57
 * In this case, before launching the application you have to set the env variable
58
 * to the file you want to use. On Linux and OSX use the export statement
59
 *
60
 *     export MY_APP_SETTINGS='/path/to/config/file.php'
61
 *
62
 * or somewhere in your app bootstrap phase before constructing the Api instance
63
 *
64
 *     putenv('MY_APP_SETTINGS=/path/to/config/file.php');
65
 *
66
 */
67
68
#[\AllowDynamicProperties]
69
class Config extends Arguments implements Configuration
70
{
71
    private bool $silent = false;
72
73
    /**
74
     * Config constructor.
75
     *
76
     * @param string $root Path to which files are read relative from.
77
     *                                  When the config object is created by an application/library
78
     *                                  this is the application's root path
79
     * @param Data|null $defaults [optional] An Optional config object with default values
80
     */
81 29
    public function __construct(
82
        public string $root = '',
83
        Data $defaults = null)
84
    {
85 29
        parent::__construct($defaults?->toArray() ?? []);
86 29
        $this->root = $root ?: getcwd();
87
    }
88
89
    /**
90
     * Bad method calls can be suppressed and allow the app
91
     * to continue execution by setting the silent(true).
92
     *
93
     * The app should handle their configuration appropriately.
94
     *
95
     * @param string $name Method name
96
     * @param array|null $arguments [optional]
97
     * @return Configuration
98
     * @throws Exception
99
     */
100 3
    public function __call(string $name, array|null $arguments): Configuration
101
    {
102 3
        if (false === $this->silent) {
103 2
            throw new Exception('Unable to load the configuration file ' . current($arguments));
104
        }
105 1
        return $this;
106
    }
107
108 1
    public function build(string $context): Configuration
109
    {
110 1
        throw new Exception('Configuration factory should implement the method ' . __METHOD__);
111
    }
112
113 1
    public function withParameters(array $parameters): Configuration
114
    {
115 1
        return $this->import($parameters);
116
    }
117
118 3
    public function fromObject(Configuration|string $object): Configuration
119
    {
120 3
        if (is_string($object) && class_exists($object) && is_a($object, Configuration::class, true)) {
121 1
            $object = new $object;
122
        }
123 3
        $this->root = $object->root;
124 3
        return $this->import(iterator_to_array($object));
0 ignored issues
show
Bug introduced by
It seems like $object can also be of type string; however, parameter $iterator of iterator_to_array() does only seem to accept Traversable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

124
        return $this->import(iterator_to_array(/** @scrutinizer ignore-type */ $object));
Loading history...
125
    }
126
127 2
    public function fromJsonFile(string $filename): Configuration
128
    {
129 2
        return $this->loadDataFrom($filename,
130 2
            fn() => json_decode(file_get_contents($filename), true)
131 2
        );
132
    }
133
134 3
    public function fromIniFile(string $filename): Configuration
135
    {
136 3
        return $this->loadDataFrom($filename,
137 3
            fn() => parse_ini_file($filename, true, INI_SCANNER_TYPED) ?: []
138 3
        );
139
    }
140
141 4
    public function fromEnvFile(string $filename, string $namespace = ''): Configuration
142
    {
143
        try {
144 4
            $data = parse_ini_file($this->filename($filename), true, INI_SCANNER_TYPED) ?: [];
145 3
            env('', null, $this->filter($data, $namespace, false));
146 1
        } catch (Exception $e) {
147 1
            error_log('[Configuration error]: ' . $e->getMessage());
148
        } finally {
149 4
            return $this;
150
        }
151
    }
152
153 5
    public function fromEnvVariable(string $variable): Configuration
154
    {
155 5
        if (false === empty($filename = getenv($variable))) {
156 3
            $extension = ucfirst(pathinfo($filename, PATHINFO_EXTENSION));
0 ignored issues
show
Bug introduced by
It seems like pathinfo($filename, Kode...lib\PATHINFO_EXTENSION) can also be of type array; however, parameter $string of ucfirst() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

156
            $extension = ucfirst(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_EXTENSION));
Loading history...
157 3
            return call_user_func([$this, "from{$extension}File"], $filename);
158
        }
159 2
        if (false === $this->silent) {
160 1
            throw new Exception(strtr('The environment variable ":variable" is not set
161
            and as such configuration could not be loaded. Set this variable and
162 1
            make it point to a configuration file', [':variable' => $variable]));
163
        }
164 1
        error_log('[Configuration error]: ' . (error_get_last()['message'] ?? "env var: $variable"));
165 1
        return $this;
166
    }
167
168 4
    public function fromPhpFile(string $filename): Configuration
169
    {
170 4
        return $this->loadDataFrom($filename, fn() => include $filename);
171
    }
172
173 5
    public function fromEnvironment(
174
        array  $variableNames,
175
        string $namespace = '',
176
        bool   $lowercase = true,
177
        bool   $trim = true): Configuration
178
    {
179 5
        $data = [];
180 5
        foreach ($variableNames as $variable) {
181 5
            $value = getenv($variable);
182 5
            $data[] = $variable . '=' . (false === $value ? 'null' : $value);
183
        }
184 5
        $data = parse_ini_string(join(PHP_EOL, $data), true, INI_SCANNER_TYPED) ?: [];
185 5
        $this->import($this->filter($data, $namespace, $lowercase, $trim));
186 5
        return $this;
187
    }
188
189 3
    public function silent(bool $silent): Configuration
190
    {
191 3
        $this->silent = $silent;
192 3
        return $this;
193
    }
194
195 1
    public function namespace(
196
        string $prefix,
197
        bool   $lowercase = true,
198
        bool   $trim = true): static
199
    {
200 1
        return (new static($this->root))->import(
201 1
            $this->filter($this->toArray(), $prefix, $lowercase, $trim)
202 1
        );
203
    }
204
205 9
    protected function loadDataFrom(string $filename, callable $loader): Configuration
206
    {
207 9
        return $this->import($loader($this->filename($filename)));
208
    }
209
210 13
    private function filename(string $filename): string
211
    {
212 13
        return ('/' !== $filename[0])
213 5
            ? $this->root . '/' . $filename
214 13
            : $filename;
215
    }
216
}
217