Passed
Pull Request — master (#6824)
by
unknown
08:38
created

Version20250926174000::collectFromFixtures()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 4
nop 2
dl 0
loc 20
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
8
9
use Chamilo\CoreBundle\DataFixtures\SettingsCurrentFixtures;
10
use Chamilo\CoreBundle\DataFixtures\SettingsValueTemplateFixtures;
11
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
12
use Chamilo\CoreBundle\Settings\SettingsManager;
13
use Doctrine\DBAL\Schema\Schema;
14
15
use const JSON_UNESCAPED_SLASHES;
16
use const JSON_UNESCAPED_UNICODE;
17
18
final class Version20250926174000 extends AbstractMigrationChamilo
19
{
20
    public function getDescription(): string
21
    {
22
        return 'Upsert settings (title/comment/category/value) + settings_value_template.';
23
    }
24
25
    public function up(Schema $schema): void
26
    {
27
        // Collect settings (existing + new) from fixtures
28
        $rows = [];
29
        $this->collectFromFixtures($rows, (array) SettingsCurrentFixtures::getExistingSettings());
30
        $this->collectFromFixtures($rows, (array) SettingsCurrentFixtures::getNewConfigurationSettings());
31
32
        // Index by variable (last occurrence wins)
33
        $byVariable = [];
34
        foreach ($rows as $r) {
35
            $byVariable[$r['variable']] = $r;
36
        }
37
38
        // Defaults from SettingsManager schemas
39
        $defaults = $this->buildDefaultsFromSchemas();
40
41
        foreach ($byVariable as $variable => $row) {
42
            if (!$this->isValidIdentifier($variable)) {
43
                error_log(sprintf('[SKIP] Invalid variable name: "%s".', $variable));
44
                continue;
45
            }
46
            $category = $row['category'];
47
            if (!$this->isValidIdentifier($category)) {
48
                error_log(sprintf('[SKIP] Invalid category for "%s": "%s".', $variable, $category));
49
                continue;
50
            }
51
52
            $title   = (string) $row['title'];
53
            $comment = (string) $row['comment'];
54
55
            try {
56
                $exists = (int) $this->connection->fetchOne(
57
                    'SELECT COUNT(*) FROM settings WHERE variable = ?',
58
                    [$variable]
59
                );
60
            } catch (\Throwable $e) {
61
                error_log(sprintf('[ERROR] Existence check failed for "%s": %s', $variable, $e->getMessage()));
62
                continue;
63
            }
64
65
            if ($exists > 0) {
66
                $sql    = 'UPDATE settings SET category = ?, title = ?, comment = ? WHERE variable = ?';
67
                $params = [$category, $title, $comment, $variable];
68
                $this->addSql($sql, $params);
69
            } else {
70
                $defaultValue = $defaults[$variable] ?? '';
71
                $sql    = 'INSERT INTO settings (variable, category, title, comment, selected_value, access_url_changeable) VALUES (?, ?, ?, ?, ?, 0)';
72
                $params = [$variable, $category, $title, $comment, (string) $defaultValue];
73
                $this->addSql($sql, $params);
74
            }
75
        }
76
77
        $this->dryRunTemplatesUpsertAndLink();
78
    }
79
80
    public function down(Schema $schema): void
81
    {
82
        error_log('[MIGRATION] Down is a no-op (dry-run migration).');
83
    }
84
85
    /**
86
     * Read fixtures and normalize to: [variable, category, title, comment]
87
     */
88
    private function collectFromFixtures(array &$out, array $byCategory): void
89
    {
90
        foreach ($byCategory as $categoryKey => $settings) {
91
            $category = strtolower((string) $categoryKey);
92
93
            foreach ((array) $settings as $setting) {
94
                $variable = (string) ($setting['name'] ?? $setting['variable'] ?? '');
95
                if ($variable === '') {
96
                    error_log(sprintf('[WARN] Missing "name" in fixture entry for category "%s" - skipping.', $category));
97
                    continue;
98
                }
99
100
                $title   = (string) ($setting['title'] ?? $variable);
101
                $comment = (string) ($setting['comment'] ?? '');
102
103
                $out[] = [
104
                    'variable' => $variable,
105
                    'category' => $category,
106
                    'title'    => $title,
107
                    'comment'  => $comment,
108
                ];
109
            }
110
        }
111
    }
112
113
    /**
114
     * Build default values map by scanning SettingsManager schemas.
115
     * Returns: [ variable => defaultValueAsString, ... ]
116
     */
117
    private function buildDefaultsFromSchemas(): array
118
    {
119
        $map = [];
120
121
        $manager = null;
122
        try {
123
            if ($this->container->has('chamilo.settings_manager')) {
0 ignored issues
show
Bug introduced by
The method has() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

123
            if ($this->container->/** @scrutinizer ignore-call */ has('chamilo.settings_manager')) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
124
                $manager = $this->container->get('chamilo.settings_manager');
125
            } elseif ($this->container->has('chamilo.core.settings_manager')) {
126
                $manager = $this->container->get('chamilo.core.settings_manager');
127
            } elseif ($this->container->has(SettingsManager::class)) {
128
                $manager = $this->container->get(SettingsManager::class);
129
            }
130
        } catch (\Throwable $e) {
131
            error_log('[WARN] Unable to retrieve SettingsManager from container: ' . $e->getMessage());
132
        }
133
134
        if (!$manager) {
135
            error_log('[WARN] SettingsManager not found; defaults map will be empty.');
136
            return $map;
137
        }
138
139
        try {
140
            $schemas = $manager->getSchemas();
141
        } catch (\Throwable $e) {
142
            error_log('[WARN] getSchemas() failed on SettingsManager: ' . $e->getMessage());
143
            return $map;
144
        }
145
146
        foreach (array_keys($schemas) as $serviceIdOrAlias) {
147
            $namespace = str_replace('chamilo_core.settings.', '', (string) $serviceIdOrAlias);
148
149
            try {
150
                $settingsBag = $manager->load($namespace);
151
            } catch (\Throwable $e) {
152
                error_log(sprintf('[WARN] load("%s") failed: %s', $namespace, $e->getMessage()));
153
                continue;
154
            }
155
156
            $parameters = [];
157
158
            try {
159
                if (method_exists($settingsBag, 'getParameters')) {
160
                    $parameters = (array) $settingsBag->getParameters();
161
                } elseif (method_exists($settingsBag, 'all')) {
162
                    $parameters = (array) $settingsBag->all();
163
                } elseif (method_exists($settingsBag, 'toArray')) {
164
                    $parameters = (array) $settingsBag->toArray();
165
                }
166
            } catch (\Throwable $e) {
167
                error_log(sprintf('[WARN] Could not extract parameters for "%s": %s', $namespace, $e->getMessage()));
168
                $parameters = [];
169
            }
170
171
            if (empty($parameters)) {
172
                try {
173
                    $keys = [];
174
                    if (method_exists($settingsBag, 'keys')) {
175
                        $keys = (array) $settingsBag->keys();
176
                    } elseif (method_exists($settingsBag, 'getIterator')) {
177
                        $keys = array_keys(iterator_to_array($settingsBag->getIterator()));
178
                    }
179
                    foreach ($keys as $k) {
180
                        try {
181
                            $parameters[$k] = $settingsBag->get($k);
182
                        } catch (\Throwable $e) {
183
                            // ignore
184
                        }
185
                    }
186
                } catch (\Throwable $e) {
187
                    error_log(sprintf('[WARN] Parameter keys iteration failed for "%s": %s', $namespace, $e->getMessage()));
188
                }
189
            }
190
191
            foreach ($parameters as $var => $val) {
192
                $var = (string) $var;
193
                if ($var === '') {
194
                    continue;
195
                }
196
                if (!array_key_exists($var, $map)) {
197
                    $map[$var] = is_scalar($val) ? (string) $val : (string) json_encode($val);
198
                }
199
            }
200
        }
201
202
        return $map;
203
    }
204
205
    /**
206
     * Upsert settings_value_template and preview linking to settings.value_template_id.
207
     * For safety, we:
208
     *  - validate variable
209
     *  - JSON-encode examples with proper flags
210
     *  - do existence checks with SELECTs
211
     */
212
    private function dryRunTemplatesUpsertAndLink(): void
213
    {
214
        $grouped = [];
215
        try {
216
            $grouped = (array) SettingsValueTemplateFixtures::getTemplatesGrouped();
217
        } catch (\Throwable $e) {
218
            error_log('[WARN] Unable to load SettingsValueTemplateFixtures::getTemplatesGrouped(): ' . $e->getMessage());
219
            return;
220
        }
221
222
        foreach ($grouped as $category => $items) {
223
            foreach ((array) $items as $setting) {
224
                $variable    = (string) ($setting['variable'] ?? $setting['name'] ?? '');
225
                $jsonExample = $setting['json_example'] ?? null;
226
227
                if ($variable === '' || !$this->isValidIdentifier($variable)) {
228
                    error_log(sprintf('[SKIP] Invalid or empty template variable in category "%s".', (string) $category));
229
                    continue;
230
                }
231
232
                // Serialize JSON example safely (string for SQL param preview)
233
                try {
234
                    $jsonEncoded = json_encode($jsonExample, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
235
                } catch (\Throwable $e) {
236
                    error_log(sprintf('[WARN] JSON encoding failed for variable "%s": %s', $variable, $e->getMessage()));
237
                    $jsonEncoded = 'null';
238
                }
239
240
                // Check if template exists
241
                try {
242
                    $templateId = $this->connection->fetchOne(
243
                        'SELECT id FROM settings_value_template WHERE variable = ?',
244
                        [$variable]
245
                    );
246
                } catch (\Throwable $e) {
247
                    error_log(sprintf('[ERROR] Failed to check template existence for "%s": %s', $variable, $e->getMessage()));
248
                    continue;
249
                }
250
251
                if ($templateId) {
252
                    // UPDATE PREVIEW on existing template
253
                    $sql    = 'UPDATE settings_value_template SET json_example = ?, updated_at = NOW() WHERE id = ?';
254
                    $params = [$jsonEncoded, $templateId];
255
                    $this->addSql($sql, $params);
256
                } else {
257
                    // INSERT PREVIEW new template
258
                    $sql    = 'INSERT INTO settings_value_template (variable, json_example, created_at, updated_at) VALUES (?, ?, NOW(), NOW())';
259
                    $params = [$variable, $jsonEncoded];
260
                    $this->addSql($sql, $params);
261
262
                    // Try to discover what ID it would be (optional best-effort)
263
                    try {
264
                        // We DO NOT insert, so we cannot call lastInsertId().
265
                        // Instead, try to SELECT id if it exists already after a previous run; otherwise log NULL.
266
                        $templateId = $this->connection->fetchOne(
267
                            'SELECT id FROM settings_value_template WHERE variable = ?',
268
                            [$variable]
269
                        );
270
                    } catch (\Throwable $e) {
271
                        $templateId = false;
272
                    }
273
                }
274
275
                // Link PREVIEW: settings.value_template_id = $templateId
276
                if ($templateId) {
277
                    $sql    = 'UPDATE settings SET value_template_id = ? WHERE variable = ?';
278
                    $params = [$templateId, $variable];
279
                    $this->addSql($sql, $params);
280
                } else {
281
                    error_log(sprintf('[INFO] Skipping link preview for "%s" (no template id available in dry-run).', $variable));
282
                }
283
            }
284
        }
285
    }
286
287
    /** Allow letters, numbers, underscore, dash and dot. */
288
    private function isValidIdentifier(string $s): bool
289
    {
290
        return (bool) preg_match('/^[A-Za-z0-9_.-]+$/', $s);
291
    }
292
}
293