Skip to main content
Version: CANARY 🚧

Microservices

Introduction​

About the microservice package

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 through several transports: TCP, Redis, RabbitMQ, gRPC and Kafka.

Two communication modes are supported:

  • Request/Response (RPC) β€” via send(), equivalent to @MessagePattern on the server side.
  • Fire-and-forget (Event) β€” via emit(), equivalent to @EventPattern on the server side.

The core of the package is framework-agnostic; only the service provider Bow\Microservice\Bow\MicroserviceConfiguration 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 the options of each transport under a key of the same name. If the file is absent, 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​

TransportUsageRequired extension
tcpRaw socket, no brokernone
redisRedis pub/sub + RPCphpredis
rabbitmqDurable queuephp-amqplib
grpcClient only (third-party server)grpc (PECL)
kafkaHigh-throughput streamingrdkafka

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 for app('microservice.client').

Configuring the ClientProxy​

The ClientProxy shared by the container is built only once, when the binding is resolved. Its parameters can be defined at five levels, from the broadest to the most specific:

1. The config/microservice.php file​

This is the recommended approach for most applications. The provider reads transport, timeout and the options 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 absent, the provider falls back to the MICROSERVICE_* variables (see the Environment variables section). This is handy in CI / containers, without having to publish the configuration file.

3. Manually, with ClientFactory::create()​

For testing, to open several 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:

Transportoptions keys
tcphost, port
redishost, port, password
rabbitmqhost, port, user, password, queue, vhost
kafkabrokers, topic, reply_topic
grpchost, port, channel_options

4. Configuring multiple clients​

The bundled provider exposes a single shared ClientProxy (ClientProxy::class + 'microservice.client'). To add extra 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, with no 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 a different 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 correspond to the options keys listed in the table of 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 entire 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 send.

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​

Generating a consumer​

php bow add:consumer OrderConsumer

The file is created at app/Consumers/OrderConsumer.php from the bundled stub.

Declaring 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,
],

Starting the listener​

php bow microservice:listen

The command starts a MicroserviceServer on the configured transport and blocks on listen(). Several options let you override the configuration per process:

php bow microservice:listen \
--transport=redis \
--controllers="App\Consumers\OrderConsumer" \
--patterns="order.find,user.registered"
OptionTransportEffect
--transport=...allOverrides config('microservice.transport')
--controllers=Foo,BarallComma-separated list of FQCNs
--host, --port, --passwordallTransport connection options
--patterns=a,bredisPub/sub channels to subscribe to
--queue=namerabbitmqQueue to consume
--user, --vhostrabbitmqBroker credentials / vhost
--topics=a,bkafkaTopics to subscribe to
--brokers, --groupkafkaKafka cluster + consumer group

Commands​

CommandDescription
microservice:publish-configCopies config/microservice.php into the host application (--force to overwrite).
add:consumer <Name>Generates a consumer class in app/Consumers/.
microservice:listenStarts the server on the configured transport.

Environment variables​

If config/microservice.php is absent, the provider reads:

VariableDescription
MICROSERVICE_TRANSPORTActive transport (default: redis)
MICROSERVICE_TIMEOUTRPC timeout in seconds (default: 5.0)
MICROSERVICE_HOSTTransport host
MICROSERVICE_PORTTransport port
MICROSERVICE_REDIS_PASSWORDRedis password
MICROSERVICE_RABBIT_USERRabbitMQ user
MICROSERVICE_RABBIT_PASSWORDRabbitMQ password
MICROSERVICE_QUEUERabbitMQ queue name
MICROSERVICE_BROKERSList of Kafka brokers (host:port,…)
MICROSERVICE_TOPICDefault 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 first:

  1. That the PHP extension for the chosen transport is installed (see the table above).
  2. That the broker credentials are correct (MICROSERVICE_* or config/microservice.php).
  3. 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.