├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── rabbitmq.php ├── docker ├── docker-compose.yml └── php.dockerfile └── src ├── Commands ├── ConsumeMessages.php └── DeclareExchanges.php ├── RabbitMQ.php ├── RabbitMQConsumer.php ├── RabbitMQDispatcher.php ├── RabbitMQExchange.php ├── RabbitMQMessage.php ├── RabbitMQQueue.php ├── RabbitMQServiceProvider.php └── Support └── ShouldPublish.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `:package_name` will be documented in this file. 4 | 5 | ## Add vhost option & read configs from env - 2023-04-11 6 | 7 | - added vhost option 8 | - configs to target env 9 | 10 | ## Laravel 10 & PHP 8.1 support - 2023-03-12 11 | 12 | drop support for laravel < 10 13 | drop support for PHP < 8.1 14 | cleaner syntax for consumer configuration 15 | 16 | ## Changed config and added document for package - 2023-03-06 17 | 18 | - changed config to be more intuitive 19 | - document usage of package 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) sokanacademy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A package to work with RabbitMQ in an elegant way. 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/sokanacademy/laravel-fluent-rabbitmq.svg?style=flat-square)](https://packagist.org/packages/sokanacademy/laravel-fluent-rabbitmq) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/sokanacademy/laravel-fluent-rabbitmq/run-tests?label=tests)](https://github.com/sokanacademy/laravel-fluent-rabbitmq/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/sokanacademy/laravel-fluent-rabbitmq/Check%20&%20fix%20styling?label=code%20style)](https://github.com/sokanacademy/laravel-fluent-rabbitmq/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/sokanacademy/laravel-fluent-rabbitmq.svg?style=flat-square)](https://packagist.org/packages/sokanacademy/laravel-fluent-rabbitmq) 7 | 8 | This package allows your laravel applications to easily communicate with each other in an event driven way. 9 | 10 | One service can publish an event and another one can consume the event and take actions accordingly. 11 | 12 | ## Installation 13 | 14 | You can install the package via composer: 15 | 16 | ```bash 17 | composer require sokanacademy/laravel-fluent-rabbitmq:^1.0 18 | ``` 19 | 20 | Then you should publish the package config with running this command: 21 | 22 | ```bash 23 | php artisan vendor:publish --tag="laravel-fluent-rabbitmq-config" 24 | ``` 25 | 26 | This is the contents of the published config file: 27 | 28 | ```php 29 | env('RABBITMQ_HOST', '127.0.0.1'), 33 | 'port' => env('RABBITMQ_PORT', 5672), 34 | 'user' => env('RABBITMQ_USER', 'guest'), 35 | 'password' => env('RABBITMQ_PASSWORD', 'guest'), 36 | 'vhost' => env('RABBITMQ_VHOST', '/'), 37 | 38 | 'consumers' => [ 39 | // [ 40 | // 'event' => '\App\Events\MyEvent', 41 | // 'routing_key' => 'my_routing_key', // if this event does not use routing key then remove this line 42 | // 'map_into' => '\App\Events\MapIntoEvent', // if you want to use the same event then remove this line 43 | // ], 44 | ], 45 | ]; 46 | ``` 47 | 48 | ## Usage 49 | 50 | ### Mark an event to be published on RabbitMQ 51 | 52 | The only thing you must do is to make sure your event implements `Sokanacademy\RabbitMQ\Support\ShouldPublish` interface 53 | and that's it. 54 | All of the event's public properties will be published, and you can have access to them in your consumer. Make sure these properties are primitive or Arrayable. 55 | 56 | If you want your event to be published using a routing key, then consider adding routingKey method to your event: 57 | 58 | ```php 59 | public function routingKey(): string 60 | { 61 | return 'routing_key'; 62 | } 63 | ``` 64 | 65 | ### declare exchanges in rabbitmq server 66 | 67 | When a laravel application wants to publish events, you must run this command to create appropriate exchanges on 68 | RabbitMQ. 69 | For each event it will create an exchange with the name of event class. 70 | You can read more on exchanges types [here](https://www.rabbitmq.com/tutorials/amqp-concepts.html). 71 | 72 | The default type for exchanges will be 'fanout'. If you want to alter the type of exchange for an event you can add this 73 | property to your event: 74 | 75 | ```php 76 | private static string $exchangeType = 'topic'; 77 | ``` 78 | 79 | ## Consume events from RabbitMQ 80 | In the `rabbitmq.php` config file you should list all the events you want to consume. 81 | 82 | ```php 83 | 'consumers' => [ 84 | // [ 85 | // 'event' => '\App\Events\MyEvent', 86 | // 'routing_key' => 'my_routing_key', // if this event does not use routing key then remove this line 87 | // 'map_into' => '\App\Events\MapIntoEvent', // if you want to use the same event then remove this line 88 | // ], 89 | ], 90 | ``` 91 | If you have same event in both services (publisher and consumer) then you can omit the map_into option for the event. 92 | 93 | Then you can start consuming events with the following command: 94 | 95 | ```bash 96 | php artisan rabbitmq:consume 97 | ``` 98 | 99 | ## Changelog 100 | 101 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 102 | 103 | ## Contributing 104 | 105 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 106 | 107 | ## Security Vulnerabilities 108 | 109 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 110 | 111 | ## Credits 112 | 113 | - [Sokanacademy](https://github.com/sokanacademy) 114 | - [All Contributors](../../contributors) 115 | 116 | ## License 117 | 118 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 119 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sokanacademy/laravel-fluent-rabbitmq", 3 | "description": "integrate rabbitmq in a laravel application", 4 | "keywords": [ 5 | "laravel rabbitmq", 6 | "laravel", 7 | "rabbitmq" 8 | ], 9 | "homepage": "https://github.com/sokanacademy/Laravel-Fluent-RabbitMQ", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "alireza jazayerei", 14 | "email": "alireza.jazayerei@gmail.com", 15 | "role": "Developer" 16 | }, 17 | { 18 | "name": "Mohammadhossein Fereydouni", 19 | "email": "mohammadhfereydouni@gmail.com", 20 | "role": "Developer" 21 | }, 22 | { 23 | "name": "Sajjad Ranjbar", 24 | "email": "sajadranjabr1911@gmail.com", 25 | "role": "Developer" 26 | } 27 | ], 28 | "require": { 29 | "php": "^8.1", 30 | "illuminate/contracts": "^10", 31 | "php-amqplib/php-amqplib": ">=3.0", 32 | "ext-json": "*" 33 | }, 34 | "require-dev": { 35 | "nunomaduro/collision": "^6", 36 | "nunomaduro/larastan": "^2.0", 37 | "orchestra/testbench": "^8", 38 | "pestphp/pest": "^1.21", 39 | "phpstan/extension-installer": "^1.1", 40 | "phpstan/phpstan-deprecation-rules": "^1.0", 41 | "phpstan/phpstan-phpunit": "^1.0", 42 | "phpunit/phpunit": "^9.5" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Sokanacademy\\RabbitMQ\\": "src" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Sokanacademy\\RabbitMQ\\Tests\\": "tests" 52 | } 53 | }, 54 | "scripts": { 55 | "analyse": "vendor/bin/phpstan analyse", 56 | "test": "vendor/bin/pest", 57 | "test-coverage": "vendor/bin/pest coverage" 58 | }, 59 | "config": { 60 | "sort-packages": true, 61 | "allow-plugins": { 62 | "pestphp/pest-plugin": true, 63 | "phpstan/extension-installer": true 64 | } 65 | }, 66 | "extra": { 67 | "laravel": { 68 | "providers": [ 69 | "Sokanacademy\\RabbitMQ\\RabbitMQServiceProvider" 70 | ] 71 | } 72 | }, 73 | "minimum-stability": "dev", 74 | "prefer-stable": true 75 | } 76 | -------------------------------------------------------------------------------- /config/rabbitmq.php: -------------------------------------------------------------------------------- 1 | env('RABBITMQ_HOST', '127.0.0.1'), 5 | 'port' => env('RABBITMQ_PORT', 5672), 6 | 'user' => env('RABBITMQ_USER', 'guest'), 7 | 'password' => env('RABBITMQ_PASSWORD', 'guest'), 8 | 'vhost' => env('RABBITMQ_VHOST', '/'), 9 | 10 | 'consumers' => [ 11 | // [ 12 | // 'event' => '\App\Events\MyEvent', 13 | // 'routing_key' => 'my_routing_key', // if this event does not use routing key then remove this line 14 | // 'map_into' => '\App\Events\MapIntoEvent', // if you want to use the same event then remove this line 15 | // ], 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | php: 3 | build: 4 | context: . 5 | dockerfile: php.dockerfile 6 | volumes: 7 | - ./../:/var/www/html 8 | rabbitmq: 9 | image: rabbitmq:3-management-alpine 10 | ports: 11 | - "15672:15672" 12 | - "5672:5672" 13 | composer: 14 | build: 15 | context: . 16 | dockerfile: php.dockerfile 17 | volumes: 18 | - ./../:/var/www/html 19 | working_dir: /var/www/html 20 | entrypoint: [ "composer" ] 21 | 22 | -------------------------------------------------------------------------------- /docker/php.dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-fpm-alpine 2 | 3 | RUN docker-php-ext-install bcmath sockets 4 | 5 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer 6 | -------------------------------------------------------------------------------- /src/Commands/ConsumeMessages.php: -------------------------------------------------------------------------------- 1 | events = collect(config('rabbitmq.consumers')) 23 | ->map(function ($event) { 24 | return [ 25 | 'base_event' => $event['event'], 26 | 'map_into_event' => $event['map_into'] ?? $event['event'], 27 | 'routing_key' => $event['routing_key'] ?? '', 28 | ]; 29 | }) 30 | ->toArray(); 31 | } 32 | 33 | public function handle(RabbitMQ $rabbitmq): int 34 | { 35 | $queue = $rabbitmq 36 | ->queue() 37 | ->durable() 38 | ->name(config('app.name')) 39 | ->declare(); 40 | 41 | foreach ($this->events as $event) { 42 | $queue->bindTo(class_basename($event['base_event']), $event['routing_key']); 43 | } 44 | 45 | $rabbitmq 46 | ->consume() 47 | ->acknowledge() 48 | ->from(config('app.name'), [$this, 'fireEvent']) 49 | ->receive(); 50 | 51 | return Command::SUCCESS; 52 | } 53 | 54 | public function fireEvent(array $payload, string $routingKey) 55 | { 56 | $event = Arr::first($this->events, function (array $event) use ($routingKey, $payload) { 57 | return $payload['event.name'] === class_basename($event['base_event']) 58 | && Str::is($event['routing_key'], $routingKey); 59 | })['map_into_event']; 60 | 61 | event(resolve($event, $payload)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Commands/DeclareExchanges.php: -------------------------------------------------------------------------------- 1 | getEvents() 22 | ->filter(function (string $event) { 23 | return in_array( 24 | ShouldPublish::class, 25 | (new ReflectionClass($event))->getInterfaceNames() 26 | ); 27 | }) 28 | ->each(function (string $event) use ($rabbitmq) { 29 | $rabbitmq 30 | ->exchange() 31 | ->durable() 32 | ->type($this->determineExchangeType($event)) 33 | ->name(class_basename($event)) 34 | ->declare(); 35 | 36 | $this->info('declared ' . class_basename($event)); 37 | $this->newLine(); 38 | }); 39 | 40 | return Command::SUCCESS; 41 | } 42 | 43 | private function getEvents(): Collection 44 | { 45 | $events = []; 46 | 47 | foreach ($this->laravel->getProviders(EventServiceProvider::class) as $provider) { 48 | $providerEvents = array_merge_recursive( 49 | $provider->shouldDiscoverEvents() 50 | ? $provider->discoverEvents() 51 | : [], 52 | $provider->listens() 53 | ); 54 | 55 | $events = array_merge_recursive($events, $providerEvents); 56 | } 57 | 58 | return collect($events)->keys(); 59 | } 60 | 61 | private function determineExchangeType(string $event): string 62 | { 63 | $reflection = (new ReflectionClass($event)); 64 | 65 | if (! $reflection->hasProperty('exchangeType')) { 66 | return 'fanout'; 67 | } 68 | 69 | $property = $reflection->getProperty('exchangeType'); 70 | 71 | $property->setAccessible(true); 72 | 73 | return $property->getValue(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/RabbitMQ.php: -------------------------------------------------------------------------------- 1 | connection = new AMQPStreamConnection( 17 | config('rabbitmq.host'), 18 | config('rabbitmq.port'), 19 | config('rabbitmq.user'), 20 | config('rabbitmq.password'), 21 | config('rabbitmq.vhost') 22 | ); 23 | 24 | $this->channel = $this->connection->channel(); 25 | } 26 | 27 | public function queue(): RabbitMQQueue 28 | { 29 | return new RabbitMQQueue($this->channel); 30 | } 31 | 32 | public function exchange(): RabbitMQExchange 33 | { 34 | return new RabbitMQExchange($this->connection); 35 | } 36 | 37 | public function message(): RabbitMQMessage 38 | { 39 | return new RabbitMQMessage($this->channel); 40 | } 41 | 42 | public function consume(): RabbitMQConsumer 43 | { 44 | return new RabbitMQConsumer($this->channel); 45 | } 46 | 47 | public function __destruct() 48 | { 49 | $this->connection->close(); 50 | $this->channel->close(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/RabbitMQConsumer.php: -------------------------------------------------------------------------------- 1 | channel = $channel; 19 | } 20 | 21 | public function receiveWithoutAcknowledgement(int $numberOfMessages): RabbitMQConsumer 22 | { 23 | $this->qos = $numberOfMessages; 24 | 25 | return $this; 26 | } 27 | 28 | public function from(string $queue, callable $handle): RabbitMQConsumer 29 | { 30 | $this->channel->basic_qos(null, $this->qos, null); 31 | 32 | $this->channel->basic_consume( 33 | $queue, 34 | '', 35 | false, 36 | ! $this->acknowledge, 37 | false, 38 | false, 39 | function (AMQPMessage $message) use ($handle) { 40 | call_user_func_array($handle, [ 41 | json_decode($message->getBody(), true), 42 | $message->getRoutingKey(), 43 | ]); 44 | 45 | if ($this->acknowledge) { 46 | $message->ack(); 47 | } 48 | } 49 | ); 50 | 51 | return $this; 52 | } 53 | 54 | public function acknowledge(): RabbitMQConsumer 55 | { 56 | $this->acknowledge = true; 57 | 58 | return $this; 59 | } 60 | 61 | public function receive(): void 62 | { 63 | while ($this->channel->is_open()) { 64 | $this->channel->wait(); 65 | } 66 | } 67 | 68 | public function __destruct() 69 | { 70 | $this->channel->close(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/RabbitMQDispatcher.php: -------------------------------------------------------------------------------- 1 | message() 22 | ->persistent() 23 | ->viaExchange(class_basename($event)) 24 | ->when( 25 | method_exists($event, 'routingKey'), 26 | fn (RabbitMQMessage $message) => $message 27 | ->route($event->routingKey()) 28 | ) 29 | ->withPayload( 30 | array_map( 31 | function ($property) { 32 | return $this->formatProperty($property); 33 | }, 34 | call_user_func('get_object_vars', $event) 35 | ) + ['event.name' => class_basename($event)] 36 | ) 37 | ->publish(); 38 | 39 | return parent::dispatch($event, $payload, $halt); 40 | } 41 | 42 | private function formatProperty($property) 43 | { 44 | if ($property instanceof Arrayable) { 45 | return $property->toArray(); 46 | } 47 | 48 | return $property; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/RabbitMQExchange.php: -------------------------------------------------------------------------------- 1 | channel = $connection->channel(); 21 | } 22 | 23 | public function name(string $name): RabbitMQExchange 24 | { 25 | $this->name = $name; 26 | 27 | return $this; 28 | } 29 | 30 | public function type(string $type): RabbitMQExchange 31 | { 32 | $this->type = $type; 33 | 34 | return $this; 35 | } 36 | 37 | public function durable(): RabbitMQExchange 38 | { 39 | $this->durable = true; 40 | 41 | return $this; 42 | } 43 | 44 | public function declare() 45 | { 46 | $this->channel->exchange_declare( 47 | $this->name, 48 | $this->type, 49 | false, 50 | $this->durable, 51 | false 52 | ); 53 | } 54 | 55 | public function __destruct() 56 | { 57 | $this->channel->close(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/RabbitMQMessage.php: -------------------------------------------------------------------------------- 1 | channel = $channel; 26 | } 27 | 28 | public function withPayload(array $payload = []): RabbitMQMessage 29 | { 30 | $this->payload = $payload; 31 | 32 | return $this; 33 | } 34 | 35 | public function persistent(): RabbitMQMessage 36 | { 37 | $this->persistent = true; 38 | 39 | return $this; 40 | } 41 | 42 | public function route(string $routingKey): RabbitMQMessage 43 | { 44 | $this->routingKey = $routingKey; 45 | 46 | return $this; 47 | } 48 | 49 | public function viaExchange(string $exchange): RabbitMQMessage 50 | { 51 | $this->exchange = $exchange; 52 | 53 | return $this; 54 | } 55 | 56 | public function publish(): void 57 | { 58 | $this->channel->basic_publish( 59 | new AMQPMessage(json_encode($this->payload), $this->properties()), 60 | $this->exchange, 61 | $this->routingKey 62 | ); 63 | } 64 | 65 | private function properties(): array 66 | { 67 | $properties = []; 68 | 69 | if ($this->persistent) { 70 | $properties['delivery_mode'] = AMQPMessage::DELIVERY_MODE_PERSISTENT; 71 | } 72 | 73 | return $properties; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/RabbitMQQueue.php: -------------------------------------------------------------------------------- 1 | channel = $channel; 18 | } 19 | 20 | public function durable(): RabbitMQQueue 21 | { 22 | $this->durable = true; 23 | 24 | return $this; 25 | } 26 | 27 | public function name(string $name): RabbitMQQueue 28 | { 29 | $this->name = $name; 30 | 31 | return $this; 32 | } 33 | 34 | public function declare(): RabbitMQQueue 35 | { 36 | $this->channel->queue_declare( 37 | $this->name, 38 | false, 39 | $this->durable, 40 | false, 41 | false 42 | ); 43 | 44 | return $this; 45 | } 46 | 47 | public function bindTo(string $exchange, string $route = ''): void 48 | { 49 | $this->channel->queue_bind($this->name, $exchange, $route); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/RabbitMQServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(RabbitMQ::class, function () { 16 | return new RabbitMQ(); 17 | }); 18 | 19 | $this->app->extend('events', function (Dispatcher $dispatcher, $app) { 20 | return (new RabbitMQDispatcher($app))->setQueueResolver(function () use ($app) { 21 | return $app->make(QueueFactoryContract::class); 22 | }); 23 | }); 24 | 25 | $this->mergeConfigFrom( 26 | __DIR__ . '/../config/rabbitmq.php', 27 | 'rabbitmq' 28 | ); 29 | } 30 | 31 | public function boot() 32 | { 33 | if ($this->app->runningInConsole()) { 34 | $this->commands([ 35 | DeclareExchanges::class, 36 | ConsumeMessages::class, 37 | ]); 38 | } 39 | 40 | $this->publishes([ 41 | __DIR__ . '/../config/rabbitmq.php' => config_path('rabbitmq.php'), 42 | ], 'laravel-fluent-rabbitmq-config'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Support/ShouldPublish.php: -------------------------------------------------------------------------------- 1 |