sylius/gmv-bundle
Sylius GMV Bundle adds Gross Merchandise Value analytics to Sylius stores. It calculates and reports GMV metrics from order data, helping merchants track sales performance, revenue trends, and growth through simple reporting tools.
1. Check your Sylius version before anything else
v1.1.0 is a Sylius 2.3 compatibility release (single PR: "Sylius 2.3 upgrade"). That almost certainly means the composer.json constraint moved from Sylius 1.x to Sylius 2.3, which is a breaking platform requirement, not a feature bump:
composer show sylius/gmv-bundle --all # see available versions + requires
^1.1.1.0.0 (and accept it is now unmaintained upstream).Read vendor/sylius/gmv-bundle/composer.json after install. It tells you the PHP floor (Sylius 2.x ⇒ PHP 8.2+) and the exact Sylius constraint faster than any doc.
2. Install and enable
composer require sylius/gmv-bundle:^1.1
Register the bundle in config/bundles.php:
return [
// ...
Sylius\GmvBundle\GmvBundle::class => ['all' => true],
];
and import the bundle's app config if it ships one:
# config/packages/sylius_gmv.yaml
imports:
- { resource: "@GmvBundle/Resources/config/app/config.yaml" }
Then confirm the services actually landed — an upgrade release can rename IDs:
bin/console debug:container --tag=sylius.gmv.provider
bin/console debug:container --tag=sylius.gmv.calculator
bin/console lint:container
3. Where to look first, in this order
| File | Why |
|---|---|
composer.json |
The Sylius/PHP constraint — the actual headline of v1.1.0 |
src/DependencyInjection/Configuration.php |
The real config reference: periods, channels, calculators |
src/DependencyInjection/GmvExtension.php |
Tagged services, parameters, injectable aliases |
src/Resources/config/services.xml (or .php) |
Service IDs + tags = your extension points |
src/Calculator/ or src/Provider/ |
The GMV formula — read before trusting any number |
| GitHub PR #8 diff | What the "Sylius 2.3 upgrade" actually touched |
4. First use case: GMV for the current channel, last 30 days
Skip the admin surface. Start with the service, because that's what ends up in a widget, a console command, and a nightly export anyway.
use Sylius\Component\Channel\Context\ChannelContextInterface;
use Sylius\GmvBundle\Provider\GmvProviderInterface; // adapt to actual FQCN
final class GmvSnapshotCommand extends Command
{
protected static $defaultName = 'app:gmv:snapshot';
public function __construct(
private GmvProviderInterface $gmvProvider,
private ChannelContextInterface $channelContext,
) { parent::__construct(); }
protected function execute(InputInterface $in, OutputInterface $out): int
{
$gmv = $this->gmvProvider->forPeriod(
$this->channelContext->getChannel(),
new \DateTimeImmutable('-30 days'),
new \DateTimeImmutable('now'),
);
$out->writeln(sprintf('GMV: %s', $gmv->getTotal())); // mind the currency
return Command::SUCCESS;
}
}
If that class doesn't exist, grep the tag out of services.xml and run debug:container --tag=.... Guessing tag names from class names is the fastest way to burn an afternoon.
Treat the bundle's result object as a value object, not a number. Most GMV implementations return a total plus currency and a breakdown (orders, items, refunds). Persist the whole thing; summing raw floats across currencies silently produces nonsense.
Scope every query by channel and currency. Sylius is multi-channel and multi-currency by design. A GMV number without a channel is almost always wrong:
$gmv = $gmvProvider->forPeriod($channel, $from, $to);
// then, if the channel has multiple currencies:
foreach ($channel->getCurrencies() as $currency) { /* per-currency query */ }
Do not convert-then-sum with CurrencyConverter for reporting — historical rates drift and your numbers stop reconciling with accounting. Store native-currency totals; convert at read time, if at all.
Exclude by order state, not by date. Cancelled/refunded orders inflate GMV if you filter only on created_at. Use Sylius' state machine states explicitly:
$qb->andWhere('o.state NOT IN (:excluded)')
->setParameter('excluded', [OrderInterface::STATE_CANCELLED]);
Refunds are the classic trap: they may live in sylius_payment or a refund plugin, not on the order. Document what your GMV definition includes before finance asks why it differs from the gateway.
Prefer decoration over forking when the formula is wrong. If you need to change the definition (exclude shipping, count only paid orders), decorate the calculator/provider behind a config flag:
# config/services.yaml
App\Reporting\ExcludeShippingGmvProvider:
decorates: sylius.gmv.provider
arguments: ['@App\Reporting\ExcludeShippingGmvProvider.inner']
Keep it toggleable so you can compare against the vanilla number in staging. Decoration also survives minor upgrades far better than a fork — worth preferring now that upstream is actually shipping releases.
Aggregate into a stats table once volume grows. Joining sylius_order_item to sylius_order over 12 months on a busy store will time out. Nightly job → app_gmv_daily(channel_id, currency_code, day, order_count, item_count, total) with a unique index. Read from that table everywhere; recompute retroactively when the formula changes. Free audit trail when someone disputes a number.
Reuse Sylius' reporting surfaces. If the bundle ships an admin route, wire your widget into the Dashboard rather than inventing a page. Sylius 2.x's admin is Twig-hook based and the grid system still gives you filters and CSV export for free — define a grid over your aggregate table and you skip writing a controller entirely.
Test against deterministic fixtures. Use sylius.fixtures with a fixed clock and known orders (one paid, one cancelled, one refunded, one multi-currency) and assert exact totals. Snapshot the expected breakdown in YAML and diff it in CI. These fixtures become your regression net for the 2.3 upgrade itself.
v1.1.0 is a compatibility release, not a feature release. One PR, "Sylius 2.3 upgrade", by a first-time contributor. Expect the only changes to be composer constraints and whatever Sylius 2.3 made incompatible (deprecated Symfony service syntax, removed interfaces). Practical consequences:
1.0.0 removed; add 1.1.0 after Sylius itself boots. Two simultaneous upgrades is one too many.debug:container and your test suite immediately after — that's where the breakage will be, not in your own services.Pin the version deliberately. Now that a minor bump has already changed the Sylius constraint, use "^1.1" plus a committed composer.lock, and read the changelog on every bump. A patch or minor here can move your platform requirement, which is a major-category risk in practice even if it isn't in semver.
The repo is official (github.com/Sylius/GmvBundle) but adoption is still near-zero. That changes the "no upstream patch is coming" calculus — a new contributor and a 2.3 upgrade signal real maintenance — but it does not give you a community to lean on for edge cases. Vendor-forking one class is still a perfectly reasonable escape hatch; just fork from the tagged version and keep the tag in a branch name.
The definition of GMV is the whole product. Gross merchandise value normally means item subtotals, excluding shipping and tax, before or after discount depending on who you ask. Read the calculator source, write the formula in your README, and version it. If it includes shipping, marketplace revenue reports will be wrong and finance will find out before you do.
Line-item totals may not sum to the order total. Order-level promotions, shipping discounts, and order-level taxes mean SUM(order_item.total) ≠ order.total. Pick one source of truth and never mix them in one report.
Timezone skew. Sylius stores timestamps in UTC; "today" for a Berlin merchant isn't "today" in UTC. Pass explicit DateTimeImmutable in the reporting timezone and convert at the boundary, or your daily figures are off by a few hours of orders every single day.
Mixed-currency sums are meaningless. If the provider returns one scalar total and your channel has three currencies, that number is a bug. Verify per-currency aggregation; if absent, call it per currency yourself.
Caching without invalidation. Cache keyed on (channel, currency, from, to) and invalidate on order state transitions (sylius.order.post_complete, refund events) — or expire aggressively. A stale dashboard number is worse than a slow one.
Extension points are tags, not inheritance. Look for tagged services in services.xml (sylius.gmv.calculator, sylius.gmv.provider, or similar). Register your own with the tag and a priority, then confirm:
bin/console debug:container --tag=sylius.gmv.calculator
bin/console debug:container --show-private sylius.gmv.provider
Watch the query count. Profile the provider against a realistic order table (100k+ orders) before shipping it into an admin page. A missing index on (channel_id, created_at) turns a 40 ms dashboard into a 9-second page load that nobody attributes to the reporting bundle.
No meaningful upstream test suite means you own the contract. Write a thin integration suite around the provider's output for your fixtures. When Sylius 2.4/3.0 lands and the bundle lags, those tests are the specification you hand to whoever patches or forks it.
How can I help you explore Laravel packages today?