PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 2.0.11 All 82 releases
yatra / app / Migrations / BaseMigration.php

BaseMigration.php in Yatra – Travel Booking & Tour Operator Software trunk, at app/Migrations/BaseMigration.php

515 lines 19.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yatra\Migration;
4
5 use Yatra\Utils\Logger;
6 use Yatra\Migration\MigrationProgress;
7
8 /**
9 * BaseMigration provides shared helpers and access to the MigrationService state.
10 */
11 abstract class BaseMigration
12 {
13 protected MigrationProgress $service;
14 protected \wpdb $wpdb;
15
16 public function __construct(MigrationProgress $service)
17 {
18 $this->service = $service;
19 $this->wpdb = $service->getWpdb();
20 }
21
22 protected function isForceMigration(): bool
23 {
24 return $this->service->isForceMigration();
25 }
26
27 protected function updateProgress(
28 string $dataType,
29 string $status,
30 int $migrated,
31 int $skipped,
32 int $failed,
33 int $total,
34 ?string $startedAt,
35 ?string $completedAt
36 ): void {
37 $this->service->updateProgress($dataType, $status, $migrated, $skipped, $failed, $total, $startedAt, $completedAt);
38 }
39
40 protected function generateUniqueSlug(string $baseSlug, string $table): string
41 {
42 return $this->service->generateUniqueSlug($baseSlug, $table);
43 }
44
45 protected function getPostMeta(int $postId): array
46 {
47 return $this->service->getPostMeta($postId);
48 }
49
50 protected function getRawPostMeta(int $postId, string $metaKey): ?string
51 {
52 return $this->service->getRawPostMeta($postId, $metaKey);
53 }
54
55 protected function setRawPostMeta(int $postId, string $metaKey, string $metaValue): void
56 {
57 $this->service->setRawPostMeta($postId, $metaKey, $metaValue);
58 }
59
60 protected function getRawTermMeta(int $termId, string $metaKey): ?string
61 {
62 return $this->service->getRawTermMeta($termId, $metaKey);
63 }
64
65 protected function setRawTermMeta(int $termId, string $metaKey, string $metaValue): void
66 {
67 $this->service->setRawTermMeta($termId, $metaKey, $metaValue);
68 }
69
70 protected function getLegacyMetaValue(array $meta, array $keys, $default = null)
71 {
72 return $this->service->getLegacyMetaValue($meta, $keys, $default);
73 }
74
75 protected function tableExists(string $table): bool
76 {
77 return $this->service->tableExists($table);
78 }
79
80 protected function isTripMigrated(int $oldTripId): bool
81 {
82 return $this->service->isTripMigrated($oldTripId);
83 }
84
85 protected function getMigratedTripId(int $oldTripId): ?int
86 {
87 return $this->service->getMigratedTripId($oldTripId);
88 }
89
90 protected function migrateTripDestinations(int $oldTripId, int $newTripId): void
91 {
92 $this->service->migrateTripDestinations($oldTripId, $newTripId);
93 }
94
95 protected function migrateTripActivities(int $oldTripId, int $newTripId): void
96 {
97 $this->service->migrateTripActivities($oldTripId, $newTripId);
98 }
99
100 protected function deleteTripRelationships(int $tripId): void
101 {
102 $this->service->deleteTripRelationships($tripId);
103 }
104
105 protected function truncateTableIfExists(string $table): void
106 {
107 $this->service->truncateTableIfExists($table);
108 }
109
110 /**
111 * Fix existing draft records to publish status
112 * This ensures all destinations/activities are published
113 */
114 protected function fixDraftRecordsToPublish(string $table): void
115 {
116 $fullTable = $this->wpdb->prefix . $table;
117
118 // Update all draft records to publish
119 $updated = $this->wpdb->query(
120 "UPDATE {$fullTable} SET status = 'publish' WHERE status = 'draft'"
121 );
122
123 if ($updated > 0) {
124 Logger::info("Fixed {$updated} draft records to publish status in {$table}", [
125 'source' => 'migration',
126 'table' => $table,
127 'updated_count' => $updated
128 ]);
129 }
130 }
131
132 /**
133 * Shared taxonomy migration handler.
134 *
135 * Migrates old WordPress taxonomy terms into the unified ClassificationsTable
136 * (wp_yatra_classifications) using the `type` column to differentiate.
137 *
138 * @param string|array<int, string> $taxonomy One taxonomy slug, or several (e.g. `trip_category` + `tour_category`) merged into one classification type.
139 * @param string $classificationType The classification type value for the new table (e.g. 'destination', 'activity', 'attribute')
140 * @param string $dataType Progress tracking key (e.g. 'destinations', 'activities', 'attributes')
141 */
142 protected function migrateTaxonomy($taxonomy, string $classificationType, string $dataType): array
143 {
144 $migrated = 0;
145 $skipped = 0;
146 $failed = 0;
147
148 $classificationsTable = \Yatra\Database\Tables\ClassificationsTable::getTableName();
149
150 $taxes = is_array($taxonomy) ? array_values(array_filter($taxonomy, static fn ($t) => is_string($t) && $t !== '')) : [$taxonomy];
151 if ($taxes === []) {
152 return compact('migrated', 'skipped', 'failed');
153 }
154
155 $placeholders = implode(',', array_fill(0, count($taxes), '%s'));
156 $sql = "SELECT t.*, tt.description, tt.parent, tt.taxonomy AS source_taxonomy
157 FROM {$this->wpdb->terms} t
158 INNER JOIN {$this->wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
159 WHERE tt.taxonomy IN ({$placeholders})";
160
161 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- placeholders match $taxes count
162 $terms = $this->wpdb->get_results($this->wpdb->prepare($sql, ...$taxes));
163
164 $total = count($terms);
165
166 foreach ($terms as $term) {
167 $termTax = isset($term->source_taxonomy) && is_string($term->source_taxonomy) && $term->source_taxonomy !== ''
168 ? $term->source_taxonomy
169 : (string) $taxes[0];
170 $metaKey = sprintf('_yatra_migrated_%s_id', $termTax);
171
172 try {
173 // Prepare slug
174 $baseSlug = $term->slug;
175 if (empty($baseSlug)) {
176 $baseSlug = function_exists('sanitize_title')
177 ? sanitize_title($term->name ?: uniqid($termTax . '-'))
178 : preg_replace('/[^a-z0-9\-]+/i', '-', strtolower($term->name ?: uniqid($termTax . '-')));
179 }
180
181 $slug = $baseSlug;
182 $existingId = null;
183
184 if ($this->isForceMigration()) {
185 // Force migration: generate unique slug
186 $counter = 1;
187 $uniqueSlug = $slug;
188 while ($this->wpdb->get_var($this->wpdb->prepare(
189 "SELECT id FROM {$classificationsTable} WHERE type = %s AND slug = %s",
190 $classificationType,
191 $uniqueSlug
192 ))) {
193 $uniqueSlug = $slug . '-' . $counter++;
194 }
195 $slug = $uniqueSlug;
196 } else {
197 // Regular migration: Check if already exists by type+slug
198 $existingId = $this->wpdb->get_var($this->wpdb->prepare(
199 "SELECT id FROM {$classificationsTable} WHERE type = %s AND slug = %s",
200 $classificationType,
201 $baseSlug
202 ));
203
204 if (!$existingId) {
205 // Generate unique slug for new insert
206 $counter = 1;
207 $uniqueSlug = $slug;
208 while ($this->wpdb->get_var($this->wpdb->prepare(
209 "SELECT id FROM {$classificationsTable} WHERE type = %s AND slug = %s",
210 $classificationType,
211 $uniqueSlug
212 ))) {
213 $uniqueSlug = $slug . '-' . $counter++;
214 }
215 $slug = $uniqueSlug;
216 }
217 }
218
219 // Featured image: old sites used different term meta keys (yatra_term_image, thumbnail_id, …).
220 $termImage = $this->resolveLegacyTermFeaturedImageId((int) $term->term_id);
221 $metadata = [];
222
223 // When updating an existing row, merge with current JSON so we do not wipe featured_image / other keys if only part of the resolver runs.
224 if ($existingId && !$this->isForceMigration()) {
225 $existingJson = $this->wpdb->get_var($this->wpdb->prepare(
226 "SELECT metadata FROM {$classificationsTable} WHERE id = %d",
227 (int) $existingId
228 ));
229 if (is_string($existingJson) && $existingJson !== '') {
230 $decoded = json_decode($existingJson, true);
231 if (is_array($decoded)) {
232 $metadata = $decoded;
233 }
234 }
235 }
236
237 if ($termImage !== null && $termImage !== '') {
238 $metadata['featured_image'] = $termImage;
239 }
240
241 $data = [
242 'type' => $classificationType,
243 'name' => $term->name,
244 'slug' => $slug,
245 'description' => $term->description ?? '',
246 'parent_id' => !empty($term->parent) ? $this->getParentClassificationId((int) $term->parent, $termTax, $classificationType) : null,
247 'metadata' => $metadata !== [] ? wp_json_encode($metadata) : null,
248 'status' => 'publish',
249 'updated_at' => current_time('mysql'),
250 ];
251
252 // 3.x admin + single templates read `icon` (serialized {type: image, value: attachment_id}), not only metadata.featured_image.
253 if ($termImage !== null && $termImage !== '' && (int) $termImage > 0) {
254 $data['icon'] = maybe_serialize([
255 'type' => 'image',
256 'value' => (int) $termImage,
257 ]);
258 }
259
260 if ($existingId && !$this->isForceMigration()) {
261 // Regular migration: Update existing record
262 $updated = $this->wpdb->update(
263 $classificationsTable,
264 $data,
265 ['id' => $existingId]
266 );
267
268 if ($updated !== false) {
269 $newId = $existingId;
270 $this->setRawTermMeta((int) $term->term_id, $metaKey, (string) $newId);
271 $migrated++;
272 $this->updateProgress($dataType, 'running', $migrated, $skipped, $failed, $total, null, null);
273 usleep(50000);
274 } else {
275 $failed++;
276 Logger::error("Failed to update {$termTax}: {$this->wpdb->last_error}", [
277 'source' => 'migration',
278 'taxonomy' => $termTax,
279 'term_slug' => $term->slug
280 ]);
281 $this->updateProgress($dataType, 'running', $migrated, $skipped, $failed, $total, null, null);
282 }
283 } else {
284 // Force migration OR new record: Insert new
285 $data['created_at'] = current_time('mysql');
286 $inserted = $this->wpdb->insert(
287 $classificationsTable,
288 $data
289 );
290
291 if ($inserted) {
292 $newId = (int) $this->wpdb->insert_id;
293 $this->setRawTermMeta((int) $term->term_id, $metaKey, (string) $newId);
294 $migrated++;
295 $this->updateProgress($dataType, 'running', $migrated, $skipped, $failed, $total, null, null);
296 usleep(50000);
297 } else {
298 $failed++;
299 Logger::error("Failed to insert {$termTax}: {$this->wpdb->last_error}", [
300 'source' => 'migration',
301 'taxonomy' => $termTax,
302 'term_slug' => $term->slug
303 ]);
304 $this->updateProgress($dataType, 'running', $migrated, $skipped, $failed, $total, null, null);
305 }
306 }
307 } catch (\Throwable $e) {
308 $failed++;
309 Logger::error("Exception migrating {$termTax}: " . $e->getMessage(), [
310 'source' => 'migration',
311 'taxonomy' => $termTax,
312 'term_slug' => $term->slug ?? 'unknown'
313 ]);
314 $this->updateProgress($dataType, 'running', $migrated, $skipped, $failed, $total, null, null);
315 }
316 }
317
318 return compact('migrated', 'skipped', 'failed');
319 }
320
321 /**
322 * Legacy taxonomy terms stored the featured image attachment ID under different meta keys (or as a media URL).
323 *
324 * Yatra 2.x core uses destination_image_id and activity_image_id on term meta
325 * (old class-yatra-taxonomy-destination.php / class-yatra-taxonomy-activity.php).
326 */
327 protected function resolveLegacyTermFeaturedImageId(int $termId): ?string
328 {
329 $keys = [
330 'destination_image_id',
331 'activity_image_id',
332 'yatra_term_image',
333 'thumbnail_id',
334 '_thumbnail_id',
335 'yatra_destination_image',
336 'yatra_activity_image',
337 'yatra_category_image',
338 'yatra_trip_category_image',
339 'yatra_tour_category_image',
340 'trip_category_image',
341 'tour_category_image',
342 'category_thumbnail_id',
343 'term_thumbnail',
344 'featured_image',
345 'featured_image_id',
346 'image',
347 'term_image',
348 'photo',
349 'cover_image',
350 'banner_image',
351 ];
352
353 foreach ($keys as $key) {
354 // Prefer latest meta row if duplicates exist (admin re-saves).
355 $fetched = $this->getRawTermMetaLatest($termId, $key);
356 if ($fetched === null || $fetched === '' || $fetched === '0') {
357 continue;
358 }
359 $trimmed = trim($fetched);
360 if ($trimmed === '') {
361 continue;
362 }
363 $resolved = $this->coerceLegacyTermImageToAttachmentId($trimmed);
364 if ($resolved !== null) {
365 return $resolved;
366 }
367 }
368
369 return $this->scanAllTermMetaForFeaturedImage($termId);
370 }
371
372 /**
373 * Last-write-wins term meta (same key can exist more than once in edge-case DBs).
374 */
375 private function getRawTermMetaLatest(int $termId, string $metaKey): ?string
376 {
377 return $this->wpdb->get_var(
378 $this->wpdb->prepare(
379 "SELECT meta_value FROM {$this->wpdb->termmeta} WHERE term_id = %d AND meta_key = %s ORDER BY meta_id DESC LIMIT 1",
380 $termId,
381 $metaKey
382 )
383 );
384 }
385
386 /**
387 * Fallback: inspect every term meta row for attachment IDs / URLs when known keys miss (imports, Pro, custom).
388 */
389 private function scanAllTermMetaForFeaturedImage(int $termId): ?string
390 {
391 $rows = $this->wpdb->get_results(
392 $this->wpdb->prepare(
393 "SELECT meta_key, meta_value FROM {$this->wpdb->termmeta} WHERE term_id = %d ORDER BY meta_id DESC",
394 $termId
395 )
396 );
397 if (empty($rows)) {
398 return null;
399 }
400
401 foreach ($rows as $row) {
402 $key = (string) ($row->meta_key ?? '');
403 if ($key === '' || str_starts_with($key, '_yatra_migrated_')) {
404 continue;
405 }
406 if (! preg_match('/(image|thumb|photo|cover|banner|featured|attachment|media|picture)/i', $key)) {
407 continue;
408 }
409 $raw = (string) ($row->meta_value ?? '');
410 $resolved = $this->coerceLegacyTermImageToAttachmentId(trim($raw));
411 if ($resolved !== null) {
412 return $resolved;
413 }
414 }
415
416 return null;
417 }
418
419 /**
420 * Normalize a single term meta value to an attachment ID string (3.x uses metadata.featured_image as ID).
421 */
422 private function coerceLegacyTermImageToAttachmentId(string $raw): ?string
423 {
424 if ($raw === '' || $raw === '0') {
425 return null;
426 }
427
428 if (is_numeric($raw)) {
429 $id = (int) $raw;
430
431 return $id > 0 ? (string) $id : null;
432 }
433
434 if (function_exists('is_serialized') && is_serialized($raw)) {
435 $un = maybe_unserialize($raw);
436 if (is_numeric($un)) {
437 $id = (int) $un;
438
439 return $id > 0 ? (string) $id : null;
440 }
441 if (is_array($un)) {
442 foreach (['value', 'id', 'attachment_id', 'image_id', 'thumbnail_id', 'attachment', 'image'] as $k) {
443 if (isset($un[$k]) && is_numeric($un[$k])) {
444 $id = (int) $un[$k];
445
446 return $id > 0 ? (string) $id : null;
447 }
448 }
449 foreach ($un as $v) {
450 if (is_numeric($v)) {
451 $id = (int) $v;
452
453 return $id > 0 ? (string) $id : null;
454 }
455 }
456 }
457 }
458
459 $json = json_decode($raw, true);
460 if (is_array($json)) {
461 foreach (['id', 'value', 'attachment_id', 'image_id'] as $k) {
462 if (isset($json[$k]) && is_numeric($json[$k])) {
463 $id = (int) $json[$k];
464
465 return $id > 0 ? (string) $id : null;
466 }
467 }
468 }
469
470 if (function_exists('attachment_url_to_postid')) {
471 if (filter_var($raw, FILTER_VALIDATE_URL)) {
472 $aid = attachment_url_to_postid($raw);
473 if ($aid > 0) {
474 return (string) $aid;
475 }
476 } elseif (strlen($raw) > 0 && $raw[0] === '/' && function_exists('home_url')) {
477 $aid = attachment_url_to_postid(home_url($raw));
478 if ($aid > 0) {
479 return (string) $aid;
480 }
481 }
482 }
483
484 return null;
485 }
486
487 /**
488 * Look up the new classifications table ID for a parent term
489 */
490 private function getParentClassificationId(int $parentTermId, string $taxonomy, string $classificationType): ?int
491 {
492 $metaKey = sprintf('_yatra_migrated_%s_id', $taxonomy);
493 $parentNewId = $this->getRawTermMeta($parentTermId, $metaKey);
494 if ($parentNewId) {
495 return (int) $parentNewId;
496 }
497 // Parent may belong to a sibling taxonomy merged into the same classification type (e.g. tour_category vs trip_category).
498 $alternates = [];
499 if ($taxonomy === 'trip_category') {
500 $alternates[] = 'tour_category';
501 } elseif ($taxonomy === 'tour_category') {
502 $alternates[] = 'trip_category';
503 }
504 foreach ($alternates as $alt) {
505 $altKey = sprintf('_yatra_migrated_%s_id', $alt);
506 $pid = $this->getRawTermMeta($parentTermId, $altKey);
507 if ($pid) {
508 return (int) $pid;
509 }
510 }
511
512 return null;
513 }
514 }
515