Skip to content

Automatisches Bild-Management für Varianten #9

@csaeum

Description

@csaeum

Beschreibung

Automatisches Setzen von Variantenbildern basierend auf:

  1. Eigenschaftsbilder (z.B. Farboption "Rot" hat ein Bild → wird Varianten-Cover)
  2. Parent-Produktbilder (optional alle Bilder des Hauptartikels an Variante anhängen)

Use Case

Beispiel: Jacke mit Farbvarianten

Hauptartikel "Premium Leather Jacket":

  • Bild 1: Produktfoto Vorderseite
  • Bild 2: Produktfoto Rückseite
  • Bild 3: Detailaufnahme

Optionen:

  • Farbe "Schwarz": Hat Eigenschaftsbild black-swatch.jpg
  • Farbe "Braun": Hat Eigenschaftsbild brown-swatch.jpg
  • Größe "XL": Kein Bild

Variante "XL / Schwarz":

  • Cover (Hauptbild): black-swatch.jpg (von Eigenschaft "Schwarz")
  • Weitere Bilder (optional): Bild 1, 2, 3 vom Parent

Ergebnis im Frontend:

  • Kunde sieht schwarze Jacke als Hauptbild
  • Kann durch Parent-Bilder browsen (Vorder-/Rückseite, Details)

Anforderungen

1. Eigenschaftsbild als Cover setzen

Funktionalität

  • Prüfe alle Optionen der Variante (z.B. [XL, Schwarz])
  • Falls Option ein media hat → setze als Varianten-Cover
  • Falls mehrere Optionen Bilder haben → Priorität konfigurierbar

Priorität bei mehreren Bildern

Option A: Erste gefundene Option (Empfohlen)

Variante: [Größe: XL (mit Bild), Farbe: Schwarz (mit Bild)]
→ Cover: Bild von "XL" (da zuerst in Options-Array)

Option B: Konfigurierbare Priorität

Plugin-Config: "Farbe > Größe > Material"
Variante: [Größe: XL (mit Bild), Farbe: Schwarz (mit Bild)]
→ Cover: Bild von "Schwarz" (da Farbe höher priorisiert)

Empfehlung: Option B mit Standard-Priorität nach PropertyGroup-Namen

Edge Cases

  • Keine Option hat Bild: Behalte bestehendes Cover (oder Parent-Cover)
  • Option ohne Media-Objekt: Überspringe, prüfe nächste Option
  • Variante hat bereits Cover: Überschreibe nur wenn --force-images gesetzt

2. Parent-Bilder anhängen (Optional)

Funktionalität

  • Kopiere alle media des Parent-Produkts zur Variante
  • Reihenfolge beibehalten (Position aus Parent übernehmen)
  • Eigenschaftsbild bleibt Cover (Position 0)
  • Parent-Bilder ab Position 1

Beispiel

Parent "jacket-001":

media: [
  {id: img1, position: 0, isCover: true},  // Vorderseite
  {id: img2, position: 1, isCover: false}, // Rückseite
  {id: img3, position: 2, isCover: false}  // Detail
]

Variante mit Eigenschaftsbild "Schwarz":

media: [
  {id: img_schwarz, position: 0, isCover: true},   // Eigenschaftsbild
  {id: img1, position: 1, isCover: false},         // Parent Bild 1
  {id: img2, position: 2, isCover: false},         // Parent Bild 2
  {id: img3, position: 3, isCover: false}          // Parent Bild 3
]

Duplikat-Vermeidung

  • Prüfe ob Bild bereits bei Variante existiert (via mediaId)
  • Verhindere doppelte Bilder

3. Konfigurationsoptionen

CLI Flags

# Nur Eigenschaftsbilder als Cover setzen
bin/console wsc:variant:update --product-numbers="jacket-001" --update-images

# Eigenschaftsbilder + Parent-Bilder anhängen
bin/console wsc:variant:update --product-numbers="jacket-001" --update-images --inherit-parent-images

# Nur Parent-Bilder anhängen (ohne Eigenschaftsbilder)
bin/console wsc:variant:update --product-numbers="jacket-001" --inherit-parent-images

# Bestehende Bilder überschreiben
bin/console wsc:variant:update --product-numbers="jacket-001" --update-images --force-images

Plugin-Konfiguration (Admin)

<config>
    <!-- Feature-Toggle -->
    <input-field name="updateImages" type="bool">
        <label>Eigenschaftsbilder als Cover setzen</label>
        <defaultValue>false</defaultValue>
    </input-field>
    
    <input-field name="inheritParentImages" type="bool">
        <label>Parent-Bilder an Varianten anhängen</label>
        <defaultValue>false</defaultValue>
    </input-field>
    
    <input-field name="forceImageUpdate" type="bool">
        <label>Bestehende Bilder überschreiben</label>
        <defaultValue>false</defaultValue>
    </input-field>
    
    <!-- Priorität bei mehreren Eigenschaftsbildern -->
    <input-field name="imagePropertyPriority" type="text">
        <label>Eigenschafts-Priorität (komma-separiert)</label>
        <placeholder>Farbe,Material,Größe</placeholder>
        <helpText>PropertyGroup-Namen in Prioritäts-Reihenfolge</helpText>
    </input-field>
</config>

Admin Interface (Issue #4)

  • Checkboxen analog zu --name-only, --number-only
  • "Bilder aktualisieren": Setzt Eigenschaftsbilder als Cover
  • "Parent-Bilder übernehmen": Hängt Parent-Bilder an

4. Technische Umsetzung

Neue Service-Methode

class VariantUpdateService
{
    public function updateVariantImages(
        ProductEntity $variant,
        ProductEntity $parent,
        bool $inheritParentImages = false,
        bool $force = false
    ): void {
        // 1. Eigenschaftsbild als Cover setzen
        $optionImage = $this->findPriorityOptionImage($variant);
        if ($optionImage) {
            $this->setVariantCover($variant, $optionImage, $force);
        }
        
        // 2. Parent-Bilder anhängen (optional)
        if ($inheritParentImages) {
            $this->inheritParentImages($variant, $parent);
        }
    }
    
    private function findPriorityOptionImage(ProductEntity $variant): ?MediaEntity
    {
        $options = $variant->getOptions();
        $priority = $this->getPriorityList(); // Aus Config
        
        // Sortiere Optionen nach Priorität
        foreach ($priority as $propertyGroupName) {
            foreach ($options as $option) {
                if ($option->getGroup()->getName() === $propertyGroupName 
                    && $option->getMedia() !== null) {
                    return $option->getMedia();
                }
            }
        }
        
        // Fallback: Erste Option mit Bild
        foreach ($options as $option) {
            if ($option->getMedia() !== null) {
                return $option->getMedia();
            }
        }
        
        return null;
    }
    
    private function setVariantCover(
        ProductEntity $variant, 
        MediaEntity $media, 
        bool $force
    ): void {
        // Prüfe ob Variante bereits Cover hat
        if (!$force && $variant->getCover() !== null) {
            $this->logger->info('Variant already has cover, skipping', [
                'variant_id' => $variant->getId(),
            ]);
            return;
        }
        
        // Erstelle ProductMedia-Eintrag
        $productMedia = [
            'productId' => $variant->getId(),
            'mediaId' => $media->getId(),
            'position' => 0,
        ];
        
        $this->productMediaRepository->create([$productMedia], $this->context);
        
        // Setze als Cover
        $this->productRepository->update([[
            'id' => $variant->getId(),
            'coverId' => $productMedia['id'], // Nach create verfügbar
        ]], $this->context);
    }
    
    private function inheritParentImages(
        ProductEntity $variant, 
        ProductEntity $parent
    ): void {
        $parentMedia = $parent->getMedia();
        if ($parentMedia === null || $parentMedia->count() === 0) {
            return;
        }
        
        $existingMediaIds = $variant->getMedia()
            ?->map(fn($pm) => $pm->getMediaId())
            ?? [];
        
        $newMedia = [];
        $position = 1; // Start ab 1 (0 ist Cover)
        
        foreach ($parentMedia as $parentProductMedia) {
            // Duplikat-Check
            if (in_array($parentProductMedia->getMediaId(), $existingMediaIds)) {
                continue;
            }
            
            $newMedia[] = [
                'productId' => $variant->getId(),
                'mediaId' => $parentProductMedia->getMediaId(),
                'position' => $position++,
            ];
        }
        
        if (!empty($newMedia)) {
            $this->productMediaRepository->create($newMedia, $this->context);
        }
    }
}

DAL Associations erweitern

$criteria->addAssociation('options.group');
$criteria->addAssociation('options.media'); // NEU
$criteria->addAssociation('media');         // Varianten-Bilder
$criteria->addAssociation('cover');         // Varianten-Cover
$criteria->addAssociation('parent.media');  // Parent-Bilder für Inherit

Repository-Injection

public function __construct(
    private readonly EntityRepository $productRepository,
    private readonly EntityRepository $productMediaRepository, // NEU
    private readonly LoggerInterface $logger
) {}

5. Logging & Dry-Run

Dry-Run Output

Variante: jacket-001-xl-schwarz
  ✓ Name: Premium Leather Jacket XL Schwarz
  ✓ Nummer: jacket-001-xl-schwarz
  📷 Cover würde gesetzt: black-swatch.jpg (von Option "Schwarz")
  📷 3 Parent-Bilder würden angehängt

Log-Einträge

$this->logger->info('Variant image updated', [
    'variant_id' => $variant->getId(),
    'cover_media_id' => $media->getId(),
    'source_option' => $option->getName(),
    'inherited_images_count' => count($newMedia),
]);

6. Edge Cases & Fehlerbehandlung

Szenario Verhalten
Option ohne Media-Objekt Überspringe, prüfe nächste
Alle Optionen ohne Bild Behalte bestehendes Cover
Parent ohne Bilder Inherit-Feature macht nichts
Variante hat bereits identisches Bild Duplikat vermeiden
Media-Entity nicht gefunden Logge Warnung, überspringe
Variante hat 10 eigene Bilder Hänge Parent-Bilder trotzdem an (Position 11+)

7. Performance-Überlegungen

Problem

Bei 10.000 Varianten mit je 5 Parent-Bildern:

  • 50.000 ProductMedia-Inserts
  • Potenziell hohe DB-Last

Lösung: Batch-Inserts

// Sammle alle ProductMedia für Batch
$allProductMedia = [];

foreach ($variants as $variant) {
    $newMedia = $this->prepareInheritedImages($variant, $parent);
    $allProductMedia = array_merge($allProductMedia, $newMedia);
    
    // Insert alle 500 Einträge
    if (count($allProductMedia) >= 500) {
        $this->productMediaRepository->create($allProductMedia, $this->context);
        $allProductMedia = [];
    }
}

// Rest inserieren
if (!empty($allProductMedia)) {
    $this->productMediaRepository->create($allProductMedia, $this->context);
}

Akzeptanzkriterien

  • Eigenschaftsbilder werden als Varianten-Cover gesetzt
  • Priorität bei mehreren Bildern konfigurierbar
  • Parent-Bilder können optional angehängt werden
  • Keine Duplikat-Bilder
  • Dry-Run zeigt Bild-Änderungen an
  • CLI-Flags --update-images und --inherit-parent-images funktionieren
  • Admin-Konfiguration für Bild-Features
  • Logging für Audit-Trail
  • Performance-Test: 1.000 Varianten mit 5 Bildern < 2 Min

Testfälle

Unit Tests

public function testFindPriorityOptionImage(): void
{
    // Test Priority: Farbe > Größe
    // Variante mit [Größe (mit Bild), Farbe (mit Bild)]
    // Erwartet: Farb-Bild wird gewählt
}

public function testInheritParentImagesNoDuplicates(): void
{
    // Variante hat bereits Bild X
    // Parent hat Bilder X, Y, Z
    // Erwartet: Nur Y, Z werden angehängt
}

Integration Tests

public function testUpdateVariantImages(): void
{
    $parent = $this->createProductWithImages(['img1', 'img2']);
    $variant = $this->createVariantWithColorOption($parent, 'Schwarz', 'black.jpg');
    
    $service->updateVariantImages($variant, $parent, inheritParentImages: true);
    
    // Assert: Cover ist black.jpg
    // Assert: Variante hat 3 Bilder (black.jpg + img1 + img2)
}

Abhängigkeiten


Dokumentation

README-Erweiterung

Beispiel: Bild-Management

# Eigenschaftsbilder als Cover setzen
bin/console wsc:variant:update --product-numbers="jacket-001" --update-images

# Eigenschaftsbilder + Parent-Bilder übernehmen
bin/console wsc:variant:update \
  --product-numbers="jacket-001" \
  --update-images \
  --inherit-parent-images

# Für alle Produkte
bin/console wsc:variant:update-all \
  --update-images \
  --inherit-parent-images \
  --force

Prioritäts-Konfiguration

Einstellungen → Plugins → WSC Variant Updater
→ "Eigenschafts-Priorität": Farbe,Material,Größe

Dies bedeutet: Bei Variante mit Optionen [Größe: XL (mit Bild), Farbe: Schwarz (mit Bild)]
wird das Bild von "Schwarz" als Cover gewählt, da "Farbe" höhere Priorität hat.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions