Passed
Push — master ( 30bd85...331dab )
by Mihail
02:11
created

Config.php (2 issues)

Labels
Severity
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
class Config extends Arguments implements Configuration
68
{
69
    private bool $silent = false;
70
71
    /**
72
     * Config constructor.
73
     *
74
     * @param string $root Path to which files are read relative from.
75
     *                                  When the config object is created by an application/library
76
     *                                  this is the application's root path
77
     * @param Data|null $defaults [optional] An Optional config object with default values
78
     */
79 27
    public function __construct(
80
        public string $root = '',
81
        Data $defaults = null)
82
    {
83 27
        parent::__construct($defaults?->toArray() ?? []);
84 27
        $this->root = $root ?: getcwd();
85
    }
86
87
    /**
88
     * Bad method calls can be suppressed and allow the app
89
     * to continue execution by setting the silent(true).
90
     *
91
     * The app should handle their configuration appropriately.
92
     *
93
     * @param string $name Method name
94
     * @param array|null $arguments [optional]
95
     * @return Configuration
96
     * @throws Exception
97
     */
98 3
    public function __call(string $name, array|null $arguments): Configuration
99
    {
100 3
        if (false === $this->silent) {
101 2
            throw new Exception('Unable to load the configuration file ' . current($arguments));
102
        }
103 1
        return $this;
104
    }
105
106 1
    public function build(string $context): Configuration
107
    {
108 1
        throw new Exception('Configuration factory should implement the method ' . __METHOD__);
109
    }
110
111 1
    public function withParameters(array $parameters): Configuration
112
    {
113 1
        return $this->import($parameters);
114
    }
115
116 3
    public function fromObject(Configuration|string $object): Configuration
117
    {
118 3
        if (is_string($object) && class_exists($object) && is_a($object, Configuration::class, true)) {
119 1
            $object = new $object;
120
        }
121 3
        $this->root = $object->root;
122 3
        return $this->import(iterator_to_array($object));
0 ignored issues
show
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

122
        return $this->import(iterator_to_array(/** @scrutinizer ignore-type */ $object));
Loading history...
123
    }
124
125 2
    public function fromJsonFile(string $filename): Configuration
126
    {
127 2
        return $this->loadDataFrom($filename,
128 2
            fn() => json_decode(file_get_contents($filename), true)
129 2
        );
130
    }
131
132 3
    public function fromIniFile(string $filename): Configuration
133
    {
134 3
        return $this->loadDataFrom($filename,
135 3
            fn() => parse_ini_file($filename, true, INI_SCANNER_TYPED) ?: []
136 3
        );
137
    }
138
139 4
    public function fromEnvFile(string $filename, string $namespace = ''): Configuration
140
    {
141
        try {
142 4
            $data = parse_ini_file($this->filename($filename), true, INI_SCANNER_TYPED) ?: [];
143 3
            env('', null, $this->filter($data, $namespace, false));
144 1
        } catch (Exception $e) {
145 1
            error_log('[Configuration error]: ' . $e->getMessage());
146
        } finally {
147 4
            return $this;
148
        }
149
    }
150
151 5
    public function fromEnvVariable(string $variable): Configuration
152
    {
153 5
        if (false === empty($filename = getenv($variable))) {
154 3
            $extension = ucfirst(pathinfo($filename, PATHINFO_EXTENSION));
0 ignored issues
show
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

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