Markus Michalski FAQ

Contact Requests: Technical Details

Sylius Contact Requests Plugin - architecture of the two mailer decorators, plugin structure, table and migration, how storing failures are logged

Deutsche Version

Back to overview

Contact Requests: Technical Details

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

Architecture

graph LR
    A["Twig contact form (shop)"] --> D["sylius_shop.mailer.contact_email_manager"]
    B["Shop API SendContactRequest"] --> E["sylius.mailer.contact_email_manager"]
    D --> F["PersistingContactEmailManager (decorator)"]
    E --> F
    F --> G["Store ContactRequest"]
    F --> H["Original manager: send e-mail"]

For sending contact requests, Sylius ships two independent services of the same mailer (ContactEmailManager, interface ContactEmailManagerInterface): sylius.mailer.contact_email_manager is used by the Shop API path, sylius_shop.mailer.contact_email_manager by the classic Twig contact form, which ShopBundle registers on its own. The plugin decorates both:

The second service is an implementation detail of ShopBundle, not an official contract. The compiler pass therefore checks with hasDefinition() whether it exists. If a future Sylius version removes it, the plugin only stores requests that come in through the Shop API, instead of letting the container build fail.

Failure-safe storing

When storing fails, Doctrine closes the entity manager (this happens after every failed flush()). The decorator therefore calls ManagerRegistry::resetManager(). Otherwise every later write in the same request would fail with "The EntityManager is closed".

Only the exception class and code are logged, never getMessage() and never the exception object:

Failed to persist contact request: Doctrine\DBAL\Exception\... (code 0)

The reason: with conversion errors, Doctrine's message often contains the offending field value in plain text, and this log has no retention policy of its own.

Plugin structure

src/
β”œβ”€β”€ Command/            # PurgeContactRequestsCommand, SetupCommand
β”œβ”€β”€ DependencyInjection/
β”‚   └── Compiler/        # DecorateShopContactEmailManagerPass
β”œβ”€β”€ Entity/              # ContactRequest, ContactRequestInterface
β”œβ”€β”€ Mailer/              # PersistingContactEmailManager (for both services)
β”œβ”€β”€ Menu/                # Entry in the Admin sidebar
β”œβ”€β”€ Migrations/          # One Doctrine migration
β”œβ”€β”€ Repository/          # ContactRequestRepository (purgeOlderThan)
└── Twig/                # ContactRequestsExtension (version display in Admin)
config/
β”œβ”€β”€ grids/               # One Admin grid
β”œβ”€β”€ routes/              # admin.yaml (shop.yaml is empty, there are no shop routes of its own)
└── twig_hooks/          # Title row and detail view in Admin
templates/
└── admin/contact_request/   # Templates for the title row and detail view

Table and migration

One table, one migration: ContactRequest lives in mmd_sylius_contact_requests_contact_request (Version20260904121117). The columns are id, createdAt (indexed), name (optional), email, subject (optional), message (LONGTEXT) and consent (BOOLEAN, default false).

Version display in Admin

The title row in Admin shows the installed plugin version (mmd_contact_requests_plugin_version(), reads Composer\InstalledVersions). For a tagged Packeton release this is the semver number, for a local path installation it is dev-main plus the first seven characters of the commit hash.