Kleinunternehmer: Technische Details
Sylius-Plugin Kleinunternehmer §19 UStG: Architektur, Plugin-Struktur, Tabellen und Migrationen, Admin-Routen und Hooks, Gutschrift-Compiler-Pass sowie ersetzbare Services.
Kleinunternehmer: Technische Details
Live Demo: Teste dieses und weitere Plugins live — ohne Installation, ohne Risiko: demo.markus-michalski.net
Architektur
graph TD
A["KleinunternehmerSettings (Singleton)"] --> B["NoticeApplicabilityChecker"]
C["ZeroTaxCategoryProvider"] --> D["ProductPriceMigrationService"]
D --> E["MigratedProductVariant (Tabelle der migrierten Varianten)"]
B --> F["KleinunternehmerExtension (Twig)"]
B --> G["ShopNoticeNormalizer (Shop API)"]
F --> H["Twig Hooks: Storefront"]
F --> I["credit_memo.html.twig (Refund-Plugin)"]
J["Admin: Einstellungsseite"] --> A
J --> D
K["Konsole: migrate-prices"] --> D
Eine Klasse entscheidet, ob der Hinweis gilt: NoticeApplicabilityChecker. Storefront-Twig, Shop API und Gutschrift-PDF fragen alle sie, deshalb stimmen die drei Wege immer überein.
Plugin-Struktur
src/
├── Command/ # MigrateProductPricesCommand, SetupCommand
├── Controller/Admin/ # KleinunternehmerSettingsController, MigrateProductPricesAction
├── DependencyInjection/ # Extension, Configuration, Compiler/CreditMemoPdfTemplatePass
├── Entity/ # KleinunternehmerSettings, MigratedProductVariant
├── Form/Type/ # KleinunternehmerSettingsType
├── Menu/ # AdminMenuListener, MenuGroup
├── Migrations/ # drei Doctrine-Migrationen
├── Provider/ # ZeroTaxCategoryProvider
├── Repository/ # Repositories für beide Entities
├── Serializer/ # ShopNoticeNormalizer
├── Service/ # NoticeApplicabilityChecker, ProductPriceMigrationService
└── Twig/ # KleinunternehmerExtension
config/
├── routes/ # admin.yaml (shop.yaml ist leer)
├── services/ # Service-Definitionen
└── twig_hooks/ # admin_settings.yaml, shop_product_prices.yaml
templates/
├── admin/kleinunternehmer/ # Einstellungsseite, Migrationskarte
├── shop/ # Hinweis-Templates (Absatz und Tabellenzeile)
└── refund/ # credit_memo.html.twig
translations/ # messages und flashes, Deutsch und Englisch
Das Plugin definiert keine konfigurierbaren Schlüssel (Configuration ist leer) und liest keine Umgebungsvariablen.
Datenbank
erDiagram
mmd_kleinunternehmer_settings {
integer id PK
boolean active
text noticeTextDe
text noticeTextEn
}
mmd_kleinunternehmer_migrated_variant {
integer id PK
string productVariantCode UK
string originalTaxCategoryCode
datetime_immutable migratedAt
}
| Tabelle | Zweck |
|---|---|
mmd_kleinunternehmer_settings |
Genau eine Zeile mit id 1: Schalter und optionale Hinweistexte (je maximal 1000 Zeichen). Ohne Zeile liefert das Plugin Standardwerte, ohne sie zu speichern |
mmd_kleinunternehmer_migrated_variant |
Eine Zeile je aktuell migrierter Variante mit ursprünglicher Steuerkategorie, damit --restore die richtige Kategorie zurückbringt |
Die Entities sind einfache Doctrine-Entities mit PHP-Attributen, keine Sylius-Ressourcen. Es gibt deshalb kein Grid und keine CRUD-Routen.
Drei Migrationen unter dem Namensraum Mmd\SyliusKleinunternehmerPlugin\Migrations:
| Version | Inhalt |
|---|---|
Version20260828121602 |
Tabelle mmd_kleinunternehmer_settings |
Version20260828132209 |
Tabelle mmd_kleinunternehmer_migrated_variant mit eindeutigem Index auf productVariantCode |
Version20260829160426 |
Spalten noticeTextDe und noticeTextEn |
Die Migrationen nutzen die Schema-API von Doctrine statt rohem SQL, damit sie nicht an einer Datenbankplattform hängen. Der Namensraum muss in der Host-Anwendung eindeutig bleiben.
Wie das Bundle sich einhängt
- Doctrine-Mapping und Migrationen: Die Erweiterung hängt das ORM-Mapping für
src/Entity/und die Migrationen inprepend()ein. - Twig Hooks: Auch die YAML-Dateien aus
config/twig_hooks/lädt die Erweiterung selbst inprepend(). Inload()ginge es nicht, weil Symfony dort für jede Erweiterung einen leeren Container-Builder reicht, in demsylius_twig_hooksnie registriert ist. - Gutschrift-Template: Der Compiler-Pass
CreditMemoPdfTemplatePasstauscht im Servicesylius_refund.generator.credit_memo_pdf_filedas Template@SyliusRefundPlugin/download/credit_memo.html.twig(Argument 2) gegen@MmdSyliusKleinunternehmerPlugin/refund/credit_memo.html.twigaus. Der Pass tut nichts, wenn das Refund-Plugin fehlt oder der Generator schon ein anderes Template nutzt. Registriert wird er inMmdSyliusKleinunternehmerPlugin::build(). - Admin-Menü:
AdminMenuListenerhängt den Eintrag unter die gemeinsame Gruppe „MMKreativ Plugins“.MenuGroupist bewusst in jedes MMKreativ-Plugin kopiert, nur der Gruppen-Codemmd_pluginsmuss übereinstimmen. Wer zuerst läuft, legt die Gruppe an. - Admin-Routen: Beide Pfade bauen auf dem Parameter
sylius_admin.path_nameauf statt auf festem/admin, damit sie innerhalb der Firewall bleiben, falls du den Admin-Pfad änderst.
Admin-Hooks
Die Einstellungsseite rendert unter dem Hook-Präfix sylius_admin.mmd_kleinunternehmer_settings.index und erbt das Grundgerüst (Sidebar, Navbar, Kopfzeile) von sylius_admin.common.index. Die Hookables in config/twig_hooks/admin_settings.yaml sind activation_error (Priorität 110), form (100), additional_languages_hint (75) und migration (50) im Inhaltsbereich sowie title (100) und actions (0) im Titelblock.
Routen
| Route | Pfad | Methoden |
|---|---|---|
mmd_sylius_kleinunternehmer_admin_settings |
/<admin>/kleinunternehmer/settings |
GET, POST |
mmd_sylius_kleinunternehmer_admin_migrate |
/<admin>/kleinunternehmer/migrate |
POST |
Steuerkategorie und Zone
Die Konstanten für Kategorie und Steuersatz sind kleinunternehmer_zero und kleinunternehmer_zero_rate. Die Zone DE ist fest im Code verankert. Das ist eine bewusste Vereinfachung, die Zone wird nicht dynamisch abgeleitet.
Erweiterbarkeit
Das Plugin definiert keine eigenen Events, Service-Tags oder Interfaces für Dritte. Über die normale Symfony-Service-Dekoration lassen sich drei öffentliche Services ersetzen:
| Interface | Standard-Implementierung | Aufgabe |
|---|---|---|
ZeroTaxCategoryProviderInterface |
ZeroTaxCategoryProvider |
0-%-Kategorie und Steuersatz anlegen |
ProductPriceMigrationServiceInterface |
ProductPriceMigrationService |
Preise migrieren und zurücksetzen |
NoticeApplicabilityCheckerInterface |
NoticeApplicabilityChecker |
Entscheidet, ob der Hinweis gilt |
Die Methoden im Überblick:
| Interface | Methoden |
|---|---|
NoticeApplicabilityCheckerInterface |
applies(?order, ?variant, ?product), appliesToTaxTotal(int) |
ProductPriceMigrationServiceInterface |
migrateToSmallBusiness(?callable), restoreFromSmallBusiness(?callable), countPendingMigration(), countMigrated() |
ZeroTaxCategoryProviderInterface |
getOrCreate() |
Zusätzlich liefert KleinunternehmerSettingsRepository::getSettings() den Einstellungs-Datensatz. Fehlt die Zeile, gibt es einen nicht gespeicherten Standardwert zurück, damit ein Seitenaufruf in der Storefront nie schreibt.
Ersetzt du NoticeApplicabilityChecker, gilt deine Entscheidung für Storefront, Shop API und Gutschrift-PDF gleichermaßen. Ansonsten hängt sich das Plugin ohne Besonderheit ein: AdminMenuListener hört auf das Sylius-Event sylius.menu.admin.main, und ShopNoticeNormalizer ist ein normaler Serializer-Normalizer mit Priorität 60.
Versionsanzeige
Unter dem Titel der Einstellungsseite steht die installierte Version (kleinunternehmer_plugin_version(), liest Composer\InstalledVersions). Bei einem getaggten Release ist das die Versionsnummer, bei einer lokalen Pfad-Installation dev-main plus die ersten sieben Zeichen des Commit-Hashes.