Markus Michalski FAQ

Small Business: Technical details

Sylius plugin Small Business Β§19 UStG: architecture, plugin structure, tables and migrations, admin routes and hooks, credit memo compiler pass and replaceable services.

Deutsche Version

Back to overview

Small Business: Technical details

Live Demo: Try this and other plugins live β€” no installation, no risk: demo.markus-michalski.net

Architecture

graph TD
    A["KleinunternehmerSettings (singleton)"] --> B["NoticeApplicabilityChecker"]
    C["ZeroTaxCategoryProvider"] --> D["ProductPriceMigrationService"]
    D --> E["MigratedProductVariant (tracking table)"]
    B --> F["KleinunternehmerExtension (Twig)"]
    B --> G["ShopNoticeNormalizer (Shop API)"]
    F --> H["Twig hooks: storefront"]
    F --> I["credit_memo.html.twig (refund plugin)"]
    J["Admin: settings page"] --> A
    J --> D
    K["Console: migrate-prices"] --> D

One class decides whether the notice applies: NoticeApplicabilityChecker. Storefront Twig, Shop API and credit memo PDF all ask it, so the three paths always agree.

Plugin structure

src/
β”œβ”€β”€ Command/              # MigrateProductPricesCommand, SetupCommand
β”œβ”€β”€ Controller/Admin/     # KleinunternehmerSettingsController, MigrateProductPricesAction
β”œβ”€β”€ DependencyInjection/  # Extension, Configuration, Compiler/CreditMemoPdfTemplatePass
β”œβ”€β”€ Entity/               # KleinunternehmerSettings, MigratedProductVariant
β”œβ”€β”€ Form/Type/            # KleinunternehmerSettingsType
β”œβ”€β”€ Menu/                 # AdminMenuListener, MenuGroup
β”œβ”€β”€ Migrations/           # three Doctrine migrations
β”œβ”€β”€ Provider/             # ZeroTaxCategoryProvider
β”œβ”€β”€ Repository/           # repositories for both entities
β”œβ”€β”€ Serializer/           # ShopNoticeNormalizer
β”œβ”€β”€ Service/              # NoticeApplicabilityChecker, ProductPriceMigrationService
└── Twig/                 # KleinunternehmerExtension
config/
β”œβ”€β”€ routes/               # admin.yaml (shop.yaml is empty)
β”œβ”€β”€ services/             # service definitions
└── twig_hooks/           # admin_settings.yaml, shop_product_prices.yaml
templates/
β”œβ”€β”€ admin/kleinunternehmer/   # settings page, migration card
β”œβ”€β”€ shop/                     # notice templates (paragraph and table row)
└── refund/                   # credit_memo.html.twig
translations/             # messages and flashes, German and English

The plugin defines no configurable keys (Configuration is empty) and reads no environment variables.

Database

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
    }
Table Purpose
mmd_kleinunternehmer_settings Exactly one row with id 1: the switch and the optional notice texts (at most 1000 characters each). Without a row the plugin returns defaults without saving them
mmd_kleinunternehmer_migrated_variant One row per currently migrated variant with its original tax category, so that --restore puts the right category back

The entities are plain Doctrine entities with PHP attributes, not Sylius resources. That is why there is no grid and no CRUD routes.

Three migrations under the namespace Mmd\SyliusKleinunternehmerPlugin\Migrations:

Version Content
Version20260828121602 Table mmd_kleinunternehmer_settings
Version20260828132209 Table mmd_kleinunternehmer_migrated_variant with a unique index on productVariantCode
Version20260829160426 Columns noticeTextDe and noticeTextEn

The migrations use Doctrine's Schema API instead of raw SQL so they do not depend on one database platform. The namespace must stay unique in the host application.

How the bundle hooks in

Admin hooks

The settings page renders under the hook prefix sylius_admin.mmd_kleinunternehmer_settings.index and inherits its basic frame (sidebar, navbar, header) from sylius_admin.common.index. The hookables in config/twig_hooks/admin_settings.yaml are activation_error (priority 110), form (100), additional_languages_hint (75) and migration (50) in the content area, as well as title (100) and actions (0) in the title block.

Routes

Route Path Methods
mmd_sylius_kleinunternehmer_admin_settings /<admin>/kleinunternehmer/settings GET, POST
mmd_sylius_kleinunternehmer_admin_migrate /<admin>/kleinunternehmer/migrate POST

Tax category and zone

The constants for category and tax rate are kleinunternehmer_zero and kleinunternehmer_zero_rate. The zone DE is hard-coded. This is a deliberate simplification, the zone is not derived dynamically.

Extensibility

The plugin defines no events, service tags or interfaces of its own for third parties. Through normal Symfony service decoration you can replace three public services:

Interface Default implementation Task
ZeroTaxCategoryProviderInterface ZeroTaxCategoryProvider Create the 0% category and tax rate
ProductPriceMigrationServiceInterface ProductPriceMigrationService Migrate and restore prices
NoticeApplicabilityCheckerInterface NoticeApplicabilityChecker Decides whether the notice applies

The methods at a glance:

Interface Methods
NoticeApplicabilityCheckerInterface applies(?order, ?variant, ?product), appliesToTaxTotal(int)
ProductPriceMigrationServiceInterface migrateToSmallBusiness(?callable), restoreFromSmallBusiness(?callable), countPendingMigration(), countMigrated()
ZeroTaxCategoryProviderInterface getOrCreate()

In addition, KleinunternehmerSettingsRepository::getSettings() returns the settings record. If the row is missing, it returns an unsaved default so that a storefront page view never writes.

If you replace NoticeApplicabilityChecker, your decision applies equally to storefront, Shop API and credit memo PDF. Otherwise the plugin hooks in without anything special: AdminMenuListener listens to the Sylius event sylius.menu.admin.main, and ShopNoticeNormalizer is a regular serializer normalizer with priority 60.

Version display

Below the title of the settings page, the installed version is shown (kleinunternehmer_plugin_version(), reads Composer\InstalledVersions). For a tagged release this is the version number, for a local path installation dev-main plus the first seven characters of the commit hash.