Product Custom Inputs: Technical
Technical details of Product Custom Inputs: flow diagram, entities and tables, plugin structure, migrations, separate validation for storefront and Shop API, tests.
Product Custom Inputs: Technical
This documentation always describes the latest published version.
Flow
graph TD
A["Add-to-cart form"] --> B["Collect values on submit"]
B --> C["sylius.cart_item_add, priority -100"]
C --> D{"New line or merge?"}
D -->|New line| E["Save values"]
D -->|Merge| F["Overwrite existing values"]
G["Shop API POST"] --> H{"Plugin enabled, cart, field matches?"}
H -->|No| I["404 or 422"]
H -->|Yes| E
E --> J["Display via Twig function"]
F --> J
The listener runs at priority -100 and thus after the Sylius listener. Only then is it known whether the line is new or was merged into an existing one.
Entities
erDiagram
FIELD ||--o{ FIELD_TRANSLATION : has
FIELD }o--o{ PRODUCT : attached_to
FIELD ||--o{ ORDER_ITEM_VALUE : filled_by
ORDER_ITEM ||--o{ ORDER_ITEM_VALUE : holds
| Table | Content |
|---|---|
mmd_sylius_product_custom_inputs_field |
Field definition: type, required, position, limits, select options (JSON) |
mmd_sylius_product_custom_inputs_field_translation |
Label and placeholder per locale, unique per field and locale |
mmd_sylius_product_custom_inputs_field_product |
Field to product attachment (many-to-many) |
mmd_sylius_product_custom_inputs_order_item_value |
Customer input, unique per order item and field |
mmd_sylius_product_custom_inputs_settings |
Singleton row (ID 1) holding the "Enabled" switch |
The attachment follows the ProductOption to Product pattern and sits on the field's side. The foreign key of the values lives on the value table, because OrderItem is a mapped superclass in Sylius that a plugin cannot extend with a relation.
Structure
src/
Api/ Provider, processor, ShopOrderItemResolver
Command/ SetupCommand
Controller/ Settings page
DependencyInjection/
Entity/ Fields, translation, values, settings, field type enum
EventListener/ CartItemAddCustomInputsListener
Factory/ Factory for fields
Form/ Admin forms, extensions for CartItemType and the product form
Repository/ Repositories for fields, values and settings
Menu/ AdminMenuListener, MenuGroup
Migrations/ Three Doctrine migrations
Twig/ OrderItemCustomInputValuesExtension
Validation/ ProductCustomInputFieldStringValueValidator
config/
grids/ routes/ twig_hooks/ services.php
templates/ admin/ and shop/
Migrations
Three migrations in the namespace Mmd\SyliusProductCustomInputsPlugin\Migrations, running after the Sylius core migrations: tables for field, translation and value, then the settings table with its initial row, finally the switch to globally attachable fields. They are executed through the setup command, which considers only these migrations and first checks that the Sylius core ones have run.
Validation
The storefront validates with typed Symfony form constraints, the Shop API with ProductCustomInputFieldStringValueValidator on the text format. These are two separate paths on purpose, because the input arrives in a different shape in each. The validator checks dates and times strictly, so values like 25:00 or 2026-02-30 are rejected.
Tests
PHPUnit: unit tests and functional tests (admin CRUD, API, form extensions, hook registration). The Behat scaffold exists but contains no scenarios yet. Static analysis with PHPStan at level max, coding standard with ECS.