ecotone / tempest
Ecotone for Tempest — CQRS, Event Sourcing, Sagas, Durable Workflows, and Outbox via PHP attributes.
Fund package maintenance!
Requires
- php: ^8.4
- ecotone/ecotone: ~2.0.0-beta.6
- tempest/framework: ^3.11
Requires (Dev)
- doctrine/dbal: ^4.0
- ecotone/dbal: ~2.0.0-beta.6
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0|^12.0
- symfony/expression-language: ^6.4|^7.0|^8.0
Suggests
- ecotone/dbal: Enables DBAL integration, database repositories, multi-tenant connections via TempestConnectionReference, and the DbalModule for transactional outbox, sagas, and event sourcing backed by relational databases.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-19 20:46:21 UTC
README
To contribute make use of Ecotone-Dev repository.
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
Ecotone for Tempest — 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,EventBusavailable via dependency injection - Database integration via
ecotone/dbalwith Tempest'sDatabaseConfig - 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 to learn more.
Installation
composer require ecotone/tempest
Ecotone auto-discovery is enabled via Tempest's package discovery system. No service provider registration is needed.
Getting Started
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\Api\Attribute\CommandHandler; use Ecotone\Api\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
CommandBus, QueryBus, and EventBus are automatically registered in the Tempest container and can be injected anywhere:
use Ecotone\Api\Gateway\CommandBus; use Ecotone\Api\Gateway\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
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( licenceKey: getenv('ECOTONE_LICENCE_KEY') ?: '', ); } }
Or simply bind EcotoneConfig in a Tempest config file:
// config/ecotone.php (or any #[ServiceContext] provider) use Ecotone\Tempest\EcotoneConfig; return new EcotoneConfig( licenceKey: getenv('ECOTONE_LICENCE_KEY') ?: '', );
The service name, default serialization media type, default error channel and the other application settings are
set on ServiceConfiguration, returned from exactly one #[ServiceContext] method:
#[ServiceContext] public function serviceConfiguration(): ServiceConfiguration { return ServiceConfiguration::createWithDefaults() ->withServiceName('my_service') ->withDefaultErrorChannel('errorChannel'); }
EcotoneConfig Reference
| Property | Type | Default | Description |
|---|---|---|---|
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 |
enabledModulePackageNames |
?array |
null (every installed package) |
Module packages to load in addition to Core, as ModulePackageList constants; [] loads Core alone |
test |
bool |
false |
Load the test support module |
licenceKey |
string |
'' |
Enterprise licence key |
Any other named argument is refused when the configuration is constructed, with a ConfigurationException naming
the arguments EcotoneConfig reads. The 1.x skippedModulePackageNames and the earlier 2.0 modulePackages are
refused with their replacement: skippedModulePackageNames listed packages to skip, enabledModulePackageNames lists
packages to load.
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 the production cache is in use.
Database Integration (requires ecotone/dbal)
Install the DBAL module:
composer require ecotone/dbal
Single-Tenant Connection
Register a TempestConnectionReference via #[ServiceContext] to bridge Tempest's DatabaseConfig to Ecotone's DBAL module:
use Ecotone\Api\Attribute\ServiceContext; use Ecotone\Api\Tempest\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
Use MultiTenantConfiguration together with per-tenant TempestConnectionReference instances. A tenant header on each message selects the correct database:
use Ecotone\Api\Dbal\ExtensionObject\MultiTenantConfiguration; use Ecotone\Api\Attribute\ServiceContext; use Ecotone\Api\Tempest\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 the production cache is in use, the
DatabaseConfigfor eachTempestConnectionReference::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:clearafter credential rotation.
Production Caching
The container cache mode is derived, there is no setting for it. When APP_ENV=prod or APP_ENV=production, or when
Tempest's own discovery cache is on (DISCOVERY_CACHE=true), the dumped messaging system is reused as is from
sys_get_temp_dir()/ecotone_tempest/; clear it after deployment:
./tempest ecotone:cache:clear
In every other case the dumped container is rebuilt whenever a scanned file changes.
Expression Language
Ecotone supports Symfony Expression Language in #[Payload] and #[Header] attributes. The parameter() function reads from environment variables via TempestConfigurationVariableService:
use Ecotone\Api\Attribute\Payload; use Ecotone\Api\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
Use issue tracking system for new feature request and bugs. Please verify that it's not already reported by someone else.
Contact
If you want to talk or ask questions about Ecotone
Support Ecotone
If you want to help building and improving Ecotone consider becoming a sponsor:
Tags
PHP, Ecotone, Tempest, CQRS, Event Sourcing, Sagas, Durable Workflows, Outbox, Messaging, EIP, DDD