Passed
Branch master (30bd85)
by Mihail
02:09
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_string;
24
use function iterator_to_array;
25
use function join;
26
use function json_decode;
27
use function parse_ini_file;
28
use function parse_ini_string;
29
use function pathinfo;
30
use function strtr;
31
use function ucfirst;
32
33
/**
34
 * Class Config works as a parameter bag that provides ways to fill it
35
 * from files or other Config instances. There are 2 common patterns
36
 * to populate the config,
37
 *
38
 * either you can fill the Config instance from config files:
39
 *
40
 *     $app->config()->fromPhpFile('myconfig.php');
41
 *     $app->config()->fromJsonFile('myconfig.json');
42
 *     $app->config()->fromEnvFile('.env');
43
 *     $app->config()->fromIniFile('myconfig.ini');
44
 *
45
 * or alternatively you can define the configuration options in the instance
46
 * that calls `fromObject`,
47
 *
48
 *     $app->config()->fromObject(MyConfig::class);
49
 *     $app->config()->fromObject($myconfig); // $myconfig is instance of Config
50
 *
51
 * Other interesting way to load configuration is from an environment variable
52
 * that points to a file
53
 *
54
 *     $app->config()->fromEnvVariable('MY_APP_SETTINGS');
55
 *
56
 * In this case, before launching the application you have to set the env variable
57
 * to the file you want to use. On Linux and OSX use the export statement
58
 *
59
 *     export MY_APP_SETTINGS='/path/to/config/file.php'
60
 *
61
 * or somewhere in your app bootstrap phase before constructing the Api instance
62
 *
63
 *     putenv('MY_APP_SETTINGS=/path/to/config/file.php');
64
 *
65
 */
66
class Config extends Arguments implements Configuration
67
{
68
    private bool $silent = false;
69
70
    /**
71
     * Config constructor.
72
     *
73
     * @param string $root Path to which files are read relative from.
74
     *                                  When the config object is created by an application/library
75
     *                                  this is the application's root path
76
     * @param Data|null $defaults [optional] An Optional config object with default values
77
     */
78 25
    public function __construct(
79
        public string $root = '',
80
        Data $defaults = null)
81
    {
82 25
        parent::__construct($defaults?->toArray() ?? []);
83 25
        $this->root ??= getcwd();
84
    }
85
86
    /**
87
     * Bad method calls can be suppressed and allow the app
88
     * to continue execution by setting the silent(true).
89
     *
90
     * The app should handle their configuration appropriately.
91
     *
92
     * @param string $name Method name
93
     * @param array|null $arguments [optional]
94
     * @return Configuration
95
     * @throws Exception
96
     */
97 3
    public function __call(string $name, array|null $arguments): Configuration
98
    {
99 3
        if (false === $this->silent) {
100 2
            throw new Exception('Unable to load the configuration file ' . current($arguments));
101
        }
102 1
        return $this;
103
    }
104
105 1
    public function build(string $context): Configuration
106
    {
107 1
        throw new Exception('Configuration factory should implement the method ' . __METHOD__);
108
    }
109
110 1
    public function withParameters(array $parameters): Configuration
111
    {
112 1
        return $this->import($parameters);
113
    }
114
115 2
    public function fromObject(object|string $object): Configuration
116
    {
117 2
        if (is_string($object) && class_exists($object)) {
118 1
            $object = new $object;
119
        }
120 2
        $this->root ??= $object->root;
121 2
        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

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

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