> For the complete documentation index, see [llms.txt](https://docs.ecotone.tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ecotone.tech/modules/tempest/tempest-models-as-aggregates.md).

# Tempest Models as Aggregates

Ecotone comes with out-of-the-box integration for using Tempest's active-record models (those using the `IsDatabaseModel` trait) as [State-Stored Aggregates](/modelling/command-handling/state-stored-aggregate.md#state-stored-aggregate). This is the Tempest equivalent of [Eloquent on Laravel](/modules/laravel/eloquent.md) and [Doctrine ORM on Symfony](/modules/symfony/doctrine-orm.md).

## Your Models as Aggregates

Mark your model with the `#[Aggregate]` attribute and add Command Handlers:

```php
use Ecotone\Modelling\Attribute\Aggregate;
use Ecotone\Modelling\Attribute\CommandHandler;
use Ecotone\Modelling\Attribute\IdentifierMethod;
use Ecotone\Modelling\Attribute\QueryHandler;
use Tempest\Database\IsDatabaseModel;
use Tempest\Database\PrimaryKey;

#[Aggregate]
final class Product
{
    use IsDatabaseModel;

    public PrimaryKey $id;
    public string $name;
    public int $price;

    #[CommandHandler] // 1. factory method
    public static function register(RegisterProduct $command): self
    {
        $product = new self();
        $product->name = $command->name;
        $product->price = $command->price;
        $product->save(); // 3. Saving

        return $product;
    }

    #[CommandHandler('product.changePrice')] // 2. action method
    public function changePrice(ChangePrice $command): void
    {
        $this->price = $command->price;
    }

    #[QueryHandler('product.getPrice')]
    public function getPrice(): int
    {
        return $this->price;
    }

    #[IdentifierMethod('id')] // 4. expose the scalar identifier
    public function getId(): int
    {
        return $this->id->value;
    }
}
```

1. Calling the factory method:

```php
$id = $this->commandBus->send(new RegisterProduct('Milk', 100));
```

2. Calling the action method:

```php
$this->commandBus->sendWithRouting('product.changePrice', new ChangePrice(200), metadata: ['aggregate.id' => $id]);
```

3. Aggregates require state to be always valid. Tempest assigns the auto-increment `PrimaryKey` on `save()`, so call `save()` in the factory to obtain the identifier. If you generate identifiers outside the database, this step is not needed.
4. `#[IdentifierMethod]` exposes the scalar identifier Ecotone uses to load and route to the aggregate (Tempest stores it as a `PrimaryKey` value object).

{% hint style="success" %}
You can use routing for your Message Handlers or direct Message Classes — whatever works best in your context.
{% endhint %}

## Repository and Business Interface

Because a Tempest model is a state-stored Aggregate, Ecotone persists it automatically through the `TempestRepository` when a Command Handler returns or mutates it — no repository wiring is required.

You can additionally declare a [DBAL Business Interface](/modelling/command-handling/business-interface/working-with-database.md) (`#[DbalQuery]` / `#[DbalWrite]`) for read-side queries over the same connection.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.ecotone.tech/modules/tempest/tempest-models-as-aggregates.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
