N+1 queries in OrderItemUnit::getAdjustments() on cart mutations
Sylius version(s) affected
2.3.x-dev (branch 2.3, commit 70cf6ca6, 2026-08-17). Not a crash or regression tied to a specific release — the affected code (OrderItemUnit::getAdjustments()) has been structurally the same for a long time, so this likely also affects released 2.2.x.
Description
Not a user-facing bug — a performance finding: OrderItemUnit::getAdjustments() does one extra Doctrine query per order item unit (N+1) during order/cart recalculation, because it reads a lazy-loaded adjustments collection without eager-loading or batch-loading it first.
Found by running query-guard (a PHPUnit extension that traces Doctrine queries during a normal test run) over the existing tests/Api/Shop functional/API test suite — no new tests, no synthetic scenarios, just the SQL those tests already produce. query-guard fingerprints queries by call site and SQL shape, flags a call site as n-plus-one when it fires the same-shaped query 3+ times with differing bound values inside one test (batched IN (...) lookups are excluded), and separately inspects the call stack at query time: when it finds an uninitialized Doctrine PersistentCollection for a named entity property, the finding is upgraded from a heuristic guess to a confirmed fact — that's the case here.
src/Sylius/Component/Order/Model/OrderItemUnit.php:63:
public function getAdjustments(?string $type = null): Collection
{
if (null === $type) {
return $this->adjustments; // ← uninitialized PersistentCollection
}
return $this->adjustments->filter(...);
}Triggered during order/cart recalculation after a cart-mutating command is handled — observed from two entry points that both end up touching OrderItemUnit::$adjustments per unit: removing an item from the cart (src/Sylius/Bundle/ApiBundle/Controller/DeleteOrderItemAction.php:45 → RemoveItemFromCart command → order recalculation) and updating an item's quantity in the cart (CartTest::it_updates_item_quantity_in_cart). Both are ordinary shop-facing checkout actions, not admin/rare operations — every add-to-cart, quantity change, or item removal that goes through order recalculation pays one query per existing order item unit just to check/total its adjustments.
Observed: 3 queries from this lazy-load in each of the two traced scenarios (cart with a handful of units) — scales with the number of distinct units in the cart.
vendor/bin/phpstan analyse and vendor/bin/ecs check (the project's own configured tools) both pass clean on OrderItemUnit.php and DeleteOrderItemAction.php, so this isn't tangled up with an existing lint/type issue.
How to reproduce
Step 1: Check out Sylius/Sylius at 2.3 (or run the existing test suite on any recent 2.3 commit).
Step 2: Run tests/Api/Shop with a Doctrine query tracer/profiler attached (e.g. query-guard, or manually log queries via a DBAL middleware), or reproduce manually: remove an item from a cart, or change an item's quantity in a cart, where the cart has several distinct order item units, with the Symfony profiler's Doctrine panel open.
Step 3: Observe one extra query per existing order item unit during the recalculation triggered by the mutation, instead of adjustments being batch-loaded once for all units.
Possible Solution
Batch-load adjustments for all of an order's item units in one query (keyed by order_item_unit_id IN (...)) before the recalculation pass, instead of touching $unit->getAdjustments() per unit inside the recalculation loop.
Additional Context
Not applicable as a log — this was found via query trace analysis (query-guard), not an error log. Happy to share the full trace/counts if useful. Related: two other N+1 findings from the same query-guard run are filed separately (order totals in the admin order list, product reviews) since they're independent code paths.
Source: Sylius/Sylius