Microservices
Introductionβ
The bowphp/microservice package provides a microservice client and server for Bow, inspired by NestJS's @nestjs/microservices. It lets your applications communicate with each other over several transports: TCP, Redis, RabbitMQ, gRPC, and Kafka.
Two communication modes are supported:
- Request/Response (RPC) β via
send(), the equivalent of@MessagePatternon the server side. - Fire-and-forget (Event) β via
emit(), the equivalent of@EventPatternon the server side.
The core of the package is framework-agnostic; only the Bow\Microservice\Bow\MicroserviceConfiguration service provider is coupled to Bow.
Installationβ
composer require bowphp/microservice
Then register the provider in your application's kernel:
// app/Kernel.php
public function configurations(): array
{
return [
\Bow\Microservice\Bow\MicroserviceConfiguration::class,
];
}
Publish the default configuration file to config/microservice.php:
php bow microservice:publish-config
Configurationβ
The config/microservice.php file selects the active transport and groups each transport's options under a matching key. If the file is missing, the provider falls back to the MICROSERVICE_* environment variables.
// config/microservice.php
return [
'transport' => app_env('MICROSERVICE_TRANSPORT', 'redis'),
'timeout' => (float) app_env('MICROSERVICE_TIMEOUT', 5.0),
'controllers' => [
// \App\Consumers\OrderConsumer::class,
],
'redis' => [
'host' => app_env('MICROSERVICE_HOST', '127.0.0.1'),
'port' => (int) app_env('MICROSERVICE_PORT', 6379),
'password' => app_env('MICROSERVICE_REDIS_PASSWORD', null),
'patterns' => [],
],
'tcp' => [/* host, port */],
'rabbitmq' => [/* host, port, user, password, queue */],
'grpc' => [/* host, port */],
'kafka' => [/* brokers, topic */],
];
Available transportsβ
| Transport | Usage | Required extension |
|---|---|---|
tcp | Raw socket, no broker | none |
redis | Redis pub/sub + RPC | phpredis |
rabbitmq | Durable queue | php-amqplib |
grpc | Client only (third-party server) | grpc (PECL) |
kafka | High-throughput streaming | rdkafka |
Client sideβ
The provider exposes two interchangeable bindings in the container:
Bow\Microservice\Client\ClientProxy::classβ to be injected by type.'microservice.client'β string alias forapp('microservice.client').
Configuring the ClientProxyβ
The ClientProxy shared by the container is built only once, when the binding is resolved. Its parameters can be set at five levels, from the broadest to the most specific:
1. The config/microservice.php fileβ
This is the recommended path for most applications. The provider reads transport, timeout, and the option block of the active transport:
// config/microservice.php
return [
'transport' => 'redis',
'timeout' => 3.0,
'redis' => [
'host' => 'redis.internal',
'port' => 6379,
'password' => app_env('MICROSERVICE_REDIS_PASSWORD'),
],
];
2. The MICROSERVICE_* environment variablesβ
If config/microservice.php is missing, the provider falls back to the MICROSERVICE_* variables (see the Environment variables section). Handy in CI / containers, without having to publish the configuration file.
3. Manually, with ClientFactory::create()β
For tests, to open multiple clients toward different brokers, or to build a proxy outside the container, instantiate it directly:
use Bow\Microservice\Client\ClientFactory;
$client = ClientFactory::create(
transport: ClientFactory::REDIS,
options: [
'host' => '127.0.0.1',
'port' => 6379,
'password' => null,
],
defaultTimeout: 10.0,
);
$client->connect();
$user = $client->send('user.find', ['id' => 42]);
$client->close();
The constants ClientFactory::TCP, REDIS, RABBITMQ, KAFKA, GRPC cover the supported transports. Each transport accepts its own keys in options:
| Transport | options keys |
|---|---|
tcp | host, port |
redis | host, port, password |
rabbitmq | host, port, user, password, queue, vhost |
kafka | brokers, topic, reply_topic |
grpc | host, port, channel_options |
4. Configuring multiple clientsβ
The shipped provider exposes a single shared ClientProxy (ClientProxy::class + 'microservice.client'). To add additional clients β for example a Redis one for business RPCs and a RabbitMQ one for billing β extend ClientProxy once per broker, then register each subclass in your ApplicationConfiguration. The main benefit: each client becomes injectable by type, without a string key.
Step 1 β Subclass ClientProxyβ
A subclass locks in the client's transport and timeout. Instantiate the transport class directly (the options are the same as those documented above):
namespace App\Microservice;
use Bow\Microservice\Client\ClientProxy;
use Bow\Microservice\Transport\RabbitMqClientTransport;
final class BillingClient extends ClientProxy
{
public function __construct()
{
parent::__construct(
new RabbitMqClientTransport(
queue: 'billing_rpc',
host: 'rabbit.billing.internal',
port: 5672,
user: 'guest',
password: 'guest',
),
defaultTimeout: 10.0,
);
}
}
For a second client on another transport, repeat the same structure:
namespace App\Microservice;
use Bow\Microservice\Client\ClientProxy;
use Bow\Microservice\Transport\RedisClientTransport;
final class SearchClient extends ClientProxy
{
public function __construct()
{
parent::__construct(
new RedisClientTransport(
host: 'redis.search.internal',
port: 6379,
),
defaultTimeout: 2.0,
);
}
}
The available transport classes are TcpClientTransport, RedisClientTransport, RabbitMqClientTransport, KafkaClientTransport, and GrpcClientTransport β all under Bow\Microservice\Transport\. Their parameters match the options keys listed in the table in the previous section.
Step 2 β Register the clients in ApplicationConfigurationβ
Bind each subclass in the host application's Configuration (often named ApplicationConfiguration):
namespace App\Configurations;
use App\Microservice\BillingClient;
use App\Microservice\SearchClient;
use Bow\Configuration\Configuration;
use Bow\Configuration\Loader;
class ApplicationConfiguration extends Configuration
{
public function create(Loader $config): void
{
$this->container->bind(BillingClient::class, static function (): BillingClient {
$client = new BillingClient();
$client->connect();
return $client;
});
$this->container->bind(SearchClient::class, static function (): SearchClient {
$client = new SearchClient();
$client->connect();
return $client;
});
}
public function run(): void
{
// Eager resolution: transport errors surface at boot
// rather than on the first send().
$this->container->make(BillingClient::class);
$this->container->make(SearchClient::class);
}
}
Then declare the class in the kernel, after MicroserviceConfiguration:
// app/Kernel.php
public function configurations(): array
{
return [
\Bow\Microservice\Bow\MicroserviceConfiguration::class,
\App\Configurations\ApplicationConfiguration::class,
];
}
The container caches the first resolution: each subclass returns the same connected instance for the lifetime of the process.
Step 3 β Consume the clientsβ
Inject each client by type, like any other service:
namespace App\Controllers;
use App\Microservice\BillingClient;
use App\Microservice\SearchClient;
class InvoiceController
{
public function __construct(
private BillingClient $billing,
private SearchClient $search,
) {}
public function create(int $orderId)
{
$matches = $this->search->send('orders.lookup', ['order_id' => $orderId]);
return $this->billing->send('invoice.create', [
'order_id' => $orderId,
'lines' => $matches,
]);
}
}
ClientProxy::class remains bound to the package's default client β your subclasses live alongside it without conflict.
5. Per-call overrideβ
The default timeout (config('microservice.timeout')) can be overridden on a specific send() call:
$report = $this->client->send('reporting.generate', $payload, timeout: 30.0);
emit() does not accept a timeout: it is a fire-and-forget dispatch.
Request / response β send()β
use Bow\Microservice\Client\ClientProxy;
class OrderController
{
public function __construct(private ClientProxy $client) {}
public function show(string $id)
{
$order = $this->client->send('order.find', ['id' => $id]);
return response()->json($order);
}
}
The optional third argument overrides the timeout on a per-call basis:
$this->client->send('billing.compute', $payload, timeout: 10.0);
Fire-and-forget β emit()β
$this->client->emit('user.registered', [
'id' => $user->id,
'email' => $user->email,
]);
emit() does not wait for a response and does not apply a timeout: use it for domain events that other services consume asynchronously.
Server sideβ
Generate a consumerβ
php bow add:consumer OrderConsumer
The file is created at app/Consumers/OrderConsumer.php from the bundled stub.
Declare handlersβ
Annotate your methods with #[MessagePattern] (RPC) or #[EventPattern] (event):
namespace App\Consumers;
use Bow\Microservice\Consumer\MessagePattern;
use Bow\Microservice\Consumer\EventPattern;
class OrderConsumer
{
#[MessagePattern('order.find')]
public function find(array $payload): array
{
return Order::find($payload['id'])->toArray();
}
#[EventPattern('user.registered')]
public function onUserRegistered(array $payload): void
{
// Side effect only β no response returned.
}
}
Then register the consumer in config/microservice.php:
'controllers' => [
\App\Consumers\OrderConsumer::class,
],
Start listeningβ
php bow microservice:listen
The command starts a MicroserviceServer on the configured transport and blocks on listen(). Several options let you override the configuration on a per-process basis:
php bow microservice:listen \
--transport=redis \
--controllers="App\Consumers\OrderConsumer" \
--patterns="order.find,user.registered"
| Option | Transport | Effect |
|---|---|---|
--transport=... | all | Replaces config('microservice.transport') |
--controllers=Foo,Bar | all | Comma-separated list of FQCNs |
--host, --port, --password | all | Transport connection options |
--patterns=a,b | redis | Pub/sub channels to subscribe to |
--queue=name | rabbitmq | Queue to consume |
--user, --vhost | rabbitmq | Broker credentials / vhost |
--topics=a,b | kafka | Topics to subscribe to |
--brokers, --group | kafka | Kafka cluster + consumer group |
Commandsβ
| Command | Description |
|---|---|
microservice:publish-config | Copies config/microservice.php into the host application (--force to overwrite). |
add:consumer <Name> | Generates a consumer class in app/Consumers/. |
microservice:listen | Starts the server on the configured transport. |
Environment variablesβ
If config/microservice.php is missing, the provider reads:
| Variable | Description |
|---|---|
MICROSERVICE_TRANSPORT | Active transport (default: redis) |
MICROSERVICE_TIMEOUT | RPC timeout in seconds (default: 5.0) |
MICROSERVICE_HOST | Transport host |
MICROSERVICE_PORT | Transport port |
MICROSERVICE_REDIS_PASSWORD | Redis password |
MICROSERVICE_RABBIT_USER | RabbitMQ user |
MICROSERVICE_RABBIT_PASSWORD | RabbitMQ password |
MICROSERVICE_QUEUE | RabbitMQ queue name |
MICROSERVICE_BROKERS | List of Kafka brokers (host:port,β¦) |
MICROSERVICE_TOPIC | Default Kafka topic |
Debuggingβ
Transport configuration errors are deliberately thrown at boot (the client is resolved eagerly in MicroserviceConfiguration::run()), not on the first call. If your application refuses to start after enabling the package, check the following first:
- That the PHP extension for the chosen transport is installed (see the table above).
- That the broker credentials are correct (
MICROSERVICE_*orconfig/microservice.php). - That the broker is reachable from the application's container / host.
Is something missing?
If you run into problems with the documentation or have suggestions to improve the documentation or the project in general, please open an issue for us, or send a tweet mentioning the Twitter account @bowframework or directly on github.