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