Zielsetzung

Am Produktbild, dem Shopware 6 Produktbild, sollen Badges angezeigt werden die Aussagen über das Produkt vermitteln sollen. In diesem speziellen Fall geht es um Auszeichnungen bei den Produkten eines Weinguts. Auszeichnungen der Kammer sollen so direkt dem Kunden ersichtlich sein.

Problemstellung

Wir haben verschiedene Produktbilder für ein Produkt und wollen jeweils für jedes Bild entsprechende Badges setzen. Außerdem ist zu gewährleisten, dass die Badges mit der Zoom-Funktion für das Shopware 6 Produktbild keine ungewollten Seiteneffekte erzeugen.

Lösungsweg

Um die Zielsetzung zu erreichen sind folgende Schritte notwendig

  • Im Frontend muss für jedes einzelne Shopware 6 Produktbild entschieden werden ob es einen entsprechenden Badge erhalten soll. Dazu muss differenziert werden ob es Badges gibt und welches Bild davon betroffen ist.
  • Das Template zur Ausgabe verarbeitet diese Conditions und zeigt dementsprechend Badges am Produktbild an.

Um das Beispiel an dieser Stelle nicht ausufern zu lassen, werden wir die Frontendlogik für das definieren der Badges einfach festlegen.

Voraussetzungen

Umsetzung

Als erstes der vollständige Verzeichnisbaum des fertigen Produkts:

Shopware 6 Produktbild

Bevor wir Badges im Frontend anzeigen können, müssen wir einen Event Subscriber zur Verfügung stellen, der die MediaEntity um die zusätzlichen Informationen zu dem Shopware 6 Produktbild erweitert.

<?php declare(strict_types=1);

namespace Weinklang\ProductImageBadge\Subscriber;

use Shopware\Core\Content\Media\MediaEntity;
use Shopware\Core\Content\Product\ProductEvents;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityLoadedEvent;
use Shopware\Core\Framework\Struct\ArrayStruct;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ProductLoadedSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            ProductEvents::PRODUCT_LOADED_EVENT => 'onProductLoaded'
        ];
    }

    public function onProductLoaded(EntityLoadedEvent $productEntityEvent): void
    {
        // don't break the admin
        $source = $productEntityEvent->getContext()->getSource();
        if (method_exists($source, 'isAdmin') && $source->isAdmin()) {
            return;
        }

        /** @var SalesChannelProductEntity $product */
        foreach ($productEntityEvent->getEntities() as $product) {
            $media = $product->getMedia();
            if (null === $media) {
                // the product listing works with the cover and has no loaded media
                $productMediaEntity = $product->getCover();
                $mediaEntity = $productMediaEntity->getMedia();
                $this->addProductImageBadges($mediaEntity);
            } else {
                // the product details has loaded media
                $productMediaCollection = $product->getMedia();
                foreach ($productMediaCollection as $productMediaEntity) {
                    $mediaEntity = $productMediaEntity->getMedia();
                    $this->addProductImageBadges($mediaEntity);
                }
            }
        }
    }

    private function addProductImageBadges(MediaEntity $mediaEntity): void
    {
        // we fake the badges in this blog post
        $index = rand(0, 1);
        $badges = [
            'images/goldene-kammerpreismuenze.png',
            'images/silberne-kammerpreismuenze.png'
        ];
        unset($badges[$index]);

        // extend the media entity to contain the badges informations
        $mediaEntity->addExtension('product_image_badges', new ArrayStruct(['badges' => $badges]));
    }
}

Um diesen Event Subscriber beim Laden des Produkts auszuführen, müssen wir den Service definieren und mitteilen, dass es sich hierbei um einen Event Subscriber handelt.

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>

        <service id="Weinklang\ProductImageBadge\Subscriber\ProductLoadedSubscriber">
            <tag name="kernel.event_subscriber" />
        </service>

    </services>
</container>

Jetzt werden alle Badgedaten geladen und können in das Frontend integriert werden. Zunächst einmal möchte ich die Bilder für die Badges zur Verfügung stellen. Das Verzeichnis /src/Resources/public wird bei einem Plugin dazu verwendet um Assets in das öffentliche Verzeichnis zu kopieren, in diesem Falle nach /bundles/weinklangproductimagebadge/assets/images/…, diese müssen dann öffentlich zugänglich gemacht werden mit:

bin/console assets:install

Jetzt können wir beginnen das erste Template zu überschreiben. Um Templates als Plugin zu überschreiben wird der Dateipfad wie einem Theme verwendet, /src/Resources/views/storefront und dann das entsprechende zu überschreibende Template. Um das korrekte Template zu finden suchen wir in /vendor/shopware/storefront/Resources/views/storefront.

Wir fangen mit dem Produktlisting an und ergänzen unsere Badges am Bild. Hierbei ist zu beachten, dass im Produktlisting nur das Cover geladen wird, keine anderen Product Media Entities.

{% sw_extends '@Storefront/storefront/component/product/card/badges.html.twig' %}

{% block component_product_badges %}
    {{ parent() }}
    <div class="product-image-badges">
        {% for badge in product.cover.media.extensions.product_image_badges.badges %}
            {% set image = asset('bundles/weinklangproductimagebadge/assets/' ~ badge) %}
            <span class="product-image-badge"><img src="{{ image }}" /></span>
        {% endfor %}
    </div>
{% endblock %}

Sobald der später noch hinzugefügt Stylesheet eingesetzt wird erhalten wir folgendes Gesamtbild. Hinzugekommen sind am Shopware 6 Produktbild die Kreisförmigen Kammerpreismünzen.

Shopware 6 Produktbild

Das selbe wollen wir jetzt für die Produktdetailseite implementieren. Hier verhält es sich etwas anders. Hier werden alle Bilder geladen und wir nutzen nicht das Cover, da jedes Bild seine eigenen Badges haben kann.

Der Folgende Code ist etwas umfangreicher, da wir ein ganzes Segment überschreiben müssen und zwischen mehreren und einem einzelnen Bild differenzieren.

{% sw_extends '@Storefront/storefront/element/cms-element-image-gallery.html.twig' %}

{% block element_image_gallery_inner_item %}
    <div class="gallery-slider-item-container">
        <div class="gallery-slider-item is-{{ displayMode }} js-magnifier-container"{% if minHeight and  (displayMode == "cover" or displayMode == "contain" ) %} style="min-height: {{ minHeight }}"{% endif %}>
            {% block product_image_badges %}
                <div class="product-image-badges">
                    {% for badge in image.extensions.product_image_badges.badges %}
                        {% set image = asset('bundles/weinklangproductimagebadge/assets/' ~ badge) %}
                        <span class="product-image-badge"><img src="{{ image }}" /></span>
                    {% endfor %}
                </div>
            {% endblock %}
            {% set attributes = {
                'class': 'img-fluid gallery-slider-image magnifier-image js-magnifier-image',
                'alt': (image.translated.alt ?: fallbackImageTitle),
                'title': (image.translated.title ?: fallbackImageTitle),
                'data-full-image': image.url
            } %}

            {% if displayMode == 'cover' or displayMode == 'contain' %}
                {% set attributes = attributes|merge({ 'data-object-fit': displayMode }) %}
            {% endif %}

            {% if isProduct %}
                {% set attributes = attributes|merge({ 'itemprop': 'image' }) %}
            {% endif %}

            {% sw_thumbnails 'gallery-slider-image-thumbnails' with {
                media: image
            } %}
        </div>
    </div>
{% endblock %}

{% block element_image_gallery_inner_single %}
    <div class="gallery-slider-single-image is-{{ displayMode }} js-magnifier-container"{% if minHeight %} style="min-height: {{ minHeight }}"{% endif %}>
        {% block product_image_badges_single %}
            <div class="product-image-badges">
                {% for productMediaEntity in page.product.media %}
                    {% for badge in productMediaEntity.media.extensions.product_image_badges.badges %}
                        {% set image = asset('bundles/weinklangproductimagebadge/assets/' ~ badge) %}
                        <span class="product-image-badge"><img src="{{ image }}" /></span>
                    {% endfor %}
                {% endfor %}
            </div>
        {% endblock %}
        {% if imageCount > 0 %}
            {% set attributes = {
                'class': 'img-fluid gallery-slider-image magnifier-image js-magnifier-image',
                'alt': (mediaItems|first.translated.alt ?: fallbackImageTitle),
                'title': (mediaItems|first.translated.title ?: fallbackImageTitle),
                'data-full-image': mediaItems|first.url
            } %}

            {% if displayMode == 'cover' or displayMode == 'contain' %}
                {% set attributes = attributes|merge({ 'data-object-fit': displayMode }) %}
            {% endif %}

            {% if isProduct %}
                {% set attributes = attributes|merge({ 'itemprop': 'image' }) %}
            {% endif %}

            {% sw_thumbnails 'gallery-slider-image-thumbnails' with {
                media: mediaItems|first,
            } %}
        {% else %}
            {% sw_icon 'placeholder' style {
                'size': 'fluid'
            } %}
        {% endif %}
    </div>
{% endblock %}

Hinzugefügt wurden lediglich die Blöcke

  • {% block product_image_badges %}
  • {% block product_image_badges_single %}

Damit das ganze auch entsprechend funktioniert und nach was aussieht brauchen wir noch ein wenig CSS mit dem wird die Bilder positionieren. Diese SCSS Datei wird für Plugins in das Verzeichnis /src/Resources/app/storefront/src/scss abgespeichert. Shopware sucht an dieser Stelle nach den SCSS Dateien.

.gallery-slider-item-container {
  position: relative;
}

.product-image-badges {
  position:absolute;
  top:10px;
  right:10px;
  z-index: 1000;
}

Und am Ende sieht dies dann so aus:

Shopware 6 Produktbild

Ja, die Position ist noch nicht so optimal und sollte näher am Bild sein, dass ist nämlich wesentlich kleiner, aber sollte für hier erstmal genügen. Wenn man die Seite weiterblättert, erhält man ein anderes Badge am Shopware 6 Produktbild.

Shopware 6 Produktbild

Das ganze Plugin kann hier runtergeladen werden:

Shopware 6 Produktbild

Für Weingenießer lohnt sich ein Blick auf die Webseite von martin’s weinklang. Im Onlineshop von martin’s weinklang kann man die Extension im Einsatz sehen.