setono / sylius-gift-card-plugin
Gift card plugin for Sylius
Package info
github.com/Setono/SyliusGiftCardPlugin
Type:sylius-plugin
pkg:composer/setono/sylius-gift-card-plugin
Requires
- php: >=8.1
- ext-filter: *
- ext-mbstring: *
- api-platform/core: ^2.6
- doctrine/collections: ^1.8
- doctrine/dbal: ^2.13 || ^3.0
- doctrine/orm: ^2.7
- doctrine/persistence: ^1.3 || ^2.5 || ^3.0
- fakerphp/faker: ^1.21
- knplabs/knp-menu: ^3.3
- knplabs/knp-snappy: ^1.4
- knplabs/knp-snappy-bundle: ^1.9
- setono/doctrine-object-manager-trait: ^1.1
- sylius/api-bundle: ^1.11
- sylius/channel: ^1.0
- sylius/channel-bundle: ^1.0
- sylius/core: ^1.0
- sylius/core-bundle: ^1.0
- sylius/currency: ^1.0
- sylius/customer: ^1.0
- sylius/grid-bundle: ^1.0
- sylius/locale: ^1.0
- sylius/locale-bundle: ^1.0
- sylius/mailer-bundle: ^1.6 || ^2.0
- sylius/money-bundle: ^1.0
- sylius/order: ^1.10
- sylius/order-bundle: ^1.0
- sylius/product: ^1.0
- sylius/product-bundle: ^1.0
- sylius/promotion: ^1.0
- sylius/resource-bundle: ^1.8
- sylius/shipping-bundle: ^1.0
- sylius/ui-bundle: ^1.0
- sylius/user: ^1.0
- symfony/config: ^5.4 || ^6.4
- symfony/dependency-injection: ^5.4 || ^6.4
- symfony/event-dispatcher: ^5.4 || ^6.4
- symfony/form: ^5.4 || ^6.4
- symfony/http-foundation: ^5.4 || ^6.4
- symfony/http-kernel: ^5.4 || ^6.4
- symfony/messenger: ^5.4 || ^6.4
- symfony/options-resolver: ^5.4 || ^6.4
- symfony/property-access: ^5.4 || ^6.4
- symfony/routing: ^5.4 || ^6.4
- symfony/security-core: ^5.4 || ^6.4
- symfony/serializer: ^5.4 || ^6.4
- symfony/translation-contracts: ^2.4
- symfony/validator: ^5.4 || ^6.4
- twig/twig: ^2.15 || ^3.5
- webimpress/safe-writer: ^2.2
- webmozart/assert: ^1.11
Requires (Dev)
- babdev/pagerfanta-bundle: ^3.8
- behat/behat: ^3.14
- doctrine/doctrine-bundle: ^2.7
- infection/infection: ^0.27.10
- jms/serializer-bundle: ^4.2
- lexik/jwt-authentication-bundle: ^2.17
- matthiasnoback/symfony-config-test: ^4.3 || ^5.1
- matthiasnoback/symfony-dependency-injection-test: ^4.3 || ^5.1
- php-http/message-factory: ^1.1
- phpspec/phpspec: ^7.4
- phpspec/prophecy-phpunit: ^2.0.2
- phpunit/phpunit: ^9.6.17
- polishsymfonycommunity/symfony-mocker-container: ^1.0.7
- psalm/plugin-phpunit: ^0.18.4
- psalm/plugin-symfony: ^5.0
- setono/code-quality-pack: ^2.7
- setono/sylius-behat-pack: ^0.2.2
- sylius/sylius: ~1.12.13
- symfony/debug-bundle: ^5.4 || ^6.4
- symfony/dotenv: ^5.4 || ^6.4
- symfony/intl: ^5.4 || ^6.4
- symfony/property-info: ^5.4 || ^6.4
- symfony/web-profiler-bundle: ^5.4 || ^6.4
- symfony/webpack-encore-bundle: ^1.17.2
- weirdan/doctrine-psalm-plugin: ^2.9
- willdurand/negotiation: ^3.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- 1.x-dev
- 0.12.x-dev
- v0.12.0
- v0.12.0-beta
- v0.12.0-alpha.8
- v0.12.0-alpha.7
- v0.12.0-alpha.6
- v0.12.0-alpha.5
- v0.12.0-alpha.4
- v0.12.0-alpha.3
- v0.12.0-alpha.2
- v0.12.0-alpha
- 0.11.x-dev
- v0.11.10
- v0.11.9
- v0.11.8
- v0.11.7
- v0.11.6
- v0.11.5
- v0.11.4
- v0.11.3
- v0.11.2
- v0.11.1
- v0.11.0
- v0.10.5
- v0.10.4
- v0.10.3
- v0.10.2
- v0.10.1
- v0.10.0
- v0.9.0
- v0.8.7
- v0.8.6
- v0.8.5
- v0.8.4
- v0.8.3
- v0.8.2
- v0.8.1
- v0.8.0
- v0.7.0
- v0.6.5
- v0.6.4
- v0.6.3
- v0.6.2
- v0.6.1
- v0.6.0
- v0.5.0
- v0.4.2
- v0.4.1
- v0.4.0
- v0.3.1
- v0.3.0
- v0.2.2
- v0.2.1
- v0.2.0
- v0.1.0
- dev-test/ui-journeys
- dev-test/admin-actions
- dev-fix/optimistic-lock
- dev-test/admin-units
- dev-test/cart-gift-card-figures
- dev-test/payment-plumbing
- dev-test/persistence-and-fixtures
- dev-fix/rate-limit
- dev-test/purchase-flow
- dev-test/gift-card-delivery
- dev-fix/refund-handling
- dev-test/repositories-and-mapping
- dev-fix/stale-coverage
- dev-fix/physical-email
- dev-fix/js-preview
- dev-fix/setup-warning
- dev-fix/base-currency
- dev-fix/e2e-diagnostics
- dev-fix/decorate-interfaces
- dev-refactor
- dev-fix-psalm-totally-typed
- dev-fix-19-compatibility
This package is auto-updated.
Last update: 2026-09-24 16:09:49 UTC
README
Add gift card functionality to your Sylius store:
- Buy gift cards — customers choose the amount, a design and an optional message, and pick whether the gift card is virtual (delivered by email as a PDF) or physical (shipped like a normal product).
- Redeem gift cards — customers apply a gift card code in the cart, and it becomes a real payment against the order rather than a discount on it.
- Admin management — a gift card grid, gift card designs, a one-click "create gift card product" scaffold, manual balance adjustments (with an audit ledger), and an outstanding-balance dashboard.
This is the
1.xline, for Sylius 1.13 and up. It is a ground-up rewrite of the0.12.xplugin. There is no API layer in 1.x — seeUPGRADE-1.0.mdif you are coming from0.12.x.
Table of contents
How it works
Virtual vs physical
Whether a gift card is virtual or physical is derived from the chosen product variant's shipping required flag — there is no special product type. The recommended setup is a single gift card product with a "delivery" product option producing a non-shippable Virtual variant and a shippable Physical variant. Virtual-only stores work too: just create a single non-shippable variant and the delivery selector disappears. Use the Create gift card product button in the admin gift card list to scaffold this in one click.
The delivery type also decides what the buyer is emailed when the order is paid. A virtual card is delivered by the email: the code is in the body and the card is attached as a PDF. A physical card is shipped with the code printed on it, so its email only says that the card will be shipped — emailing the code would make the card spendable before it arrives, and duplicate what is in the envelope. Set delivery.email_physical_cards: true if you want the code and the PDF emailed for physical cards anyway, as a digital backup. Send email on a gift card in the admin always includes the code and the PDF, whatever the delivery type: that is how you replace a physical card the customer lost or never received.
Buying a gift card
The customer chooses the amount, a design and an optional message on the product page (with a live preview). A disabled gift card is created per order item unit at add-to-cart time; at checkout completion it is reconciled against the final amounts, and when the order is paid it is enabled and emailed to the customer (virtual cards with their PDF attached, see Virtual vs physical). Cancelling the order, or refunding it in full, disables the cards it bought; a partial refund does not, because it does not say which items the money went back for.
Redeeming a gift card
The customer enters a gift card code in the cart. The order total stays intact and each applied gift card becomes a completed Payment using a lazily-created offline gift card payment method; the remainder is charged through the normal gateway, and the payment step is skipped automatically when gift cards cover the whole order.
A gift card is treated as a means of payment rather than a discount, because that is what it is: selling one takes money for a liability the shop settles later, so redeeming it settles that liability instead of reducing what the order is worth. It also keeps gift cards out of the way of promotions, and matches what order management and accounting systems expect to receive.
Gift cards cannot be used to buy other gift cards, balances are committed when the order is placed and restored, once per payment, when the gift card payment is refunded (cancelling the order refunds it), and every balance change is recorded in an append-only ledger.
Requirements
| Requirement | Version |
|---|---|
| PHP | >= 8.1 |
| Sylius | 1.13 and up (the 1.x line) |
| Symfony | ^6.4 |
| ORM | doctrine/orm (the only supported driver) |
Installation
Require the plugin with composer
composer require setono/sylius-gift-card-plugin
Register the plugin
Add it to config/bundles.php before SyliusGridBundle:
$bundles = [ // ... Setono\SyliusGiftCardPlugin\SetonoSyliusGiftCardPlugin::class => ['all' => true], Sylius\Bundle\GridBundle\SyliusGridBundle::class => ['all' => true], // ... ];
The plugin auto-configures the state machine, grids, UI events, email templates and image filters for you — you do not need to import any bundle configuration manually.
The "Gift card" checkbox on the admin product form is rendered by the setono_gift_card block on the sylius.admin.product.tab_details UI event, so you can move or disable it through your own sylius_ui configuration.
Both state machine adapters Sylius supports are covered: the plugin registers winzou callbacks and the
equivalent Symfony Workflow subscribers, so it behaves the same whichever adapter
sylius_core.state_machine.default_adapter is set to. Only the adapter actually applying a transition emits
its events, so the work is never done twice.
Import routing
# config/routes/setono_sylius_gift_card.yaml setono_sylius_gift_card: resource: "@SetonoSyliusGiftCardPlugin/Resources/config/routes.yaml"
Apply the traits/interfaces to your entities
Apply the plugin traits to your Product, Order, OrderItem and OrderItemUnit entities. The traits carry their
Doctrine mapping as PHP 8 attributes and as annotations, so they work whether your application maps its entities
with type: attribute (the Sylius-Standard default in config/packages/doctrine.yaml) or type: annotation. The
samples below are attribute-mapped:
// src/Entity/Product/Product.php use Doctrine\ORM\Mapping as ORM; use Setono\SyliusGiftCardPlugin\Model\ProductInterface as SetonoSyliusGiftCardProductInterface; use Setono\SyliusGiftCardPlugin\Model\ProductTrait as SetonoSyliusGiftCardProductTrait; use Sylius\Component\Core\Model\Product as BaseProduct; #[ORM\Entity] #[ORM\Table(name: 'sylius_product')] class Product extends BaseProduct implements SetonoSyliusGiftCardProductInterface { use SetonoSyliusGiftCardProductTrait; }
// src/Entity/Order/Order.php use Doctrine\ORM\Mapping as ORM; use Setono\SyliusGiftCardPlugin\Model\OrderInterface as SetonoSyliusGiftCardOrderInterface; use Setono\SyliusGiftCardPlugin\Model\OrderTrait as SetonoSyliusGiftCardOrderTrait; use Sylius\Component\Core\Model\Order as BaseOrder; #[ORM\Entity] #[ORM\Table(name: 'sylius_order')] class Order extends BaseOrder implements SetonoSyliusGiftCardOrderInterface { use SetonoSyliusGiftCardOrderTrait { SetonoSyliusGiftCardOrderTrait::__construct as private __giftCardTraitConstruct; } public function __construct() { $this->__giftCardTraitConstruct(); parent::__construct(); } }
// src/Entity/Order/OrderItem.php use Doctrine\ORM\Mapping as ORM; use Setono\SyliusGiftCardPlugin\Model\OrderItemTrait as SetonoSyliusGiftCardOrderItemTrait; use Sylius\Component\Core\Model\OrderItem as BaseOrderItem; #[ORM\Entity] #[ORM\Table(name: 'sylius_order_item')] class OrderItem extends BaseOrderItem { use SetonoSyliusGiftCardOrderItemTrait; }
// src/Entity/Order/OrderItemUnit.php use Doctrine\ORM\Mapping as ORM; use Setono\SyliusGiftCardPlugin\Model\OrderItemUnitInterface as SetonoSyliusGiftCardOrderItemUnitInterface; use Setono\SyliusGiftCardPlugin\Model\OrderItemUnitTrait as SetonoSyliusGiftCardOrderItemUnitTrait; use Sylius\Component\Core\Model\OrderItemUnit as BaseOrderItemUnit; #[ORM\Entity] #[ORM\Table(name: 'sylius_order_item_unit')] class OrderItemUnit extends BaseOrderItemUnit implements SetonoSyliusGiftCardOrderItemUnitInterface { use SetonoSyliusGiftCardOrderItemUnitTrait; }
Register the entity overrides in config/packages/_sylius.yaml (see tests/Application for a complete, attribute-mapped working example).
Update the database
bin/console doctrine:migrations:diff bin/console doctrine:migrations:migrate
Create the default gift card design
Customers pick a design on the gift card product page, and a channel without any enabled designs simply skips the picker. To make the bundled "Classic" design available in every channel, run the command below. It is idempotent — it creates the design when it is missing and adds it to the channels it is not in yet — so run it again after you add a channel.
bin/console setono:gift-card:create-default-design
A channel without an enabled design still works: the product page shows no design picker and the card renders its framed default. The admin does point it out, though: while a channel sells gift cards without an enabled design, every admin page carries a warning in the top bar and the gift card and design indexes explain how to fix it.
Install assets
bin/console assets:install
Configuration
All settings are optional and shown here with their defaults:
# config/packages/setono_sylius_gift_card.yaml setono_sylius_gift_card: code_length: 16 # significant characters in a generated code (shown grouped, e.g. ABCD-EFGH-…); minimum 12, because a code is a bearer token and must not be guessable default_validity_period: '3 years' # any strtotime-compatible interval, or null to never expire purchase: minimum_amount: 100 # minor units (e.g. cents) maximum_amount: ~ # null = no maximum maximum_message_length: 200 # characters a customer may write on the card delivery: email_physical_cards: false # true also emails the code and the PDF of a *physical* card when the order is paid, as a backup redemption: payment_method_code: gift_card # code of the (auto-created) payment method a redeemed gift card is paid with rate_limiter: limiter.setono_sylius_gift_card_apply # throttles attempts to apply a code per visitor (session), see below; ~ turns it off ip_rate_limiter: limiter.setono_sylius_gift_card_apply_ip # throttles them per client IP, see below; ~ turns it off pdf: page_size: A6 # any page size supported by dompdf; the card scales to fill it
maximum_message_length is what both forms allow: it sets the shop textarea's maxlength and remaining-characters
counter, and it is the limit enforced by the GiftCardMessageLength constraint on the gift card and on the shop's
gift card information, so raising the setting raises the limit everywhere. The card shows the message with its line
breaks intact and clamps it to four lines, so a message much longer than the default will be cut off on the gift
card and in its PDF.
Protecting codes from guessing
Attempts to apply a code are throttled so codes cannot be brute forced. Each attempt counts against two buckets, and both have to accept it: one per visitor (their session), 10 attempts per minute, and one per client IP, 50 attempts per minute. The IP bucket stops a guesser that throws its session cookie away after every attempt. Everyone behind one address shares it, an office NAT or a mobile carrier's CGNAT for example, which is why it allows five times as many attempts as a single visitor gets (the ratio Symfony's login throttling uses as well).
The plugin registers both limiters under framework.rate_limiter.limiters, as setono_sylius_gift_card_apply and
setono_sylius_gift_card_apply_ip (sliding windows); change their values in your own framework configuration, or
point rate_limiter and ip_rate_limiter at any rate limiter
you configured yourself.
Behind a reverse proxy or load balancer, configure
framework.trusted_proxies. Without it, Symfony takes
the proxy's address for the client IP of every request, so all the shop's customers share one IP bucket: 50 attempts
a minute between them, after which everybody who tries a gift card code is told to wait, customers with a real card
included. If you cannot configure trusted proxies, set ip_rate_limiter: ~ and rely on the session bucket alone.
Every rejected code gives the customer the same message, whatever the reason (unknown, disabled, expired,
empty, wrong channel or currency), so the form cannot be used to find out which codes exist. The actual
reason is written to the log at info level, with the code masked down to its last four characters
(************MNOP): a code is a bearer token, and logs travel.
Customization
Every extension point below is a plain service or template you replace — no configuration flags required.
The plugin's services use each other through their interfaces, so to change one, decorate (or replace) the service
registered under its interface, e.g. Setono\SyliusGiftCardPlugin\Calculator\EligibleTotalCalculatorInterface. What
you register there is what the whole plugin uses, the state machine callbacks included. The resources are the
exception: they follow Sylius' conventions, see Overriding models, repositories and factories.
Customizing the PDF
Gift cards render to PDF with dompdf. Override @SetonoSyliusGiftCardPlugin/shop/gift_card/pdf.html.twig to change the layout, or replace/decorate Setono\SyliusGiftCardPlugin\Pdf\GiftCardPdfGeneratorInterface to use a different engine.
The card is laid out on a fixed 560×396 pixel grid — A6 landscape — and is scaled onto whatever pdf.page_size is configured, so the layout is defined in one place and works on any paper.
Customizing the emails
The plugin sends two emails: setono_sylius_gift_card__gift_card (a single gift card, sent when one is created in the admin panel) and setono_sylius_gift_card__gift_cards_from_order (all gift cards from a paid order, sent to the buyer). Override their templates at @SetonoSyliusGiftCardPlugin/email/gift_card.html.twig and @SetonoSyliusGiftCardPlugin/email/gift_cards_from_order.html.twig, or redefine the emails under the sylius_mailer key to change the sender or subject. Both include @SetonoSyliusGiftCardPlugin/email/_gift_cards.html.twig, which renders the cards themselves — override that one to change how a card is presented in both emails at once.
Changing what gift cards may pay for
By default gift cards may pay for everything except gift-card line items. To change this, decorate Setono\SyliusGiftCardPlugin\Calculator\EligibleTotalCalculatorInterface:
# config/services.yaml services: App\GiftCard\EligibleTotalCalculator: decorates: Setono\SyliusGiftCardPlugin\Calculator\EligibleTotalCalculatorInterface arguments: ['@.inner']
Customizing the add-to-cart command
To capture the amount, message and design the customer picks, the plugin decorates Sylius' sylius.factory.add_to_cart_command so it produces a Setono\SyliusGiftCardPlugin\Order\AddToCartCommand — which implements Setono\SyliusGiftCardPlugin\Order\AddToCartCommandInterface and carries the gift card information — and the add-to-cart form binds to that class.
If your application needs its own add-to-cart command, make it extend AddToCartCommand (or implement AddToCartCommandInterface) and point the command class parameter at it:
# config/services.yaml parameters: setono_sylius_gift_card.order.model.add_to_cart_command.class: App\Order\AddToCartCommand
The plugin verifies this at container compile time and fails with an actionable message if the configured class does not implement the interface. Its factory decorator is idempotent and applied outermost, so it also composes cleanly with a decorator of your own on sylius.factory.add_to_cart_command.
Overriding models, repositories and factories
The gift_card, gift_card_design and gift_card_transaction resources follow the standard Sylius resource configuration, so you can swap any model, repository, controller or factory for your own class:
setono_sylius_gift_card: resources: gift_card: classes: model: App\Entity\GiftCard\GiftCard
Development
composer install (cd tests/Application && yarn install && yarn build) composer phpunit # unit + functional tests composer analyse # PHPStan (max level) composer check-style # ECS
See CLAUDE.md for the full development workflow, including Playwright-based UI verification against the bundled tests/Application.
License
This plugin is released under the MIT License.