- Startseite
- Blog
- 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.sh4. 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
| Fehler | Folge | Besser |
|---|---|---|
| Core-Klassen überschrieben | Bricht beim nächsten Update | Event oder Decoration |
| Komplettes Twig-Template kopiert | Verpasste Core-Fixes, kaputtes Layout | Nur den nötigen Block überschreiben |
| Direkte SQL-Queries statt DAL | Kein Caching, keine Events, keine Versionierung | Repository und Criteria nutzen |
| Keine Migration, Tabelle manuell angelegt | Läuft lokal, nicht auf Produktion | Migration-Klasse schreiben |
| Übersetzungen hart im Template | Nicht mehrsprachfähig | Snippets 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 --diffFazit
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.