🚀 Hostinger Optimized
🖥️ Server: LiteSpeed
💻 System: Linux s20058.bom1.stableserver.net 5.14.0-611.55.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue May 19 15:19:29 EDT 2026 x86_64
👤 User: skhata (1324)
🐘 PHP: 8.4.20
🚫 Disabled: ✨ NONE

💻 Terminal

📁 /home/skhata/chandrasons.com/public
$

📄 BackfillProductMediaCommand.php

📁 Path: /home/skhata/new.chandrapharma.com/app/Console/Commands/BackfillProductMediaCommand.php
📊 Size: 7.75 KB
🔒 Perm: 0600
📝 MIME: text/x-php
<?php

namespace App\Console\Commands;

use App\Modules\Catalog\Models\Product;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;

/**
 * One-time backfill: migrates legacy product_images rows (created by
 * products:download-images — plain files under storage/app/public, only
 * ever referenced via the product_images table, no Spatie Media row) into
 * real Spatie MediaLibrary entries on the featured_image/gallery
 * collections.
 *
 * Why this is needed: Filament's SpatieMediaLibraryFileUpload component
 * (used for native, in-dropzone image previews on the edit-product form)
 * only ever reads from Spatie Media. Products created/edited through the
 * app already get real Spatie Media rows automatically — but the ~1400
 * products bulk-imported earlier only have product_images rows pointing
 * at plain files, so without this backfill they'd show an empty dropzone
 * on edit even though the images clearly exist.
 *
 * This command is additive and non-destructive:
 *   - It does NOT delete or move the original files (addMediaFromDisk()
 *     is called with preservingOriginal(), so Spatie copies the file into
 *     its own managed media directory rather than adopting the original).
 *   - It does NOT touch product_images rows except to fill in media_id
 *     once a matching Spatie Media record exists, per the column's
 *     original documented purpose (see the create_product_images_table
 *     migration).
 *   - Products that already have Spatie media in featured_image/gallery
 *     are skipped by default (assumed already migrated / created via the
 *     app directly) — pass --force to re-scan them anyway.
 *
 * Usage:
 *   php artisan products:backfill-media --dry-run   # report only, writes nothing
 *   php artisan products:backfill-media              # apply for real
 *   php artisan products:backfill-media --force       # also re-scan products that already have Spatie media
 */
class BackfillProductMediaCommand extends Command
{
    protected $signature = 'products:backfill-media
        {--dry-run : Report what would happen without writing any data}
        {--force : Also re-scan products that already have Spatie media in featured_image/gallery}';

    protected $description = 'Backfill legacy product_images records into Spatie Media Library so all products get native FilePond previews';

    public function handle(): int
    {
        // This command processes ~1400 products (with image conversions —
        // thumb/medium/large — generated synchronously per image via GD)
        // in one long-running process. Two things Laravel/PHP normally rely
        // on short-lived requests for can silently pile up over that many
        // iterations and blow past the default 128M memory_limit:
        //   1. Eloquent's query log, which is NOT capped by default and
        //      records every query for the life of the process.
        //   2. Cyclic references (e.g. Eloquent models holding their own
        //      loaded relations) that only PHP's cycle-collecting GC frees,
        //      not simple refcounting — and it doesn't run on every free().
        // Fixing both here means this command is memory-safe regardless of
        // how it's invoked (no need to remember a -d memory_limit= flag).
        DB::connection()->disableQueryLog();
        ini_set('memory_limit', '1024M');

        $dryRun = (bool) $this->option('dry-run');
        $force  = (bool) $this->option('force');

        $total = Product::query()->has('images')->count();
        $this->info(($dryRun ? '[DRY RUN] ' : '') . "Scanning {$total} product(s) with legacy images...");

        $bar = $this->output->createProgressBar($total);
        $bar->start();

        $stats = [
            'products scanned'                    => 0,
            'products skipped (already has media)' => 0,
            'products migrated'                    => 0,
            'thumbnails migrated'                  => 0,
            'gallery images migrated'              => 0,
            'rows skipped (already had media_id)'  => 0,
            'files missing on disk (skipped)'      => 0,
        ];

        Product::query()
            ->has('images')
            ->with(['images' => fn ($q) => $q->orderByDesc('is_primary')->orderBy('sort_order')])
            ->chunkById(50, function ($products) use (&$stats, $dryRun, $force, $bar) {
                foreach ($products as $product) {
                    $stats['products scanned']++;
                    $bar->advance();

                    $hasSpatieMedia = $product->getMedia('featured_image')->isNotEmpty()
                        || $product->getMedia('gallery')->isNotEmpty();

                    if ($hasSpatieMedia && ! $force) {
                        $stats['products skipped (already has media)']++;
                        continue;
                    }

                    $migratedAny = false;

                    foreach ($product->images as $image) {
                        if ($image->media_id) {
                            $stats['rows skipped (already had media_id)']++;
                            continue;
                        }

                        $disk = $image->disk ?: 'public';
                        $relativePath = $this->relativePathFromUrl($image->url);

                        if (! $relativePath || ! Storage::disk($disk)->exists($relativePath)) {
                            $stats['files missing on disk (skipped)']++;
                            continue;
                        }

                        $collection = $image->is_primary ? 'featured_image' : 'gallery';

                        if ($dryRun) {
                            $migratedAny = true;
                            $image->is_primary ? $stats['thumbnails migrated']++ : $stats['gallery images migrated']++;
                            continue;
                        }

                        $media = $product
                            ->addMediaFromDisk($relativePath, $disk)
                            ->preservingOriginal()
                            ->usingName($image->alt_text ?: $product->getTranslation('name', 'en'))
                            ->toMediaCollection($collection);

                        $image->update(['media_id' => $media->id]);

                        $migratedAny = true;
                        $image->is_primary ? $stats['thumbnails migrated']++ : $stats['gallery images migrated']++;
                    }

                    if ($migratedAny) {
                        $stats['products migrated']++;
                    }
                }

                // Release this chunk's models/relations (and anything Spatie's
                // FileAdder/Intervention Image left behind processing them)
                // before the next chunk loads — see the memory note in handle().
                unset($products);
                gc_collect_cycles();
            });

        $bar->finish();
        $this->newLine(2);

        foreach ($stats as $label => $value) {
            $this->line(ucfirst($label) . ': ' . $value);
        }

        if ($dryRun) {
            $this->comment('Dry run only — no data was written. Re-run without --dry-run to apply.');
        } else {
            $this->info('Done.');
        }

        return self::SUCCESS;
    }

    /**
     * Convert a stored public URL (e.g. "http://host/storage/products/seeded/x.jpg"
     * or "/storage/products/seeded/x.jpg") back into a disk-relative path
     * (e.g. "products/seeded/x.jpg") that Storage::disk('public') understands.
     */
    protected function relativePathFromUrl(?string $url): ?string
    {
        if (! $url) {
            return null;
        }

        $path = parse_url($url, PHP_URL_PATH) ?: $url;
        $path = preg_replace('#^/?storage/#', '', $path);

        return ltrim($path, '/');
    }
}
← Back📥 Raw✏️ Edit🔒 Chmod
✨ File Manager Magic ✨