category table} '; protected $description = 'Import artworks from legacy DB (wallz) into new artworks table'; private function coerceUnsignedInt(mixed $value, int $default = 0): int { if ($value === null) { return $default; } if (is_bool($value)) { return $value ? 1 : 0; } if (is_int($value)) { return max(0, $value); } if (is_float($value)) { return max(0, (int) $value); } if (is_string($value)) { $trimmed = trim($value); if ($trimmed === '') { return $default; } if (is_numeric($trimmed)) { return max(0, (int) $trimmed); } } return $default; } private function coerceString(mixed $value, string $default = ''): string { if ($value === null) { return $default; } $stringValue = trim((string) $value); return $stringValue !== '' ? $stringValue : $default; } public function handle(): int { $chunk = (int) $this->option('chunk'); $limit = $this->option('limit') ? (int) $this->option('limit') : null; $dryRun = (bool) $this->option('dry-run'); $legacyConn = $this->option('legacy-connection'); $legacyTable = $this->option('legacy-table'); $connectedTable = $this->option('connected-table'); $this->info("Starting import from {$legacyConn}.{$legacyTable} (chunk={$chunk})"); $query = DB::connection($legacyConn)->table($legacyTable)->orderBy('id'); $processed = 0; $query->chunkById($chunk, function ($rows) use (&$processed, $limit, $dryRun, $legacyConn, $connectedTable) { foreach ($rows as $row) { if ($limit !== null && $processed >= $limit) { return false; // stop chunking } $legacyId = $row->id ?? null; $title = $row->name ?? $row->title ?? ($row->headline ?? ('legacy-' . ($legacyId ?? Str::random(6)))); $description = $row->description ?? $row->desc ?? null; $slugBase = Str::slug(substr((string) $title, 0, 120)); // Use cleaned title slug directly. If no title, fallback to artwork-. $slug = $slugBase ? $slugBase : 'artwork-' . ($legacyId ?? Str::random(8)); $publishedAt = null; if (! empty($row->datum)) { $publishedAt = date('Y-m-d H:i:s', strtotime($row->datum)); } elseif (! empty($row->created_at)) { $publishedAt = $row->created_at; } // File mapping — try common legacy fields. Normalize and ensure file_path is not null. $rawFileName = $row->pic ?? $row->picture ?? $row->file ?? $row->fname ?? null; $fileName = null; $filePath = ''; if (! empty($rawFileName) && trim((string) $rawFileName) !== '') { $fileName = trim((string) $rawFileName); // store legacy path under legacy/ folder, but do not move files here — admin can handle file migration $filePath = 'legacy/uploads/' . ltrim($fileName, '/'); } // derive mime type if missing (use extension mapping), fallback to application/octet-stream $mime = $row->mimetype ?? $row->mime ?? null; if (empty($mime) && $fileName) { $ext = strtolower(pathinfo((string) $fileName, PATHINFO_EXTENSION)); $map = [ 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'bmp' => 'image/bmp', 'webp' => 'image/webp', 'svg' => 'image/svg+xml', 'ico' => 'image/x-icon', 'zip' => 'application/zip', 'pdf' => 'application/pdf', ]; $mime = $map[$ext] ?? null; } if (empty($mime)) { $mime = 'application/octet-stream'; } $data = [ 'id' => $row->id ?? null, // NOTE: artworks.user_id is NOT NULL (no FK constraint, but column cannot be null) 'user_id' => $row->user_id ?? 1, 'title' => (string) $title, 'slug' => (string) $slug, 'description' => $description, 'file_name' => $fileName, // ensure non-null file_path to satisfy NOT NULL DB constraints 'file_path' => $filePath ?? '', // legacy DB sometimes has no filesize; default to 0 to satisfy NOT NULL 'file_size' => isset($row->filesize) && $row->filesize !== null ? (int) $row->filesize : (isset($row->size) && $row->size !== null ? (int) $row->size : 0), 'mime_type' => $mime, 'width' => $row->width ?? null, 'height' => $row->height ?? null, 'is_public' => isset($row->visible) ? (bool) $row->visible : true, 'is_approved' => isset($row->approved) ? (bool) $row->approved : true, 'published_at' => $publishedAt, ]; // Coerce required NOT NULL columns to safe defaults (legacy data can be messy) $data['user_id'] = $this->coerceUnsignedInt($data['user_id'], 1); $data['file_name'] = $this->coerceString($data['file_name'], 'legacy-' . ($legacyId ?? Str::random(8))); $data['file_path'] = $this->coerceString($data['file_path'], 'legacy/uploads/' . $data['file_name']); $data['mime_type'] = $this->coerceString($data['mime_type'], 'application/octet-stream'); $data['file_size'] = $this->coerceUnsignedInt($data['file_size'], 0); $data['width'] = $this->coerceUnsignedInt($data['width'], 0); $data['height'] = $this->coerceUnsignedInt($data['height'], 0); $this->line('Importing legacy id=' . ($legacyId ?? 'unknown') . ' title=' . $data['title']); if ($dryRun) { $processed++; continue; } try { $art = null; DB::connection()->transaction(function () use (&$art, $data, $legacyId, $legacyConn, $connectedTable) { // create artwork (guard against unique slug collisions) $baseSlug = $data['slug']; $attempt = 0; $slug = $baseSlug; while (Artwork::where('slug', $slug)->exists()) { $attempt++; $slug = $baseSlug . '-' . $attempt; } $data['slug'] = $slug; // Preserve legacy primary ID if available and safe to do so. if (! empty($legacyId) && is_numeric($legacyId) && (int) $legacyId > 0) { $preserveId = (int) $legacyId; if (Artwork::where('id', $preserveId)->exists()) { // Avoid overwriting an existing artwork with the same id. throw new \RuntimeException("Artwork with id {$preserveId} already exists; skipping import for this legacy id."); } $data['id'] = $preserveId; } // If we need to preserve the legacy primary id, perform a raw insert // so auto-increment doesn't assign a different id. Otherwise use Eloquent. if (! empty($data['id'])) { $insert = $data; $ts = date('Y-m-d H:i:s'); if (! array_key_exists('created_at', $insert)) { $insert['created_at'] = $ts; } if (! array_key_exists('updated_at', $insert)) { $insert['updated_at'] = $ts; } DB::table('artworks')->insert($insert); $art = Artwork::find($insert['id']); } else { $art = Artwork::create($data); } // attach categories if connected table exists if (DB::connection($legacyConn)->getSchemaBuilder()->hasTable($connectedTable)) { // attempt to find category ids from connected table; common column names: wallz_id, art_id, connected_id $rows = DB::connection($legacyConn)->table($connectedTable) ->where(function ($q) use ($legacyId) { $q->where('wallz_id', $legacyId) ->orWhere('art_id', $legacyId) ->orWhere('item_id', $legacyId); })->get(); $categoryIds = []; foreach ($rows as $r) { $cid = $r->category_id ?? $r->cat_id ?? $r->category ?? null; if ($cid) { // try to find matching Category in new DB by id or slug if (is_numeric($cid) && \App\Models\Category::where('id', $cid)->exists()) { $categoryIds[] = (int) $cid; } else { // maybe legacy stores slug $cat = \App\Models\Category::where('slug', $cid)->first(); if ($cat) { $categoryIds[] = $cat->id; } } } } if (! empty($categoryIds)) { $art->categories()->syncWithoutDetaching(array_values(array_unique($categoryIds))); } } }); // Post-insert verification: if we attempted to preserve the legacy id, // confirm the row exists with that id. Log mapping if preservation failed. if (! empty($legacyId) && is_numeric($legacyId) && (int) $legacyId > 0) { $preserveId = (int) $legacyId; $exists = Artwork::where('id', $preserveId)->exists(); if (! $exists) { // If $art was created but with a different id, log mapping for manual reconciliation if ($art instanceof Artwork) { Log::warning('Imported legacy artwork but failed to preserve id', [ 'legacy_id' => $preserveId, 'created_id' => $art->id, 'slug' => $art->slug ?? null, ]); } else { Log::warning('Legacy artwork not found after import', ['legacy_id' => $preserveId]); } } } $processed++; } catch (Throwable $e) { $this->error('Failed to import legacy id=' . ($legacyId ?? 'unknown') . ': ' . $e->getMessage()); Log::error('ImportLegacyArtworks error', [ 'legacy_id' => $legacyId, 'error' => $e->getMessage(), 'data' => $data ?? null, 'trace' => $e->getTraceAsString(), ]); } } return null; }, 'id'); $this->info('Import complete. Processed: ' . $processed); return 0; } }