setono/sylius-shipping-countdown-plugin
A Sylius plugin that adds a countdown timer showing the time remaining until the next shipping cut-off, aiming to nudge customers toward faster checkout. It is a small, low-adoption package, so check maintenance activity and Sylius version compatibility before relying on it in production.
composer require setono/sylius-shipping-countdown-plugin
Register the bundle in config/bundles.php (Sylius/Symfony Flex will not always do this for you):
Setono\SyliusShippingCountdownPlugin\SetonoSyliusShippingCountdownPlugin::class => ['all' => true],
Then the standard Sylius post-install routine:
bin/console cache:clear
bin/console doctrine:migrations:migrate # only if the plugin ships migrations
bin/console assets:install public --symlink
Where to look first: open vendor/setono/sylius-shipping-countdown-plugin/src/ and read, in this order:
DependencyInjection/Configuration.php — this is the source of truth for what is configurable (cutoff times, channel mapping, formatting).Resources/config/services.xml (or config/services.yaml) — to see which services are public/decoratable and what parameters exist.Resources/views/ and twig/ — to learn the exact Twig function/extension name and the template namespace you'll override.Resources/translations/ — for the message keys you'll want to translate or replace.The README.md on GitHub is thin for a package this size; treat the DI config as documentation.
First use case: get a "Order within Xh Ym for same-day dispatch" countdown onto the storefront layout. The fastest path is to render the plugin's Twig helper inside templates/bundles/SyliusShopBundle/_header.html.twig (or your theme's layout), confirm it renders with default config, then tune the cutoff time to your warehouse reality.
Render the countdown in the layout. The plugin exposes a Twig extension (typically something like shipping_countdown() or a render() call on a controller). Start by rendering it unconditionally in your layout to prove wiring, then gate it.
Gate by cart/context. In practice you only want the countdown when it's actionable — a non-empty cart, a shippable address, or a specific shipping method:
{% if sylius.cart is not null and not sylius.cart.empty %}
{{ shipping_countdown() }}
{% endif %}
Per-channel configuration. Sylius shops are multi-channel; a B2B channel and a retail channel rarely share a cutoff. Wire the cutoff into channel settings (via a ChannelConfigurationTypeExtension) or into parameters.yaml keyed by channel code. Do this early — retrofitting per-channel config after you've hardcoded a cutoff is annoying.
Template overriding. Never edit vendor/. Copy the plugin template into your theme:
templates/bundles/SetonoSyliusShippingCountdownPlugin/<path from vendor views>
This is the standard Symfony bundle override path and it survives composer update. Keep overrides minimal — copy the file, change the wrapper markup, leave the logic.
Live-updating UI. Server-rendered countdown goes stale the moment it hits the browser. The plugin's template usually contains a data-* attribute or an inline timer. Either let its bundled JS tick it down, or read the target timestamp from the DOM and drive it with your own small script (Stimulus controller, Alpine, or plain setInterval). Prefer "target timestamp" over "seconds remaining" so you don't fight browser throttling of background tabs.
Caching. If you have HTTP cache or a Twig cache in front of the layout, exclude the countdown fragment. Options, in order of preference:
render_esi sub-request so it can have its own short TTL.Testing. Add a functional test that freeze-clocks (ClockMock or Carbon::setTestNow) to just before and just after the cutoff, and assert the rendered message switches from "order within…" to "order now for next dispatch." This is the single highest-value test for this plugin because cutoff boundaries are where every bug lives.
Timezone is the bug. Cutoffs are wall-clock times in a channel's timezone. Sylius channels carry their own timezone; PHP's default, the DB's, and the channel's will all disagree at some point. Resolve the cutoff against $channel->getTimezone() explicitly and never against date_default_timezone_get(). Test with a channel whose timezone differs from your server's.
Cutoff ≠ shipping method availability. This plugin estimates dispatch time; Sylius's shipping calculator decides what's actually shippable. They will disagree on weekends and holidays. If "same-day dispatch" is a commercial promise, add holiday/weekend logic on top — don't assume the plugin knows your warehouse calendar.
JS clock drift. If you compute remaining time from the client clock, a user with a wrong system clock sees garbage (negative timers, "order in 47 hours"). Compute from the server-provided target timestamp and clamp negatives to zero in the JS.
No migrations, probably. Most plugins of this shape are stateless Twig + service config. If bin/console doctrine:migrations:migrate reports nothing, that's expected — don't chase it.
Extension points to reach for. Rather than forking, decorate the plugin's calculator/service (standard Symfony decorates: in services.yaml) to inject holiday calendars, per-product handling times, or carrier-specific rules. If the DI config marks the service private, you can still decorate it — decoration doesn't require public services.
Low-traffic vendor risk. This is a 2-star, 0-dependent package with a single recent release. That's not a reason to avoid it — it's a reason to (a) pin the version in composer.json, (b) put the cutoff logic behind your own small service so replacing the plugin later is a one-file change, and (c) read the source before upgrading. The surface area is small enough that vendoring the one template plus a ~40-line service is a legitimate fallback if the package ever stalls.
Debugging checklist when nothing renders. In order: bundle registered in config/bundles.php → cache:clear run → Twig namespace override path matches exactly (a typo in SetonoSyliusShippingCountdownPlugin silently falls back to the vendor template) → the extension is actually registered (bin/console debug:twig | grep -i countdown) → the channel has a cutoff configured, since a null cutoff often means "render nothing" rather than "render 24h."
Translation keys. Override the plugin's Resources/translations/*.yaml from your app's translations/ directory first; only fall back to template overrides if the message structure itself needs changing. Keeps the diff small on upgrade.
How can I help you explore Laravel packages today?