kenny1911 / doctrine-aggrolock
Doctrine ORM extension, that supports optimistic lock for aggregate entities
Requires
- php: ^8.1
- doctrine/event-manager: ^1.2 || ^2
- doctrine/orm: ^2.15 || ^3.0
Requires (Dev)
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2025-04-07 21:44:42 UTC
README
DoctrineAggroLock is Doctrine ORM extension that adds support optimistic lock of aggregate entities (associations). DoctrineAggroLock prevents data loss during concurrent changes using an aggregate versioning mechanism.
Features
- Optimistic lock for aggregates and their associations
- Integration with Doctrine ORM
- Configuration via interfaces (
AggregateRoot
andAggregateEntity
) - Automatic aggregates versioning
- DDD (Domain-Driven Design) support
Installation
composer require kenny1911/doctrine-aggrolock
Register the Doctrine Event Subscriber Kenny1911\DoctrineAggroLock\AggroLock
.
Symfony Framework Example:
services: Kenny1911\DoctrineAggroLock\AggregateEntitySubscriber: tags: - name: doctrine.event_subscriber
Example
Aggregate Root:
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\ReadableCollection; use Doctrine\ORM\Mapping as ORM; use Kenny1911\DoctrineAggroLock\AggregateRoot; use Ramsey\Uuid\UuidInterface as Uuid; /** * @final */ #[ORM\Entity] class Order implements AggregateRoot { #[ORM\Version] #[ORM\Column(type: 'integer')] private int $version = 1; /** @var Collection<array-key, OrderItem> */ #[ORM\OneToMany(targetEntity: OrderItem::class, mappedBy: 'order', cascade: ['persist', 'remove'], orphanRemoval: true)] private Collection $items; public function __construct( #[ORM\Id] #[ORM\Column(type: 'uuid')] private Uuid $id, ) { $this->id = $id; $this->customerName = $customerName; $this->items = new ArrayCollection(); } /** * @param non-negative-int $quantity * @param non-negative-int $price */ public function addItem(Uuid $productId, int $quantity, int $price): void { $this->items->add(new OrderItem($this, $productId, $quantity, $price)); // TODO } /** * @return ReadableCollection<array-key, OrderItem> */ public function getItems(): ReadableCollection { return $this->items; } }
Aggregate Entity
use Doctrine\ORM\Mapping as ORM; use Kenny1911\DoctrineAggroLock\AggregateEntity; use Kenny1911\DoctrineAggroLock\AggregateRoot; use Ramsey\Uuid\UuidInterface as Uuid; /** * @final */ #[ORM\Entity] class OrderItem implements AggregateEntity { /** * @param non-negative-int $quantity * @param non-negative-int $price */ public function __construct( #[ORM\ManyToOne(targetEntity: Order::class, inversedBy: 'items')] private Order $order, #[ORM\Column(type: 'uuid')] private Uuid $productId, #[ORM\Column(type: 'integer')] private int $quantity, #[ORM\Column(type: 'integer')] private float $price ) {} public function getAggregateRoot(): AggregateRoot { return $this->order; } }
Explanations
-
Aggregate Root (
Order
) implements theAggregateRoot
interface. It guarantees that the object is the root of the aggregate and not just an entity. -
Aggregate Entities (
OrderItem
) implement theAggregateEntity
interface and are associated with the aggregate root through aManyToOne
relation. -
Optimistic Locking is implemented using the standard Doctrine ORM approach with a special version field (
version
).