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

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');
}
}