[Promotion] Add version locking to promotions and coupons#18921
[Promotion] Add version locking to promotions and coupons#18921TheMilek merged 1 commit intoSylius:2.3from
Conversation
📝 WalkthroughWalkthroughAdds optimistic locking to Promotion and PromotionCoupon (version fields, ORM mappings, migrations), updates interfaces to be Versioned, and refactors AtomicOrderPromotionsUsageModifier to obtain entity locks via the EntityManager and delegate increment/decrement to a decorated modifier. Changes
Sequence DiagramsequenceDiagram
participant Client as Client Code
participant AOUM as AtomicOrderPromotionsUsageModifier
participant EM as EntityManager
participant DM as Decorated Modifier
participant DB as Database
Client->>AOUM: increment(order, promotions, coupon?)
Note over AOUM: Refresh & optimistic lock phase
AOUM->>EM: refresh(promotion)
EM->>DB: SELECT promotion
DB-->>EM: promotion row (version=N)
AOUM->>EM: lock(promotion, OPTIMISTIC, N)
EM-->>AOUM: locked (version checked)
alt coupon present
AOUM->>EM: refresh(coupon)
EM->>DB: SELECT coupon
DB-->>EM: coupon row (version=M)
AOUM->>EM: lock(coupon, OPTIMISTIC, M)
EM-->>AOUM: locked (version checked)
end
Note over AOUM: Delegate actual usage change
AOUM->>DM: increment(order, promotions, coupon?)
DM->>DB: apply updates (via repositories/ORM)
DB-->>DM: updates persisted
DM-->>AOUM: done
AOUM-->>Client: complete
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
❗ Preview Environment delete from Bunnyshell failedCheck https://github.com/Sylius/Sylius/actions/runs/24092157737 for details. See: Environment Details Available commands:
|
c3b79ea to
2379276
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/Sylius/Bundle/CoreBundle/tests/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifierTest.php (1)
46-221: Add one lock-failure test to pin non-delegation behavior.Current tests are all happy-path. Please add a case where locking fails and assert the decorated modifier is not called.
Proposed test addition
+ public function testDoesNotDelegateWhenLockFailsDuringIncrement(): void + { + $order = $this->createMock(OrderInterface::class); + $promotion = $this->createMock(PromotionInterface::class); + + $order->method('getPromotions')->willReturn(new ArrayCollection([$promotion])); + $order->method('getPromotionCoupon')->willReturn(null); + $promotion->method('getVersion')->willReturn(1); + + $this->entityManager->expects($this->once())->method('refresh')->with($promotion); + $this->entityManager + ->expects($this->once()) + ->method('lock') + ->with($promotion, LockMode::OPTIMISTIC, 1) + ->willThrowException(new \RuntimeException('Lock failed')) + ; + + $this->decoratedModifier->expects($this->never())->method('increment'); + + $this->expectException(\RuntimeException::class); + $this->atomicModifier->increment($order); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Sylius/Bundle/CoreBundle/tests/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifierTest.php` around lines 46 - 221, Add a negative test in AtomicOrderPromotionsUsageModifierTest that simulates EntityManager::lock throwing an exception and asserts the decorated modifier is not called; create an OrderInterface mock with a Promotion (and optionally a Coupon), have the promotion return a version, mock $this->entityManager->expects(...)->method('refresh')->with(...) as usual and mock ->method('lock') to throw an exception (e.g. new ORMException), then assert $this->decoratedModifier->expects($this->never())->method('increment') (or 'decrement' for the other case) and call $this->atomicModifier->increment($order) inside an expectException or try/catch to verify the exception is propagated (or handled) while ensuring decorated modifier was not invoked.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/Sylius/Bundle/CoreBundle/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifier.php`:
- Around line 57-58: The foreach in AtomicOrderPromotionsUsageModifier passes
Promotion objects from $order->getPromotions() (Component promotion interface)
into refreshAndLock(), which currently type-hints a Core PromotionInterface;
change the parameter type to the more general VersionedInterface used by
refreshAndLock() (since it only calls getVersion()), and add "use
Sylius\Resource\Model\VersionedInterface;" to the imports so
refreshAndLock(VersionedInterface $resource) accepts both promotion variants;
update any docblocks or type hints referencing
Sylius\Component\Core\Model\PromotionInterface accordingly.
In `@src/Sylius/Component/Core/Model/PromotionCoupon.php`:
- Around line 20-21: The protected property $version in class PromotionCoupon is
untyped; change its declaration to use a native int type (e.g., update property
$version to be typed as int) so it matches the accessor contract
(getVersion/setVersion) and avoids mixed runtime state; ensure the default value
remains 1 and adjust any code that previously relied on it being untyped if
necessary.
In `@src/Sylius/Component/Core/Model/PromotionCouponInterface.php`:
- Line 19: You've added VersionedInterface to PromotionCouponInterface which is
a breaking change for third-party implementers; revert that direct extension and
instead introduce a new interface (e.g., PromotionCouponVersionedInterface) that
extends PromotionCouponInterface and VersionedInterface, update internal
type-hints/usages to prefer the new PromotionCouponVersionedInterface where
versioning is required, and add a deprecation docblock/note on
PromotionCouponInterface (or a clear release note) indicating the planned
migration path so existing implementers are not forced to add version methods
immediately.
---
Nitpick comments:
In
`@src/Sylius/Bundle/CoreBundle/tests/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifierTest.php`:
- Around line 46-221: Add a negative test in
AtomicOrderPromotionsUsageModifierTest that simulates EntityManager::lock
throwing an exception and asserts the decorated modifier is not called; create
an OrderInterface mock with a Promotion (and optionally a Coupon), have the
promotion return a version, mock
$this->entityManager->expects(...)->method('refresh')->with(...) as usual and
mock ->method('lock') to throw an exception (e.g. new ORMException), then assert
$this->decoratedModifier->expects($this->never())->method('increment') (or
'decrement' for the other case) and call
$this->atomicModifier->increment($order) inside an expectException or try/catch
to verify the exception is propagated (or handled) while ensuring decorated
modifier was not invoked.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: efa55b4b-ae22-40fc-84fe-db4e864db1a5
📒 Files selected for processing (12)
src/Sylius/Bundle/CoreBundle/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifier.phpsrc/Sylius/Bundle/CoreBundle/Migrations/Version20260216120000.phpsrc/Sylius/Bundle/CoreBundle/Migrations/Version20260216120001.phpsrc/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/Promotion.orm.xmlsrc/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/PromotionCoupon.orm.xmlsrc/Sylius/Bundle/CoreBundle/Resources/config/services/promotion.phpsrc/Sylius/Bundle/CoreBundle/tests/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifierTest.phpsrc/Sylius/Component/Core/Model/Promotion.phpsrc/Sylius/Component/Core/Model/PromotionCoupon.phpsrc/Sylius/Component/Core/Model/PromotionCouponInterface.phpsrc/Sylius/Component/Core/Model/PromotionInterface.phpsrc/Sylius/Component/Core/tests/Model/PromotionCouponTest.php
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/Sylius/Component/Core/Model/PromotionCouponInterface.php (1)
17-19:⚠️ Potential issue | 🟠 MajorBC break: extending
VersionedInterfaceforces third-party implementers to add version methods.Adding
VersionedInterfaceto the interface contract is a breaking change for any external implementations ofPromotionCouponInterface. The PR description states "No BC breaks," but this change requires all implementers to addgetVersion()andsetVersion()methods.Consider:
- Documenting this as an explicit BC break in the changelog, or
- Introducing a new interface (e.g.,
VersionedPromotionCouponInterface) that extends both interfaces, allowing a deprecation path🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Sylius/Component/Core/Model/PromotionCouponInterface.php` around lines 17 - 19, The change made PromotionCouponInterface to extend VersionedInterface which introduces a BC break; revert that direct extension and instead create a new interface VersionedPromotionCouponInterface that extends PromotionCouponInterface and VersionedInterface (so existing implementers are unaffected), move any internal usages that require versioning to type-hint VersionedPromotionCouponInterface, and ensure getVersion()/setVersion() remain only on the new interface; alternatively, if you intend to keep the change, document it as an explicit BC break in the changelog and note implementers must add getVersion()/setVersion() to their implementations.
🧹 Nitpick comments (1)
src/Sylius/Bundle/CoreBundle/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifier.php (1)
70-76: Runtime assertion guards type safety for optimistic locking.The method signature accepts
PromotionCouponInterface|PromotionInterface(wherePromotionInterfaceis from the Component namespace), butgetVersion()is only guaranteed byVersionedInterface. The runtimeAssert::isInstanceOfcheck handles this gap.This works correctly in practice because Sylius's actual entity implementations extend the Core interfaces that include
VersionedInterface. However, if third-party code provides a pure Component-level implementation without versioning, this will fail at runtime rather than compile-time.Consider documenting this expectation or using
VersionedInterfacedirectly in the type signature if only versioned entities are supported.Alternative: Use VersionedInterface in the type signature
- private function refreshAndLock(PromotionCouponInterface|PromotionInterface $entity): void + private function refreshAndLock(VersionedInterface $entity): void { $this->entityManager->refresh($entity); - Assert::isInstanceOf($entity, VersionedInterface::class); $this->entityManager->lock($entity, LockMode::OPTIMISTIC, $entity->getVersion()); }This would require updating
lockEntities()to filter or assert types before callingrefreshAndLock().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Sylius/Bundle/CoreBundle/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifier.php` around lines 70 - 76, The method refreshAndLock currently accepts PromotionCouponInterface|PromotionInterface but relies on VersionedInterface::getVersion(); change refreshAndLock to require VersionedInterface (i.e. signature accepts VersionedInterface) and update lockEntities (the caller) to either filter the collection to only VersionedInterface instances or assert/cast each entity before calling refreshAndLock, so no runtime Assert is needed; alternatively add an inline docblock on refreshAndLock stating the requirement if you prefer to keep the broader signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/Sylius/Component/Core/Model/PromotionCouponInterface.php`:
- Around line 17-19: The change made PromotionCouponInterface to extend
VersionedInterface which introduces a BC break; revert that direct extension and
instead create a new interface VersionedPromotionCouponInterface that extends
PromotionCouponInterface and VersionedInterface (so existing implementers are
unaffected), move any internal usages that require versioning to type-hint
VersionedPromotionCouponInterface, and ensure getVersion()/setVersion() remain
only on the new interface; alternatively, if you intend to keep the change,
document it as an explicit BC break in the changelog and note implementers must
add getVersion()/setVersion() to their implementations.
---
Nitpick comments:
In
`@src/Sylius/Bundle/CoreBundle/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifier.php`:
- Around line 70-76: The method refreshAndLock currently accepts
PromotionCouponInterface|PromotionInterface but relies on
VersionedInterface::getVersion(); change refreshAndLock to require
VersionedInterface (i.e. signature accepts VersionedInterface) and update
lockEntities (the caller) to either filter the collection to only
VersionedInterface instances or assert/cast each entity before calling
refreshAndLock, so no runtime Assert is needed; alternatively add an inline
docblock on refreshAndLock stating the requirement if you prefer to keep the
broader signature.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5d21e5ee-83dd-443e-bbde-37c040f12d50
📒 Files selected for processing (12)
src/Sylius/Bundle/CoreBundle/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifier.phpsrc/Sylius/Bundle/CoreBundle/Migrations/Version20260216120000.phpsrc/Sylius/Bundle/CoreBundle/Migrations/Version20260216120001.phpsrc/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/Promotion.orm.xmlsrc/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/PromotionCoupon.orm.xmlsrc/Sylius/Bundle/CoreBundle/Resources/config/services/promotion.phpsrc/Sylius/Bundle/CoreBundle/tests/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifierTest.phpsrc/Sylius/Component/Core/Model/Promotion.phpsrc/Sylius/Component/Core/Model/PromotionCoupon.phpsrc/Sylius/Component/Core/Model/PromotionCouponInterface.phpsrc/Sylius/Component/Core/Model/PromotionInterface.phpsrc/Sylius/Component/Core/tests/Model/PromotionCouponTest.php
🚧 Files skipped from review as they are similar to previous changes (7)
- src/Sylius/Component/Core/Model/PromotionInterface.php
- src/Sylius/Component/Core/Model/Promotion.php
- src/Sylius/Bundle/CoreBundle/tests/Doctrine/ORM/Promotion/Modifier/AtomicOrderPromotionsUsageModifierTest.php
- src/Sylius/Component/Core/Model/PromotionCoupon.php
- src/Sylius/Component/Core/tests/Model/PromotionCouponTest.php
- src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/Promotion.orm.xml
- src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/PromotionCoupon.orm.xml
Summary by CodeRabbit
New Features
Refactor
Database
Tests