Разработка/ PHP/ Быстрые решения

Laravel: оптимизация загруженных фото в очереди

JPEG или HEIC на 10–30 МБ не стоит конвертировать внутри HTTP-запроса. Контроллер сохраняет оригинал и ставит Job после commit, а воркер уже читает EXIF, исправляет ориентацию и создаёт WebP.

HTTP-запрос: только сохранить

$path = $request->file('image')->store('photos/originals', 'public');

$photo = DB::transaction(fn () => Photo::create([
    'image' => $path,
    'processing_status' => 'pending',
]));

OptimizePhoto::dispatch($photo->id)->afterCommit();

В Job передаётся ID записи, а не UploadedFile и не объект Imagick. Такие объекты нельзя надёжно сериализовать в очередь. afterCommit() снимает гонку, когда воркер стартует раньше появления строки в БД.

Процессор изображения

Пример рассчитан на intervention/image 4.x с драйвером Imagick:

use Illuminate\Support\Facades\Storage;
use Intervention\Image\Drivers\Imagick\Driver;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\ImageManager;

final class ImageProcessor
{
    public function optimize(string $source, string $disk = 'public'): string
    {
        $storage = Storage::disk($disk);
        $image = (new ImageManager(new Driver))
            ->decodePath($storage->path($source));

        $image->orient()->scaleDown(width: 2400, height: 2400);

        $directory = pathinfo($source, PATHINFO_DIRNAME);
        $basename = pathinfo($source, PATHINFO_FILENAME);
        $target = $directory.'/'.$basename.'.webp';

        $storage->put(
            $target,
            (string) $image->encode(
                new WebpEncoder(quality: 80, strip: true),
            ),
        );

        return $target;
    }
}

orient() вызывается до удаления метаданных, иначе вертикальное фото с iPhone останется повёрнутым. scaleDown() не растягивает маленькие файлы.

Job с ограниченным retry

final class OptimizePhoto implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $timeout = 300;
    public array $backoff = [60, 300, 900];

    public function __construct(public int $photoId) {}

    public function handle(ImageProcessor $processor): void
    {
        $photo = Photo::find($this->photoId);

        if (!$photo || !$photo->image) {
            return;
        }

        $target = $processor->optimize($photo->image);

        $photo->updateQuietly([
            'optimized_image' => $target,
            'processing_status' => 'ready',
        ]);
    }
}

Исходник не удаляется до успешной записи WebP и обновления модели. Если Job упадёт между этими действиями, повтор просто перезапишет тот же target — операция остаётся идемпотентной.

Для полного pipeline EXIF читается до encode(... strip: true). Метаданные камеры и даты сохраняются выборочно, GPS — только если он действительно нужен продукту.

Проверка Imagick внутри контейнера

php -r 'var_dump(extension_loaded("imagick"));'
php -r 'print_r(Imagick::queryFormats("WEBP"));'
php -r 'print_r(Imagick::queryFormats("HEIC"));'

Наличие расширения не гарантирует поддержку HEIC: формат зависит от того, с какими delegates собран ImageMagick. Если queryFormats("HEIC") возвращает пустой массив, загрузку HEIC нужно отклонить ещё на валидации.

Что проверить

  • вертикальный JPEG с EXIF Orientation отображается правильно;
  • маленькое изображение не увеличивается, большое укладывается в 2400×2400;
  • повтор одного Job не создаёт новые имена файлов;
  • удаление или замена фото чистит оригинал, WebP и responsive-варианты;
  • воркер перезапускается после deploy с новым кодом.

Настройка самого воркера разобрана в заметке «Очереди Laravel на Redis».

Теги

Читать дальше