1.x rewrite (Sylius 1.13/1.14, no API layer)#269
Conversation
Phase 1 (skeleton): - New 1.x branch; drop API layer, GiftCardConfiguration family, Behat, psalm, knp-snappy, safe-writer, jms/lexik - Tooling per SyliusPluginSkeleton 1.14.x: PHPStan (max), Rector, Infection, shipmonk dependency analysis, playwright MCP; Symfony ^6.4, Sylius ~1.13/~1.14 - Bundle extension auto-configures host app via prepend() - Redemption mode config (adjustment|payment) selects service file Phase 2 (domain model): - Reshaped GiftCard (deliveryType enum, design FK, optimistic version, transaction ledger, isUsable/isPending); explicit initialAmount - New GiftCardDesign (translatable, front/back images, channels) + lazy seeding hooks - New GiftCardTransaction append-only balance ledger - Grouped unambiguous code generator + normalizer; SQL balance aggregation - All quality gates green; schema validates; container boots on Sylius 1.14
- GiftCardDesignProvider with lazy Classic seeding (bundled default image, concurrency-guarded) - Admin grid (prepended sylius_grid) with thumbnail field, admin form template, menu entry - Design fixture + example factory (front/back image upload) - Clean 1.x translations - Verified: fixtures load against DB, Classic design + image persist, 20 cards created - All quality gates green
- GiftCardInformation DTO + form (amount, message, design picker); designs unified across virtual/physical - AddToCartTypeExtension + CartGiftCardHandler: pending disabled GiftCard per OrderItemUnit at add-to-cart (units exist at POST_SUBMIT via quantity data mapper) - ValidGiftCardAmount constraint + channel-aware amount limits provider - PendingGiftCardCleanupListener (onFlush) removes pending cards for deleted units - Product-page section (sylius_ui prepend) with live HTML preview (vanilla JS/CSS) - gift_card_product fixture: product + delivery option + virtual/physical variants (verified: correct per-variant shipping_required) - Quality gates green; fixtures load clean
- OrderGiftCardOperator: reconcile (checkout complete), enable (pay), disable (cancel) - Winzou callbacks prepended (verified registered) - reconcile snapshots final amount, creates cards for quantity-bumped units, associates customer - send-on-pay deferred to phase 10 (email), scaffold to phase 11 Also: yarn install + build for the test app (shop assets)
Shared core: - EligibleTotalCalculator (excludes gift-card line items) - GiftCardCoverageCalculator + GiftCardCoverage VO (stacking, caps, skips unusable/mismatched) - GiftCardBalanceOperator: sole balance mutator, append-only ledger, idempotency, InsufficientGiftCardBalanceException, manual adjust() - GiftCardApplicator rewritten with guards, delegates to aliased redemption method - GiftCardRedemptionMethodInterface + abstract RedemptionMethod base - GiftCardIsApplicable compound constraint Adjustment mode: - GiftCardAdjustmentProcessor (negative order_gift_card adjustments from coverage) - AdjustmentRedemptionMethod (commit/rollback via balance operator, idempotency keys) - One pair of winzou callbacks (create->commit, cancel->rollback) for both modes Container lints, callbacks registered, quality gates green
- Apply action (POST + GiftCardIsApplicable validation), remove action (POST + CSRF) - Twig redemption extension/runtime (apply form, coverage, remaining total) - Cart apply box + totals partials via sylius_ui prepend - Playwright-verified end-to-end (adjustment mode): product gift card form + live preview render; add-to-cart creates the pending disabled card with correct amount/deliveryType/design/message; cart apply box attaches a card to the order - Test app: add StateMachineAbstractionBundle, remove stale 0.12.x overrides, fixture channel fallback, encore strict_mode off
- DompdfGiftCardPdfGenerator + pluggable interface; two-page pdf.html.twig - GiftCardEmailManager (Sylius mailer, in-memory PDF attachments via tempfile) - OrderGiftCardOperator.send() + pay->send winzou callback - SendGiftCardEmailSubscriber for admin-created cards - Admin PDF download + design preview-PDF actions/routes - Browser-verified: admin PDF download produces a valid 22KB PDF
…pages render styled
resolve-url-loader 3.x fails with dart-sass ('PostCSS received undefined');
disabling it lets Encore emit the Sylius CSS. Also gitignore the local dev router.php.
- GiftCardPaymentChecker + lazy GiftCardPaymentMethodProvider (offline gateway) - PaymentRedemptionMethod: commit creates completed gift-card payments + redeems balance; rollback refunds + restores (same winzou callbacks as adjustment mode) - GiftCardAwareOrderPaymentProcessor decorates checkout + after_checkout to size the gateway payment to (total - coverage) - Payment-step-skip checker + methods/default resolver decorators hide the gift-card method from checkout - Verified: container boots + lints in payment mode, decorators active
- Gift card admin grid (prepend): code/customer/amount/deliveryType/enabled/createdAt, filters, create/update/download-pdf/delete actions, hides pending cards - Balance dashboard action + template (SQL findBalance aggregation by currency) - Admin gift card create enabled (channel field on new cards) - Design form example-PDF preview button; menu items for designs + balance - Browser-verified: gift card list, balance dashboard render styled
- Gift card grid actions (adjust-balance, download PDF), balance dashboard (SQL) - AdjustGiftCardBalanceAction + form (delta + reason -> ledger via balance operator) - CreateGiftCardProductAction scaffold (verified: creates disabled gift card product with virtual + physical variants, redirects to edit) - Admin gift card create (channel field), design preview-PDF button, menus, translations - Note: local admin form submits blocked by node-sass/arm64 broken admin JS (test-app infra); covered by functional tests
- Unit suite (28): model, code normalizer, eligible-total + coverage calculators, balance operator (redeem/restore/adjust/idempotency/insufficient), configuration - Functional suite (3): balance operator + ledger + findBalance against a real DB - composer phpunit -> OK (31 tests, 53 assertions); PHPStan max / ECS / Rector clean - README rewritten for 1.x; UPGRADE-1.0.md clean-break guide Completes the 1.x rewrite (all 12 phases).
Follows Setono/SyliusPluginSkeleton@80cc9db: - package.json: use @sylius-ui/frontend (Dart Sass) instead of node-sass, which failed to compile against Node 22's V8 API on arm64; pin jquery via resolutions so jquery.dirtyforms loads (fixes 'jQuery.dirtyForms is not a function' console error that broke admin form submits) - .nvmrc: pin Node 20 for the asset build - webpack.config.js: build the vendor shop/admin entries directly (drops the redundant local assets/ re-export files) - Document the Node 20 requirement in CLAUDE.md - Add missing admin CRUD heading translations (edit/create gift card + design) Verified: yarn install + build succeed, admin renders fully styled, console is clean, and the adjust-balance form now submits end-to-end (balance 30000->35000 with a manual ledger row).
- EligibleTotalCalculator: add back applied order_gift_card adjustments so the per-card coverage shown in the cart stays stable once a card is applied. Previously, in adjustment mode, an applied card that fully covered the order drove the total to 0 and its own displayed coverage collapsed to $0.00 (the actual adjustment/ledger were always correct). +unit test. - Remove tests/Application stale 0.12.x Cart/summary.html.twig override that referenced a non-existent setono-sylius-gift-card-add-gift-card-to-order.js (404 + 'jQuery.addGiftCardToOrder is not a function' on the cart page) Both found via Playwright verification of the adjustment-mode redemption flow.
OrderItemTrait::equals() returned false for a gift card item even when compared to itself, so Sylius' OrderItemController::resolveAddedOrderItem() (getItems()->filter(equals)->first()) found nothing and ->first() returned false, raising a TypeError -> 500 on every gift card add-to-cart. Add an identity short-circuit so an item still equals itself while distinct gift card lines remain unmergeable. +regression test. Found via Playwright verification of the gift card purchase flow.
- Design name was required in every locale: a NotBlank form constraint on the translation was applied to all rendered locales. Move it to the GiftCardDesignTranslation entity validation so only the default locale (kept by ResourceTranslationsType) is required. +validators messages. - Design grid 'image' field 500'd (Can't read property 'image'): the twig field passed resource.image; add path: '.' so the template receives the design. - Add missing translations: new_gift_card_design heading, no_image label; drop stale 0.12.x gift_card_configuration/search validator keys. All found via Playwright verification of the design create/edit flow.
Sylius' PaymentMethodFactory::createWithGateway() only sets the gateway config factoryName, leaving gatewayName null. gateway_name is a NOT NULL column, so placing an order in payment mode (which lazily creates the offline gift card payment method) failed with a 500 integrity-constraint violation. Set gatewayName to the payment method code. Found via Playwright verification of payment-mode checkout.
…cale
- Fix GiftCardEmailManager::sendGiftCard(): it did not pass localeCode, so the
Sylius email layout threw 'Variable localeCode does not exist' (breaks admin
resend / single-card send). sendGiftCardsFromOrder was unaffected. Found by
the new functional test.
- Give the PDF attachment a clean customer-facing filename (gift-card-<code>.pdf)
via a per-send temp directory instead of the raw tempnam name.
- Functional tests (KernelTestCase, in-memory MessageEvent capture, CI-friendly):
* GiftCardEmailManagerTest: emails the customer with a valid PDF attachment +
clean filename; skips when there is no customer.
* GiftCardPaymentMethodProviderTest: lazily creates a persistable offline
payment method with gatewayName set (guards the payment-mode 500 regression).
* Extract GiftCardFunctionalTestCase base (schema + channel helpers).
- Verified real delivery + PDF attachment through the docker mailcatcher SMTP.
- docker-compose: drop obsolete 'version' key.
Full suite: 38 tests, 71 assertions.
…gn form Addresses reported issues: - expiresAt is now pinned to 23:59:59 (factory + admin date picker with a model transformer); the admin field is a date picker, not a datetime one. +unit test. - Add a gift card show page with a details panel and the transaction ledger (explicit show route + template + grid Show action). - PDF rendered 3 pages; two full-height cards + html/body height:100% (no explicit page break) now yields exactly 2 pages. +functional test. - Gift card grid defaults to 100 per page (limits [100, 200, 500, 1000]). - Adjust-balance and balance-dashboard pages now use the proper admin layout (header macro + breadcrumbs + form theme). - Design translations use the default Sylius translationForm accordion (locale tabs). - Design image upload: guidelines (dimensions/format, accept=image/*) + a preview thumbnail of already-uploaded images via a custom form widget. Full suite: 41 tests, 76 assertions. All browser-verified.
- Add UniqueDesignImageTypes constraint on GiftCardDesign so a design can have at most one image per type (front/back). Previously two 'Front' images could be added. +constraint validator with unit tests; browser-verified the error. - Restructure the design edit form into two columns (Details | Design images).
Mimics a clean minimalist design: - Front (dark, framed): brand mark + channel name, 'GIFT CARD' tag, 'The gift of choice' eyebrow, large 'Gift Card' heading, VALUE + amount, NO. + code. - Back (cream): dark stripe band, 'How to redeem' instructions, a dashed 'Redemption code' panel, a barcode strip, and a terms + brand/hostname footer (terms note the expiry date when set). - Designs with an uploaded front image still render it full-bleed with a scrim showing the amount + code; back falls back to the cream layout. - New pdf.* translations. Still exactly 2 pages (covered by the functional test).
…tial) - Extract the gift card front into a shared Twig partial (_card.html.twig) + CSS (_cardStyle.html.twig), used by BOTH the PDF and the on-site preview — one source of truth, no html2canvas. - Product page: render the shared card as a prominent, full-width live preview at the top of the gift card section; it updates amount (currency-formatted via Intl), message, and the selected design image live, scaled to fit via a transform. Rewrote product-gift-card.js/.css accordingly. - PDF template consumes the same partial for its front; back unchanged. Still 2 pages (functional test green).
The live preview only handled designs with a front image, so selecting an image-less design left it unchanged. Render both the image and framed layouts in preview mode and toggle .ssgc-card--has-image from JS based on whether the selected design has an image — matching what the PDF produces for that design. Also show the customer message in the image scrim.
templates/bundles/SyliusShopBundle/Taxon/_horizontalMenu.html.twig dropped the 'ui large stackable menu' wrapper (and used taxon.children instead of enabledChildren), so the top navigation rendered as unstyled left-aligned links. Deleting the override restores the vendor default (centered, styled menu).
…nfig - All plugin view folders and files are now snake_case (admin/gift_card/..., shop/gift_card/_card_style.html.twig, email/gift_cards_from_order.html.twig, admin/gift_card/grid/field/delivery_type.html.twig, etc.). Updated every reference (grid/ui/mailer prepend config, routes, pdf service arg, the two balance controllers, and the include/form_theme paths inside templates). - Removed dead 0.12.x leftovers that the extension never loads: grids.yaml + grids/, sylius_ui.yaml (root), routes_no_locale.yaml, state_machine/, and the templates only they referenced (Grid/Action/*, item_units_order, Order/coveredByGiftCards, giftCardBalance, GiftCard/create). Verified: container + twig lint, PHPStan, 43 tests, and browser rendering of the product preview, admin grid, and PDF.
…ntation - Every plugin-defined service id is now its class FQCN, and each interface is aliased directly to the concrete FQCN (autowiring-idiomatic). Updated all argument references, route _controller values, the winzou state-machine callbacks, and test/container references accordingly. - Fixed YAML escaping: the state-machine 'do' service refs use single quotes so the FQCN backslashes aren't treated as escape sequences. - Intentional exceptions kept with dotted ids: the two Sylius service overrides (sylius.factory.add_to_cart_command, sylius.form.type.add_to_cart), the Sylius ImagesUploadListener instance, and the two GiftCardAwareOrderPaymentProcessor decorators (same class -> can't both be the FQCN id). Convenience aliases kept: setono_sylius_gift_card.redemption_method (public, for the state machine) and setono_sylius_gift_card.pdf.generator, both pointing at the concrete FQCN. Verified: container lint, PHPStan, 43 tests, and an end-to-end redemption checkout (winzou -> redemption_method -> operator all resolve; balance debited with a ledger row).
Previously the plugin replaced two Sylius services outright: - sylius.factory.add_to_cart_command was redefined with our factory class - sylius.form.type.add_to_cart was redefined with our command as data_class Both are now non-destructive: - AddToCartCommandFactory decorates sylius.factory.add_to_cart_command and delegates to the inner factory, then wraps the result in our command with the gift card information (also gives it a proper FQCN id). - The form's data_class is set from AddToCartTypeExtension::configureOptions (the extension already extends AddToCartType), so no service override is needed. Verified: container lint, PHPStan, ECS, 43 tests, and browser add-to-cart of both a gift card (pending card created) and a regular product.
…ecting a manager Injecting ObjectManager/EntityManager couples a service to one manager and is brittle in multi-manager setups. Every service that persisted/flushed now injects Doctrine's ManagerRegistry (service id 'doctrine') and uses Setono\Doctrine\ORMTrait to resolve the right manager per entity via $this->getManager($entity): - GiftCardBalanceOperator, OrderGiftCardOperator, CartGiftCardHandler, GiftCardDesignProvider, GiftCardPaymentMethodProvider, CreateGiftCardProductAction switched from a manager argument to ManagerRegistry + the trait. - The three actions already using the abandoned setono/doctrine-object-manager-trait (ORMManagerTrait) moved to the new setono/doctrine-orm-trait (ORMTrait). Replaced the abandoned setono/doctrine-object-manager-trait dependency with setono/doctrine-orm-trait, and updated the unit/functional tests that built these services directly. Verified: PHPStan, ECS, container lint, 43 tests (incl. functional DB tests that exercise getManager()->persist/flush), and a live add-to-cart persisting a card.
Guards the seam the plugin creates by decorating sylius.factory.add_to_cart_command: - The decorator is now idempotent: if the inner factory already produced a gift card aware command (AddToCartCommandInterface), it is returned untouched instead of being rebuilt. An application that extends our command (and points the command class parameter at it) now composes cleanly. - decoration-priority=-10 makes our decorator outermost deterministically, so the idempotency check sees whatever the rest of the decoration chain produced. - ValidateAddToCartCommandClassPass fails container compilation with an actionable message when the configured add-to-cart command class does not implement AddToCartCommandInterface, instead of surfacing later as an opaque form 'view data is the wrong type' error. Rejected a broader 'detect any customization' compiler pass: the more damaging conflict (another form extension setting a different data_class) is not statically detectable, so such a pass would give false confidence. Adds unit tests for both decorator branches and the compiler pass. Verified: PHPStan, ECS, container lint, 48 tests, and a live add-to-cart.
Adds a 'Customizing the add-to-cart command' section explaining the seam: extend AddToCartCommand (or implement AddToCartCommandInterface) and point the setono_sylius_gift_card.order.model.add_to_cart_command.class parameter at it; notes the compile-time validation and idempotent decorator. Also fixes a stale pre-snake_case PDF template path (Shop/GiftCard -> shop/gift_card).
- Add a table of contents and a Requirements section (PHP/Sylius/Symfony/ORM). - Promote the extension points out of 'Configuration' into a dedicated 'Customization' section, and document two more seams: overriding the emails and swapping resource model/repository/factory classes. - Add an explicit License section. No behavioural claims changed except tightening the email description to match what is actually wired (single-card email fires on admin post_create; there is no resend route).
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 1.x #269 +/- ##
======================================
Coverage ? 21.25%
Complexity ? 585
======================================
Files ? 93
Lines ? 2352
Branches ? 0
======================================
Hits ? 500
Misses ? 1852
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| /tests/Application/yarn.lock | ||
| /.playwright-mcp/ | ||
| /tests/Application/public/build/ | ||
| /tests/Application/public/router.php |
There was a problem hiding this comment.
Use symfony serve instead of built-in php web server. Add this to CLAUDE.md
There was a problem hiding this comment.
Done in 6b78b5e — removed tests/Application/public/router.php and its .gitignore entry. Running the server with symfony serve is already documented in CLAUDE.md.
The plugin does not depend on Sylius 1.13-specific APIs, so the granular component/bundle packages (channel, core, order, product, ui-bundle, …) are relaxed from ^1.13 to ^1.0 to avoid over-constraining host apps. The real minimum is carried by the packages that matter — sylius/grid-bundle (^1.11) and sylius/resource-bundle (^1.10) — and the test suite stays pinned to Sylius 1.13/1.14 via require-dev (sylius/sylius: ~1.13.0 || ~1.14.0). sylius/mailer-bundle keeps ^1.8 || ^2.0 because ^1.8 is its genuine Symfony 6.4-compatible floor.
…rsion note admin-gift-cards.jpeg was an unreferenced dev-time screenshot at the repo root showing the Symfony debug toolbar and an unstyled (unbuilt) admin menu. Also correct the UPGRADE note: 0.12.x required Sylius ^1.11, not ~1.12.
| @@ -1,3 +1,3 @@ | |||
| imports: | |||
There was a problem hiding this comment.
This file should be removed
There was a problem hiding this comment.
Done in 6b78b5e — removed. The functional tests use the test service container (self::getContainer()) to reach private services, so the public: true default is no longer needed; all 48 tests still pass.
| @@ -3,3 +3,4 @@ webpack_encore: | |||
| builds: | |||
| shop: '%kernel.project_dir%/public/build/shop' | |||
| admin: '%kernel.project_dir%/public/build/admin' | |||
| strict_mode: false | |||
There was a problem hiding this comment.
This should not be added
There was a problem hiding this comment.
Done in 6b78b5e — removed strict_mode: false. Verified the admin grid and shop pages render without any Encore entry errors.
| not_blank: Please enter a name | ||
| max_length: The name must not be longer than {{ limit }} characters |
There was a problem hiding this comment.
The default validation messages are fine, so just remove these
There was a problem hiding this comment.
Done in 6b78b5e — removed the custom max_length messages here and the matching maxMessage options on the Length constraints (GiftCard.code and GiftCardDesignTranslation.name), so the Symfony default message is used. Kept the domain-specific and friendlier not_blank messages.
| @@ -0,0 +1,36 @@ | |||
| winzou_state_machine: | |||
There was a problem hiding this comment.
Configuration that is prepended should be written as php arrays instead of loading files like this
There was a problem hiding this comment.
Done in 527838e — prepend() now builds the configuration (winzou_state_machine, liip_imagine, sylius_mailer, sylius_ui, sylius_grid) as PHP arrays directly in the extension, and the prepend/*.yaml files are removed. Verified identical output via container lint and debug:config.
| run: "vendor/bin/phpunit --coverage-clover=.build/logs/clover.xml" | ||
|
|
||
| - name: "Send code coverage report to Codecov.io" | ||
| uses: "codecov/codecov-action@v4" |
There was a problem hiding this comment.
Use the newest action and I don't think you need to explicitly give it the files
There was a problem hiding this comment.
Done in 6b78b5e — bumped to codecov-action@v5 and removed the explicit files input.
Per review: build the prepended configuration (winzou_state_machine, liip_imagine, sylius_mailer, sylius_ui, sylius_grid) as PHP arrays directly in the extension's prepend() and drop the src/Resources/config/prepend/*.yaml files it used to parse. Verified the resulting config is identical via container lint + debug:config and by rendering the admin grid, the shop gift card form, and the winzou callbacks.
- Remove tests/Application/public/router.php and its .gitignore entry; the web server is run with `symfony serve` (documented in CLAUDE.md). - Remove tests/Application/config/services_test.yaml (the public: true hack); functional tests use the test service container instead. - Remove strict_mode: false from the test app webpack_encore config. - Drop the custom max_length messages on GiftCard.code and GiftCardDesignTranslation.name; Symfony's default Length message is fine. - Bump codecov-action to v5 and drop the explicit files: input.
Summary
This is the ground-up 1.x rewrite of the plugin, targeting Sylius 1.13/1.14, PHP ≥ 8.1, Symfony ^6.4. It replaces the
0.12.xarchitecture (base branch of this PR).The rewrite simplifies aggressively and hardens what remains — see
REWRITE.mdfor the full architecture, decisions and progress log, andUPGRADE-1.0.mdfor the migration inventory.Highlights
variant->isShippingRequired(), not a special product type.OrderItemUnitat add-to-cart, reconciled to final amounts at checkout completion, enabled + emailed (PDF attachment) on payment, disabled on cancel.setono_sylius_gift_card.redemption.mode:adjustment(default) — negativeorder_gift_cardorder adjustments.payment— one realPaymentper card via a lazily-created offline gift card payment method; payment step skipped at full coverage.GiftCardTransactionledger (audit + idempotency); balance mutations go through a single balance operator; nothing below controllers flushes.Notes for reviewers
1.x rewrite phase N …) followed by polish, bug fixes and refactors.tests/Application.prepend(); host apps only register the bundle, import routes and apply the entity traits.