You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Prooph中如何通过非ID属性聚合及获取聚合根?

Handling Non-ID Aggregate Operations in Prooph & Fetching Aggregates by Unique Attributes

Got it, let's break down how to solve your problem with Prooph Event Sourcing. Prooph is built around aggregate IDs as the primary lookup key, but we can extend it to support operations based on unique attributes like ownerId for your Cart aggregate.


1. Core Setup: Define the Cart Aggregate Root

First, make sure your Cart aggregate properly encapsulates both the aggregate ID and the unique ownerId:

use Prooph\EventSourcing\AggregateRoot;
use Ramsey\Uuid\UuidInterface;

class Cart extends AggregateRoot
{
    private CartId $cartId;
    private UuidInterface $ownerId;

    // Factory method to create a new Cart
    public static function create(CartId $cartId, UuidInterface $ownerId): self
    {
        $cart = new self();
        $cart->recordThat(CartCreated::withData($cartId, $ownerId));
        return $cart;
    }

    // Getters for aggregate ID and owner ID
    public function cartId(): CartId
    {
        return $this->cartId;
    }

    public function ownerId(): UuidInterface
    {
        return $this->ownerId;
    }

    // Required: Return the aggregate's unique identifier as a string
    protected function aggregateId(): string
    {
        return $this->cartId->toString();
    }

    // Apply events to update aggregate state
    protected function applyCartCreated(CartCreated $event): void
    {
        $this->cartId = $event->cartId();
        $this->ownerId = $event->ownerId();
    }
}

2. Fetching Cart by ownerId (Without Knowing Cart ID)

Prooph's default AggregateRepository only loads aggregates by their ID, so we need a way to map ownerId to CartId. The cleanest approach is to use a projection read model to maintain this mapping.

Step 2.1: Create a Projection for Owner-to-Cart ID Mapping

This projection listens to cart events and updates a lookup table that links ownerId to CartId:

use Prooph\EventStore\Projection\AbstractReadModelProjection;

class CartOwnerLookupProjection extends AbstractReadModelProjection
{
    public function init(): void
    {
        $this->fromStream('cart_stream')
            ->when([
                CartCreated::class => function ($state, CartCreated $event) {
                    $this->readModel()->stack('insert', [
                        'owner_id' => $event->ownerId()->toString(),
                        'cart_id' => $event->cartId()->toString()
                    ]);
                },
                // Handle owner ID changes if your Cart supports them
                CartOwnerUpdated::class => function ($state, CartOwnerUpdated $event) {
                    $this->readModel()->stack('update', [
                        'owner_id' => $event->newOwnerId()->toString()
                    ], [
                        'cart_id' => $event->cartId()->toString()
                    ]);
                }
            ]);
    }
}

Step 2.2: Implement the Read Model for Lookups

This read model manages the lookup table and provides a method to find CartId by ownerId:

use Prooph\EventStore\Projection\ReadModel;

class CartOwnerLookupReadModel implements ReadModel
{
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function init(): void
    {
        $stmt = $this->pdo->prepare('CREATE TABLE IF NOT EXISTS cart_owner_lookup (
            owner_id VARCHAR(36) PRIMARY KEY,
            cart_id VARCHAR(36) NOT NULL UNIQUE
        )');
        $stmt->execute();
    }

    public function stack(string $operation, array $data, array $where = []): void
    {
        match ($operation) {
            'insert' => $this->insert($data),
            'update' => $this->update($data, $where),
            default => throw new \InvalidArgumentException("Unsupported operation: $operation")
        };
    }

    private function insert(array $data): void
    {
        $stmt = $this->pdo->prepare('INSERT INTO cart_owner_lookup (owner_id, cart_id) VALUES (:owner_id, :cart_id)');
        $stmt->execute($data);
    }

    private function update(array $data, array $where): void
    {
        $stmt = $this->pdo->prepare('UPDATE cart_owner_lookup SET owner_id = :owner_id WHERE cart_id = :cart_id');
        $stmt->execute(array_merge($data, $where));
    }

    // Add this method to fetch CartId by OwnerId
    public function findCartIdByOwnerId(string $ownerId): ?string
    {
        $stmt = $this->pdo->prepare('SELECT cart_id FROM cart_owner_lookup WHERE owner_id = :owner_id');
        $stmt->execute(['owner_id' => $ownerId]);
        return $stmt->fetchColumn() ?: null;
    }

    // Implement required ReadModel methods (isInitialized, reset, delete)
    public function isInitialized(): bool
    {
        $stmt = $this->pdo->prepare('SHOW TABLES LIKE "cart_owner_lookup"');
        $stmt->execute();
        return $stmt->rowCount() > 0;
    }

    public function reset(): void
    {
        $stmt = $this->pdo->prepare('TRUNCATE TABLE cart_owner_lookup');
        $stmt->execute();
    }

    public function delete(): void
    {
        $stmt = $this->pdo->prepare('DROP TABLE IF EXISTS cart_owner_lookup');
        $stmt->execute();
    }
}

Step 2.3: Extend the Repository for Clean Lookups

Wrap the lookup logic in a custom repository to make it easy to fetch carts by ownerId:

use Prooph\EventSourcing\AggregateRepository;
use Prooph\EventStore\EventStore;
use Prooph\EventSourcing\AggregateFactory;
use Prooph\EventSourcing\Snapshot\SnapshotStore;

class CartRepository extends AggregateRepository
{
    private CartOwnerLookupReadModel $lookupReadModel;

    public function __construct(
        EventStore $eventStore,
        AggregateFactory $aggregateFactory,
        string $aggregateRootType,
        ?SnapshotStore $snapshotStore,
        CartOwnerLookupReadModel $lookupReadModel
    ) {
        parent::__construct($eventStore, $aggregateFactory, $aggregateRootType, $snapshotStore);
        $this->lookupReadModel = $lookupReadModel;
    }

    /**
     * Load a Cart by its owner's ID
     * @throws CartNotFoundForOwner If no cart exists for the given owner ID
     */
    public function loadByOwnerId(UuidInterface $ownerId): Cart
    {
        $cartIdString = $this->lookupReadModel->findCartIdByOwnerId($ownerId->toString());

        if ($cartIdString === null) {
            throw new CartNotFoundForOwner($ownerId);
        }

        return $this->load(CartId::fromString($cartIdString));
    }
}

3. Performing Operations on Cart via ownerId

Once you can load the cart by ownerId, you can perform any aggregate operations just like you would with the cart ID:

// Example: Add an item to a cart using ownerId
public function handle(AddItemToCart $command): void
{
    $ownerId = $command->ownerId();
    $cart = $this->cartRepository->loadByOwnerId($ownerId);

    $cart->addItem($command->itemId(), $command->quantity());

    $this->cartRepository->save($cart);
}

Enforce ownerId Uniqueness

To prevent duplicate carts for the same owner, add a check in your command handler before creating a cart:

public function handle(CreateCart $command): void
{
    $ownerId = $command->ownerId();

    if ($this->cartOwnerLookupReadModel->findCartIdByOwnerId($ownerId->toString()) !== null) {
        throw new CartAlreadyExistsForOwner($ownerId);
    }

    $cart = Cart::create($command->cartId(), $ownerId);
    $this->cartRepository->save($cart);
}

Key Takeaways

  • Prooph is designed around aggregate IDs, so we use a projection to maintain a mapping between your unique attribute (ownerId) and the aggregate ID.
  • Custom repositories encapsulate the lookup logic, keeping your domain code clean.
  • Always enforce uniqueness of your attribute at the command level to avoid conflicts.

内容的提问来源于stack exchange,提问作者gargi258

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 11:14:39