PluginProbe
The Innovative Form Builder – IvyForms / 1.5.1
The Innovative Form Builder – IvyForms v1.5.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / src / Services / InstallActions / Migrations / AddPageIdToFields.php

AddPageIdToFields.php in The Innovative Form Builder – IvyForms 1.5.1, at backend/src/Services/InstallActions/Migrations/AddPageIdToFields.php

302 lines 9.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @copyright © Melograno Venture Studio. All rights reserved.
5 * @licence See LICENCE.md for license details.
6 */
7
8 namespace IvyForms\Services\InstallActions\Migrations;
9
10 use IvyForms\Common\Exceptions\InvalidArgumentException;
11 use IvyForms\Services\InstallActions\DB\Field\FieldsTable;
12 use IvyForms\Services\InstallActions\DB\Form\FormsTable;
13 use IvyForms\Services\Page\PageIdNormalizer;
14 use IvyForms\Services\Translations\BackendStrings;
15
16 /**
17 * Migration: Add pageId column to fields table and normalise page IDs.
18 *
19 * On every plugin activation this migration:
20 * 1. Adds the `pageId` column to `wp_ivyforms_fields` if it is missing.
21 * 2. Iterates every form and converts the random UUID page IDs stored in
22 * `settings.pages[*].id` and in `fields.pageId` to the deterministic
23 * `page_1`, `page_2`, … format (unique per form via the existing
24 * `formId` FK – no separate unique-key needed).
25 *
26 * Safe to re-run: rows that already carry a normalised ID are detected by
27 * `PageIdNormalizer::isDeterministicPageIdForForm()` and skipped.
28 *
29 * @package IvyForms\Services\InstallActions\Migrations
30 */
31 class AddPageIdToFields
32 {
33 /**
34 * Run the migration.
35 *
36 * @return bool True if successful, false otherwise
37 * @throws InvalidArgumentException
38 */
39 public static function run(): bool
40 {
41 if (!self::addPageIdColumn()) {
42 return false;
43 }
44
45 global $wpdb;
46 $formsTable = FormsTable::getTableName();
47
48 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
49 $forms = $wpdb->get_results("SELECT `id`, `settings` FROM `{$formsTable}`", ARRAY_A);
50
51 if (!is_array($forms)) {
52 return true;
53 }
54
55 return self::processForms($forms);
56 }
57
58 // Private helpers
59
60 /**
61 * Add the pageId column to the fields table if it does not yet exist.
62 */
63 private static function addPageIdColumn(): bool
64 {
65 global $wpdb;
66 $fieldsTable = FieldsTable::getTableName();
67
68 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
69 $columnExists = $wpdb->get_results("SHOW COLUMNS FROM `{$fieldsTable}` LIKE 'pageId'");
70
71 if (!empty($columnExists)) {
72 return true;
73 }
74
75 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
76 $result = $wpdb->query(
77 "ALTER TABLE `{$fieldsTable}`
78 ADD COLUMN `pageId` VARCHAR(255) NOT NULL DEFAULT 'page_1' AFTER `parentId`"
79 );
80
81 if ($result === false) {
82 error_log(sprintf(
83 BackendStrings::getExceptionStrings()['migration_error_add_column'],
84 'pageId',
85 $fieldsTable
86 ));
87 return false;
88 }
89
90 return true;
91 }
92
93 /**
94 * Iterate every form row and run all normalisation steps.
95 *
96 * @param array<int, array<string, mixed>> $forms
97 */
98 private static function processForms(array $forms): bool
99 {
100 foreach ($forms as $formRow) {
101 $formId = isset($formRow['id']) ? (int) $formRow['id'] : 0;
102 if ($formId <= 0) {
103 continue;
104 }
105
106 $settings = json_decode((string) ($formRow['settings'] ?? '{}'), true);
107 if (!is_array($settings)) {
108 $settings = [];
109 }
110
111 $pagesData = self::normalizeFormSettings($settings);
112 self::persistFormSettings($formId, $settings);
113 self::normalizeFormFields($formId, $pagesData);
114 }
115
116 return true;
117 }
118
119 /**
120 * Normalise the pages array and pageNavigationSettings inside $settings.
121 * Returns the pagesData array produced by PageIdNormalizer.
122 *
123 * @param array<string, mixed> $settings Modified in place.
124 * @return array{pages: array<int, array<string, mixed>>, pageIdMap: array<string, string>, firstPageId: string}
125 */
126 private static function normalizeFormSettings(array &$settings): array
127 {
128 $pages = isset($settings['pages']) && is_array($settings['pages']) ? $settings['pages'] : [];
129 $pagesData = PageIdNormalizer::normalizePagesForForm($pages);
130
131 $settings['pages'] = $pagesData['pages'];
132
133 self::normalizePageNavSettings($settings, $pagesData);
134
135 return $pagesData;
136 }
137
138 /**
139 * Re-key pageNavigationSettings using the normalised page IDs.
140 *
141 * @param array<string, mixed> $settings Modified in place.
142 * @param array{pages: array<int, array<string, mixed>>, pageIdMap: array<string, string>,
143 * firstPageId: string} $pagesData
144 */
145 private static function normalizePageNavSettings(array &$settings, array $pagesData): void
146 {
147 if (
148 !isset($settings['formActionButtons']['pageNavigationSettings'])
149 || !is_array($settings['formActionButtons']['pageNavigationSettings'])
150 ) {
151 return;
152 }
153
154 $normalized = [];
155 foreach ($settings['formActionButtons']['pageNavigationSettings'] as $oldId => $pageSettings) {
156 $targetId = self::resolveTargetPageId((string) $oldId, $pagesData);
157 $normalized[$targetId] = is_array($pageSettings) ? $pageSettings : [];
158 }
159
160 $settings['formActionButtons']['pageNavigationSettings'] = $normalized;
161 }
162
163 /**
164 * Map an old/unknown page ID to its normalised deterministic equivalent.
165 *
166 * @param array{pageIdMap: array<string, string>, firstPageId: string} $pagesData
167 */
168 private static function resolveTargetPageId(string $oldId, array $pagesData): string
169 {
170 if (isset($pagesData['pageIdMap'][$oldId])) {
171 return $pagesData['pageIdMap'][$oldId];
172 }
173
174 if (PageIdNormalizer::isDeterministicPageIdForForm($oldId)) {
175 return $oldId;
176 }
177
178 return $pagesData['firstPageId'];
179 }
180
181 /**
182 * Persist the updated settings JSON back to the database.
183 *
184 * @param array<string, mixed> $settings
185 */
186 private static function persistFormSettings(int $formId, array $settings): void
187 {
188 global $wpdb;
189
190 $result = $wpdb->update(
191 FormsTable::getTableName(),
192 ['settings' => wp_json_encode($settings)],
193 ['id' => $formId],
194 ['%s'],
195 ['%d']
196 );
197
198 if ($result === false) {
199 error_log(
200 sprintf(
201 'IvyForms migration error: failed to update settings for form ID %d. DB error: %s',
202 $formId,
203 (string) $wpdb->last_error
204 )
205 );
206 }
207 }
208
209 /**
210 * Normalise every fields.pageId value for a single form.
211 *
212 * @param array{pageIdMap: array<string, string>, firstPageId: string} $pagesData
213 */
214 private static function normalizeFormFields(int $formId, array $pagesData): void
215 {
216 global $wpdb;
217 $fieldsTable = FieldsTable::getTableName();
218
219 $fieldRows = $wpdb->get_results(
220 $wpdb->prepare(
221 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
222 "SELECT `id`, `pageId` FROM `{$fieldsTable}` WHERE `formId` = %d",
223 $formId
224 ),
225 ARRAY_A
226 );
227
228 if (!is_array($fieldRows)) {
229 return;
230 }
231
232 foreach ($fieldRows as $fieldRow) {
233 self::normalizeFieldRow($fieldRow, $fieldsTable, $pagesData);
234 }
235 }
236
237 /**
238 * Update a single field row if its pageId needs to change.
239 *
240 * @param array<string, mixed> $fieldRow
241 * @param array{pageIdMap: array<string, string>, firstPageId: string} $pagesData
242 */
243 private static function normalizeFieldRow(array $fieldRow, string $fieldsTable, array $pagesData): void
244 {
245 global $wpdb;
246
247 $fieldId = isset($fieldRow['id']) ? (int) $fieldRow['id'] : 0;
248 $currentPageId = isset($fieldRow['pageId']) ? (string) $fieldRow['pageId'] : '';
249
250 if ($fieldId <= 0) {
251 return;
252 }
253
254 $targetPageId = self::resolveTargetPageId($currentPageId, $pagesData);
255
256 if ($currentPageId === $targetPageId) {
257 return; // Nothing to update
258 }
259
260 $result = $wpdb->update(
261 $fieldsTable,
262 ['pageId' => $targetPageId],
263 ['id' => $fieldId],
264 ['%s'],
265 ['%d']
266 );
267
268 if ($result === false) {
269 error_log(
270 sprintf(
271 'IvyForms migration error: failed to update pageId for field ID %d in table %s. DB error: %s',
272 $fieldId,
273 $fieldsTable,
274 (string) $wpdb->last_error
275 )
276 );
277 }
278 }
279
280 /**
281 * Rollback the migration (development only).
282 *
283 * @return bool True if successful, false otherwise
284 */
285 public static function rollback(): bool
286 {
287 global $wpdb;
288
289 $fieldsTable = FieldsTable::getTableName();
290
291 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
292 $columnExists = $wpdb->get_results("SHOW COLUMNS FROM `{$fieldsTable}` LIKE 'pageId'");
293
294 if (empty($columnExists)) {
295 return true;
296 }
297
298 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
299 return $wpdb->query("ALTER TABLE `{$fieldsTable}` DROP COLUMN `pageId`") !== false;
300 }
301 }
302