PHPackages                             ecotone/tempest - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. ecotone/tempest

ActiveLibrary[Queues &amp; Workers](/categories/queues)

ecotone/tempest
===============

Ecotone for Tempest — CQRS, Event Sourcing, Sagas, Durable Workflows, and Outbox via PHP attributes.

1.320.0(3w ago)00Apache-2.0PHPPHP ^8.4

Since Jun 12Pushed 5d agoCompare

[ Source](https://github.com/ecotoneframework/tempest)[ Packagist](https://packagist.org/packages/ecotone/tempest)[ Docs](https://docs.ecotone.tech/)[ GitHub Sponsors](https://github.com/dgafka)[ RSS](/packages/ecotone-tempest/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (14)Versions (6)Used By (0)

This is Read Only Repository
============================

[](#this-is-read-only-repository)

To contribute make use of [Ecotone-Dev repository](https://github.com/ecotoneframework/ecotone-dev).

[ ![](https://github.com/ecotoneframework/ecotone-dev/raw/main/ecotone_small.png?raw=true)](https://ecotone.tech)

[![Github Actions](https://github.com/ecotoneFramework/ecotone-dev/actions/workflows/split-testing.yml/badge.svg)](https://github.com/ecotoneFramework/ecotone-dev/actions/workflows/split-testing.yml/badge.svg)[![Latest Stable Version](https://camo.githubusercontent.com/c07cc9214a6932b6dac5822c246d035bbbca5b85d8e9da40303ac384756fb67f/68747470733a2f2f706f7365722e707567782e6f72672f65636f746f6e652f74656d706573742f762f737461626c65)](https://packagist.org/packages/ecotone/tempest)[![License](https://camo.githubusercontent.com/0bdad8ba241344b0812a1b040e236469c405c8edd3dc8bd2045c38cf297b605d/68747470733a2f2f706f7365722e707567782e6f72672f65636f746f6e652f74656d706573742f6c6963656e7365)](https://packagist.org/packages/ecotone/tempest)[![Total Downloads](https://camo.githubusercontent.com/2a6688ea85afe14131fc229068ec331eb3a53042b80b9b5617a0bad3b55e0be5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f65636f746f6e652f74656d70657374)](https://packagist.org/packages/ecotone/tempest)[![PHP Version Require](https://camo.githubusercontent.com/47f47d7440f917c5d2c97a3cfd37a3cf5b4e306f33c42b1bb594a915b5eaf366/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f65636f746f6e652f74656d706573742f7068702e737667)](https://packagist.org/packages/ecotone/tempest)

**Ecotone is the PHP architecture layer that grows with your system — without rewrites.**

From `#[CommandHandler]` on day one, to event sourcing, sagas, outbox, and distributed messaging at scale — one package, same codebase, no forced migrations between growth stages. Declarative PHP 8 attributes on the classes you already have.

ecotone/tempest
---------------

[](#ecotonetempest)

Ecotone for [Tempest](https://tempestphp.com) — CQRS, Event Sourcing, Sagas, Durable Workflows, and Outbox via PHP attributes. Zero-config auto-discovery derives your application namespaces from your `composer.json` PSR-4 roots. Handlers, aggregates, sagas, and projections are found automatically without any registration boilerplate.

- Zero-config auto-discovery of handlers from your app's PSR-4 namespaces
- CQRS — `CommandBus`, `QueryBus`, `EventBus` available via dependency injection
- Database integration via `ecotone/dbal` with Tempest's `DatabaseConfig`
- Multi-tenant connections with per-tenant database switching
- Async messaging, sagas, outbox, event sourcing (with appropriate modules)
- Console commands: `ecotone:list`, `ecotone:run`, `ecotone:cache:clear`

Visit [ecotone.tech](https://ecotone.tech) to learn more.

Installation
------------

[](#installation)

```
composer require ecotone/tempest
```

Ecotone auto-discovery is enabled via Tempest's package discovery system. No service provider registration is needed.

Getting Started
---------------

[](#getting-started)

### Zero-Config Handler Discovery

[](#zero-config-handler-discovery)

Ecotone derives your application namespaces from the PSR-4 roots declared in your `composer.json`. Any class with an Ecotone attribute (`#[CommandHandler]`, `#[QueryHandler]`, `#[EventHandler]`, etc.) in those namespaces is discovered automatically.

```
// src/Order/PlaceOrderHandler.php
namespace App\Order;

use Ecotone\Modelling\Attribute\CommandHandler;
use Ecotone\Modelling\Attribute\QueryHandler;

final class PlaceOrderHandler
{
    private array $orders = [];

    #[CommandHandler('order.place')]
    public function place(string $orderId): void
    {
        $this->orders[] = $orderId;
    }

    #[QueryHandler('order.all')]
    public function all(): array
    {
        return $this->orders;
    }
}
```

That is all that is needed. No configuration file, no registration — Ecotone discovers it from your namespace.

### Using the Buses

[](#using-the-buses)

`CommandBus`, `QueryBus`, and `EventBus` are automatically registered in the Tempest container and can be injected anywhere:

```
use Ecotone\Modelling\CommandBus;
use Ecotone\Modelling\QueryBus;

final class OrderController
{
    public function __construct(
        private CommandBus $commandBus,
        private QueryBus $queryBus,
    ) {}

    public function place(string $orderId): array
    {
        $this->commandBus->sendWithRouting('order.place', $orderId);

        return $this->queryBus->sendWithRouting('order.all');
    }
}
```

Optional Configuration
----------------------

[](#optional-configuration)

Create a class that provides `EcotoneConfig` to the Tempest container to customise behaviour:

```
// src/Configuration/EcotoneConfiguration.php
namespace App\Configuration;

use Ecotone\Tempest\EcotoneConfig;
use Tempest\Container\Singleton;

#[Singleton]
final class EcotoneConfiguration
{
    public function ecotoneConfig(): EcotoneConfig
    {
        return new EcotoneConfig(
            serviceName: 'my-service',
            licenceKey: getenv('ECOTONE_LICENCE_KEY') ?: '',
            cacheConfiguration: true,
        );
    }
}
```

Or simply bind `EcotoneConfig` in a Tempest config file:

```
// config/ecotone.php  (or any #[ServiceContext] provider)
use Ecotone\Tempest\EcotoneConfig;

return new EcotoneConfig(
    serviceName: 'my-service',
    licenceKey: getenv('ECOTONE_LICENCE_KEY') ?: '',
);
```

### `EcotoneConfig` Reference

[](#ecotoneconfig-reference)

PropertyTypeDefaultDescription`serviceName``string``''` (from `ECOTONE_SERVICE_NAME` env)Identifies this service in distributed tracing and logs`namespaces``array``[]`Explicit namespaces to scan. When empty and `loadAppNamespaces` is true, derived from `composer.json``loadAppNamespaces``bool``true`Auto-derive scan namespaces from your app's PSR-4 roots`cacheConfiguration``bool``false` (from `ECOTONE_CACHE_CONFIGURATION` env)Cache the messaging system definition for production`defaultSerializationMediaType``string``''`Override the default message serialization format`defaultErrorChannel``string``''`Channel name for unhandled async exceptions`skippedModulePackageNames``array``[]`Module packages to skip loading (useful for testing)`licenceKey``string``''`Enterprise licence keyConsole Commands
----------------

[](#console-commands)

Ecotone registers Tempest console commands automatically:

```
# List all registered consumers and handlers
./tempest ecotone:list

# Run an asynchronous consumer (requires ecotone/dbal or another async transport)
./tempest ecotone:run notifications

# Clear the Ecotone configuration cache
./tempest ecotone:cache:clear
```

The `ecotone:cache:clear` command removes the cached messaging system definition from `sys_get_temp_dir()/ecotone_tempest/`. Use it after deploying changes when `cacheConfiguration` is enabled.

Database Integration (requires `ecotone/dbal`)
----------------------------------------------

[](#database-integration-requires-ecotonedbal)

Install the DBAL module:

```
composer require ecotone/dbal
```

### Single-Tenant Connection

[](#single-tenant-connection)

Register a `TempestConnectionReference` via `#[ServiceContext]` to bridge Tempest's `DatabaseConfig` to Ecotone's DBAL module:

```
use Ecotone\Messaging\Attribute\ServiceContext;
use Ecotone\Tempest\Config\TempestConnectionReference;

final class EcotoneConfiguration
{
    #[ServiceContext]
    public function dbalConnection(): TempestConnectionReference
    {
        return TempestConnectionReference::defaultConnection();
    }
}
```

`TempestConnectionReference::defaultConnection()` resolves Tempest's default `DatabaseConfig` from the container at runtime.

To use a specific config:

```
use Tempest\Database\Config\PostgresConfig;

#[ServiceContext]
public function dbalConnection(): TempestConnectionReference
{
    return TempestConnectionReference::create('myConnection', new PostgresConfig(
        host: getenv('DB_HOST') ?: 'localhost',
        port: getenv('DB_PORT') ?: '5432',
        username: getenv('DB_USER') ?: 'app',
        password: getenv('DB_PASSWORD') ?: '',
        database: getenv('DB_NAME') ?: 'app',
    ));
}
```

### Multi-Tenant Connection

[](#multi-tenant-connection)

Use `MultiTenantConfiguration` together with per-tenant `TempestConnectionReference` instances. A `tenant` header on each message selects the correct database:

```
use Ecotone\Dbal\MultiTenant\MultiTenantConfiguration;
use Ecotone\Messaging\Attribute\ServiceContext;
use Ecotone\Tempest\Config\TempestConnectionReference;
use Tempest\Database\Config\MysqlConfig;
use Tempest\Database\Config\PostgresConfig;

final class EcotoneConfiguration
{
    #[ServiceContext]
    public function multiTenantConfiguration(): MultiTenantConfiguration
    {
        return MultiTenantConfiguration::create(
            tenantHeaderName: 'tenant',
            tenantToConnectionMapping: [
                'tenant_a' => TempestConnectionReference::create('tenant_a', new PostgresConfig(
                    host: getenv('TENANT_A_DB_HOST'),
                    username: getenv('TENANT_A_DB_USER'),
                    password: getenv('TENANT_A_DB_PASSWORD'),
                    database: getenv('TENANT_A_DB_NAME'),
                )),
                'tenant_b' => TempestConnectionReference::create('tenant_b', new MysqlConfig(
                    host: getenv('TENANT_B_DB_HOST'),
                    username: getenv('TENANT_B_DB_USER'),
                    password: getenv('TENANT_B_DB_PASSWORD'),
                    database: getenv('TENANT_B_DB_NAME'),
                )),
            ],
        );
    }
}
```

Route commands to the correct tenant by setting the header:

```
$commandBus->sendWithRouting('order.place', $order, metadata: ['tenant' => 'tenant_a']);
```

> **Security note:** When `cacheConfiguration` is enabled, the `DatabaseConfig` for each `TempestConnectionReference::create(name, config)` call is serialized into the on-disk cache file. This means database credentials (username, password, DSN) are written to disk in base64-encoded serialized form. Keep the cache directory (`sys_get_temp_dir()/ecotone_tempest/`) non-world-readable and rotate credentials if the cache file is exposed. Use `./tempest ecotone:cache:clear` after credential rotation.

Production Caching
------------------

[](#production-caching)

Enable the configuration cache for production to avoid re-scanning annotations on every request:

```
new EcotoneConfig(
    cacheConfiguration: true,
);
```

Or set the `ECOTONE_CACHE_CONFIGURATION=1` environment variable. The cache is stored in `sys_get_temp_dir()/ecotone_tempest/`. Clear it after deployment:

```
./tempest ecotone:cache:clear
```

When `APP_ENV=prod` or `APP_ENV=production`, the production cache path is used automatically regardless of the `cacheConfiguration` setting.

Expression Language
-------------------

[](#expression-language)

Ecotone supports Symfony Expression Language in `#[Payload]` and `#[Header]` attributes. The `parameter()` function reads from environment variables via `TempestConfigurationVariableService`:

```
use Ecotone\Messaging\Attribute\Parameter\Payload;
use Ecotone\Modelling\Attribute\CommandHandler;

final class CalculatorHandler
{
    #[CommandHandler('calculator.multiply')]
    public function multiply(
        #[Payload("parameter('APP_MULTIPLIER') * payload['value']")] int $result
    ): void {
        // $result = APP_MULTIPLIER * payload['value']
    }
}
```

Requires `symfony/expression-language`:

```
composer require symfony/expression-language
```

Feature requests and issue reporting
------------------------------------

[](#feature-requests-and-issue-reporting)

Use [issue tracking system](https://github.com/ecotoneframework/ecotone-dev/issues) for new feature request and bugs. Please verify that it's not already reported by someone else.

Contact
-------

[](#contact)

If you want to talk or ask questions about Ecotone

- [**Twitter**](https://twitter.com/EcotonePHP)
- ****
- [**Community Channel**](https://discord.gg/GwM2BSuXeg)

Support Ecotone
---------------

[](#support-ecotone)

If you want to help building and improving Ecotone consider becoming a sponsor:

- [Sponsor Ecotone](https://github.com/sponsors/dgafka)
- [Contribute to Ecotone](https://github.com/ecotoneframework/ecotone-dev).

Tags
----

[](#tags)

PHP, Ecotone, Tempest, CQRS, Event Sourcing, Sagas, Durable Workflows, Outbox, Messaging, EIP, DDD

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance97

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 88.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~5 days

Total

5

Last Release

25d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/1b6139b0dc15c550b22c76954ec6a96ac6c46b4c19b4a3c1e93c2146481e109e?d=identicon)[dgafka](/maintainers/dgafka)

---

Top Contributors

[![ecotoneframework-bot](https://avatars.githubusercontent.com/u/174240763?v=4)](https://github.com/ecotoneframework-bot "ecotoneframework-bot (8 commits)")[![dgafka](https://avatars.githubusercontent.com/u/6060791?v=4)](https://github.com/dgafka "dgafka (1 commits)")

---

Tags

workflowmessagingdddevent sourcingcqrsecotoneeiptempestsagasoutboxdurable-workflows

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ecotone-tempest/health.svg)

```
[![Health](https://phpackages.com/badges/ecotone-tempest/health.svg)](https://phpackages.com/packages/ecotone-tempest)
```

###  Alternatives

[ecotone/symfony-bundle

Ecotone for Symfony — CQRS, Event Sourcing, Sagas, Durable Workflows, and Outbox on top of Symfony Messenger, via PHP attributes.

11249.0k1](/packages/ecotone-symfony-bundle)[ecotone/laravel

Ecotone for Laravel — CQRS, Event Sourcing, Sagas, Durable Workflows, and Outbox on top of Laravel Queue, via PHP attributes.

21318.6k3](/packages/ecotone-laravel)[ecotone/ecotone

Enterprise architecture layer for Laravel and Symfony — CQRS, Event Sourcing, Durable Workflows (Sagas, Orchestrators), Projections, and Outbox messaging via PHP attributes.

564576.7k54](/packages/ecotone-ecotone)[broadway/broadway

Infrastructure and testing helpers for creating CQRS and event sourced applications.

1.5k2.0M63](/packages/broadway-broadway)[prooph/humus-amqp-producer

HumusAmqp Producer for Prooph Service Bus

1223.3k3](/packages/prooph-humus-amqp-producer)[prooph/psb-enqueue-producer

Enqueue Message Producer for Prooph Service Bus

1095.2k](/packages/prooph-psb-enqueue-producer)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
