Skip to content

Commit 3ea3f55

Browse files
authored
feat: Message Broker Quickstart Example (#583)
1 parent 1b80698 commit 3ea3f55

File tree

6 files changed

+267
-1
lines changed

6 files changed

+267
-1
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Message Broker with Ecotone - RabbitMQ, Kafka, SQS, Redis
2+
3+
This example demonstrates how incredibly easy it is to set up **asynchronous messaging** with a message broker using [Ecotone Framework](https://ecotone.tech). With just a few lines of code, you can publish and consume messages from **RabbitMQ**, and switch to **Kafka**, **Amazon SQS**, **Redis**, or **Database (DBAL)** with a single line change.
4+
5+
## Quick Start
6+
7+
```bash
8+
# Install dependencies
9+
composer install
10+
11+
# Run publisher (sends message to RabbitMQ)
12+
php publisher.php
13+
14+
# Run consumer (receives and processes message)
15+
php consumer.php
16+
```
17+
18+
**Output:**
19+
```
20+
Message sent to queue 'orders'
21+
Starting consumer for queue 'orders'...
22+
Processing order 123: Milk
23+
Consumer finished.
24+
```
25+
26+
## Why Ecotone Makes It Easy
27+
28+
### 1. Single Line Channel Configuration
29+
30+
Setting up a RabbitMQ-backed message channel requires just **one line**:
31+
32+
```php
33+
AmqpBackedMessageChannelBuilder::create('orders')
34+
```
35+
36+
That's it. No complex queue bindings, no exchange declarations, no consumer configuration. Ecotone handles everything.
37+
38+
### 2. Switch Message Brokers Instantly
39+
40+
Want to use a different message broker? Just change the channel builder:
41+
42+
| Message Broker | Configuration | Package |
43+
|----------------|---------------|---------|
44+
| **RabbitMQ** | `AmqpBackedMessageChannelBuilder::create('orders')` | `ecotone/amqp` |
45+
| **Amazon SQS** | `SqsBackedMessageChannelBuilder::create('orders')` | `ecotone/sqs` |
46+
| **Redis** | `RedisBackedMessageChannelBuilder::create('orders')` | `ecotone/redis` |
47+
| **Kafka** | `KafkaMessageChannelBuilder::create('orders')` | `ecotone/kafka` |
48+
| **Database** | `DbalBackedMessageChannelBuilder::create('orders')` | `ecotone/dbal` |
49+
50+
Your business logic remains **completely unchanged**. The `#[Asynchronous('orders')]` attribute works with any of these brokers.
51+
52+
### 3. Type-Safe Commands
53+
54+
Use proper PHP classes for your messages instead of raw arrays:
55+
56+
```php
57+
class PlaceOrder
58+
{
59+
public function __construct(
60+
public string $orderId,
61+
public string $product
62+
) {}
63+
}
64+
65+
// Send type-safe command
66+
$ecotone->getCommandBus()->send(new PlaceOrder('123', 'Milk'));
67+
```
68+
69+
Ecotone automatically serializes and deserializes your objects.
70+
71+
## How It Works Under the Hood
72+
73+
### Publishing Flow
74+
75+
1. **Send Command**`CommandBus::send(new PlaceOrder(...))`
76+
2. **Serialize** → Command is converted to JSON (or other format)
77+
3. **Publish** → Message is sent to RabbitMQ queue named `orders`
78+
79+
### Consuming Flow
80+
81+
1. **Poll** → Consumer calls `$ecotone->run('orders')`
82+
2. **Receive** → Message is fetched from RabbitMQ queue
83+
3. **Deserialize** → JSON is converted back to `PlaceOrder` object
84+
4. **Invoke Handler**`OrderHandler::handle(PlaceOrder $command)` is called
85+
5. **Acknowledge** → Message is removed from queue on success
86+
87+
### The Asynchronous Attribute
88+
89+
```php
90+
#[Asynchronous('orders')]
91+
#[CommandHandler(endpointId: 'orderHandler')]
92+
public function handle(PlaceOrder $command): void
93+
{
94+
// This runs in the consumer process, not the publisher
95+
}
96+
```
97+
98+
- `#[Asynchronous('orders')]` - Routes the command to the `orders` message channel
99+
- `#[CommandHandler]` - Registers this method as a command handler
100+
- `endpointId` - Unique identifier for this endpoint (required for async handlers)
101+
102+
### Message Channel Architecture
103+
104+
```
105+
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
106+
│ Publisher │────▶│ Message Broker │────▶│ Consumer │
107+
│ (PHP CLI) │ │ (RabbitMQ) │ │ (PHP CLI) │
108+
└──────────────┘ └─────────────────┘ └──────────────┘
109+
│ │
110+
▼ ▼
111+
CommandBus CommandHandler
112+
.send(PlaceOrder) .handle(PlaceOrder)
113+
```
114+
115+
## Try It Yourself
116+
117+
1. **Clone this repository** and run the example
118+
2. **Check RabbitMQ Management UI** at `http://localhost:15672` (guest/guest)
119+
3. **Modify the handler** to see how messages are processed
120+
4. **Switch to a different broker** by changing one line
121+
122+
## Learn More
123+
124+
- 📚 [Ecotone Documentation](https://docs.ecotone.tech)
125+
126+
## Keywords
127+
128+
PHP Message Queue, RabbitMQ PHP, Kafka PHP, Amazon SQS PHP, Redis Pub/Sub PHP, Async PHP, PHP Message Broker, PHP Event-Driven Architecture, CQRS PHP, PHP Microservices, Ecotone Framework, PHP Messaging, Asynchronous PHP Processing, PHP Queue System, PHP Event Bus
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "ecotone/message-broker-quickstart",
3+
"license": "MIT",
4+
"authors": [
5+
{
6+
"name": "Dariusz Gafka",
7+
"email": "[email protected]"
8+
}
9+
],
10+
"require": {
11+
"ecotone/lite-amqp-starter": "^1.0.1"
12+
},
13+
"require-dev": {
14+
"phpunit/phpunit": "^9.6|^10.5|^11.0",
15+
"wikimedia/composer-merge-plugin": "^2.1"
16+
},
17+
"config": {
18+
"sort-packages": true,
19+
"allow-plugins": {
20+
"wikimedia/composer-merge-plugin": true
21+
}
22+
},
23+
"extra": {
24+
"merge-plugin": {
25+
"include": [
26+
"../../packages/local_packages.json"
27+
]
28+
}
29+
},
30+
"minimum-stability": "dev",
31+
"prefer-stable": true
32+
}
33+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
use Ecotone\Amqp\AmqpBackedMessageChannelBuilder;
4+
use Ecotone\Lite\EcotoneLite;
5+
use Ecotone\Messaging\Attribute\Asynchronous;
6+
use Ecotone\Messaging\Config\ServiceConfiguration;
7+
use Ecotone\Messaging\Endpoint\ExecutionPollingMetadata;
8+
use Ecotone\Modelling\Attribute\CommandHandler;
9+
use Enqueue\AmqpExt\AmqpConnectionFactory;
10+
11+
require __DIR__ . "/vendor/autoload.php";
12+
13+
// Command class - same definition must exist in publisher.php
14+
class PlaceOrder
15+
{
16+
public function __construct(
17+
public string $orderId,
18+
public string $product
19+
) {}
20+
}
21+
22+
// Handler class - same definition must exist in publisher.php
23+
class OrderHandler
24+
{
25+
#[Asynchronous('orders')]
26+
#[CommandHandler(endpointId: 'orderHandler')]
27+
public function handle(PlaceOrder $command): void
28+
{
29+
// This runs when consumer processes the message
30+
echo "Processing order {$command->orderId}: {$command->product}\n";
31+
}
32+
}
33+
34+
$channelName = 'orders';
35+
36+
$ecotone = EcotoneLite::bootstrap(
37+
classesToResolve: [PlaceOrder::class, OrderHandler::class],
38+
containerOrAvailableServices: [
39+
new OrderHandler(),
40+
AmqpConnectionFactory::class => new AmqpConnectionFactory([
41+
'dsn' => getenv('RABBIT_HOST') ?: 'amqp://guest:guest@localhost:5672/%2f'
42+
]),
43+
],
44+
configuration: ServiceConfiguration::createWithDefaults()
45+
->withExtensionObjects([
46+
AmqpBackedMessageChannelBuilder::create($channelName),
47+
])
48+
);
49+
50+
echo "Starting consumer for queue '{$channelName}'...\n";
51+
$ecotone->run($channelName, ExecutionPollingMetadata::createWithDefaults()->withHandledMessageLimit(1));
52+
echo "Consumer finished.\n";
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
use Ecotone\Amqp\AmqpBackedMessageChannelBuilder;
4+
use Ecotone\Lite\EcotoneLite;
5+
use Ecotone\Messaging\Attribute\Asynchronous;
6+
use Ecotone\Messaging\Config\ServiceConfiguration;
7+
use Ecotone\Modelling\Attribute\CommandHandler;
8+
use Enqueue\AmqpExt\AmqpConnectionFactory;
9+
use Ramsey\Uuid\Uuid;
10+
11+
require __DIR__ . "/vendor/autoload.php";
12+
13+
// Command class - same definition must exist in consumer.php
14+
class PlaceOrder
15+
{
16+
public function __construct(
17+
public string $orderId,
18+
public string $product
19+
) {}
20+
}
21+
22+
// Handler class - same definition must exist in consumer.php
23+
class OrderHandler
24+
{
25+
#[Asynchronous('orders')]
26+
#[CommandHandler(endpointId: 'orderHandler')]
27+
public function handle(PlaceOrder $command): void
28+
{
29+
// This runs asynchronously when consumer processes the message
30+
echo "Processing order {$command->orderId}: {$command->product}\n";
31+
}
32+
}
33+
34+
$channelName = 'orders';
35+
36+
$ecotone = EcotoneLite::bootstrap(
37+
classesToResolve: [PlaceOrder::class, OrderHandler::class],
38+
containerOrAvailableServices: [
39+
new OrderHandler(),
40+
AmqpConnectionFactory::class => new AmqpConnectionFactory([
41+
'dsn' => getenv('RABBIT_HOST') ?: 'amqp://guest:guest@localhost:5672/%2f'
42+
]),
43+
],
44+
configuration: ServiceConfiguration::createWithDefaults()
45+
->withExtensionObjects([
46+
AmqpBackedMessageChannelBuilder::create($channelName),
47+
])
48+
);
49+
50+
$ecotone->getCommandBus()->send(new PlaceOrder(Uuid::uuid4()->toString(), 'Milk'));
51+
52+
echo "Message sent to queue '{$channelName}'\n";

quickstart-examples/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"(cd EventSourcing && composer update && php run_example.php)",
3434
"(cd EventProjecting/PartitionedProjection && composer update && php run_example.php)",
3535
"(cd WorkingWithAggregateDirectly && composer update && php run_example.php)",
36+
"(cd MessageBroker && composer update && php publisher.php && php consumer.php)",
3637
"(cd Microservices && composer update && php run_example.php)",
3738
"(cd MicroservicesAdvanced && composer update && php run_example.php)",
3839
"(cd Schedule && composer update && php run_example.php)",

quickstart-examples/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ services:
1010
working_dir: "/data/app"
1111
command: sleep 999999
1212
environment:
13-
RABBIT_HOST: "amqp://rabbitmq:5672"
13+
RABBIT_HOST: "amqp://guest:guest@rabbitmq:5672/%2f"
1414
DATABASE_DSN: pgsql://ecotone:secret@database:5432/ecotone
1515
SECONDARY_DATABASE_DSN: mysql://ecotone:secret@database-mysql:3306/ecotone
1616
networks:

0 commit comments

Comments
 (0)