Version 1.0.0

This commit is contained in:
2025-12-12 11:41:44 +04:00
commit df7e485650
30 changed files with 7761 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
vendor/

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright 2025 Viktor S. <thinlineseverywhere@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the “Software”),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

448
README.md Normal file
View File

@@ -0,0 +1,448 @@
# Laravel RabbitMQ Events
A package for working with RabbitMQ messages as Laravel events. Automatically serialize and publish events to RabbitMQ, and consume messages from queues by converting them back into events.
## Features
**Automatic event serialization** — Events implementing the `Broadcast` interface are automatically serialized and sent to RabbitMQ
**Message consumption** — Command to consume messages from RabbitMQ with automatic deserialization and event dispatching
**Default bindings** — The `BroadcastEvent` trait sets standard connection parameters for an event
**Flexible configuration** — Support for multiple connections and parameterization for each event
**Microservices architecture** — Ideal for data exchange between services
## Requirements
- PHP 8.1+
- Laravel 10, 11 or 12
- RabbitMQ server
## Installation
Install via Composer:
```bash
composer require diffhead/laravel-rabbitmq
```
The package will be automatically registered thanks to Laravel Service Provider Discovery.
## Configuration
Publish the configuration file:
```bash
php artisan vendor:publish --provider="Diffhead\PHP\LaravelRabbitMQ\ServiceProvider"
```
ThisEnvironment Variables
```env
# RabbitMQ Connection
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
RABBITMQ_VHOST=/
# Default event parameters
RABBITMQ_EVENT_CONNECTION=default
RABBITMQ_EVENT_QUEUE=default
RABBITMQ_EVENT_EXCHANGE=amq.direct
RABBITMQ_EVENT_EXCHANGE_TYPE=direct
RABBITMQ_EVENT_EXCHANGE_IS_DEFAULT=true
RABBITMQ_EVENT_ROUTING_KEY=
```
### Configuration Example
```php
return [
'connections' => [
'default' => [
'host' => env('RABBITMQ_HOST', 'localhost'),
'port' => env('RABBITMQ_PORT', 5672),
'user' => env('RABBITMQ_USER', 'guest'),
'password' => env('RABBITMQ_PASSWORD', 'guest'),
'vhost' => env('RABBITMQ_VHOST', '/'),
],
'secondary' => [
'host' => env('RABBITMQ_SECONDARY_HOST', 'localhost'),
'port' => env('RABBITMQ_SECONDARY_PORT', 5672),
'user' => env('RABBITMQ_SECONDARY_USER', 'guest'),
'password' => env('RABBITMQ_SECONDARY_PASSWORD', 'guest'),
'vhost' => env('RABBITMQ_SECONDARY_VHOST', '/'),
]
],
'message' => [
'serializer' => \Diffhead\PHP\LaravelRabbitMQ\Service\Serializer::class,
'unserializer' => \Diffhead\PHP\LaravelRabbitMQ\Service\Unserializer::class,
],
'event' => [
'defaults' => [
'connection' => env('RABBITMQ_EVENT_CONNECTION', 'default'),
'queue' => env('RABBITMQ_EVENT_QUEUE', 'default'),
'exchange' => env('RABBITMQ_EVENT_EXCHANGE', 'amq.direct'),
'exchange_type' => env('RABBITMQ_EVENT_EXCHANGE_TYPE', 'direct'),
'exchange_is_default' => (bool) env('RABBITMQ_EVENT_EXCHANGE_IS_DEFAULT', true),
'routing_key' => env('RABBITMQ_EVENT_ROUTING_KEY', ''),
],
'mapper' => \Diffhead\PHP\LaravelRabbitMQ\Service\EventMapper::class,
'map' => [
/**
* Map events to queues and routing keys
*/
\App\Events\User\UserCreated::class => [
'queues' => ['portal.users'],
'routing_keys' => ['user.created'],
],
\App\Events\Meeting\MeetingCreated::class => [
'queues' => ['portal.meetings'],
'routing_keys' => ['meeting.created'],
],
],
]
];
```
## Usage
### 1. Creating an Event for Publishing to RabbitMQ
Create an event that implements the `Broadcast` interface:
```php
namespace App\Events;
use Diffhead\PHP\LaravelRabbitMQ\Event\Broadcast;
use Diffhead\PHP\LaravelRabbitMQ\Trait\BroadcastEvent;
use Illuminate\Foundation\Events\Dispatchable;
class UserCreated implements Broadcast
{
use Dispatchable, BroadcastEvent;
public function __construct(
public int $userId,
public string $email,
public string $name,
) {}
public function jsonSerialize(): array
{
return [
'userId' => $this->userId,
'email' => $this->email,
'name' => $this->name,
];
}
}
```
#### Event Parameters
When implementing the `Broadcast` interface, you must define the following methods:
- `getConnection(): string` — RabbitMQ connection name
- `getQueue(): string` — Queue name
- `getExchange(): string` — Exchange name
- `getExchangeType(): string` — Exchange type (direct, topic, fanout, headers)
- `getExchangeIsDefault(): bool` — Whether to use the default exchange
- `getRoutingKey(): string` — Routing key for the message
#### Using the BroadcastEvent Trait
The `BroadcastEvent` trait provides implementations of all methods using default parameters from configuration:
```php
namespace App\Events;
use Diffhead\PHP\LaravelRabbitMQ\Event\Broadcast;
use Diffhead\PHP\LaravelRabbitMQ\Trait\BroadcastEvent;
class UserCreated implements Broadcast
{
use BroadcastEvent;
public function __construct(
public int $userId,
public string $email,
) {}
public function jsonSerialize(): array
{
return [
'userId' => $this->userId,
'email' => $this->email,
];
}
}
```
If you need special parameters for a specific event, override the necessary methods:
```php
namespace App\Events;
use Diffhead\PHP\LaravelRabbitMQ\Event\Broadcast;
use Diffhead\PHP\LaravelRabbitMQ\Trait\BroadcastEvent;
class CriticalAlert implements Broadcast
{
use BroadcastEvent;
public function __construct(
public string $message,
) {}
public function getRoutingKey(): string
{
return 'alert.critical';
}
public function getExchange(): string
{
return 'alerts.topic';
}
public function getExchangeType(): string
{
return 'topic';
}
public function jsonSerialize(): array
{
return [
'message' => $this->message,
];
}
}
```
### 2. Publishing Events
Events are automatically published when dispatched:
```php
use App\Events\UserCreated;
/**
* Events implementing Broadcast are automatically sent to RabbitMQ
*/
UserCreated::dispatch(userId: 1, email: 'user@example.com', name: 'John Doe');
```
### 3. Consuming Messages from RabbitMQ
Use the `rabbitmq:consume` command to listen for messages:
```bash
#####################################################################################
#
# Has following options:
#
# --connection=default - Connection name from config
# --queue=default - Queue
# --exchange=amq.direct - Exchange name
# --exchange-type=direct - Exchange type
# --exchange-is-default - Exchange is default, required for default exchanges
# --routing-key=user.* - Listen routing keys, required for topic exchanges
# --tag=myconsumer - Consumer tag for rabbitmq
#
#####################################################################################
php artisan rabbitmq:consume
```
#### Full Consumer Startup Example
```bash
php artisan rabbitmq:consume \
--connection=default \
--queue=service.users \
--exchange=amq.direct \
--exchange-type=direct \
--routing-key=user.* \
--tag=service-users-consumer
```
### 4. Handling Received Events
When a message is received from RabbitMQ, it is automatically deserialized and dispatched as a Laravel event. You can listen to these events normally:
```php
namespace App\Listeners;
use App\Events\UserCreated;
use Illuminate\Support\Log;
class SendWelcomeEmail
{
public function handle(UserCreated $event): void
{
Log::info("User created: {$event->email}");
}
}
```
Register the listener in `app/Providers/EventServiceProvider.php`:
```php
protected $listen = [
\App\Events\UserCreated::class => [
\App\Listeners\SendWelcomeEmail::class,
],
];
```
## Architecture
### Event Publishing Flow
```
Laravel Event (Broadcast)
PublishEvent (Listener)
Serializer (JSON)
RabbitMQ Exchange
Queue
```
### Message Consumption Flow
```
RabbitMQ Queue
Message Consumer
Unserializer (JSON)
EventMapper (Event)
EventEmitter (Service)
Event Listeners
```
## Microservices Architecture Example
### Service 1: Publishes event
```php
namespace App\Events;
use Diffhead\PHP\LaravelRabbitMQ\Event\Broadcast;
use Diffhead\PHP\LaravelRabbitMQ\Trait\BroadcastEvent;
class UserCreated implements Broadcast
{
use BroadcastEvent;
public function __construct(
public int $id,
public string $email,
public string $name,
) {}
public function getRoutingKey(): string
{
return 'user.created';
}
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'email' => $this->email,
'name' => $this->name,
];
}
}
```
```php
use App\Events\UserCreated;
/**
* Controller method
*/
public function store(Request $request)
{
$user = User::create($request->validated());
UserCreated::dispatch($user->id, $user->email, $user->name);
return response()->json($user, 201);
}
```
### Service 2: Receives event
Map event using configuration:
```php
'map' => [
\App\Events\UserCreated::class => [
'queues' => ['service2.users'],
'routing_keys' => ['user.created']
]
]
```
Then implement and register event listener:
```php
namespace App\Listeners;
use App\Events\UserCreated;
class SyncUserToCalendar
{
public function handle(UserCreated $event): void
{
CalendarUser::create([
'external_id' => $event->id,
'email' => $event->email,
'name' => $event->name,
]);
}
}
```
Start consumer
```bash
php artisan rabbitmq:consume --queue=service2.users --routing-key=user.* --exchange=amq.topic --exchange-type=topic --exchange-is-default
```
## Serialization
The package uses JSON for serialization/deserialization of data via `Serializer` and `Unserializer` interfaces.
### Custom Serialization
You can use your own serialization classes by implementing
interfaces and overriding following configuration entities:
```php
'message' => [
'serializer' => \App\Services\CustomSerializer::class,
'unserializer' => \App\Services\CustomUnserializer::class,
],
```
## Mapping
The package maps rabbitmq message to application events
### Custom mapping
You can use your own mapping logic by implementing EventMapper
interface and overriding the following configuration entity:
```php
'event' => [
'mapper' => \App\Services\CustomEventMapper::class,
]
```

42
composer.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "diffhead/laravel-rabbitmq",
"description": "A laravel package for events emitting between services using RabbitMQ as message broker.",
"type": "library",
"license": "MIT",
"version": "1.0.0",
"keywords": [
"laravel", "rabbitmq", "event", "emit", "microservice",
"pipeline", "data exchanging", "message broker"
],
"autoload": {
"psr-4": {
"Diffhead\\PHP\\LaravelRabbitMQ\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Diffhead\\PHP\\LaravelRabbitMQ\\ServiceProvider"
]
}
},
"require": {
"php": "^8.1",
"laravel/framework": "^10 || ^11.0 || ^12.0",
"php-amqplib/php-amqplib": "^3.7",
"diffhead/php-dto": "^1.0"
},
"scripts": {
"test": "phpunit",
"post-install-cmd": [
"php artisan vendor:publish --provider=\"Diffhead\\PHP\\LaravelRabbitMQ\\ServiceProvider\""
]
},
"minimum-stability": "stable",
"authors": [
{
"name": "Viktor S.",
"email": "thinlineseverywhere@gmail.com"
}
]
}

6296
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

43
config/rabbitmq.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
return [
'connections' => [
'default' => [
'host' => env('RABBITMQ_HOST', 'localhost'),
'port' => env('RABBITMQ_PORT', 5672),
'user' => env('RABBITMQ_USER', 'guest'),
'password' => env('RABBITMQ_PASSWORD', 'guest'),
'vhost' => env('RABBITMQ_VHOST', '/'),
]
],
'message' => [
'serializer' => \Diffhead\PHP\LaravelRabbitMQ\Service\Serializer::class,
'unserializer' => \Diffhead\PHP\LaravelRabbitMQ\Service\Unserializer::class,
],
'event' => [
'defaults' => [
'connection' => env('RABBITMQ_EVENT_CONNECTION', 'default'),
'queue' => env('RABBITMQ_EVENT_QUEUE', 'default'),
'exchange' => env('RABBITMQ_EVENT_EXCHANGE', 'amq.direct'),
'exchange_type' => env('RABBITMQ_EVENT_EXCHANGE_TYPE', 'direct'),
'exchange_is_default' => (bool) env('RABBITMQ_EVENT_EXCHANGE_IS_DEFAULT', true),
'routing_key' => (string) env('RABBITMQ_EVENT_ROUTING_KEY', ''),
],
'mapper' => \Diffhead\PHP\LaravelRabbitMQ\Service\EventMapper::class,
'map' => [
/**
* Example:
*
* \App\Shared\Event\User\UserCreated::class => [
* 'queues' => ['portal.calendar.users'],
* 'routing_keys' => ['user.created'],
* ],
* \App\Shared\Event\Meeting\MeetingCreated::class => [
* 'queues' => ['portal.calendar.meetings'],
* 'routing_keys' => ['meeting.created'],
* ],
*/
],
]
];

118
src/Command/Consume.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Command;
use Diffhead\PHP\LaravelRabbitMQ\Dto\ConsumerParameters;
use Diffhead\PHP\LaravelRabbitMQ\Object\Connection;
use Diffhead\PHP\LaravelRabbitMQ\Object\Exchange;
use Diffhead\PHP\LaravelRabbitMQ\Object\ExchangeDeclaration;
use Diffhead\PHP\LaravelRabbitMQ\Object\Queue;
use Diffhead\PHP\LaravelRabbitMQ\Object\QueueBindings;
use Diffhead\PHP\LaravelRabbitMQ\Object\QueueDeclaration;
use Diffhead\PHP\LaravelRabbitMQ\Service\Configuration;
use Diffhead\PHP\LaravelRabbitMQ\Service\Connector;
use Diffhead\PHP\LaravelRabbitMQ\Service\Message;
use Exception;
use Illuminate\Console\Command;
use PhpAmqpLib\Message\AMQPMessage;
use Psr\Log\LoggerInterface;
class Consume extends Command
{
/**
* @var string
*/
protected $signature = 'rabbitmq:consume {--connection=} {--queue=} {--exchange=} {--exchange-type=} {--exchange-is-default} {--routing-key=} {--tag=}';
/**
* @var string
*/
protected $description = 'Consume messages from RabbitMQ';
private ?Connection $connection = null;
public function handle(
Configuration $configuration,
Connector $connector,
Message $handler,
LoggerInterface $logger
): void {
$params = ConsumerParameters::fromArray($this->options());
$config = $configuration->get($params->connection->value() ?? 'default');
$queue = $this->getQueue($params);
$this->connection = $connector->connect($config, $queue);
$tag = $params->tag->value() ?? 'rabbitmq-laravel-consumer';
$consumer = function (AMQPMessage $message) use ($handler, $queue, $logger): void {
try {
$this->info(sprintf('Received message: %s', $message->getBody()));
$mergedQueue = $this->getMergedQueue($queue, $message);
$handler->handle($mergedQueue, $message);
$message->ack();
$this->info('Message processed successfully.');
} catch (Exception $e) {
$message->nack();
$this->error(
sprintf('Processing error: %s', $e->getMessage())
);
$logger->error($e->getMessage());
}
};
$this->connection->channel()->basic_consume(
queue: $queue->name(),
consumer_tag: $tag,
callback: $consumer
);
$this->connection->channel()->consume();
}
public function __destruct()
{
$this->connection?->channel()->close();
$this->connection?->connection()->close();
}
private function getQueue(ConsumerParameters $params): Queue
{
$routingKey = $params->routingKey->value() ?? '';
return new Queue(
name: $params->queue->value() ?? 'default',
exchange: new Exchange(
name: $params->exchange->value() ?? 'amq.direct',
type: $params->exchangeType->value() ?? 'direct',
isDefault: $params->exchangeIsDefault->value(),
declaration: new ExchangeDeclaration()
),
declaration: new QueueDeclaration(),
bindings: new QueueBindings($routingKey),
);
}
private function getMergedQueue(Queue $queue, AMQPMessage $message): Queue
{
return new Queue(
name: $queue->name(),
exchange: $queue->exchange(),
declaration: $queue->declaration(),
bindings: new QueueBindings(
routingKey: $message->getRoutingKey(),
nowait: $queue->bindings()->nowait(),
arguments: $queue->bindings()->arguments(),
ticket: $queue->bindings()->ticket()
),
);
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Dto;
use Diffhead\PHP\Dto\Dto;
use Diffhead\PHP\Dto\Property;
/**
* @property \Diffhead\PHP\Dto\Property<string|null> $connection
* @property \Diffhead\PHP\Dto\Property<string|null> $queue
* @property \Diffhead\PHP\Dto\Property<string|null> $exchange
* @property \Diffhead\PHP\Dto\Property<string|null> $routingKey
* @property \Diffhead\PHP\Dto\Property<string|null> $exchangeType
* @property \Diffhead\PHP\Dto\Property<bool> $exchangeIsDefault
* @property \Diffhead\PHP\Dto\Property<string|null> $tag
*/
class ConsumerParameters extends Dto
{
protected Property $connection;
protected Property $queue;
protected Property $exchange;
protected Property $routingKey;
protected Property $exchangeType;
protected Property $exchangeIsDefault;
protected Property $tag;
}

17
src/Event/Broadcast.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Event;
use JsonSerializable;
interface Broadcast extends JsonSerializable
{
public function getConnection(): string;
public function getQueue(): string;
public function getExchange(): string;
public function getExchangeType(): string;
public function getExchangeIsDefault(): bool;
public function getRoutingKey(): string;
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Exception;
use RuntimeException;
class AssociatedEventNotFound extends RuntimeException {}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Interface;
use Diffhead\PHP\LaravelRabbitMQ\Object\Queue;
use App\Shared\Event\Event;
interface EventMapper
{
public function map(Queue $queue, array $payload): Event;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Interface;
use PhpAmqpLib\Message\AMQPMessage;
interface Serializer
{
public function serialize(object $data): AMQPMessage;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Interface;
use PhpAmqpLib\Message\AMQPMessage;
interface Unserializer
{
public function unserialize(AMQPMessage $message): array;
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Listener;
use Diffhead\PHP\LaravelRabbitMQ\Interface\Serializer;
use Diffhead\PHP\LaravelRabbitMQ\Object\Exchange;
use Diffhead\PHP\LaravelRabbitMQ\Object\ExchangeDeclaration;
use Diffhead\PHP\LaravelRabbitMQ\Object\Queue;
use Diffhead\PHP\LaravelRabbitMQ\Object\QueueBindings;
use Diffhead\PHP\LaravelRabbitMQ\Object\QueueDeclaration;
use Diffhead\PHP\LaravelRabbitMQ\Service\Configuration;
use Diffhead\PHP\LaravelRabbitMQ\Service\Connector;
use Diffhead\PHP\LaravelRabbitMQ\Event\Broadcast;
use Exception;
use Illuminate\Contracts\Queue\ShouldQueue;
class PublishEvent implements ShouldQueue
{
public function __construct(
private Configuration $configuration,
private Connector $connector,
private Serializer $serializer,
) {}
public function handle(Broadcast $event): void
{
$config = $this->configuration->get($event->getConnection());
$queue = new Queue(
name: $event->getQueue(),
exchange: new Exchange(
name: $event->getExchange(),
type: $event->getExchangeType(),
isDefault: $event->getExchangeIsDefault(),
declaration: new ExchangeDeclaration()
),
declaration: new QueueDeclaration(),
bindings: new QueueBindings(
routingKey: $event->getRoutingKey()
),
);
$connection = $this->connector->connect($config, $queue);
try {
$message = $this->serializer->serialize($event);
$connection->channel()->basic_publish(
msg: $message,
exchange: $queue->exchange()->name(),
routing_key: $queue->bindings()->routingKey(),
);
} catch (Exception $e) {
throw $e;
} finally {
$connection->channel()->close();
$connection->connection()->close();
}
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Object;
class Configuration
{
public function __construct(
private string $host,
private int $port,
private string $user,
private string $password,
private string $vhost,
) {}
public function host(): string
{
return $this->host;
}
public function port(): int
{
return $this->port;
}
public function user(): string
{
return $this->user;
}
public function password(): string
{
return $this->password;
}
public function vhost(): string
{
return $this->vhost;
}
}

26
src/Object/Connection.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Object;
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Connection\AMQPStreamConnection;
class Connection
{
public function __construct(
private AMQPStreamConnection $connection,
private AMQPChannel $channel
) {}
public function connection(): AMQPStreamConnection
{
return $this->connection;
}
public function channel(): AMQPChannel
{
return $this->channel;
}
}

35
src/Object/Exchange.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Object;
class Exchange
{
public function __construct(
private string $name = 'amq.direct',
private string $type = 'direct',
private bool $isDefault = true,
private ExchangeDeclaration $declaration,
) {}
public function name(): string
{
return $this->name;
}
public function type(): string
{
return $this->type;
}
public function isDefault(): bool
{
return $this->isDefault;
}
public function declaration(): ExchangeDeclaration
{
return $this->declaration;
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Object;
class ExchangeDeclaration
{
public function __construct(
private bool $passive = false,
private bool $durable = true,
private bool $autoDelete = false,
private bool $internal = false,
private bool $nowait = false,
private array $arguments = [],
private mixed $ticket = null,
) {}
public function passive(): bool
{
return $this->passive;
}
public function durable(): bool
{
return $this->durable;
}
public function internal(): bool
{
return $this->internal;
}
public function autoDelete(): bool
{
return $this->autoDelete;
}
public function nowait(): bool
{
return $this->nowait;
}
public function arguments(): array
{
return $this->arguments;
}
public function ticket(): ?int
{
return $this->ticket;
}
}

35
src/Object/Queue.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Object;
class Queue
{
public function __construct(
private string $name = 'default',
private Exchange $exchange,
private QueueDeclaration $declaration,
private QueueBindings $bindings,
) {}
public function name(): string
{
return $this->name;
}
public function exchange(): Exchange
{
return $this->exchange;
}
public function declaration(): QueueDeclaration
{
return $this->declaration;
}
public function bindings(): QueueBindings
{
return $this->bindings;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Object;
class QueueBindings
{
public function __construct(
private string $routingKey = '',
private bool $nowait = false,
private array $arguments = [],
private mixed $ticket = null,
) {}
public function routingKey(): string
{
return $this->routingKey;
}
public function nowait(): bool
{
return $this->nowait;
}
public function arguments(): array
{
return $this->arguments;
}
public function ticket(): mixed
{
return $this->ticket;
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Object;
class QueueDeclaration
{
public function __construct(
private bool $passive = false,
private bool $durable = true,
private bool $exclusive = false,
private bool $autoDelete = true,
private bool $nowait = false,
private array $arguments = [],
private mixed $ticket = null,
) {}
public function passive(): bool
{
return $this->passive;
}
public function durable(): bool
{
return $this->durable;
}
public function exclusive(): bool
{
return $this->exclusive;
}
public function autoDelete(): bool
{
return $this->autoDelete;
}
public function nowait(): bool
{
return $this->nowait;
}
public function arguments(): array
{
return $this->arguments;
}
public function ticket(): ?int
{
return $this->ticket;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Service;
use Diffhead\PHP\LaravelRabbitMQ\Object\Configuration as ConfigurationObject;
use RuntimeException;
class Configuration
{
public function get(string $connection = 'default'): ConfigurationObject
{
$config = config(sprintf('rabbitmq.connections.%s', $connection));
if (empty($config)) {
throw new RuntimeException(
sprintf('Not found rabbitmq config for connection %s', $connection)
);
}
return new ConfigurationObject(
$config['host'],
(int) $config['port'],
$config['user'],
$config['password'],
$config['vhost'],
);
}
}

62
src/Service/Connector.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Service;
use Diffhead\PHP\LaravelRabbitMQ\Object\Configuration;
use Diffhead\PHP\LaravelRabbitMQ\Object\Connection;
use Diffhead\PHP\LaravelRabbitMQ\Object\Queue;
use PhpAmqpLib\Connection\AMQPStreamConnection;
class Connector
{
public function connect(Configuration $config, Queue $queue): Connection
{
$connection = new AMQPStreamConnection(
$config->host(),
$config->port(),
$config->user(),
$config->password(),
$config->vhost()
);
$channel = $connection->channel();
$channel->queue_declare(
$queue->name(),
$queue->declaration()->passive(),
$queue->declaration()->durable(),
$queue->declaration()->exclusive(),
$queue->declaration()->autoDelete(),
$queue->declaration()->nowait(),
$queue->declaration()->arguments(),
$queue->declaration()->ticket()
);
if (! $queue->exchange()->isDefault()) {
$channel->exchange_declare(
$queue->exchange()->name(),
$queue->exchange()->type(),
$queue->exchange()->declaration()->passive(),
$queue->exchange()->declaration()->durable(),
$queue->exchange()->declaration()->autoDelete(),
$queue->exchange()->declaration()->internal(),
$queue->exchange()->declaration()->nowait(),
$queue->exchange()->declaration()->arguments(),
$queue->exchange()->declaration()->ticket()
);
}
$channel->queue_bind(
$queue->name(),
$queue->exchange()->name(),
$queue->bindings()->routingKey(),
$queue->bindings()->nowait(),
$queue->bindings()->arguments(),
$queue->bindings()->ticket()
);
return new Connection($connection, $channel);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Service;
use Illuminate\Contracts\Events\Dispatcher;
class EventEmitter
{
public function __construct(
private Dispatcher $dispatcher
) {}
public function emit(object $event): void
{
$this->dispatcher->dispatch($event);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Service;
use Diffhead\PHP\LaravelRabbitMQ\Exception\AssociatedEventNotFound;
use Diffhead\PHP\LaravelRabbitMQ\Interface\EventMapper as EventMapperInterface;
use Diffhead\PHP\LaravelRabbitMQ\Object\Queue;
use App\Shared\Event\Event;
use Illuminate\Contracts\Foundation\Application;
class EventMapper implements EventMapperInterface
{
public function __construct(
private Application $app
) {}
public function map(Queue $queue, array $payload): Event
{
$map = config('rabbitmq.event.map', []);
$queueName = $queue->name();
$routingKey = $queue->bindings()->routingKey();
foreach ($map as $eventClass => $config) {
$match = null;
if (! empty($config['queues'] ?? [])) {
$match = in_array($queueName, $config['queues'], true);
}
if (! empty($config['routing_keys'] ?? [])) {
$match = is_null($match) ? true : $match;
$match = $match && in_array($routingKey, $config['routing_keys'], true);
}
if (is_null($match)) {
return $this->app->make($eventClass, $payload);
}
if (is_bool($match) && $match) {
return $this->app->make($eventClass, $payload);
}
}
throw new AssociatedEventNotFound(json_encode($payload));
}
}

27
src/Service/Message.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Service;
use Diffhead\PHP\LaravelRabbitMQ\Interface\EventMapper;
use Diffhead\PHP\LaravelRabbitMQ\Interface\Unserializer;
use Diffhead\PHP\LaravelRabbitMQ\Object\Queue;
use PhpAmqpLib\Message\AMQPMessage;
class Message
{
public function __construct(
private Unserializer $unserializer,
private EventMapper $mapper,
private EventEmitter $emitter,
) {}
public function handle(Queue $queue, AMQPMessage $message): void
{
$payload = $this->unserializer->unserialize($message);
$event = $this->mapper->map($queue, $payload);
$this->emitter->emit($event);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Service;
use Diffhead\PHP\LaravelRabbitMQ\Interface\Serializer as SerializerInterface;
use InvalidArgumentException;
use JsonSerializable;
use PhpAmqpLib\Message\AMQPMessage;
class Serializer implements SerializerInterface
{
public function serialize(object $data): AMQPMessage
{
if ($data instanceof JsonSerializable) {
return new AMQPMessage(
json_encode($data->jsonSerialize())
);
}
throw new InvalidArgumentException(
'Data should be an instance of BroadcastEvent'
);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Service;
use Diffhead\PHP\LaravelRabbitMQ\Interface\Unserializer as UnserializerInterface;
use PhpAmqpLib\Message\AMQPMessage;
class Unserializer implements UnserializerInterface
{
public function unserialize(AMQPMessage $message): array
{
return json_decode($message->getBody(), true);
}
}

96
src/ServiceProvider.php Normal file
View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ;
use Diffhead\PHP\LaravelRabbitMQ\Command\Consume;
use Diffhead\PHP\LaravelRabbitMQ\Event\Broadcast;
use Diffhead\PHP\LaravelRabbitMQ\Interface\EventMapper as EventMapperInterface;
use Diffhead\PHP\LaravelRabbitMQ\Interface\Serializer as SerializerInterface;
use Diffhead\PHP\LaravelRabbitMQ\Interface\Unserializer as UnserializerInterface;
use Diffhead\PHP\LaravelRabbitMQ\Listener\PublishEvent;
use Diffhead\PHP\LaravelRabbitMQ\Service\EventMapper;
use Diffhead\PHP\LaravelRabbitMQ\Service\Serializer;
use Diffhead\PHP\LaravelRabbitMQ\Service\Unserializer;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
class ServiceProvider extends LaravelServiceProvider
{
/**
* @var array<int,string>
*/
private array $commands = [
Consume::class,
];
/**
* @var array<string,array<int,string>>
*/
private array $listeners = [
Broadcast::class => [
PublishEvent::class,
]
];
public function register(): void
{
$this->registerServices();
}
public function boot(): void
{
$this->registerConfigsPublishment();
$this->registerCommands();
$this->registerListeners();
}
private function registerServices(): void
{
$this->app->bind(
SerializerInterface::class,
config('rabbitmq.message.serializer', Serializer::class)
);
$this->app->bind(
UnserializerInterface::class,
config('rabbitmq.message.unserializer', Unserializer::class)
);
$this->app->bind(
EventMapperInterface::class,
config('rabbitmq.event.mapper', EventMapper::class)
);
}
private function registerCommands(): void
{
$this->commands($this->commands);
}
private function registerListeners(): void
{
foreach ($this->listeners as $event => $listeners) {
foreach ($listeners as $listener) {
Event::listen($event, $listener);
}
}
}
private function registerConfigsPublishment(): void
{
$this->publishes(
[
$this->configPath('config/rabbitmq.php') => config_path('rabbitmq.php'),
],
'config'
);
}
private function configPath(string $path): string
{
return sprintf('%s/../%s', __DIR__, $path);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelRabbitMQ\Trait;
trait BroadcastEvent
{
public function getConnection(): string
{
return config('rabbitmq.event.defaults.connection');
}
public function getQueue(): string
{
return config('rabbitmq.event.defaults.queue');
}
public function getExchange(): string
{
return config('rabbitmq.event.defaults.exchange');
}
public function getExchangeType(): string
{
return config('rabbitmq.event.defaults.exchange_type');
}
public function getExchangeIsDefault(): bool
{
return config('rabbitmq.event.defaults.exchange_is_default');
}
public function getRoutingKey(): string
{
return config('rabbitmq.event.defaults.routing_key');
}
}