You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix: variant selector missing on freshly-added price tier rows
The PRE_SET_DATA listener in PriceTierType added the productVariant
field only when $priceTier->getProduct() was non-null. Existing tiers
loaded from the DB had it set; brand-new tiers added via the LiveCollectionType
"Add" button did not, because Live Components builds the new row's form
*before* ProductTrait::addPriceTier() assigns the backreference.
Mirror Sylius core's product-images pattern: forward the parent product
down through entry_options.
- PriceTierType: drop the PRE_SET_DATA listener; add a `product` option
(ProductInterface|null, default null); add the productVariant field
unconditionally whenever the option is set.
- PriceTierCollectionType: replace the static entry_options default
with a normalizer that always merges `label => false` with whatever
the caller passes (so an externally-passed `product` doesn't wipe
the label default).
- ProductTypeExtension: pass `entry_options.product => $options['data']`
when adding the priceTiers field; falls back to null on the create
page where the parent product hasn't been instantiated yet.
Test updates (form tests):
- PriceTierTypeTest: drop the PRE_SET_DATA-based tests, replace with
option-driven ones (product passed -> variant field present; option
absent or null -> field absent).
- PriceTierCollectionTypeTest: add a normalizer-merge test ensuring
caller-passed entry_options (e.g. `product`) round-trip alongside
`label => false`.
- ProductTypeExtensionTest: rewrite around a captured-args helper so
both the no-data create-page case and the data-bound update-page
case can verify entry_options.product is forwarded.
CLAUDE.md gains a convention bullet warning against the PRE_SET_DATA
pattern for context-dependent per-entry fields.
61 tests / 118 assertions, ECS + PHPStan max all green. Browser-
verified in Playwright: clicking Add on product 37's price-tiers tab
produces a second row that renders the variant selector identically
to the first.
Copy file name to clipboardExpand all lines: CLAUDE.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -54,7 +54,7 @@ Wiring:
54
54
55
55
-`SetonoSyliusTierPricingPlugin` extends `AbstractResourceBundle` + `SyliusPluginTrait`, ORM-only. It overrides **both**`getPath()` (returns `dirname(__DIR__)` so Sylius sees the repo root) **and**`getConfigFilesPath()` (returns `<root>/config/doctrine/<format>` — without this override, Sylius' `AbstractResourceBundle::getConfigFilesPath()` would still look under `Resources/config/doctrine/`).
56
56
- DI services live in `config/services/*.php` (PHP DSL via `ContainerConfigurator`), imported from `config/services.php`, loaded by the extension via `PhpFileLoader`. **Do not** ship XML service config — the Setono Sylius-2 convention is PHP for IDE refactoring, PHPStan analysis at compile time, and FQCN service ids. Each leaf file starts with `namespace Symfony\Component\DependencyInjection\Loader\Configurator;` so `service()`, `param()`, etc. resolve as bare function calls. The extension implements `PrependExtensionInterface`: its `prepend()` injects the plugin's `sylius_twig_hooks` configuration directly via `prependExtensionConfig()` so consumers don't need to import anything. **Convention:** other-bundle configuration belongs in `prepend()` and **must be inlined as a PHP array** — do not read/parse a YAML file from disk and forward it.
57
-
-`Form\Extension\ProductTypeExtension` adds the `priceTiers` field to the Sylius `ProductType`. `Form\Type\PriceTierCollectionType` extends `Symfony\UX\LiveComponent\Form\Type\LiveCollectionType` (mirroring how Sylius admin handles product images) so add/delete fire server-side via Symfony UX Live Components — no client-side prototype-cloning JS. To survive LiveCollectionType's empty-bind cycle on `addCollectionItem`, `PriceTier::setQuantity()` and `setDiscount()` are nullable-and-no-op-on-null rather than using form-level `empty_data` defaults — keeps the workaround out of `PriceTierType` and makes the model the single source of truth for its defaults (`quantity = 1`, `discount = '0.0'`).
57
+
- `Form\Extension\ProductTypeExtension` adds the `priceTiers` field to the Sylius `ProductType` and forwards the parent product down as `entry_options.product` so each per-row `PriceTierType` (including freshly-added rows that aren't yet linked to a product) can render the variant selector for the current product. `Form\Type\PriceTierCollectionType` extends `Symfony\UX\LiveComponent\Form\Type\LiveCollectionType` (mirroring how Sylius admin handles product images) so add/delete fire server-side via Symfony UX Live Components — no client-side prototype-cloning JS. It uses an `entry_options` normalizer to always merge `label => false` with whatever the caller passes (so the product passed down by `ProductTypeExtension` doesn't wipe the label default). To survive LiveCollectionType's empty-bind cycle on `addCollectionItem`, `PriceTier::setQuantity()` and `setDiscount()` are nullable-and-no-op-on-null rather than using form-level `empty_data` defaults — keeps the workaround out of `PriceTierType` and makes the model the single source of truth for its defaults (`quantity = 1`, `discount = '0.0'`).
58
58
- The admin product tab is rendered by Twig hooks (`templates/admin/product/form/{side_navigation,sections}/price_tiers.html.twig`) registered against `sylius_admin.product.{update,create}.content.form.{side_navigation,sections}` — the canonical Sylius 2 hook points (declared by the core in `vendor/sylius/sylius/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/product/update.yaml`). `ProductFormMenuSubscriber` is gone in v2 — Sylius 2 has no `ProductMenuBuilderEvent`.
59
59
60
60
## Conventions
@@ -65,6 +65,7 @@ Wiring:
65
65
- CI matrix exercises PHP 8.2/8.3/8.4 × Symfony 6.4/7.4 × lowest/highest deps via the `setono/sylius-plugin/<job>@v2` composite actions. Keep changes compatible across that matrix.
66
66
- For Doctrine: ORM 3 is in play (Sylius 2 pulls it). Use `#[ORM\*]` attributes — not `@ORM\*` PHPDoc tags, which are silently ignored in attribute-mapped entities.
67
67
- Required scalar setters on entities that participate in a `LiveCollectionType` should accept null and treat it as a no-op (keeping the existing value), instead of forcing `empty_data` defaults at the form level. LiveCollectionType re-binds the parent form on `addCollectionItem` before the user types anything; non-nullable setters explode with `InvalidTypeException`, and pushing the default to the form means the model has two sources of truth for its initial state.
68
+
- Per-entry form fields inside a `LiveCollectionType` that depend on **parent-entity context** (e.g. the variant selector needing to know which product owns the tier) must receive that context via `entry_options` from the outer form extension, not via a `PRE_SET_DATA` listener on the entry type that reads `$entry->getParent()`. The PRE_SET_DATA approach fires for entities loaded from the DB but silently omits the field on freshly-added rows because Live Components build the empty entry's form *before* the parent's `add*()` assigns the backreference.
68
69
- Do **not** revert `getConfigFilesPath()` to default — Doctrine mapping discovery breaks immediately.
69
70
- Configuration of other bundles lives in `SetonoSyliusTierPricingExtension::prepend()` as inlined PHP arrays passed to `prependExtensionConfig()`. Do not read YAML files from disk inside `prepend()` and forward the parsed result.
70
71
- DI service configuration is **PHP DSL**, not XML. New services go under `config/services/<topic>.php` using `ContainerConfigurator`. Use the FQCN as the service id and alias `*Interface` to it for forward compatibility.
0 commit comments