feat: Optimize image handling for OnTicket tenant

- Added migration to optimize OnTicket images using OptimizeOnticketImagesSeeder.
- Updated OnTicketTenantSeeder to reference optimized image formats (AVIF).
- Modified OnticketImmersiveHeroCarouselSeeder to use AVIF images for hero carousel.
- Created OptimizeOnticketImagesSeeder to replace existing attachments with optimized images.
- Adjusted TestEventsSeeder to utilize AVIF images for events.
- Updated tests to verify the correct image formats and ensure optimized images are stored.
- Added new image files in AVIF format for logos, backgrounds, and event images.
This commit is contained in:
2026-09-22 10:01:05 -03:00
parent a4a8a3ef8f
commit c90a393081
22 changed files with 213 additions and 20 deletions

View File

@@ -0,0 +1,23 @@
<?php
use Database\Seeders\OptimizeOnticketImagesSeeder;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Storage;
return new class extends Migration
{
public function up(): void
{
if (app()->environment('testing')) {
Storage::fake('s3');
}
app(OptimizeOnticketImagesSeeder::class)->run();
}
public function down(): void
{
// The optimized attachments may already be in active use. Keep this
// data migration irreversible instead of restoring heavier assets.
}
};

View File

@@ -14,6 +14,8 @@ use RuntimeException;
class OnTicketTenantSeeder extends Seeder
{
private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado';
public function __construct(
private readonly TenantProvisioningService $tenantProvisioningService,
private readonly WebsiteExtraService $websiteExtraService,
@@ -49,7 +51,7 @@ class OnTicketTenantSeeder extends Seeder
'header_logo' => $this->uploadedImage('onticket_logo.png'),
'footer_logo' => $this->uploadedImage('onticket_footer_logo.png'),
'favicon' => $this->uploadedImage('onticket_favicon.svg'),
'header_bg_image' => $this->uploadedImage('onticket_header_background.png'),
'header_bg_image' => $this->uploadedImage('onticket_header_background.avif'),
]);
} else {
$tenant->update([
@@ -64,7 +66,7 @@ class OnTicketTenantSeeder extends Seeder
'header_logo' => ['relation' => 'headerLogo', 'filename' => 'onticket_logo.png'],
'footer_logo' => ['relation' => 'footerLogo', 'filename' => 'onticket_footer_logo.png'],
'favicon' => ['relation' => 'favicon', 'filename' => 'onticket_favicon.svg'],
'header_bg_image' => ['relation' => 'headerBackgroundImage', 'filename' => 'onticket_header_background.png'],
'header_bg_image' => ['relation' => 'headerBackgroundImage', 'filename' => 'onticket_header_background.avif'],
] as $field => $image) {
$attachment = $tenant->{$image['relation']};
@@ -98,7 +100,7 @@ class OnTicketTenantSeeder extends Seeder
private function uploadedImage(string $filename): UploadedFile
{
$path = public_path("images/tennants/onticket/{$filename}");
$path = public_path(self::IMAGE_DIRECTORY."/{$filename}");
if (! is_file($path)) {
throw new RuntimeException("Image not found at path: {$path}");
@@ -106,6 +108,7 @@ class OnTicketTenantSeeder extends Seeder
$mimeType = match (strtolower(pathinfo($filename, PATHINFO_EXTENSION))) {
'svg' => 'image/svg+xml',
'avif' => 'image/avif',
default => 'image/png',
};

View File

@@ -10,13 +10,15 @@ use RuntimeException;
class OnticketImmersiveHeroCarouselSeeder extends Seeder
{
private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel';
private const IMAGES = [
'onticket_hero_carousel_1.png',
'onticket_hero_carousel_2.png',
'onticket_hero_carousel_1.avif',
'onticket_hero_carousel_2.avif',
'onticket_hero_carousel_3.avif',
'onticket_hero_carousel_4.jfif',
'onticket_hero_carousel_5.jfif',
'onticket_hero_carousel_6.jfif',
'onticket_hero_carousel_4.avif',
'onticket_hero_carousel_5.avif',
'onticket_hero_carousel_6.avif',
'onticket_hero_carousel_7.avif',
'onticket_hero_carousel_8.avif',
];
@@ -39,7 +41,7 @@ class OnticketImmersiveHeroCarouselSeeder extends Seeder
}
$images = array_map(function (string $filename): UploadedFile {
$path = public_path("images/tennants/onticket/onticket_tennant_hero_carousel/{$filename}");
$path = public_path(self::IMAGE_DIRECTORY."/{$filename}");
if (! is_file($path)) {
throw new RuntimeException("Image not found at path: {$path}");

View File

@@ -0,0 +1,155 @@
<?php
namespace Database\Seeders;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Shared\Attachable\Models\Attachment;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use RuntimeException;
use Throwable;
class OptimizeOnticketImagesSeeder extends Seeder
{
private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado';
private const CAROUSEL_IMAGES = [
'onticket_hero_carousel_1.avif',
'onticket_hero_carousel_2.avif',
'onticket_hero_carousel_3.avif',
'onticket_hero_carousel_4.avif',
'onticket_hero_carousel_5.avif',
'onticket_hero_carousel_6.avif',
'onticket_hero_carousel_7.avif',
'onticket_hero_carousel_8.avif',
];
private const EVENT_IMAGES = [
'[Prueba] Noche de Música en Vivo' => 'live-music.avif',
'[Prueba] Festival de Sabores' => 'food-festival.avif',
'[Prueba] Stand Up en el Centro' => 'stand-up.avif',
];
public function run(): void
{
$tenant = Tenant::query()->where('codigo', 'onticket')->first();
if ($tenant === null) {
return;
}
$this->replaceAttachment(
$tenant->headerBackgroundImage,
'onticket_header_background.avif',
);
$carousel = $tenant->websiteExtras()
->whereHas(
'websiteTypeExtra',
fn ($query) => $query->where('codigo', 'immersiveHeroCarousel')
)
->first();
$carouselIds = is_array($carousel?->config) ? array_values($carousel->config) : [];
foreach (self::CAROUSEL_IMAGES as $index => $filename) {
$attachmentId = $carouselIds[$index] ?? null;
if (is_int($attachmentId)) {
$this->replaceAttachment(
Attachment::query()->find($attachmentId),
'onticket_tennant_hero_carousel/'.$filename,
);
}
}
foreach (self::EVENT_IMAGES as $title => $filename) {
$event = $tenant->events()->where('title', $title)->first();
$this->replaceAttachment(
$event?->attachment,
'test-events/'.$filename,
);
}
}
private function replaceAttachment(?Attachment $attachment, string $relativePath): void
{
if ($attachment === null) {
return;
}
$sourcePath = public_path(self::IMAGE_DIRECTORY.'/'.$relativePath);
if (! is_file($sourcePath)) {
throw new RuntimeException("Image not found at path: {$sourcePath}");
}
$contents = file_get_contents($sourcePath);
if (! is_string($contents) || $contents === '') {
throw new RuntimeException("Could not read image at path: {$sourcePath}");
}
$filename = basename($relativePath);
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$mimeType = match ($extension) {
'avif' => 'image/avif',
'svg' => 'image/svg+xml',
default => 'image/png',
};
$directory = trim(str_replace('\\', '/', dirname($attachment->path)), './');
$storedFilename = $attachment->key.($extension === '' ? '' : '.'.$extension);
$newPath = $directory === '' ? $storedFilename : $directory.'/'.$storedFilename;
$disk = Storage::disk('s3');
if (
$attachment->path === $newPath
&& $attachment->filename === $filename
&& $attachment->mime_type === $mimeType
&& $attachment->size === strlen($contents)
&& $disk->exists($newPath)
) {
return;
}
$oldPath = $attachment->path;
$oldContents = $disk->exists($oldPath) ? $disk->get($oldPath) : null;
if (! $disk->put($newPath, $contents)) {
throw new RuntimeException("Could not store optimized image at path: {$newPath}");
}
try {
DB::transaction(function () use (
$attachment,
$extension,
$filename,
$mimeType,
$newPath,
$contents,
): void {
$attachment->update([
'path' => $newPath,
'filename' => $filename,
'type' => 'image',
'mime_type' => $mimeType,
'extension' => $extension,
'size' => strlen($contents),
]);
});
} catch (Throwable $throwable) {
if ($newPath === $oldPath && is_string($oldContents)) {
$disk->put($oldPath, $oldContents);
} else {
$disk->delete($newPath);
}
throw $throwable;
}
if ($oldPath !== $newPath && $disk->exists($oldPath)) {
$disk->delete($oldPath);
}
}
}

View File

@@ -11,6 +11,8 @@ use RuntimeException;
class TestEventsSeeder extends Seeder
{
private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado/test-events';
public function __construct(private readonly AttachmentService $attachmentService) {}
public function run(): void
@@ -24,7 +26,7 @@ class TestEventsSeeder extends Seeder
'subtitle' => 'Una noche para cantar y bailar',
'description' => 'Evento de prueba para explorar la cartelera de OnTicket.',
'location' => 'Teatro Broadway, Rosario',
'image' => 'live-music.png',
'image' => 'live-music.avif',
'weeks_from_now' => [2],
'time_start' => '21:00:00',
'time_end' => '23:30:00',
@@ -35,7 +37,7 @@ class TestEventsSeeder extends Seeder
'subtitle' => 'Gastronomía y música para toda la familia',
'description' => 'Evento de prueba con dos jornadas disponibles.',
'location' => 'Parque de España, Rosario',
'image' => 'food-festival.png',
'image' => 'food-festival.avif',
'weeks_from_now' => [3, 3],
'day_offsets' => [0, 1],
'time_start' => '12:00:00',
@@ -47,7 +49,7 @@ class TestEventsSeeder extends Seeder
'subtitle' => 'Humor para compartir',
'description' => 'Evento de prueba para visualizar distintas propuestas.',
'location' => 'Centro Cultural La Comedia, Rosario',
'image' => 'stand-up.png',
'image' => 'stand-up.avif',
'weeks_from_now' => [4],
'time_start' => '20:30:00',
'time_end' => '22:00:00',
@@ -74,14 +76,14 @@ class TestEventsSeeder extends Seeder
}
if ($event->attachment_id === null) {
$path = public_path('images/tennants/onticket/test-events/'.$definition['image']);
$path = public_path(self::IMAGE_DIRECTORY.'/'.$definition['image']);
if (! is_file($path)) {
throw new RuntimeException("Image not found at path: {$path}");
}
$attachment = $this->attachmentService->store(
new UploadedFile($path, $definition['image'], 'image/png', null, true),
new UploadedFile($path, $definition['image'], 'image/avif', null, true),
'tenants/onticket/events',
);
$event->update(['attachment_id' => $attachment->id]);