Passed
Push — main ( 9d849a...f91a53 )
by Rafael
16:58
created

Config::doRunMigrations()   A

Complexity

Conditions 5
Paths 15

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
c 0
b 0
f 0
dl 0
loc 27
rs 9.3888
cc 5
nc 15
nop 0
1
<?php
2
3
/* Copyright (C) 2024       Rafael San José         <[email protected]>
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 3 of the License, or
8
 * any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License
16
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
 */
18
19
namespace Alxarafe\Base;
20
21
use Alxarafe\Lib\Messages;
22
use Alxarafe\Lib\Routes;
23
use Alxarafe\Lib\Trans;
24
use Alxarafe\Tools\Debug;
25
use CoreModules\Admin\Model\Migration;
26
use DebugBar\DebugBarException;
27
use Exception;
28
use stdClass;
29
30
/**
31
 * Manage the configuration file
32
 */
33
abstract class Config
34
{
35
    /**
36
     * Configuration filename.
37
     */
38
    private const CONFIG_FILENAME = 'config.json';
39
40
    /**
41
     * Defines the configuration file structure
42
     */
43
    public const  CONFIG_STRUCTURE = [
44
        'main' => [
45
            'path', // Path to the public folder (usually htdocs)
46
            'url',
47
            'data', // Route to the private folder that stores the documents.
48
            'theme',
49
            'language',
50
        ],
51
        'db' => [
52
            'type',
53
            'host',
54
            'user',
55
            'pass',
56
            'name',
57
            'port',
58
            'prefix',
59
            'charset',
60
            'collation',
61
            'encryption', // Pending review: If true, some database fields are encrypted.
62
            'encrypt_type', // Pending review: Encryption type ('0' if none, '1' if DES and '2' if AES)
63
        ],
64
        'security' => [
65
            'debug',
66
            'unique_id', // Unique identifier of the installation.
67
            'https', // If true, the use of https is forced (recommended)
68
            'jwt_secret_key',
69
        ]
70
    ];
71
72
    /**
73
     * Contains configuration file information
74
     *
75
     * @var stdClass|null
76
     */
77
    private static ?stdClass $config = null;
78
79
    /**
80
     * Gets the information defined in the configuration file.
81
     * To reload the configuration file, set $reload to true.
82
     *
83
     * @param bool $reload
84
     * @return stdClass|null
85
     */
86
    public static function getConfig(bool $reload = false): ?stdClass
87
    {
88
        if ($reload || !isset(self::$config)) {
89
            self::$config = self::loadConfig($reload);
90
        }
91
92
        return self::$config;
93
    }
94
95
    /**
96
     * Add the configuration parameters received in $data in the configuration file.
97
     *
98
     * @param stdClass $data
99
     * @return bool
100
     * @throws DebugBarException
101
     */
102
    public static function setConfig(stdClass $data): bool
103
    {
104
        /**
105
         * If the configuration file is empty, we add the parameters
106
         * that we can obtain at runtime (getDefaultMainFileInfo).
107
         */
108
        if (empty(self::$config)) {
109
            self::$config = new stdClass();
110
            self::$config->main = static::getDefaultMainFileInfo();
111
        }
112
113
        foreach (self::CONFIG_STRUCTURE as $section => $values) {
114
            foreach ($values as $key) {
115
                if (!isset($data->$section)) {
116
                    error_log($section . ' is not defined!');
117
                    continue;
118
                }
119
                if (!isset($data->$section->$key)) {
120
                    error_log($key . ' is not defined in ' . $section . '!');
121
                    continue;
122
                }
123
                if (!isset(self::$config->$section)) {
124
                    self::$config->$section = new stdClass();
125
                }
126
                self::$config->$section->$key = $data->$section->$key;
127
            }
128
        }
129
130
        /**
131
         * Save the configuration in the configuration file.
132
         */
133
        Trans::setLang(self::$config->main->language ?? Trans::FALLBACK_LANG);
134
        $ok = self::saveConfig();
135
        self::getConfig(true);
136
        Debug::initialize(true);
137
        return $ok;
138
    }
139
140
    /**
141
     * Those configuration parameters that we can obtain at run time,
142
     * or their default values, are obtained.
143
     *
144
     * @return stdClass
145
     */
146
    public static function getDefaultMainFileInfo(): stdClass
147
    {
148
        $result = new stdClass();
149
        $result->path = constant('BASE_PATH');
150
        $result->url = constant('BASE_URL');
151
        return $result;
152
    }
153
154
    /**
155
     * Updates the configuration file with the information it has in memory.
156
     *
157
     * @return bool
158
     */
159
    private static function saveConfig(): bool
160
    {
161
        if (empty(self::$config)) {
162
            return true;
163
        }
164
        return file_put_contents(self::getConfigFilename(), json_encode(self::$config, JSON_PRETTY_PRINT)) !== false;
165
    }
166
167
    /**
168
     * Returns the config.json complete path.
169
     *
170
     * @return string
171
     */
172
    private static function getConfigFilename(): string
173
    {
174
        return realpath(constant('BASE_PATH') . '/..') . DIRECTORY_SEPARATOR . self::CONFIG_FILENAME;
175
    }
176
177
    /**
178
     * Runs pending migrations in alphabetical order.
179
     * By default, migration names are preceded by the date in Japanese format,
180
     * which ensures chronological execution.
181
     *
182
     * @return bool
183
     */
184
    public static function doRunMigrations(): bool
185
    {
186
        try {
187
            $batch = 1 + Migration::getLastBatch();
188
            foreach (static::getMigrations() as $filename => $filepath) {
189
                if (Migration::where(['migration' => $filename])->first()) {
190
                    continue;
191
                }
192
193
                $migration = require_once $filepath;
194
                $migration->up();
195
                if (
196
                    !Migration::create([
197
                    'migration' => $filename,
198
                    'batch' => $batch,
199
                    ])
200
                ) {
201
                    Messages::addError('Fail creating migration ' . $filename);
202
                    return false;
203
                }
204
            }
205
        } catch (Exception $e) {
206
            Messages::addError($e->getMessage());
207
            return false;
208
        }
209
210
        return true;
211
    }
212
213
    /**
214
     * Obtains an associative array with all migrations. The index is the name of the
215
     * migration and the name of the module separated by an @ sign, which ensures that
216
     * the migration is registered as executed; and the value is the relative path of
217
     * the file containing the migration.
218
     *
219
     * @return array
220
     */
221
    public static function getMigrations(): array
222
    {
223
        $result = [];
224
225
        $routes = Routes::getAllRoutes();
226
        if (empty($routes['Migrations'])) {
227
            return $result;
228
        }
229
230
        $migrations = $routes['Migrations'];
231
232
        foreach ($migrations as $module => $data) {
233
            foreach ($data as $filename => $migration) {
234
                $route_array = explode('|', $migration);
235
                $filepath = $route_array[1];
236
237
                $result[$filename . '@' . $module] = $filepath;
238
            }
239
        }
240
        ksort($result);
241
242
        return $result;
243
    }
244
245
    public static function runSeeders(): bool
246
    {
247
        $routes = Routes::getAllRoutes();
248
        if (empty($routes['Seeders'])) {
249
            return true;
250
        }
251
252
        $seeders = $routes['Seeders'];
253
254
        foreach ($seeders as $data) {
255
            foreach ($data as $seeder) {
256
                $route_array = explode('|', $seeder);
257
                $classname = $route_array[0];
258
                try {
259
                    new $classname();
260
                } catch (Exception $e) {
261
                    Messages::addError($e->getMessage());
262
                    return false;
263
                }
264
            }
265
        }
266
267
        return true;
268
    }
269
270
    /**
271
     * Returns a stdClass with the program configuration.
272
     * If the configuration file does not exist, is not accessible, or is not correct, returns null.
273
     * The configuration is loaded from the file only once and stored in a variable. You can set $reload
274
     * to true to force a reload of the configuration file.
275
     *
276
     * @param bool $reload
277
     * @return stdClass|null
278
     */
279
    private static function loadConfig(bool $reload = false): ?stdClass
280
    {
281
        if (!$reload && isset(self::$config)) {
282
            return self::$config;
283
        }
284
285
        $filename = self::getConfigFilename();
286
        if (!file_exists($filename)) {
287
            return null;
288
        }
289
290
        $config = file_get_contents($filename);
291
        if ($config === false) {
292
            return self::$config;
293
        }
294
295
        $result = json_decode($config);
296
        if (json_last_error() === JSON_ERROR_NONE) {
297
            self::$config = $result;
298
        }
299
300
        return $result;
301
    }
302
}
303