Shopware Plugin entwickeln 2026: Die Anleitung mit einem echten Testshop

Shopware Plugin entwickeln 2026: Die Anleitung mit einem echten Testshop

Ein Shopware Plugin entwickeln klingt nach einem großen Projekt, ist im Kern aber überraschend klein. Ein funktionierendes Plugin, das auf jeder Produktseite einen konfigurierbaren Lieferhinweis anzeigt, besteht bei uns aus genau sechs Dateien und 95 Zeilen. Es lässt sich im Admin ein- und ausschalten, der Text ist pro Verkaufskanal änderbar, und es hinterlässt beim Deinstallieren keine Reste in der Datenbank. Mehr braucht ein erstes Plugin nicht.

Schwierig wird es an anderer Stelle. Shopware bringt einen Generator mit, der dir in Sekunden ein Plugin-Gerüst baut. Dieses Gerüst ist aber eine Sammlung von Beispielen, kein Startpunkt. Wir haben am 25.10.2026 einen frischen Testshop mit Shopware 6.6.8.2 aufgesetzt, den Generator ausprobiert, das Plugin von Hand gebaut, installiert, aktualisiert, deaktiviert und wieder deinstalliert. Dabei sind wir auf fünf Fallen gestoßen, die du in den meisten Anleitungen nicht findest: eine Endlosschleife im Generator, eine Store-API-Route, die mit HTTP 500 antwortet, zwei Plugins, die sich gegenseitig blockieren, eine Template-Erweiterung, die still nichts tut, und ein Schalter, der „aus“ speichert und trotzdem „an“ bleibt.

Diese Anleitung zeigt dir den Weg, der bei uns funktioniert hat. Sie richtet sich an Entwickler, die PHP können und Shopware zum ersten Mal erweitern wollen, und an Shopbetreiber, die verstehen möchten, was ihre Agentur da eigentlich baut. Wo sich Shopware 6.7 anders verhält als 6.6, sagen wir das, und zwar auf Basis des Quellcodes von 6.7.15.1, nicht aus der Erinnerung.

Video: Caribe Digital, „Shopware 6 Plugin erstellen! Mein erster, einfacher aber kompletter Plugin für Frontend.“ (November 2022, Shopware 6.4). Das Video ist älter. Der Aufbau eines Plugins ist gleich geblieben, einzelne Template-Pfade und Befehle haben sich seitdem geändert, siehe unten.

Shopware Plugin entwickeln: was ein Plugin überhaupt ist

Bevor du die erste Datei anlegst, lohnt sich ein Satz Theorie, weil er viele spätere Fehler erklärt. Ein Shopware-Plugin ist ein Symfony-Bundle mit ein paar Zusatzangaben. Shopware selbst ist eine Symfony-Anwendung. Ein Plugin klinkt sich in genau dieselben Mechanismen ein, die auch der Kern benutzt: den Service-Container, den Event-Dispatcher, das Routing und die Twig-Templates.

Das bedeutet zweierlei. Erstens kannst du in einem Plugin fast alles verändern, was Shopware tut. Zweitens kannst du dabei auch fast alles kaputt machen. Ein Plugin läuft nicht in einer abgeschotteten Umgebung, sondern mit denselben Rechten wie der Kern. Wenn dein Plugin beim Laden einer Produktseite eine Ausnahme wirft, ist die Produktseite kaputt, nicht nur dein Plugin.

Plugin oder App: die Entscheidung vor der ersten Zeile

Shopware kennt zwei Arten von Erweiterungen, und die Wahl gehört an den Anfang:

PluginApp
Läuft wo?Im Shopware-Server, als PHP-CodeAuf deinem eigenen Server, Shopware ruft ihn per Webhook auf
Geht in der Shopware Cloud (SaaS)?NeinJa
Darf eigenen PHP-Code im Shop ausführen?Ja, allesNein, nur Skripte in einer Sandbox
Eigene Datenbanktabellen?Ja, per MigrationNur über Custom Entities
Typische FälleTiefe Eingriffe, Checkout-Logik, Performance-kritischesAnbindungen an externe Dienste, Marketing, ERP-Brücken
EinstiegshürdeMittel (PHP, Symfony)Niedrig bis mittel (beliebige Sprache plus Manifest)

Die Faustregel: Wenn dein Shop selbst gehostet ist und du die Logik im Shop brauchst, ist ein Plugin der direkte Weg. Wenn du für die Shopware Cloud entwickelst oder dein Code ohnehin auf einem eigenen Server läuft, nimm eine App. Wer ein Produkt für den Shopware Store baut, das in beiden Welten laufen soll, landet heute oft bei Apps. Die Unterschiede zwischen Cloud und Self-hosted haben wir in unserem Artikel zur Shopware Cloud genauer beschrieben.

In dieser Anleitung geht es um klassische Plugins für selbst gehostete Shops.

Explodierte isometrische Darstellung eines Software-Moduls mit einem zentralen Würfel und kleineren Bausteinen für Konfiguration, Templates, Services und Events

Was du brauchst, bevor du anfängst

Du brauchst eine lokale Shopware-Installation, auf der du Dinge kaputt machen darfst. Entwickle nie direkt auf dem Live-Shop. Das klingt banal, ist aber der häufigste Grund für Notfalleinsätze: Ein Plugin wird „schnell mal“ auf dem Server angepasst, der Cache wird geleert, und die Startseite zeigt einen Stacktrace.

Für unseren Test haben wir die Entwickler-Box von dockware genommen, ein Docker-Image mit vorinstalliertem Shopware, Datenbank, Demo-Daten und Mailcatcher. Der Container war nach rund einer Minute bereit:

docker run -d --name shopware-dev -p 127.0.0.1:8888:80 dockware/dev:6.6.8.2

Im Container liefen PHP 8.3.13 und Shopware 6.6.8.2 im Entwicklermodus. Wer lieber ohne Docker arbeitet, findet in unserer Shopware-Installation mit echten Messungen den Composer-Weg mit allen Stolpersteinen.

Außerdem brauchst du:

  • PHP-Kenntnisse auf dem Niveau von Klassen, Namespaces und Typen. Du musst kein Symfony-Experte sein, aber du solltest wissen, was ein Konstruktor ist.
  • Composer in Grundzügen, weil jedes Plugin eine composer.json hat.
  • Einen Editor mit PHP-Unterstützung. PhpStorm mit dem Symfony-Plugin oder VS Code mit Intelephense. Shopware hat rund 5.600 PHP-Dateien im Kern (in unserem Container 5.655 Dateien, 382 MB unter vendor/shopware), da hilft Autovervollständigung enorm.
  • Git. Ein Plugin ist Code und gehört in ein Repository. Unsere Liste der wichtigsten Git-Befehle reicht für den Anfang.

Welche Shopware-Version du nimmst, hängt davon ab, wofür du baust. Bei Redaktionsschluss war 6.7.15.1 die neueste Version (veröffentlicht am 02.10.2026, PHP 8.2 bis 8.5). Viele Shops laufen aber noch auf 6.6. Entwickle für die Version, auf der der Zielshop läuft, und teste vor einem Update gezielt gegen die neue.

Der Generator: plugin:create im Test

Shopware bringt einen Befehl mit, der dir ein Plugin-Gerüst anlegt. Das ist der Weg, den fast jede Anleitung zuerst zeigt. Wir haben ihn ausprobiert, und er hat uns sofort zwei Überraschungen beschert.

Falle 1: Die Endlosschleife ohne Namen

Unser erster Versuch war der naheliegende: nur den Plugin-Namen angeben und mit --no-interaction die Rückfragen abschalten, wie man es in Skripten macht.

bin/console plugin:create GmLieferzeitHinweis --no-interaction

Das Ergebnis war kein Plugin, sondern 32.586 Mal dieselbe Meldung, „Answer cannot be empty“, bis PHP nach 4,3 Sekunden mit einem Segmentation Fault abstürzte. Der Grund steht im Code von 6.6: Der Befehl fragt nach dem Namespace, bekommt im nicht-interaktiven Modus eine leere Antwort, meldet einen Fehler und ruft sich selbst erneut auf. Für immer, bis der Speicher voll ist.

In Shopware 6.7 ist das behoben. Seit 6.7.9.0 bricht der Befehl ohne interaktive Eingabe sauber ab, mit der Meldung, dass entweder der interaktive Modus oder das Argument nötig ist. Die Lösung funktioniert in beiden Versionen gleich: Gib Name und Namespace immer beide als Argumente mit.

bin/console plugin:create GmLieferzeitHinweis GmLieferzeitHinweis --no-interaction

Damit lief der Befehl in 0,2 Sekunden durch.

Falle 2: Du bekommst alles, ob du willst oder nicht

Wir wollten nur einen Event-Subscriber und eine geplante Aufgabe und hatten dafür die passenden Optionen gesetzt. Angelegt wurden 23 Dateien mit 536 Zeilen: Konsolenbefehl, Store-API-Route, Storefront-Controller, Admin-Modul, JavaScript-Plugin, Zusatzfelder, Konfiguration, Tests. Ein zweiter Versuch ganz ohne Optionen ergab exakt dieselben 23 Dateien.

Der Grund: Im nicht-interaktiven Modus beantwortet Symfony jede Ja-Nein-Frage mit der Voreinstellung, und die lautet bei jedem Baustein „ja“. Die Optionen wie --create-event-subscriber können also nur etwas hinzufügen, nie etwas weglassen. In 6.6 gibt es keinen Schalter dagegen. Erst seit Shopware 6.7.13.0 gibt es die Option --no-scaffold, die nur die Pflichtdateien anlegt.

Unser Rat: Nutze den Generator als Nachschlagewerk, nicht als Start. Leg dir einmal ein vollständiges Gerüst in einen Wegwerf-Ordner, lies die Beispiele, und baue dein eigentliches Plugin von Hand. Im Gerüst steckt außerdem einiges, das du im Shop nicht haben willst. Das JavaScript-Beispiel zum Beispiel öffnet bei jedem Besucher, der ans Seitenende scrollt, ein alert()-Fenster mit dem Text „Seems like there’s nothing more to see here.“

Falle 3: Die Store-API-Route antwortet mit 500

Wir haben das Gerüst trotzdem einmal installiert, um zu sehen, was funktioniert. Der Konsolenbefehl gab brav „It works!“ aus. Der Storefront-Controller lieferte unter /example eine Seite mit „Hello world“. Die Store-API-Route dagegen antwortete mit HTTP 500:

Controller "…\ExampleRoute" cannot be fetched from the container because it is private.
Did you forget to tag the service with "controller.service_arguments"?

Der Generator von 6.6 registriert die Route als privaten Service, und Symfony darf private Services nicht als Controller holen. Die Korrektur ist ein einzelnes Attribut, public="true" in der services.xml. Der Generator von 6.7.15.1 legt die Route inzwischen öffentlich an. Wer aber ein älteres Gerüst übernommen hat oder Beispiele aus alten Tutorials kopiert, stolpert genau hier.

Falle 4: Zwei Plugins aus demselben Generator vertragen sich nicht

Die vierte Falle ist die gemeinste, weil sie erst beim Kunden auffällt. Jedes Gerüst bekommt dieselben festen Namen, egal wie dein Plugin heißt: das Zusatzfeld-Set heißt swag_example_set, das Feld swag_example_size, die geplante Aufgabe swag.example_task, das Admin-Modul swag-example, der Konsolenbefehl swag-commands:example. Wir haben 20 solcher swag-Stellen in einem einzigen Gerüst gezählt.

Als wir ein zweites Gerüst-Plugin installieren wollten, brach die Installation mit einem Datenbankfehler ab:

Integrity constraint violation: 1062 Duplicate entry 'swag_example_size'

Das zweite Plugin wollte ein Zusatzfeld anlegen, das das erste schon angelegt hatte. In einem Agenturalltag, in dem mehrere Plugins aus demselben Gerüst entstehen, ist das keine Theorie. Benenne jeden swag- und example-Bezeichner um, bevor das Plugin den Rechner verlässt. Am besten mit einem Präfix, das zu dir gehört, zum Beispiel deinem Hersteller-Kürzel.

Es kommt noch dicker. Wir haben das Gerüst-Plugin deinstalliert, ohne Daten zu behalten. Danach war die geplante Aufgabe weg, das Zusatzfeld-Set swag_example_set aber lag weiter in der Datenbank. Das Gerüst legt es bei der Installation an, räumt es bei der Deinstallation aber nicht weg. Die Methode uninstall() enthält an der Stelle nur einen Kommentar. Eine erneute Installation scheiterte wieder am selben Duplikat-Fehler, diesmal ganz ohne zweites Plugin.

Ein Plugin von Hand bauen: der Lieferhinweis

Jetzt das Plugin, das wir wirklich behalten haben. Es hat ein einziges Ziel: Unter der Lieferzeit auf jeder Produktseite steht ein Hinweis wie „Bestellt bis 14 Uhr, Versand am selben Werktag.“ Der Shopbetreiber kann den Text im Admin ändern und den Hinweis abschalten.

Die Struktur sieht so aus:

custom/plugins/GmLieferhinweis/
├── composer.json
└── src/
    ├── GmLieferhinweis.php
    ├── Subscriber/
    │   └── ProductPageSubscriber.php
    └── Resources/
        ├── config/
        │   ├── config.xml
        │   └── services.xml
        └── views/storefront/component/buy-widget/
            └── buy-widget.html.twig

Sechs Dateien, 95 Zeilen. Wir gehen sie der Reihe nach durch.

Schritt 1: Die composer.json

Die composer.json ist der Ausweis deines Plugins. Shopware liest daraus Name, Version, Anzeigename und vor allem, welche Klasse das Plugin ist.

{
    "name": "getmind/lieferhinweis",
    "description": "Zeigt einen konfigurierbaren Lieferhinweis auf der Produktseite",
    "type": "shopware-platform-plugin",
    "version": "1.0.0",
    "license": "MIT",
    "require": { "shopware/core": "~6.6.0" },
    "extra": {
        "shopware-plugin-class": "GetMind\\Lieferhinweis\\GmLieferhinweis",
        "label": { "de-DE": "Lieferhinweis", "en-GB": "Delivery notice" }
    },
    "autoload": { "psr-4": { "GetMind\\Lieferhinweis\\": "src/" } }
}

Drei Felder sind wichtig. type muss genau shopware-platform-plugin heißen, sonst erkennt Shopware das Plugin nicht. extra.shopware-plugin-class zeigt auf die Plugin-Klasse, mit vollem Namespace und doppelten Backslashes. Und require legt fest, mit welchen Shopware-Versionen dein Plugin laufen darf. ~6.6.0 heißt „6.6.irgendwas, aber nicht 6.7“. Für ein Plugin, das in 6.6 und 6.7 laufen soll, schreibst du ~6.6.0 || ~6.7.0, aber nur, wenn du es auch gegen beide getestet hast.

Der Namespace (GetMind\Lieferhinweis) darf übrigens anders heißen als der Ordner (GmLieferhinweis). Wir haben das absichtlich so gebaut, weil der Generator beides immer gleich benennt und viele denken, es müsse so sein.

Schritt 2: Die Plugin-Klasse

<?php declare(strict_types=1);

namespace GetMind\Lieferhinweis;

use Shopware\Core\Framework\Plugin;

class GmLieferhinweis extends Plugin
{
}

Das ist kein Platzhalter, das ist die fertige Klasse. Unser Plugin legt keine Tabellen an, keine Zahlungsart und keine Zusatzfelder, also muss es auch bei Installation und Deinstallation nichts tun. Die Konfiguration aus der config.xml verwaltet Shopware selbst.

Wenn dein Plugin Daten anlegt, überschreibst du hier install(), uninstall(), activate(), deactivate() und update(). Dazu gleich mehr im Abschnitt zum Lebenszyklus.

Schritt 3: Die Konfiguration im Admin

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/trunk/src/Core/System/SystemConfig/Schema/config.xsd">
    <card>
        <title>Lieferhinweis</title>
        <title lang="en-GB">Delivery notice</title>
        <input-field type="bool">
            <name>active</name>
            <label>Hinweis anzeigen</label>
            <defaultValue>true</defaultValue>
        </input-field>
        <input-field type="text">
            <name>text</name>
            <label>Text</label>
            <defaultValue>Bestellt bis 14 Uhr, Versand am selben Werktag.</defaultValue>
        </input-field>
    </card>
</config>

Aus dieser Datei baut Shopware automatisch eine Einstellungsseite im Admin, mit einem Schalter und einem Textfeld, auf Wunsch pro Verkaufskanal. Die Werte landen in der Tabelle system_config unter den Schlüsseln GmLieferhinweis.config.active und GmLieferhinweis.config.text. Der Präfix ist der Name der Plugin-Klasse, nicht der Namespace.

Die Standardwerte schreibt Shopware bei der Installation in die Datenbank. Wir haben das geprüft: Direkt nach plugin:install standen dort {"_value": true} und der Standardtext.

Schritt 4: Der Event-Subscriber

Hier passiert die eigentliche Arbeit. Shopware feuert beim Laden einer Produktseite das Ereignis ProductPageLoadedEvent. Unser Subscriber hört darauf, liest die Konfiguration und hängt den Text an die Seite.

<?php declare(strict_types=1);

namespace GetMind\Lieferhinweis\Subscriber;

use Shopware\Core\Framework\Struct\ArrayStruct;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ProductPageSubscriber implements EventSubscriberInterface
{
    public function __construct(private readonly SystemConfigService $config)
    {
    }

    public static function getSubscribedEvents(): array
    {
        return [ProductPageLoadedEvent::class => 'onProductPage'];
    }

    public function onProductPage(ProductPageLoadedEvent $event): void
    {
        $salesChannelId = $event->getSalesChannelContext()->getSalesChannelId();

        if (!$this->config->getBool('GmLieferhinweis.config.active', $salesChannelId)) {
            return;
        }

        $event->getPage()->addExtension('gmLieferhinweis', new ArrayStruct([
            'text' => $this->config->getString('GmLieferhinweis.config.text', $salesChannelId),
        ]));
    }
}

Zwei Dinge sind hier wichtig. Erstens geben wir die ID des Verkaufskanals mit. Ohne sie liest Shopware nur den globalen Wert und ignoriert, was der Betreiber für einen bestimmten Kanal eingestellt hat. Zweitens hängen wir die Daten als Extension an die Seite, statt irgendwo eine globale Variable zu setzen. Extensions sind der offizielle Weg, Shopware-Objekte um eigene Daten zu erweitern, ohne den Kern anzufassen. Im Template sind sie dann unter page.extensions.gmLieferhinweis erreichbar.

Schritt 5: Den Subscriber registrieren

Damit Symfony den Subscriber kennt, muss er in der services.xml stehen:

<?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="GetMind\Lieferhinweis\Subscriber\ProductPageSubscriber">
            <argument type="service" id="Shopware\Core\System\SystemConfig\SystemConfigService"/>
            <tag name="kernel.event_subscriber"/>
        </service>
    </services>
</container>

Das Tag kernel.event_subscriber sorgt dafür, dass Symfony die Methode getSubscribedEvents() ausliest und die Verbindung herstellt. Fehlt das Tag, passiert gar nichts, und zwar ohne jede Fehlermeldung. Ob die Verbindung steht, prüfst du so:

bin/console debug:event-dispatcher "Shopware\Storefront\Page\Product\ProductPageLoadedEvent"

Bei uns stand dort genau ein Eintrag: unser ProductPageSubscriber::onProductPage() mit Priorität 0. Dieser Befehl ist der schnellste Weg, um „mein Subscriber tut nichts“ zu klären.

Hinweis zu Shopware 6.7: Der Generator legt in 6.7 die Service-Definition nicht mehr als XML, sondern als PHP-Datei an (services.php). XML funktioniert weiter, beides ist gültig. Wenn du ein Plugin für 6.6 und 6.7 baust, bleib bei der Form, die dein Team kennt.

Kreisförmiges Lebenszyklus-Diagramm mit fünf Stationen, auf dem ein kleiner Plugin-Würfel wandert, dazu Zahnräder und ein Ein-Aus-Schalter

Schritt 6: Das Template erweitern, aber das richtige

Jetzt soll der Text auf der Seite erscheinen. Shopware-Templates sind in Blöcke aufgeteilt, und ein Plugin kann jeden Block erweitern, ohne die ganze Datei zu kopieren.

Hier sind wir in die nächste Falle getappt, und sie ist lehrreich. Viele Anleitungen erweitern die Datei page/product-detail/buy-widget.html.twig und den Block page_product_detail_delivery_informations. Das haben wir auch gemacht. Das Plugin war installiert, aktiv, der Subscriber registriert, der Cache geleert. Auf der Produktseite stand: nichts.

Die Erklärung steht in der ersten Zeile der Kerndatei:

{# @deprecated tag:v6.7.0 - File will be removed as it is no longer loaded by default. #}

Die Produktseite wird in Shopware 6.6 standardmäßig über die Erlebniswelten gerendert, und die benutzen eine andere Datei: component/buy-widget/buy-widget.html.twig mit dem Block buy_widget_delivery_informations. Die alte Datei wird nur noch für Produktseiten ohne Layout geladen. In Shopware 6.7.15.1 gibt es sie gar nicht mehr, unser Abruf auf GitHub lieferte 404.

Das funktionierende Template sieht so aus:

{% sw_extends '@Storefront/storefront/component/buy-widget/buy-widget.html.twig' %}

{% block buy_widget_delivery_informations %}
    {{ parent() }}

    {% if page.extensions.gmLieferhinweis %}
        <p class="gm-lieferhinweis alert alert-info mt-2">
            {{ page.extensions.gmLieferhinweis.text }}
        </p>
    {% endif %}
{% endblock %}

sw_extends statt extends ist Pflicht. Damit können mehrere Plugins dieselbe Datei erweitern, ohne sich gegenseitig zu überschreiben. {{ parent() }} behält den ursprünglichen Inhalt des Blocks, unser Hinweis kommt darunter. Lässt du parent() weg, ersetzt dein Plugin die Lieferzeit-Anzeige komplett. Das kann gewollt sein, ist aber meistens ein Versehen.

Und Twig escaped den Text automatisch. Wenn ein Betreiber im Admin HTML in das Textfeld schreibt, erscheint es als Text, nicht als Code. Genau so soll es sein. Wenn du hier |raw einbaust, damit „Fett“ funktioniert, öffnest du eine Lücke für jeden, der Zugriff auf die Plugin-Einstellungen hat.

Wie findest du den richtigen Block? Nicht durch Raten. Öffne die Produktseite im Browser, such im Quelltext eine Klasse in der Nähe der Stelle (bei uns product-detail-delivery-information), und such dann in vendor/shopware/storefront/Resources/views nach dieser Klasse. Damit hast du Datei und Block. Prüfe danach die erste Zeile der Datei auf @deprecated.

Drahtgitter einer Produktdetailseite, in die ein Roboterarm einen hervorgehobenen Infokasten zwischen bestehende Layoutblöcke einsetzt

Installieren, aktivieren, prüfen

Mit den sechs Dateien im Ordner custom/plugins/GmLieferhinweis läuft die Installation so:

bin/console plugin:refresh
bin/console plugin:install --activate GmLieferhinweis
bin/console cache:clear

plugin:refresh lässt Shopware den Plugin-Ordner neu einlesen. Ohne diesen Schritt kennt Shopware dein Plugin nicht. plugin:install --activate installiert und aktiviert in einem Schritt, bei uns in 2,6 Sekunden. cache:clear dauerte im Entwicklermodus 3,1 Sekunden.

Danach stand auf der Produktseite:

<p class="gm-lieferhinweis alert alert-info mt-2">
    Lieferung in 1–3 Werktagen
</p>

Die Produktseite brauchte mit Plugin im Median rund 160 ms bis zum ersten Byte, gemessen über sieben Aufrufe mit wechselndem Parameter, damit kein Seitencache greift. Im Entwicklermodus, also langsamer als im Live-Betrieb. Ein Subscriber, der zwei Konfigurationswerte liest, fällt dabei nicht ins Gewicht. Das wird erst anders, wenn dein Subscriber pro Seitenaufruf Datenbankabfragen oder externe Anfragen macht.

Falle 5: Ein Schalter, der „aus“ speichert und „an“ bleibt

Diese Falle hat uns die meiste Zeit gekostet, und sie betrifft nicht nur Plugin-Entwickler, sondern jeden, der Shopware per Kommandozeile oder Skript konfiguriert.

Wir wollten testen, ob der Hinweis verschwindet, wenn man ihn abschaltet. Also:

bin/console system:config:set GmLieferhinweis.config.active false
bin/console system:config:get GmLieferhinweis.config.active
# false

Die Ausgabe sagte „false“. Der Hinweis stand trotzdem auf der Seite. Auch nach dem Leeren aller Cache-Pools. Auch nach cache:clear.

Ein Blick in die Datenbank klärte es:

GmLieferhinweis.config.active   {"_value": "false"}

"false" in Anführungszeichen. Der Befehl hatte nicht den Wahrheitswert falsch gespeichert, sondern das Wort „false“ als Text. Und die Methode getBool() macht im Kern nichts anderes als (bool) $this->get($key). In PHP ist jeder nicht-leere Text wahr, also auch „false“. Der Hinweis blieb an.

Richtig ist:

bin/console system:config:set --json GmLieferhinweis.config.active false

Mit --json stand in der Datenbank {"_value": false}, und der Hinweis war sofort weg, ohne dass wir einen Cache leeren mussten. getBool() ist in Shopware 6.7.15.1 unverändert, das Verhalten gilt also auch dort.

Daraus folgen zwei Regeln. Für Betreiber: Setze Schalter per Kommandozeile immer mit --json. Das gilt auch für Deploy-Skripte und Ansible-Rollen, die Shopware konfigurieren. Für Entwickler: Verlass dich nicht blind darauf, dass getBool() nur echte Wahrheitswerte sieht. Wenn ein Wert über Import, API oder Skript gesetzt werden kann, prüfe ihn vorher.

Fehlersuche: Eine Lupe über einem Würfelmodul mit einem kleinen roten Warnkäfer, daneben ein Kippschalter, gestapelte Cache-Ebenen und eine Checkliste

Der Plugin-Lebenszyklus: install, activate, update, uninstall

Ein Shopware-Plugin durchläuft fünf Zustände, und für jeden gibt es eine Methode in der Plugin-Klasse. Wer sie verwechselt, baut Plugins, die beim zweiten Installieren abstürzen oder beim Deinstallieren Daten verlieren.

BefehlMethodeWas hier hingehört
plugin:installinstall()Daten anlegen, die das Plugin braucht: Zusatzfelder, Zahlungsarten, Mail-Vorlagen
plugin:activateactivate()Dinge einschalten, die schon existieren, zum Beispiel eine Zahlungsart aktiv setzen
plugin:deactivatedeactivate()Gegenstück zu activate, nichts löschen
plugin:updateupdate()Daten an eine neue Version anpassen, sofern das nicht über Migrationen läuft
plugin:uninstalluninstall()Aufräumen, aber nur, wenn der Betreiber keine Daten behalten will

Was beim Deinstallieren wirklich passiert

Wir haben unser Plugin deinstalliert und dabei bewusst nicht --keep-user-data gesetzt. Danach waren beide Konfigurationswerte aus system_config verschwunden. Bei der Neuinstallation kamen sie mit den Standardwerten zurück. Die Konfiguration räumt Shopware also selbst weg.

Alles andere ist deine Aufgabe. In uninstall() musst du prüfen, ob der Betreiber Daten behalten will, und nur dann aufräumen, wenn nicht:

public function uninstall(UninstallContext $uninstallContext): void
{
    parent::uninstall($uninstallContext);

    if ($uninstallContext->keepUserData()) {
        return;
    }

    // Eigene Tabellen, Zusatzfelder, Mail-Vorlagen hier entfernen
}

Genau hier hatte das Generator-Plugin aus Falle 4 nur einen Kommentar stehen, und das Zusatzfeld-Set blieb liegen.

Updates brauchen eine neue Versionsnummer

Wir haben die Version in der composer.json von 1.0.0 auf 1.1.0 erhöht. Danach zeigte plugin:list zunächst weiter 1.0.0, ohne Hinweis auf ein Update. Erst nach plugin:refresh stand in der Spalte „Upgrade version“ die 1.1.0. plugin:update GmLieferhinweis brauchte dann 0,5 Sekunden.

Diese Reihenfolge ist wichtig, und sie ist die Ursache für einen Klassiker im Betrieb: Neuer Code wird auf den Server kopiert, die Version wird nicht erhöht, und der Shop liefert alte JavaScript- und CSS-Dateien aus, weil Shopware von der Änderung nichts weiß. Unsere Regel: Jede Auslieferung bekommt eine neue Versionsnummer, auch wenn sich nur eine Zeile geändert hat.

Deaktivieren wirkt sofort

Zum Schluss haben wir getestet, was beim Deaktivieren passiert. Nach plugin:deactivate war der Hinweis beim nächsten Seitenaufruf weg, schon bevor wir den Cache geleert hatten. Nach plugin:activate und cache:clear war er wieder da. Das ist beruhigend: Wenn ein Plugin im Live-Shop Ärger macht, ist Deaktivieren der schnellste Notausgang.

Weitere Bausteine: was ein Plugin sonst noch kann

Unser Lieferhinweis nutzt drei Bausteine: Konfiguration, Event-Subscriber, Template. Die meisten Plugins kommen mit diesen dreien aus. Für größere Aufgaben gibt es weitere, und für alle hat der Generator Beispiele:

Eigene Datenbanktabellen legst du über eine Entity-Definition und eine Migration an. Shopware hat dafür eine eigene Datenbankschicht (DAL), die Berechtigungen, Übersetzungen, Versionierung und die Admin-API automatisch mitbringt. Mit bin/console dal:migration:create erzeugst du aus einer Entity-Definition die passende Migration. Plane hierfür deutlich mehr Zeit ein als für den Rest, die DAL hat eine eigene Lernkurve. Wenn du dich zwischen Datenbanksystemen entscheiden musst, hilft unser Vergleich PostgreSQL vs MySQL, Shopware selbst unterstützt aber nur MySQL und MariaDB.

Konsolenbefehle sind normale Symfony-Commands. Sie sind ideal für Importe, Wartungsjobs und alles, was nicht über eine Webanfrage laufen soll.

Geplante Aufgaben (Scheduled Tasks) laufen in festen Abständen, etwa jede Nacht. Sie bestehen aus zwei Klassen: der Aufgabe selbst und einem Handler, der die eigentliche Arbeit macht. Das Generator-Gerüst legt in 6.6 nur die Aufgabe an, keinen Handler. Die Aufgabe erschien bei uns nach der Installation in scheduled-task:list mit 300 Sekunden Intervall, und scheduled-task:run-single lief ohne Fehler durch. Im ganzen Plugin-Ordner gab es aber keine Handler-Klasse, also nichts, das die eigentliche Arbeit erledigt. Ein Lauf ohne Fehlermeldung beweist hier nur, dass die Aufgabe eingeplant wurde. Damit geplante Aufgaben überhaupt laufen, muss auf dem Server ein Worker für die Nachrichtenwarteschlange laufen, typischerweise als systemd-Dienst oder über einen Cronjob.

Store-API-Routen sind eigene Endpunkte für Headless-Frontends und Apps. Sie brauchen, siehe Falle 3, einen öffentlichen Service. Was eine API überhaupt ist und wie sie sich von einer Webseite unterscheidet, erklärt unser Artikel Was ist eine API?.

Admin-Module erweitern die Verwaltungsoberfläche. Sie sind in JavaScript mit Vue.js geschrieben und müssen gebaut werden, bevor sie im Admin erscheinen. Das ist der aufwendigste Teil der Plugin-Entwicklung, weil du hier zwei Welten gleichzeitig brauchst: PHP für die Daten und Vue für die Oberfläche.

Storefront-JavaScript wird ebenfalls gebaut, über bin/build-storefront.sh oder bin/console theme:compile. Ohne diesen Schritt landet dein JavaScript nicht im Shop. In unserem Test war das alert()-Beispiel aus dem Gerüst deshalb nach der Installation nicht im ausgelieferten Theme. Es wäre beim nächsten Theme-Build automatisch dazugekommen.

Video: Shopware Knowledge DE, „Your first Shopware 6 Plugin within an hour!“ (Januar 2022, englisch). Ein offizieller Workshop von Shopware selbst, älter, aber für die Grundbegriffe Subscriber, Services und Templates weiter gut. Pfade für das Buy-Widget haben sich seitdem geändert.

Ein Plugin ausliefern: ZIP, Composer oder Store

Wenn das Plugin lokal läuft, muss es auf den Live-Server. Dafür gibt es drei Wege.

Als ZIP-Datei. Pack den Plugin-Ordner in ein ZIP, das den Ordnernamen als oberste Ebene enthält. Unser Lieferhinweis war als ZIP 5.964 Bytes groß, 18 Einträge. Hochladen geht im Admin unter Erweiterungen oder per Kommandozeile mit bin/console plugin:zip-import datei.zip, bei uns in 0,2 Sekunden. Das ist der Weg für einzelne Shops ohne Deployment-Prozess.

Über Composer. Das Plugin liegt in einem privaten Git-Repository, der Shop holt es per composer require. Das ist der Weg für Agenturen und alle, die ihre Shops versioniert ausliefern. Der Vorteil: Plugin-Version und Shop-Version stehen gemeinsam in der composer.lock, und jede Auslieferung ist reproduzierbar.

Über den Shopware Store. Wer sein Plugin verkaufen will, reicht es bei Shopware zur Prüfung ein. Dafür brauchst du ein Konto im Shopware Account, und das Plugin wird vor der Veröffentlichung technisch geprüft. Plane dafür einige Tage ein, und rechne damit, dass Kleinigkeiten wie fehlende Übersetzungen oder ein unsauberes Deinstallieren zurückkommen.

Egal welcher Weg: Danach auf dem Server plugin:refresh, plugin:install --activate beziehungsweise plugin:update, und cache:clear. Wenn dein Plugin JavaScript oder SCSS mitbringt, zusätzlich theme:compile. Und bitte vorher eine Sicherung der Datenbank. Wie man das sinnvoll aufbaut, steht in unserem Artikel zur 3-2-1-Backup-Strategie.

Was die Plugin-Entwicklung kostet

Eine Frage, die uns Shopbetreiber oft stellen: Lohnt sich ein eigenes Plugin, oder kauft man besser eines im Store?

Für einen Lieferhinweis wie unseren gibt es im Store fertige Lösungen, teils kostenlos, teils als Miete. Wenn eine davon passt, ist sie fast immer günstiger als eine Eigenentwicklung, weil der Hersteller die Updates übernimmt. Die eigentlichen Kosten eines Plugins entstehen nicht beim Schreiben, sondern danach. Shopware veröffentlicht häufig neue Versionen, allein zwischen dem 17.08. und dem 02.10.2026 waren es sechs, und jede größere Version kann Templates oder Schnittstellen ändern, wie unser Buy-Widget zeigt.

Ein eigenes Plugin lohnt sich, wenn:

  • es um eine Funktion geht, die dein Geschäft von anderen unterscheidet und die es so nicht zu kaufen gibt,
  • mehrere Store-Plugins zusammen mehr kosten und mehr Ladezeit fressen würden als eine schlanke Eigenlösung,
  • du Daten aus deinen eigenen Systemen (ERP, Warenwirtschaft) in den Shop bringen willst.

Rechne für ein kleines Plugin wie unseres mit wenigen Stunden Entwicklung und dann mit einem festen Wartungsaufwand bei jedem größeren Shopware-Update. Wie sich Plugin-Kosten in die Gesamtkosten eines Shops einfügen, haben wir in Was kostet Shopware 6? und Was kostet ein Shopware-Shop? durchgerechnet.

Checkliste für dein erstes Shopware Plugin

Bevor dein Plugin den Rechner verlässt, geh diese Liste durch. Jeder Punkt kommt aus einem Fehler, den wir oder unsere Kunden schon gemacht haben.

  1. Kein swag und kein example mehr im Code. grep -rn "swag\|example" src/ muss leer sein.
  2. composer.json passt: richtiger type, richtige Plugin-Klasse, require nur für Versionen, gegen die du getestet hast.
  3. Template-Pfad geprüft: Die Kerndatei, die du erweiterst, ist nicht als @deprecated markiert.
  4. sw_extends statt extends, und parent() dort, wo der Originalinhalt bleiben soll.
  5. Kein |raw auf Werten, die ein Mensch eingeben kann.
  6. Subscriber registriert: debug:event-dispatcher zeigt deinen Eintrag.
  7. Verkaufskanal-ID beim Lesen der Konfiguration übergeben.
  8. Installieren, deinstallieren, wieder installieren funktioniert ohne Fehler, auch ohne --keep-user-data.
  9. Versionsnummer erhöht für jede Auslieferung.
  10. Auf einer Kopie des Live-Shops getestet, nicht nur auf der Demo-Installation.

Häufige Fragen zum Shopware Plugin entwickeln

Wie erstelle ich ein Shopware 6 Plugin?

Am schnellsten mit bin/console plugin:create MeinPlugin MeinPlugin und danach plugin:refresh und plugin:install --activate MeinPlugin. Gib Name und Namespace immer beide an, sonst hängt der Befehl in Shopware 6.6 in einer Endlosschleife. Für ein sauberes Plugin empfehlen wir, nur die Pflichtdateien anzulegen (in 6.7 ab 6.7.13.0 mit --no-scaffold, in 6.6 von Hand) und Bausteine gezielt hinzuzufügen.

Welche Programmiersprache brauche ich für Shopware Plugins?

PHP für die Logik, Twig für die Templates, XML oder PHP für Konfiguration und Services. Für Erweiterungen im Admin kommt JavaScript mit Vue.js dazu, für das Aussehen im Shop SCSS. Für ein einfaches Plugin wie einen Lieferhinweis reichen PHP und Twig.

Muss ich Symfony können, um Shopware Plugins zu entwickeln?

Nicht vorher, aber du lernst es dabei. Shopware basiert auf Symfony, und Begriffe wie Service-Container, Event-Subscriber und Bundle kommen direkt daher. Wer Symfony schon kennt, findet sich in Shopware deutlich schneller zurecht.

Was ist der Unterschied zwischen einem Shopware Plugin und einer App?

Ein Plugin ist PHP-Code, der im Shop läuft und alles verändern darf. Eine App läuft auf einem eigenen Server und kommuniziert über Webhooks und die API mit dem Shop. Plugins funktionieren nur in selbst gehosteten Shops, Apps auch in der Shopware Cloud.

Wo liegen Shopware Plugins im Dateisystem?

Selbst entwickelte Plugins liegen unter custom/plugins/. Plugins, die per Composer installiert werden, liegen unter vendor/. Mit der Option --static legt der Generator Plugins unter custom/static-plugins/ an, das ist für Plugins gedacht, die fest zum Projekt gehören und per Composer eingebunden werden.

Warum erscheint meine Template-Änderung nicht im Shop?

Meist aus einem von drei Gründen: Du erweiterst eine veraltete Datei, die gar nicht mehr geladen wird (bei uns page/product-detail/buy-widget.html.twig), der Cache wurde nicht geleert, oder das Plugin ist installiert, aber nicht aktiviert. Prüfe zuerst die erste Zeile der Kerndatei auf @deprecated, dann plugin:list, dann cache:clear.

Warum ist meine Plugin-Einstellung „aus“, wirkt aber trotzdem?

Wenn du sie per bin/console system:config:set ... false gesetzt hast, steht in der Datenbank das Wort „false“ als Text, und das ist in PHP wahr. Setze Wahrheitswerte immer mit --json. Über den Admin gesetzte Schalter sind davon nicht betroffen.

Wie aktualisiere ich ein Shopware Plugin?

Versionsnummer in der composer.json erhöhen, neuen Code einspielen, bin/console plugin:refresh und dann bin/console plugin:update PluginName. Ohne plugin:refresh sieht Shopware die neue Version nicht. Danach cache:clear und, wenn das Plugin Frontend-Code enthält, theme:compile.

Läuft ein Plugin für Shopware 6.6 auch in 6.7?

Nicht automatisch. Die composer.json muss 6.7 erlauben, und du musst prüfen, ob die Templates, Events und Klassen, die du nutzt, in 6.7 noch existieren. Unser Template-Pfad für das Buy-Widget aus 6.6 existiert in 6.7.15.1 zum Beispiel nicht mehr. Teste gegen eine echte 6.7-Installation, bevor du die Versionsangabe erweiterst.

Wie lange dauert es, ein Shopware Plugin zu entwickeln?

Ein kleines Plugin wie unser Lieferhinweis ist mit Erfahrung in ein bis zwei Stunden fertig, beim ersten Mal eher in einem Tag, weil man die Struktur erst verstehen muss. Plugins mit eigenen Tabellen, Admin-Modulen oder Anbindungen an externe Systeme brauchen Tage bis Wochen. Der größere Aufwand ist langfristig die Pflege über Shopware-Updates hinweg.

Kann ich ein Shopware Plugin verkaufen?

Ja, über den Shopware Store. Dafür brauchst du ein Konto im Shopware Account, und das Plugin durchläuft vor der Veröffentlichung eine technische Prüfung. Alternativ verkaufen Agenturen Plugins direkt an Kunden und liefern sie per Composer aus einem privaten Repository aus.

Fazit: Shopware Plugin entwickeln ist leicht, sauber entwickeln nicht

Das Grundgerüst eines Shopware-Plugins ist klein. Sechs Dateien, ein Subscriber, ein Template, eine Konfiguration, und du hast eine Funktion, die der Betreiber selbst steuern kann. Das schafft jeder, der PHP kann, an einem Nachmittag.

Die Arbeit steckt in den Stellen, an denen nichts passiert und nichts gemeldet wird. Ein Template, das eine veraltete Datei erweitert, zeigt einfach nichts an. Ein Subscriber ohne Tag läuft einfach nicht. Ein Schalter, der „false“ als Text speichert, bleibt einfach an. Ein Zusatzfeld, das beim Deinstallieren nicht weggeräumt wird, fällt erst bei der nächsten Installation auf. Keiner dieser Fehler wirft eine Meldung, die dich zur Ursache führt.

Unser Rat deshalb: Nimm den Generator als Lehrbuch, baue dein Plugin von Hand und klein, prüfe jeden Baustein mit dem passenden debug-Befehl, und spiel einmal den ganzen Lebenszyklus durch, bevor das Plugin in einen echten Shop geht. Installieren, ändern, aktualisieren, deaktivieren, deinstallieren, neu installieren. Was das übersteht, übersteht auch den Alltag.