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:
url→Storage::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:
- Media + ImageTypeVariant betöltés
- MediaVariant rekord létrehozás/frissítés → status:
processing - Eredeti fájl betöltés Storage-ból → Intervention Image resize + encode
- Mentés:
images/{type}/YYYY_MM_{shard}/{entity_uuid}/{width}x{height}.{format}(lásd 10. szekció) - Status →
completed,file_sizementés - 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ódus | URI | Leírás |
|---|---|---|
| GET | /image-types | Tenant 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ódus | URI | Leírás |
|---|---|---|
| POST | /image-types/{id}/variants | Variá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=true→BulkGenerateVariantsJobdispatchupdate()enable toggle →BulkGenerateVariantsJobvagyBulkDeleteVariantsJobdestroy()→BulkDeleteVariantsJobmajd 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álisimage_type_slug+entity_id,ImageStorageServicepath generálás, mentés utánGenerateVariantsForMediaJobdispatchstoreOgImage():image_type_slug+entity_idparaméterként,ImageStorageServicepath generálás,GenerateVariantsForMediaJobdispatchdestroy(): 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:
| slug | name | width | height |
|---|---|---|---|
blog_og | Blog OG kép | 1200 | 630 |
page_og | Oldal OG kép | 1200 | 630 |
content_inline | Tartalom kép | 800 | null |
product_main | Termék főkép | 1000 | 1000 |
product_thumb | Termék bélyegkép | 300 | 300 |
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.settingsheading 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>kapimage-type-slug+entity-idprop-okat- Create oldalon az
entity_ida 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.json — settings.imageTypes alá:
| Kulcs | HU | EN |
|---|---|---|
title | Kép formátumok | Image Formats |
addType | Típus hozzáadása | Add Type |
addVariant | Variáns hozzáadása | Add Variant |
format | Formátum | Format |
width | Szélesség | Width |
height | Magasság | Height |
quality | Minőség | Quality |
enabled | Bekapcsolva | Enabled |
regenerate | Újragenerálás | Regenerate |
confirmEnable | Ez {count} meglévő képre generál variánsokat. Folytatod? | This will generate variants for {count} existing images. Continue? |
confirmDisable | Ez törli a variánsokat {count} képről. Folytatod? | This will delete variants from {count} images. Continue? |
formats.jpg | JPG | JPG |
formats.webp | WebP | WebP |
formats.png | PNG | PNG |
formats.avif | AVIF | AVIF |
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_MMshard-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
mediarekordpathmező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ázis | Mit | Fájlok |
|---|---|---|
| 1 | Migrációk | 4 migráció |
| 2 | Modellek | ImageType, ImageTypeVariant, MediaVariant (új) + Media (módosítás) |
| 3 | ImageStorageService | Shard logika, path generálás |
| 4 | Resource-ök | ImageTypeResource, ImageTypeVariantResource, MediaVariantResource (új) + MediaResource (módosítás) |
| 5 | Queue Job-ok | GenerateMediaVariantJob, GenerateVariantsForMediaJob, BulkGenerateVariantsJob, BulkDeleteVariantsJob |
| 6 | Controller-ek | ImageTypeController, ImageTypeVariantController (új) + MediaController (módosítás) |
| 7 | Route-ok | image-types csoport + variáns route-ok |
| 8 | Seeder | Alapértelmezett image type-ok + variáns konfigok |
| 9 | Frontend store | imageType.ts |
| 10 | Frontend oldal | image-formats/index.vue |
| 11 | ImageEditor + blog/page frissítés | imageTypeSlug prop |
| 12 | i18n | hu.json + en.json |
Kritikus fájlok
Backend
app/Models/Media.php— image_type_slug, entity_id + variants relációapp/Models/ImageType.php— új modelapp/Models/ImageTypeVariant.php— új modelapp/Models/MediaVariant.php— új modelapp/Services/ImageStorageService.php— shard logika, path generálásapp/Jobs/GenerateMediaVariantJob.php— atomi variáns generálás (Intervention Image)app/Jobs/BulkGenerateVariantsJob.php— bulk generálás chunkById + Bus::batchapp/Http/Controllers/ImageTypeController.php— CRUDapp/Http/Controllers/ImageTypeVariantController.php— variáns konfig + triggerapp/Http/Controllers/MediaController.php— feltöltés módosításroutes/api.php— új route-ok
Frontend
src/modules/settings/stores/imageType.ts— új storesrc/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üpontsrc/navigation/horizontal/index.ts— ugyanazsrc/components/ImageEditor.vue— imageTypeSlug + entityId propsrc/plugins/i18n/locales/hu.json+en.json
Verifikáció
- Migrációk:
php artisan migratehiba nélkül - Seeder:
php artisan db:seed— image type-ok és variáns konfigok létrejönnek - Képfeltöltés: Blog OG kép feltöltés → Media rekord
image_type_slug = 'blog_og'→GenerateVariantsForMediaJobdispatch → variáns fájlok megjelennek a storage-ban - Variáns konfig: Beállítások oldalon AVIF bekapcsolás →
BulkGenerateVariantsJobelindul → fokozatosan generálja az összes képre - Variáns kikapcsolás: WebP kikapcsolás →
BulkDeleteVariantsJob→ fájlok és rekordok törölve - Media API:
GET /media/{id}visszaadja avariantsésvariant_urlsmezőket - Queue worker:
php artisan queue:work --queue=image-processingfeldolgozza a job-okat