LeistungenHeadlessReferenzenFür AgenturenBlogPreiseKontakt Kostenloses Erstgespräch
  1. Startseite
  2. Blog
  3. Entwicklung

Entwicklung

Shopware Plugin entwickeln: Der komplette Einstieg für Shopware 6

Die meisten Shopware-Probleme entstehen nicht beim Programmieren, sondern bei der Entscheidung, wo man sich einklinkt. Dieser Leitfaden zeigt die Extension-Points, die auch nach dem nächsten Update noch funktionieren.

Ein Shopware-6-Plugin ist im Kern ein Symfony-Bundle mit ein paar Zusatzregeln. Wer das verstanden hat, findet sich schnell zurecht – wer es nicht verstanden hat, überschreibt irgendwann Core-Klassen und wundert sich beim nächsten Minor-Release über einen weißen Bildschirm.

Dieser Artikel geht den Weg einmal komplett durch: Struktur, Registrierung, die wichtigsten Erweiterungspunkte und die Stellen, an denen es in der Praxis regelmäßig knallt.

1. Die Verzeichnisstruktur

Plugins liegen unter custom/plugins/. Der Verzeichnisname entspricht dem Plugin-Namen und der Hauptklasse:

custom/plugins/SwagB2bPricing/
├── composer.json
├── src/
│   ├── SwagB2bPricing.php          # Hauptklasse
│   ├── Core/                        # Geschäftslogik
│   ├── Migration/                   # Datenbank-Migrationen
│   ├── Subscriber/                  # Event-Subscriber
│   └── Resources/
│       ├── config/
│       │   ├── services.xml         # DI-Container
│       │   └── config.xml           # Plugin-Einstellungen
│       ├── views/storefront/        # Twig-Overrides
│       └── app/
│           ├── administration/      # Vue-Module
│           └── storefront/          # JS für die Storefront
└── tests/

2. composer.json und Hauptklasse

Die composer.json deklariert das Paket als Shopware-Plugin. Entscheidend sind type und der shopware-plugin-class-Eintrag unter extra:

{
  "name": "swag/b2b-pricing",
  "description": "Kundenindividuelle Preislisten für B2B-Shops",
  "version": "1.0.0",
  "type": "shopware-platform-plugin",
  "license": "MIT",
  "require": {
    "shopware/core": "~6.7.0"
  },
  "autoload": {
    "psr-4": {
      "Swag\\B2bPricing\\": "src/"
    }
  },
  "extra": {
    "shopware-plugin-class": "Swag\\B2bPricing\\SwagB2bPricing",
    "label": { "de-DE": "B2B Preislisten", "en-GB": "B2B price lists" }
  }
}

Die Hauptklasse erbt von Shopware\Core\Framework\Plugin. Oft bleibt sie leer – interessant wird sie nur, wenn du auf Installation, Update oder Deinstallation reagieren musst:

<?php declare(strict_types=1);

namespace Swag\B2bPricing;

use Shopware\Core\Framework\Plugin;
use Shopware\Core\Framework\Plugin\Context\UninstallContext;

class SwagB2bPricing extends Plugin
{
    public function uninstall(UninstallContext $context): void
    {
        parent::uninstall($context);

        // Bei "Daten behalten" nichts löschen – der Nutzer hat das so gewählt
        if ($context->keepUserData()) {
            return;
        }

        $connection = $this->container->get(\Doctrine\DBAL\Connection::class);
        $connection->executeStatement('DROP TABLE IF EXISTS `swag_b2b_price_list`');
    }
}

3. Installieren und aktivieren

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

# Nach Änderungen an der Administration
bin/build-administration.sh

# Nach Änderungen an der Storefront
bin/build-storefront.sh

4. Services registrieren

Shopware nutzt den Symfony-Container. Alles, was du in services.xml registrierst, ist per Constructor-Injection verfügbar:

<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services">
    <services>
        <service id="Swag\B2bPricing\Core\PriceListLoader">
            <argument type="service" id="swag_b2b_price_list.repository"/>
            <argument type="service" id="cache.object"/>
        </service>

        <service id="Swag\B2bPricing\Subscriber\CustomerPriceSubscriber">
            <argument type="service" id="Swag\B2bPricing\Core\PriceListLoader"/>
            <tag name="kernel.event_subscriber"/>
        </service>
    </services>
</container>

5. Die vier wichtigsten Erweiterungspunkte

Event-Subscriber

Der sauberste Weg, sich in bestehende Abläufe einzuklinken. Shopware wirft Events für so ziemlich jeden relevanten Vorgang – Listing geladen, Warenkorb berechnet, Bestellung geschrieben:

<?php declare(strict_types=1);

namespace Swag\B2bPricing\Subscriber;

use Shopware\Core\Content\Product\Events\ProductListingResultEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CustomerPriceSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private readonly PriceListLoader $priceListLoader
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            ProductListingResultEvent::class => 'onListingLoaded',
        ];
    }

    public function onListingLoaded(ProductListingResultEvent $event): void
    {
        $context  = $event->getSalesChannelContext();
        $customer = $context->getCustomer();

        if ($customer === null) {
            return;
        }

        $priceList = $this->priceListLoader->load($customer->getId(), $context);

        foreach ($event->getResult()->getEntities() as $product) {
            $priceList->applyTo($product, $context);
        }
    }
}

Service Decoration

Wenn ein Event nicht reicht, dekorierst du den bestehenden Service, statt ihn zu ersetzen. Das Original bleibt erhalten und du kannst es weiter aufrufen:

<service id="Swag\B2bPricing\Core\CartProcessorDecorator"
         decorates="Shopware\Core\Checkout\Cart\Processor">
    <argument type="service"
              id="Swag\B2bPricing\Core\CartProcessorDecorator.inner"/>
</service>

Entity Extensions

Eigene Felder an bestehenden Entitäten hängst du über eine EntityExtension an – nicht über eine Änderung der Core-Definition. Für einfache Zusatzinformationen reichen oft auch Custom Fields, die dein Team dann selbst in der Administration pflegen kann.

Twig-Overrides

Templates erweiterst du mit sw_extends und überschreibst nur den Block, den du wirklich brauchst. Kopiere niemals ein komplettes Core-Template – sonst verpasst du jede Verbesserung, die Shopware daran vornimmt:

{% sw_extends '@Storefront/storefront/component/product/card/price-unit.html.twig' %}

{% block component_product_box_price_unit %}
    {{ parent() }}

    {% if page.extensions.b2bPriceList %}
        <p class="b2b-price-hint">
            {{ "swag-b2b.listing.customerPrice"|trans }}
        </p>
    {% endif %}
{% endblock %}

6. Administration erweitern

Die Shopware Administration ist eine Vue-Anwendung. Eigene Module registrierst du in src/Resources/app/administration/src/main.js. Bestehende Komponenten erweiterst du über Component.override(), eigene legst du mit Component.register() an. Nach jeder Änderung muss die Administration neu gebaut werden – im Entwicklungsalltag läuft dafür üblicherweise ein Watcher.

7. Was in der Praxis schiefgeht

FehlerFolgeBesser
Core-Klassen überschriebenBricht beim nächsten UpdateEvent oder Decoration
Komplettes Twig-Template kopiertVerpasste Core-Fixes, kaputtes LayoutNur den nötigen Block überschreiben
Direkte SQL-Queries statt DALKein Caching, keine Events, keine VersionierungRepository und Criteria nutzen
Keine Migration, Tabelle manuell angelegtLäuft lokal, nicht auf ProduktionMigration-Klasse schreiben
Übersetzungen hart im TemplateNicht mehrsprachfähigSnippets nutzen

8. Absichern

Ein Plugin ohne Tests ist eine Wette darauf, dass sich nie etwas ändert. Minimum: PHPUnit für die Geschäftslogik und eine statische Analyse. Beides läuft problemlos in einer CI-Pipeline:

vendor/bin/phpunit --testsuite unit
vendor/bin/phpstan analyse --level=6 src/
vendor/bin/php-cs-fixer fix --dry-run --diff

Fazit

Sauberer Shopware-Code ist keine Frage von Eleganz, sondern von Betriebskosten. Ein Plugin, das nur dokumentierte Extension-Points nutzt, überlebt Updates ohne Zutun. Eines, das sich in den Core hackt, kostet bei jedem Release Entwicklerzeit – oft mehr, als die ursprüngliche Entwicklung gespart hat.

Loslegen

Dein Shop läuft nicht so,
wie er laufen sollte?

Schreib mir in zwei Sätzen, worum es geht. Ich melde mich innerhalb eines Werktags mit einer ersten Einschätzung – kostenlos und unverbindlich.

Kontakt

Direkter Draht

Kein Ticketsystem, kein Callcenter. Deine Nachricht landet direkt bei mir.

E-Mailmail@timhallmann.de Telefon+49 (0) 2661 208994-0 Mobil+49 (0) 151 41 34 95 52 WhatsAppDirekt schreiben
Sitz57520 Langenbach bei Kirburg
ErreichbarkeitMo–Fr, 9:00–12:00 & 13:00–18:00 Uhr

* Pflichtfeld · Antwort in der Regel innerhalb eines Werktags · Keine Weitergabe an Dritte · Für Aufträge gelten meine AGB

Dieses Formular ist durch reCAPTCHA geschützt. Es gelten die Datenschutzerklärung und die Nutzungsbedingungen von Google. Der Schutz wird erst aktiv, wenn du anfängst zu tippen.