Contact Requests: Technical Details
Sylius Contact Requests Plugin - architecture of the two mailer decorators, plugin structure, table and migration, how storing failures are logged
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:
sylius.mailer.contact_email_managerthroughconfig/services/mailer.xml, as a plain service decoration.sylius_shop.mailer.contact_email_managerthrough a dedicated compiler pass (DecorateShopContactEmailManagerPass). InExtension::load()there is no guarantee thatShopBundlehas already defined this service. Compiler passes only run after every extension has been loaded.
The second service is an implementation detail of
ShopBundle, not an official contract. The compiler pass therefore checks withhasDefinition()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.