Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
97 / 97
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
ConfigServiceProvider
100.00% covered (success)
100.00%
97 / 97
100.00% covered (success)
100.00%
10 / 10
40
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 boot
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 loadConfigFromFiles
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 initConfigOptions
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
4
 loadConfigFromEnv
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 getEnvValue
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
7
 loadConfigFromDb
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 parseConfigTypes
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 getConfigPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Engelsystem\Config;
6
7use Carbon\Carbon as CarbonCarbon;
8use DateTimeZone;
9use Engelsystem\Application;
10use Engelsystem\Container\ServiceProvider;
11use Engelsystem\Helpers\Carbon;
12use Engelsystem\Helpers\CarbonDay;
13use Engelsystem\Models\EventConfig;
14use Exception;
15use Illuminate\Database\QueryException;
16use Illuminate\Support\Env;
17use Illuminate\Support\Str;
18
19/**
20 * Loads the configuration from files (for database connection and app config), database and environment
21 */
22class ConfigServiceProvider extends ServiceProvider
23{
24    protected array $configFiles = ['app.php', 'config.default.php', 'config.local.php', 'config.php'];
25
26    // Remember to update ConfigServiceProviderTest, config.default.php, and README.md
27    protected array $configVarsToPruneNulls = [
28        'themes',
29        'tshirt_sizes',
30        'headers',
31        'header_items',
32        'footer_items',
33        'locales',
34        'contact_options',
35    ];
36
37    protected array $envConfig = [];
38
39    public function __construct(Application $app, protected ?EventConfig $eventConfig = null)
40    {
41        parent::__construct($app);
42    }
43
44    public function register(): void
45    {
46        /** @var Config $config */
47        $config = $this->app->make(Config::class);
48        $this->app->instance(Config::class, $config);
49        $this->app->instance('config', $config);
50
51        $this->loadConfigFromFiles($config);
52        $this->initConfigOptions($config);
53        $this->loadConfigFromEnv($config);
54        $this->parseConfigTypes($config);
55
56        if (empty($config->get(null))) {
57            throw new Exception('Configuration not found');
58        }
59
60        // Prune values with null in file config to remove them
61        foreach ($this->configVarsToPruneNulls as $key) {
62            $values = $config->get($key);
63            if (!$values) {
64                // Skip values that are not defined in files or env
65                continue;
66            }
67            $config->set($key, array_filter($values, function ($v) {
68                return !is_null($v);
69            }));
70        }
71    }
72
73    public function boot(): void
74    {
75        /** @var Config $config */
76        $config = $this->app->get('config');
77
78        $this->loadConfigFromDb($config);
79        $this->loadConfigFromEnv($config);
80
81        $this->parseConfigTypes($config);
82
83        $config->set('env_config', $this->envConfig);
84    }
85
86    protected function loadConfigFromFiles(Config $config): void
87    {
88        foreach ($this->configFiles as $file) {
89            $file = $this->getConfigPath($file);
90
91            if (!file_exists($file)) {
92                continue;
93            }
94
95            $configuration = array_replace_recursive(
96                $config->get(null),
97                require $file
98            );
99            $config->set($configuration);
100        }
101    }
102
103    protected function initConfigOptions(Config $config): void
104    {
105        $configOptions = $config['config_options'];
106        if ($configOptions['system']['config']['timezone'] ?? null) {
107            // Timezone must be set for database connection
108            $configOptions['system']['config']['timezone']['data'] = array_combine(
109                DateTimeZone::listIdentifiers(),
110                DateTimeZone::listIdentifiers()
111            );
112            $config['timezone'] = $config['timezone']
113                ?? $configOptions['system']['config']['timezone']['default']
114                ?? 'UTC';
115            $config->set('config_options', $configOptions);
116        }
117        if ($configOptions['system']['config']['theme'] ?? null) {
118            foreach ($configOptions['system']['config']['themes']['default'] ?? [] as $id => $theme) {
119                $configOptions['system']['config']['theme']['data'][$id] = $theme['name'];
120            }
121            $configOptions['system']['config']['theme']['default'] = array_key_last(
122                $configOptions['system']['config']['theme']['data']
123            );
124            $config->set('config_options', $configOptions);
125        }
126    }
127
128    protected function loadConfigFromEnv(Config $config): void
129    {
130        foreach ($config->get('config_options', []) as $options) {
131            foreach ($options['config'] as $name => $option) {
132                $value = $this->getEnvValue(
133                    empty($option['env']) ? $name : $option['env'],
134                    ($option['type'] ?? '') == 'select_multi',
135                );
136                if (is_null($value)) {
137                    continue;
138                }
139
140                $config->set($name, $value);
141            }
142        }
143    }
144
145    protected function getEnvValue(string $name, bool $isList = false): mixed
146    {
147        $name = Str::upper($name);
148        if (isset($this->envConfig[$name])) {
149            return $this->envConfig[$name];
150        }
151
152        $file = Env::get($name . '_FILE');
153        if (!is_null($file) && is_readable($file)) {
154            $value = file_get_contents($file);
155        } else {
156            $value = Env::get($name);
157        }
158
159        if ($isList && !is_null($value)) {
160            $value = empty($value) ? [] : explode(',', preg_replace('~\s+~', '', $value));
161        }
162
163        $this->envConfig[$name] = $value;
164
165        return $value;
166    }
167
168    protected function loadConfigFromDb(Config $config): void
169    {
170        if (!$this->eventConfig) {
171            $this->eventConfig = $this->app->make(EventConfig::class);
172        }
173
174        try {
175            /** @var EventConfig[] $values */
176            $values = $this->eventConfig->newQuery()->get(['name', 'value']);
177        } catch (QueryException) {
178            return;
179        }
180
181        foreach ($values as $option) {
182            $data = $option->value;
183
184            // Merge array data over defaults if it's dict with defaults
185            if (is_array($data) && !array_is_list($data) && $config->has($option->name)) {
186                $data = array_replace_recursive(
187                    $config->get($option->name),
188                    $data
189                );
190            }
191
192            $config->set($option->name, $data);
193        }
194    }
195
196    protected function parseConfigTypes(Config $config): void
197    {
198        // Parse config types
199        foreach ($config->get('config_options', []) as $page) {
200            foreach ($page['config'] as $name => $options) {
201                $value = $config->get($name, $options['default'] ?? null);
202
203                $value = match ($options['type'] ?? null) {
204                    'datetime-local' => $value && !$value instanceof CarbonCarbon
205                        ? Carbon::createFromDatetime((string) $value)
206                        : $value,
207                    'date' => $value && !$value instanceof CarbonCarbon
208                        ? CarbonDay::createFromDay((string) $value)
209                        : $value,
210                    'boolean' => !empty($value),
211                    'number' => (float) $value,
212                    default => $value,
213                };
214
215                $config->set($name, $value);
216            }
217        }
218    }
219
220    /**
221     * Get the config path
222     */
223    protected function getConfigPath(string $path = ''): string
224    {
225        return config_path($path);
226    }
227}