Skip to main content

Kép variáns/formátum rendszer

Kontextus

A képfeltöltés és ImageEditor komponens már kész (Intervention Image v3, CropperJS, HEIC/WebP/AVIF konverzió). Most egy variáns-rendszert építünk:

  • Tenant-szinten konfigurálható milyen formátumokban (JPG, WebP, PNG, AVIF) és méretekben tároljuk a képeket
  • Kép "típusok" (kontextusok): blog OG, page OG, content inline, product (később)
  • Feltöltéskor async queue job-ok generálják a variánsokat
  • Formátum be/kikapcsolásakor bulk generálás/törlés (500GB+ méretű képállomány kezelése)
  • Nyomon követjük mely variánsok léteznek egy-egy képhez

1. Migrációk

1.1 Új oszlopok a media táblán

Fájl: tenant/tenant-backend-crm/database/migrations/2026_03_18_000001_add_image_type_columns_to_media_table.php

media:
+ image_type_slug string nullable, INDEX
+ entity_id uuid nullable, INDEX (blog post / page / product UUID)

1.2 image_types tábla

Fájl: tenant/tenant-backend-crm/database/migrations/2026_03_18_000002_create_image_types_table.php

image_types:
id uuid PK
tenant_id uuid INDEX
slug string (pl. "blog_og", "page_og", "product_main")
name string (emberi olvasható)
default_width integer nullable
default_height integer nullable
timestamps
UNIQUE(tenant_id, slug)

1.3 image_type_variants tábla

Fájl: tenant/tenant-backend-crm/database/migrations/2026_03_18_000003_create_image_type_variants_table.php

image_type_variants:
id uuid PK
tenant_id uuid INDEX
image_type_id uuid FK → image_types.id CASCADE
format string (jpg, webp, png, avif)
width integer
height integer
quality integer default 85
is_enabled boolean default true
timestamps
UNIQUE(image_type_id, format, width, height)

1.4 media_variants tábla

Fájl: tenant/tenant-backend-crm/database/migrations/2026_03_18_000004_create_media_variants_table.php

media_variants:
id uuid PK
media_id uuid FK → media.id CASCADE, INDEX
image_type_variant_id uuid FK → image_type_variants.id SET NULL, INDEX
format string (denormalizált)
width integer
height integer
path string
disk string default 'media'
file_size bigint
status string default 'pending' (pending, processing, completed, failed)
error_message text nullable
timestamps
INDEX(media_id, format, width, height)

2. Modellek

2.1 ImageType model

Új fájl: tenant/tenant-backend-crm/app/Models/ImageType.php

  • HasUuids, $fillable = ['tenant_id', 'slug', 'name', 'default_width', 'default_height']
  • Relációk: variants(): HasMany(ImageTypeVariant)
  • Scope: scopeForTenant($query, $tenantId)

2.2 ImageTypeVariant model

Új fájl: tenant/tenant-backend-crm/app/Models/ImageTypeVariant.php

  • HasUuids, $fillable = ['tenant_id', 'image_type_id', 'format', 'width', 'height', 'quality', 'is_enabled']
  • Relációk: imageType(): BelongsTo(ImageType), mediaVariants(): HasMany(MediaVariant)
  • Scope: scopeEnabled($query)where('is_enabled', true)

2.3 MediaVariant model

Új fájl: tenant/tenant-backend-crm/app/Models/MediaVariant.php

  • HasUuids, $fillable = ['media_id', 'image_type_variant_id', 'format', 'width', 'height', 'path', 'disk', 'file_size', 'status', 'error_message']
  • Relációk: media(): BelongsTo(Media), imageTypeVariant(): BelongsTo(ImageTypeVariant)
  • Accessor: urlStorage::disk($this->disk)->url($this->path)

2.4 Media model módosítás

Fájl: tenant/tenant-backend-crm/app/Models/Media.php

  • $fillable += 'image_type_slug', 'entity_id'
  • Új reláció: variants(): HasMany(MediaVariant)
  • Új metódus: getStorageBasePath(): string — visszaadja az entity-alapú útvonalat (lásd 10. szekció)

3. ImageStorageService

Új fájl: tenant/tenant-backend-crm/app/Services/ImageStorageService.php

A fájl tárolási útvonalak kezelése — shard logika, path generálás.

class ImageStorageService
{
const SHARD_LIMIT = 5000; // max entity mappák egy shard-ban

// Meghatározza az aktuális shard-ot (YYYY_MM_N)
public function resolveShardPath(string $imageTypeSlug): string

// Teljes path generálás: images/{type}/{shard}/{entity_id}/
public function generateBasePath(string $imageTypeSlug, string $entityId): string

// Variáns fájl path: {basePath}/{width}x{height}.{format}
public function variantPath(string $basePath, int $width, int $height, string $format): string

// Eredeti fájl path: {basePath}/original.{ext}
public function originalPath(string $basePath, string $extension): string
}

Shard meghatározás: images/{type}/ alatt listázza a YYYY_MM_* mappákat, az utolsónál megnézi az almappák számát. Ha >= SHARD_LIMIT → shard++.


4. Queue Job-ok

Queue: 'image-processing' (külön a notifikációktól/emailektől) Minta: SendNotificationJob (Dispatchable, InteractsWithQueue, Queueable, SerializesModels)

3.1 GenerateMediaVariantJob

Új fájl: tenant/tenant-backend-crm/app/Jobs/GenerateMediaVariantJob.php

Egyetlen variáns generálása egyetlen médiához. Atomi egység.

  • Constructor: (string $mediaId, string $imageTypeVariantId)
  • $tries = 3, $backoff = [30, 60, 120], queue: 'image-processing'
  • Logika:
    1. Media + ImageTypeVariant betöltés
    2. MediaVariant rekord létrehozás/frissítés → status: processing
    3. Eredeti fájl betöltés Storage-ból → Intervention Image resize + encode
    4. Mentés: images/{type}/YYYY_MM_{shard}/{entity_uuid}/{width}x{height}.{format} (lásd 10. szekció)
    5. Status → completed, file_size mentés
    6. Hiba esetén → failed + error_message

Encode formátum szerint:

  • jpg → $image->toJpeg($quality)
  • webp → $image->toWebp($quality)
  • avif → $image->toAvif($quality)
  • png → $image->toPng()

3.2 GenerateVariantsForMediaJob

Új fájl: tenant/tenant-backend-crm/app/Jobs/GenerateVariantsForMediaJob.php

Feltöltés után hívódik — megkeresi az adott image type összes enabled variáns konfigját és dispatch-el egy-egy GenerateMediaVariantJob-ot.

  • Constructor: (string $mediaId)
  • Könnyű job, default queue-n futhat

3.3 BulkGenerateVariantsJob

Új fájl: tenant/tenant-backend-crm/app/Jobs/BulkGenerateVariantsJob.php

Új formátum bekapcsolásakor — MINDEN létező képre generálja az adott variánst.

  • Constructor: (string $imageTypeVariantId)
  • chunkById(100) + Bus::batch() + allowFailures()
  • Timeout: 0 (no timeout, let chunks handle it)

3.4 BulkDeleteVariantsJob

Új fájl: tenant/tenant-backend-crm/app/Jobs/BulkDeleteVariantsJob.php

Formátum kikapcsolásakor — törli a generált fájlokat és DB rekordokat.

  • Constructor: (string $imageTypeVariantId)
  • chunkById(200): fájl törlés disk-ről + DB rekord törlés

5. Resource-ök

4.1 ImageTypeResource

Új fájl: tenant/tenant-backend-crm/app/Http/Resources/ImageTypeResource.php

  • Mezők: id, slug, name, default_width, default_height, variants (whenLoaded), timestamps

4.2 ImageTypeVariantResource

Új fájl: tenant/tenant-backend-crm/app/Http/Resources/ImageTypeVariantResource.php

  • Mezők: id, format, width, height, quality, is_enabled, timestamps

4.3 MediaVariantResource

Új fájl: tenant/tenant-backend-crm/app/Http/Resources/MediaVariantResource.php

  • Mezők: id, format, width, height, url, file_size, status

4.4 MediaResource módosítás

Fájl: tenant/tenant-backend-crm/app/Http/Resources/MediaResource.php

  • Új mezők:
    • 'image_type_slug' => $this->image_type_slug
    • 'variants' => MediaVariantResource::collection($this->whenLoaded('variants'))
    • 'variant_urls' — completed variánsok format → size → url mapping

6. Controller-ek

5.1 ImageTypeController

Új fájl: tenant/tenant-backend-crm/app/Http/Controllers/ImageTypeController.php

MetódusURILeírás
GET/image-typesTenant image type-ok listája variánsokkal
POST/image-typesÚj image type létrehozása
PUT/image-types/{id}Image type módosítása
DELETE/image-types/{id}Image type törlése (cascade)

5.2 ImageTypeVariantController

Új fájl: tenant/tenant-backend-crm/app/Http/Controllers/ImageTypeVariantController.php

MetódusURILeírás
POST/image-types/{id}/variantsVariáns konfig hozzáadás + opcionális bulk generálás
PUT/image-types/{id}/variants/{vid}Módosítás (enable/disable → bulk gen/delete)
DELETE/image-types/{id}/variants/{vid}Törlés + bulk fájl törlés
POST/image-types/{id}/variants/{vid}/regenerateÚjragenerálás trigger

Kulcs viselkedés:

  • store() + is_enabled=trueBulkGenerateVariantsJob dispatch
  • update() enable toggle → BulkGenerateVariantsJob vagy BulkDeleteVariantsJob
  • destroy()BulkDeleteVariantsJob majd konfig törlés

5.3 MediaController módosítás

Fájl: tenant/tenant-backend-crm/app/Http/Controllers/MediaController.php

  • store(): elfogad opcionális image_type_slug + entity_id, ImageStorageService path generálás, mentés után GenerateVariantsForMediaJob dispatch
  • storeOgImage(): image_type_slug + entity_id paraméterként, ImageStorageService path generálás, GenerateVariantsForMediaJob dispatch
  • destroy(): variáns fájlok törlése (entity mappa törlés) az eredeti törlés előtt

7. Route-ok

Fájl: tenant/tenant-backend-crm/routes/api.php

// Image Types (az auth.oauth + locale middleware group-ban)
Route::prefix('image-types')->group(function () {
Route::get('/', [ImageTypeController::class, 'index'])->middleware('can:media.list');
Route::post('/', [ImageTypeController::class, 'store'])->middleware('can:media.manage');
Route::get('/{id}', [ImageTypeController::class, 'show'])->middleware('can:media.list');
Route::put('/{id}', [ImageTypeController::class, 'update'])->middleware('can:media.manage');
Route::delete('/{id}', [ImageTypeController::class, 'destroy'])->middleware('can:media.manage');

Route::post('/{id}/variants', [ImageTypeVariantController::class, 'store'])->middleware('can:media.manage');
Route::put('/{id}/variants/{vid}', [ImageTypeVariantController::class, 'update'])->middleware('can:media.manage');
Route::delete('/{id}/variants/{vid}', [ImageTypeVariantController::class, 'destroy'])->middleware('can:media.manage');
Route::post('/{id}/variants/{vid}/regenerate', [ImageTypeVariantController::class, 'regenerate'])->middleware('can:media.manage');
});

A media.manage permission a tenant.*.crm.* wildcard-on keresztül működik (Owner, Admin).


8. Seeder

Fájl: tenant/tenant-backend-crm/database/seeders/Development/CrmDataSeeder.php (vagy külön ImageTypeSeeder)

Alapértelmezett image type-ok:

slugnamewidthheight
blog_ogBlog OG kép1200630
page_ogOldal OG kép1200630
content_inlineTartalom kép800null
product_mainTermék főkép10001000
product_thumbTermék bélyegkép300300

Alapértelmezett variáns konfigok típusonként:

  • WebP azonos méretben, quality 85 (enabled)
  • JPG azonos méretben, quality 85 (enabled)
  • AVIF nem default (tenant opt-in, mert lassú az encode)

9. Frontend

9.1 Új store: src/modules/settings/stores/imageType.ts

  • Interface-ek: ImageType, ImageTypeVariant
  • State: items, current, loading, error
  • Metódusok: fetchAll(), create(), update(), destroy()
  • Variáns metódusok: addVariant(), updateVariant(), removeVariant(), regenerateVariant()
  • CRM client használata (/api/crm/image-types/...)

9.2 Új beállítások oldal: src/modules/settings/pages/image-formats/index.vue

Teljes tenant-szintű beállítások oldal ahol a felhasználó konfigurálhatja a kép típusokat és variánsokat.

Layout:

VCard
├── VCardTitle: ikon + "Kép formátumok" cím
├── VDivider
├── VCardText
│ └── Típusonként egy-egy VCard (expansion panel stílusú):
│ ├── Header: típus neve + slug + default méret + VChip (variánsok száma)
│ ├── VDivider
│ └── Expanded tartalom:
│ ├── VDataTable: variáns konfigok listája
│ │ ├── Oszlopok: Formátum (chip) | Méret (WxH) | Minőség | Státusz (toggle) | Műveletek
│ │ ├── Soronként:
│ │ │ ├── Formátum: színes VChip (jpg=kék, webp=zöld, avif=narancs, png=lila)
│ │ │ ├── Méret: "1200 × 630 px"
│ │ │ ├── Minőség: "85%"
│ │ │ ├── VSwitch: enabled/disabled (toggle → megerősítő dialog)
│ │ │ └── Gombok: Újragenerálás (tabler-refresh) | Törlés (tabler-trash)
│ │ └── Üres állapot: "Nincs variáns konfiguráció"
│ └── VBtn: "Variáns hozzáadása" (tabler-plus ikon)
└── VCardActions (ha van "Típus hozzáadása" lehetőség)

Dialógusok:

1. Variáns hozzáadása dialog:

VDialog (max-width 500)
├── VSelect: Formátum (JPG, WebP, PNG, AVIF)
├── VTextField: Szélesség (number)
├── VTextField: Magasság (number)
├── VSlider: Minőség (10-100, step 5, default 85)
├── VCheckbox: "Generálás meglévő képekre is" (default: true)
└── VCardActions: Mégse | Hozzáadás

2. Enable/Disable megerősítő dialog:

VDialog (max-width 400)
├── Szöveg: "Ez \{count\} meglévő képre generál/töröl variánsokat. Ez a háttérben történik."
└── VCardActions: Mégse | Folytatás

3. Típus hozzáadása dialog (opcionális, admin):

VDialog (max-width 500)
├── VTextField: Név (pl. "Termék bélyegkép")
├── VTextField: Slug (auto-generált, szerkeszthető)
├── VTextField: Alapértelmezett szélesség
├── VTextField: Alapértelmezett magasság
└── VCardActions: Mégse | Létrehozás

9.3 Navigáció frissítés

Fájl: tenant/tenant-frontend/src/navigation/vertical/index.ts

  • Új menüpont a nav.settings heading alá: nav.imageFormats → route: 'image-formats', icon: tabler-photo-cog

Fájl: tenant/tenant-frontend/src/navigation/horizontal/index.ts

  • Ugyanaz a menüpont

9.4 ImageEditor.vue módosítás

Fájl: tenant/tenant-frontend/src/components/ImageEditor.vue

  • Új prop-ok: imageTypeSlug?: string, entityId?: string
  • Upload FormData-hoz: formData.append('image_type_slug', ...) + formData.append('entity_id', ...)

9.5 Blog/Page create+edit oldalak frissítése

  • <ImageEditor> kap image-type-slug + entity-id prop-okat
  • Create oldalon az entity_id a mentés után érhető el — OG kép mentés a post/page létrehozása után (jelenlegi flow kompatibilis)

10. i18n

Fájlok: hu.json / en.jsonsettings.imageTypes alá:

KulcsHUEN
titleKép formátumokImage Formats
addTypeTípus hozzáadásaAdd Type
addVariantVariáns hozzáadásaAdd Variant
formatFormátumFormat
widthSzélességWidth
heightMagasságHeight
qualityMinőségQuality
enabledBekapcsolvaEnabled
regenerateÚjragenerálásRegenerate
confirmEnableEz {count} meglévő képre generál variánsokat. Folytatod?This will generate variants for {count} existing images. Continue?
confirmDisableEz törli a variánsokat {count} képről. Folytatod?This will delete variants from {count} images. Continue?
formats.jpgJPGJPG
formats.webpWebPWebP
formats.pngPNGPNG
formats.avifAVIFAVIF

11. Fájl tárolási struktúra

A fájlokat úgy szervezzük, hogy egy mappa soha ne érje el a 10.000 fájlt. Sharding: YYYY_MM_1 → ha eléri a limitet → YYYY_MM_2.

images/
blog/
2026_03_1/ ← dátum + shard szám
{blog_post_uuid}/ ← entitás UUID
original.jpg ← eredeti feltöltött kép
1200x630.webp ← variáns fájlok
1200x630.avif
600x315.webp
{blog_post_uuid}/
original.jpg
1200x630.webp
2026_03_2/ ← automatikus shard ha 2026_03_1 megtelik
...
page/
2026_03_1/
{page_uuid}/
original.jpg
1200x630.webp
product/
2026_04_1/
{product_uuid}/
original.jpg
1000x1000.webp
300x300.webp

Shard logika (ImageStorageService helper):

  • Mappa path: images/{image_type_slug}/{YYYY}_{MM}_{shard}/{entity_id}/
  • Shard meghatározás: az aktuális YYYY_MM shard-ok közül az utolsó — ha az utolsó shard-ban már >= 5000 almappa (entity) van, shard++ (új shard mappa)
  • A shard szám a media rekord path mezőjében tárolódik, nem kell külön tracking
  • Meglévő képek (régi struktúra) maradnak a helyükön — migráció nem szükséges, a variánsok az új struktúrába kerülnek

Implementációs sorrend

FázisMitFájlok
1Migrációk4 migráció
2ModellekImageType, ImageTypeVariant, MediaVariant (új) + Media (módosítás)
3ImageStorageServiceShard logika, path generálás
4Resource-ökImageTypeResource, ImageTypeVariantResource, MediaVariantResource (új) + MediaResource (módosítás)
5Queue Job-okGenerateMediaVariantJob, GenerateVariantsForMediaJob, BulkGenerateVariantsJob, BulkDeleteVariantsJob
6Controller-ekImageTypeController, ImageTypeVariantController (új) + MediaController (módosítás)
7Route-okimage-types csoport + variáns route-ok
8SeederAlapértelmezett image type-ok + variáns konfigok
9Frontend storeimageType.ts
10Frontend oldalimage-formats/index.vue
11ImageEditor + blog/page frissítésimageTypeSlug prop
12i18nhu.json + en.json

Kritikus fájlok

Backend

  • app/Models/Media.php — image_type_slug, entity_id + variants reláció
  • app/Models/ImageType.php — új model
  • app/Models/ImageTypeVariant.php — új model
  • app/Models/MediaVariant.php — új model
  • app/Services/ImageStorageService.php — shard logika, path generálás
  • app/Jobs/GenerateMediaVariantJob.php — atomi variáns generálás (Intervention Image)
  • app/Jobs/BulkGenerateVariantsJob.php — bulk generálás chunkById + Bus::batch
  • app/Http/Controllers/ImageTypeController.php — CRUD
  • app/Http/Controllers/ImageTypeVariantController.php — variáns konfig + trigger
  • app/Http/Controllers/MediaController.php — feltöltés módosítás
  • routes/api.php — új route-ok

Frontend

  • src/modules/settings/stores/imageType.ts — új store
  • src/modules/settings/pages/image-formats/index.vue — beállítások UI (típusok + variánsok kezelése)
  • src/navigation/vertical/index.ts — új "Kép formátumok" menüpont
  • src/navigation/horizontal/index.ts — ugyanaz
  • src/components/ImageEditor.vue — imageTypeSlug + entityId prop
  • src/plugins/i18n/locales/hu.json + en.json

Verifikáció

  1. Migrációk: php artisan migrate hiba nélkül
  2. Seeder: php artisan db:seed — image type-ok és variáns konfigok létrejönnek
  3. Képfeltöltés: Blog OG kép feltöltés → Media rekord image_type_slug = 'blog_og'GenerateVariantsForMediaJob dispatch → variáns fájlok megjelennek a storage-ban
  4. Variáns konfig: Beállítások oldalon AVIF bekapcsolás → BulkGenerateVariantsJob elindul → fokozatosan generálja az összes képre
  5. Variáns kikapcsolás: WebP kikapcsolás → BulkDeleteVariantsJob → fájlok és rekordok törölve
  6. Media API: GET /media/{id} visszaadja a variants és variant_urls mezőket
  7. Queue worker: php artisan queue:work --queue=image-processing feldolgozza a job-okat