| 1 |
<?php |
| 2 |
|
| 3 |
namespace Yatra\Migration; |
| 4 |
|
| 5 |
use Yatra\Constants\ClassificationTypes; |
| 6 |
use Yatra\Database\Tables\ClassificationsTable; |
| 7 |
use Yatra\Database\Tables\TripClassificationsTable; |
| 8 |
use Yatra\Utils\Logger; |
| 9 |
use Yatra\Migration\MigrationProgress; |
| 10 |
|
| 11 |
/** |
| 12 |
* Attribute Migration - Migrates trip attributes from old Yatra system |
| 13 |
* |
| 14 |
* Old System: Attributes stored as WordPress taxonomy 'attributes' terms, |
| 15 |
* and per-trip values in post meta 'tour_meta_custom_attributes' (serialized array). |
| 16 |
* New System: Unified ClassificationsTable (type='attribute') + TripClassificationsTable |
| 17 |
* |
| 18 |
* All queries use raw SQL because old taxonomies/CPTs are NOT registered in the new plugin. |
| 19 |
*/ |
| 20 |
class AttributeMigration extends BaseMigration |
| 21 |
{ |
| 22 |
public function __construct(MigrationProgress $service) |
| 23 |
{ |
| 24 |
parent::__construct($service); |
| 25 |
} |
| 26 |
|
| 27 |
public function run(): array |
| 28 |
{ |
| 29 |
$migrated = 0; |
| 30 |
$skipped = 0; |
| 31 |
$failed = 0; |
| 32 |
|
| 33 |
Logger::info("Starting attribute migration.", ['source' => 'migration']); |
| 34 |
|
| 35 |
// Step 1: Migrate attribute definitions (taxonomy terms → ClassificationsTable) |
| 36 |
$defResult = $this->migrateAttributeDefinitions(); |
| 37 |
$migrated += $defResult['migrated']; |
| 38 |
$skipped += $defResult['skipped']; |
| 39 |
$failed += $defResult['failed']; |
| 40 |
|
| 41 |
// Step 2: Migrate trip-attribute relationships (post meta → TripClassificationsTable) |
| 42 |
$relResult = $this->migrateTripAttributeRelationships(); |
| 43 |
$migrated += $relResult['migrated']; |
| 44 |
$skipped += $relResult['skipped']; |
| 45 |
$failed += $relResult['failed']; |
| 46 |
|
| 47 |
Logger::info("Attribute migration completed", [ |
| 48 |
'source' => 'migration', |
| 49 |
'migrated' => $migrated, |
| 50 |
'skipped' => $skipped, |
| 51 |
'failed' => $failed |
| 52 |
]); |
| 53 |
|
| 54 |
return compact('migrated', 'skipped', 'failed'); |
| 55 |
} |
| 56 |
|
| 57 |
/** |
| 58 |
* Migrate attribute definitions from old taxonomy 'attributes' into ClassificationsTable (type=attribute). |
| 59 |
*/ |
| 60 |
private function migrateAttributeDefinitions(): array |
| 61 |
{ |
| 62 |
$migrated = 0; |
| 63 |
$skipped = 0; |
| 64 |
$failed = 0; |
| 65 |
|
| 66 |
$classificationsTable = ClassificationsTable::getTableName(); |
| 67 |
$metaKey = '_yatra_migrated_attributes_id'; |
| 68 |
|
| 69 |
// Raw SQL: old 'attributes' taxonomy terms |
| 70 |
$terms = $this->wpdb->get_results( |
| 71 |
"SELECT t.term_id, t.name, t.slug, tt.description |
| 72 |
FROM {$this->wpdb->terms} t |
| 73 |
INNER JOIN {$this->wpdb->term_taxonomy} tt ON t.term_id = tt.term_id |
| 74 |
WHERE tt.taxonomy = 'attributes'" |
| 75 |
); |
| 76 |
|
| 77 |
if (empty($terms)) { |
| 78 |
Logger::info("No 'attributes' taxonomy terms found", ['source' => 'migration']); |
| 79 |
return compact('migrated', 'skipped', 'failed'); |
| 80 |
} |
| 81 |
|
| 82 |
$total = count($terms); |
| 83 |
Logger::info("Found {$total} attribute terms", ['source' => 'migration', 'count' => $total]); |
| 84 |
$this->updateProgress('attributes', 'running', 0, 0, 0, $total, null, null); |
| 85 |
|
| 86 |
foreach ($terms as $term) { |
| 87 |
try { |
| 88 |
$termId = (int) $term->term_id; |
| 89 |
|
| 90 |
// Check if already migrated via raw term meta |
| 91 |
$existingMappedId = $this->getRawTermMeta($termId, $metaKey); |
| 92 |
if (!$this->isForceMigration() && $existingMappedId) { |
| 93 |
$skipped++; |
| 94 |
$this->updateProgress('attributes', 'running', $migrated, $skipped, $failed, $total, null, null); |
| 95 |
continue; |
| 96 |
} |
| 97 |
|
| 98 |
$baseSlug = !empty($term->slug) ? $term->slug : sanitize_title($term->name ?: uniqid('attr-')); |
| 99 |
|
| 100 |
// Check if already exists in ClassificationsTable by type+slug |
| 101 |
$existingId = $this->wpdb->get_var($this->wpdb->prepare( |
| 102 |
"SELECT id FROM {$classificationsTable} WHERE type = %s AND slug = %s", |
| 103 |
ClassificationTypes::ATTRIBUTE, |
| 104 |
$baseSlug |
| 105 |
)); |
| 106 |
|
| 107 |
$slug = $baseSlug; |
| 108 |
|
| 109 |
if ($existingId && !$this->isForceMigration()) { |
| 110 |
// Skip if already exists and not force migration |
| 111 |
$skipped++; |
| 112 |
$this->updateProgress('attributes', 'running', $migrated, $skipped, $failed, $total, null, null); |
| 113 |
continue; |
| 114 |
} |
| 115 |
|
| 116 |
if (!$existingId) { |
| 117 |
// Generate unique slug for new insert |
| 118 |
$counter = 1; |
| 119 |
$uniqueSlug = $slug; |
| 120 |
while ($this->wpdb->get_var($this->wpdb->prepare( |
| 121 |
"SELECT id FROM {$classificationsTable} WHERE type = %s AND slug = %s", |
| 122 |
ClassificationTypes::ATTRIBUTE, |
| 123 |
$uniqueSlug |
| 124 |
))) { |
| 125 |
$uniqueSlug = $slug . '-' . $counter++; |
| 126 |
} |
| 127 |
$slug = $uniqueSlug; |
| 128 |
} |
| 129 |
|
| 130 |
// Get icon from raw term meta |
| 131 |
$icon = $this->getRawTermMeta($termId, 'icon'); |
| 132 |
|
| 133 |
// Build metadata JSON |
| 134 |
$metadata = []; |
| 135 |
$fieldType = $this->getRawTermMeta($termId, 'attribute_field_type'); |
| 136 |
if ($fieldType) { |
| 137 |
$metadata['field_type'] = $fieldType; |
| 138 |
} |
| 139 |
$fieldOptions = $this->getRawTermMeta($termId, 'field_options'); |
| 140 |
if ($fieldOptions) { |
| 141 |
$metadata['field_options'] = maybe_unserialize($fieldOptions); |
| 142 |
} |
| 143 |
|
| 144 |
$data = [ |
| 145 |
'type' => ClassificationTypes::ATTRIBUTE, |
| 146 |
'name' => $term->name, |
| 147 |
'slug' => $slug, |
| 148 |
'description' => $term->description ?? '', |
| 149 |
'icon' => $icon ?: null, |
| 150 |
'metadata' => !empty($metadata) ? json_encode($metadata) : null, |
| 151 |
'status' => 'publish', |
| 152 |
'updated_at' => current_time('mysql'), |
| 153 |
]; |
| 154 |
|
| 155 |
if ($existingId && $this->isForceMigration()) { |
| 156 |
// Update existing during force migration |
| 157 |
$this->wpdb->update($classificationsTable, $data, ['id' => $existingId]); |
| 158 |
$newId = (int) $existingId; |
| 159 |
} else { |
| 160 |
// Insert new |
| 161 |
$data['created_at'] = current_time('mysql'); |
| 162 |
$inserted = $this->wpdb->insert($classificationsTable, $data); |
| 163 |
if (!$inserted) { |
| 164 |
$errorMsg = $this->wpdb->last_error; |
| 165 |
// Check if it's a duplicate key error and handle gracefully |
| 166 |
if (strpos($errorMsg, 'Duplicate entry') !== false) { |
| 167 |
$skipped++; |
| 168 |
$this->updateProgress('attributes', 'running', $migrated, $skipped, $failed, $total, null, null); |
| 169 |
continue; |
| 170 |
} |
| 171 |
$failed++; |
| 172 |
Logger::error("Failed to insert attribute '{$term->name}': {$errorMsg}", ['source' => 'migration']); |
| 173 |
$this->updateProgress('attributes', 'running', $migrated, $skipped, $failed, $total, null, null); |
| 174 |
continue; |
| 175 |
} |
| 176 |
$newId = (int) $this->wpdb->insert_id; |
| 177 |
} |
| 178 |
|
| 179 |
// Store mapping in raw term meta |
| 180 |
$this->setRawTermMeta($termId, $metaKey, (string) $newId); |
| 181 |
$migrated++; |
| 182 |
$this->updateProgress('attributes', 'running', $migrated, $skipped, $failed, $total, null, null); |
| 183 |
|
| 184 |
} catch (\Exception $e) { |
| 185 |
$failed++; |
| 186 |
Logger::error("Exception migrating attribute: {$term->name} — {$e->getMessage()}", ['source' => 'migration']); |
| 187 |
$this->updateProgress('attributes', 'running', $migrated, $skipped, $failed, $total, null, null); |
| 188 |
} |
| 189 |
} |
| 190 |
|
| 191 |
return compact('migrated', 'skipped', 'failed'); |
| 192 |
} |
| 193 |
|
| 194 |
/** |
| 195 |
* Migrate trip-attribute relationships. |
| 196 |
* |
| 197 |
* Old system: post meta 'tour_meta_custom_attributes' = serialized array keyed by term_id. |
| 198 |
* New system: TripClassificationsTable rows with classification_type='attribute' and metadata containing value. |
| 199 |
*/ |
| 200 |
private function migrateTripAttributeRelationships(): array |
| 201 |
{ |
| 202 |
$migrated = 0; |
| 203 |
$skipped = 0; |
| 204 |
$failed = 0; |
| 205 |
|
| 206 |
$tripClassificationsTable = TripClassificationsTable::getTableName(); |
| 207 |
$metaKey = '_yatra_migrated_attributes_id'; |
| 208 |
|
| 209 |
// Raw SQL: get all tour posts with custom attributes meta |
| 210 |
$tripsWithAttributes = $this->wpdb->get_results( |
| 211 |
"SELECT post_id, meta_value |
| 212 |
FROM {$this->wpdb->postmeta} |
| 213 |
WHERE meta_key = 'tour_meta_custom_attributes' |
| 214 |
AND meta_value IS NOT NULL |
| 215 |
AND meta_value != ''" |
| 216 |
); |
| 217 |
|
| 218 |
if (empty($tripsWithAttributes)) { |
| 219 |
Logger::info("No trips with custom attributes found", ['source' => 'migration']); |
| 220 |
return compact('migrated', 'skipped', 'failed'); |
| 221 |
} |
| 222 |
|
| 223 |
$total = count($tripsWithAttributes); |
| 224 |
Logger::info("Found {$total} trips with custom attributes", ['source' => 'migration']); |
| 225 |
|
| 226 |
foreach ($tripsWithAttributes as $tripMeta) { |
| 227 |
try { |
| 228 |
$oldTripId = (int) $tripMeta->post_id; |
| 229 |
$newTripId = $this->getMigratedTripId($oldTripId); |
| 230 |
|
| 231 |
if (!$newTripId) { |
| 232 |
$skipped++; |
| 233 |
continue; |
| 234 |
} |
| 235 |
|
| 236 |
$attributes = maybe_unserialize($tripMeta->meta_value); |
| 237 |
if (!is_array($attributes)) { |
| 238 |
$skipped++; |
| 239 |
continue; |
| 240 |
} |
| 241 |
|
| 242 |
foreach ($attributes as $oldTermId => $attributeData) { |
| 243 |
// Look up the new classification ID via raw term meta |
| 244 |
$newAttrId = $this->getRawTermMeta((int) $oldTermId, $metaKey); |
| 245 |
if (!$newAttrId) { |
| 246 |
continue; |
| 247 |
} |
| 248 |
|
| 249 |
$value = $attributeData['content'] ?? $attributeData['value'] ?? ''; |
| 250 |
if (empty($value)) { |
| 251 |
continue; |
| 252 |
} |
| 253 |
|
| 254 |
// Check if relationship already exists |
| 255 |
$exists = $this->wpdb->get_var($this->wpdb->prepare( |
| 256 |
"SELECT id FROM {$tripClassificationsTable} |
| 257 |
WHERE trip_id = %d AND classification_id = %d AND classification_type = %s", |
| 258 |
$newTripId, |
| 259 |
(int) $newAttrId, |
| 260 |
ClassificationTypes::ATTRIBUTE |
| 261 |
)); |
| 262 |
|
| 263 |
$relMetadata = json_encode(['value' => $value]); |
| 264 |
|
| 265 |
if ($exists) { |
| 266 |
if ($this->isForceMigration()) { |
| 267 |
$this->wpdb->update( |
| 268 |
$tripClassificationsTable, |
| 269 |
['metadata' => $relMetadata, 'updated_at' => current_time('mysql')], |
| 270 |
['id' => $exists] |
| 271 |
); |
| 272 |
$migrated++; |
| 273 |
} |
| 274 |
} else { |
| 275 |
$this->wpdb->insert($tripClassificationsTable, [ |
| 276 |
'trip_id' => $newTripId, |
| 277 |
'classification_id' => (int) $newAttrId, |
| 278 |
'classification_type' => ClassificationTypes::ATTRIBUTE, |
| 279 |
'relationship_type' => 'primary', |
| 280 |
'metadata' => $relMetadata, |
| 281 |
'sort_order' => 0, |
| 282 |
'is_active' => 1, |
| 283 |
'created_at' => current_time('mysql'), |
| 284 |
'updated_at' => current_time('mysql'), |
| 285 |
]); |
| 286 |
$migrated++; |
| 287 |
} |
| 288 |
} |
| 289 |
|
| 290 |
} catch (\Exception $e) { |
| 291 |
$failed++; |
| 292 |
Logger::error("Exception migrating attributes for trip {$tripMeta->post_id}: {$e->getMessage()}", ['source' => 'migration']); |
| 293 |
} |
| 294 |
} |
| 295 |
|
| 296 |
return compact('migrated', 'skipped', 'failed'); |
| 297 |
} |
| 298 |
} |
| 299 |
|