diff --git a/composer.json b/composer.json index 4c52c94..3cc9cf1 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,11 @@ "Convenia\\Pigeon\\Tests\\": "tests/" } }, + "scripts": { + "coverage": [ + "XDEBUG_MODE=coverage ./vendor/bin/phpunit" + ] + }, "extra": { "laravel": { "providers": [ diff --git a/src/Drivers/Driver.php b/src/Drivers/Driver.php index d043646..480f686 100644 --- a/src/Drivers/Driver.php +++ b/src/Drivers/Driver.php @@ -4,6 +4,8 @@ use Convenia\Pigeon\Consumer\Consumer; use Convenia\Pigeon\Consumer\ConsumerContract; +use Convenia\Pigeon\Events\DispatchingEvent; +use Convenia\Pigeon\Events\EventDispatched; use Convenia\Pigeon\Exceptions\Events\EmptyEventException; use Convenia\Pigeon\Publisher\Publisher; use Convenia\Pigeon\Publisher\PublisherContract; @@ -69,7 +71,22 @@ public function dispatch(string $eventName, array $event, array $meta = []): voi $publisher->header($key, $value); } + DispatchingEvent::dispatch( + $publisher, + $eventName, + $event, + $meta + ); + + $publisher->disableEvents = true; $publisher->publish($event, [], 3); + + EventDispatched::dispatch( + $publisher, + $eventName, + $event, + $meta + ); } public function events(string $event = '#'): ConsumerContract diff --git a/src/Events/BaseEvent.php b/src/Events/BaseEvent.php new file mode 100644 index 0000000..5194ca1 --- /dev/null +++ b/src/Events/BaseEvent.php @@ -0,0 +1,25 @@ +publisher = $publisher; + $this->userData = $userData; + $this->userMetaData = $userMetaData; + } +} diff --git a/src/Events/DispatchingEvent.php b/src/Events/DispatchingEvent.php new file mode 100644 index 0000000..87db1a6 --- /dev/null +++ b/src/Events/DispatchingEvent.php @@ -0,0 +1,20 @@ +eventName = $eventName; + } +} diff --git a/src/Events/EventDispatched.php b/src/Events/EventDispatched.php new file mode 100644 index 0000000..2c44366 --- /dev/null +++ b/src/Events/EventDispatched.php @@ -0,0 +1,20 @@ +eventName = $eventName; + } +} \ No newline at end of file diff --git a/src/Events/MessagePublished.php b/src/Events/MessagePublished.php new file mode 100644 index 0000000..d55197d --- /dev/null +++ b/src/Events/MessagePublished.php @@ -0,0 +1,7 @@ +app = $app; - $this->driver = $driver; - $this->exchange = $exchange; - } - - public function routing(string $key): PublisherContract - { - $this->routing = $key; - - return $this; - } + public bool $disableEvents = false; public function bind(string $queue): PublisherContract { @@ -40,56 +20,27 @@ public function bind(string $queue): PublisherContract public function publish(array $message, array $properties = [], int $channelId = null) { + if (!$this->disableEvents) { + PublishingMessage::dispatch( + $this, + $message, + $properties + ); + } + $msg = $this->makeMessage($message, $properties); $this->driver->getChannel($channelId)->basic_publish( $msg, $this->exchange, $this->routing ); - } - - private function makeMessage(array $data, array $properties = []) - { - return new AMQPMessage( - json_encode($data), - $this->getMessageProps($properties) - ); - } - private function getMessageProps(array $userProps): array - { - return array_merge([ - 'content_type' => 'application/json', - 'content_encoding' => 'utf8', - 'correlation_id' => Str::uuid(), - 'expiration' => 60000000, - 'app_id' => $this->app['config']['app_name'], - 'application_headers' => new AMQPTable($this->getHeaders()), - ], $userProps); - } - - public function header(string $key, $value): PublisherContract - { - $this->headers = Arr::add($this->headers, $key, $value); - - return $this; - } - - public function getHeaders(): array - { - $configHeaders = Arr::dot($this->app['config']->get('pigeon.headers')); - $mapped = $this->mapToValues($configHeaders); - - return array_merge($mapped, $this->headers); - } - - protected function mapToValues(array $headers) - { - $result = []; - foreach ($headers as $key => $value) { - $result[$key] = is_callable($value) ? call_user_func($value) : $value; + if (!$this->disableEvents) { + MessagePublished::dispatch( + $this, + $message, + $properties + ); } - - return $result; } } diff --git a/src/Publisher/PublisherConcern.php b/src/Publisher/PublisherConcern.php new file mode 100644 index 0000000..01f7306 --- /dev/null +++ b/src/Publisher/PublisherConcern.php @@ -0,0 +1,83 @@ +app = $app; + $this->driver = $driver; + $this->exchange = $exchange; + } + + public function routing(string $key): PublisherContract + { + $this->routing = $key; + + return $this; + } + + public function getRoute(): ?string + { + return $this->routing; + } + + private function makeMessage(array $data, array $properties = []) + { + return new AMQPMessage( + json_encode($data), + $this->getMessageProps($properties) + ); + } + + private function getMessageProps(array $userProps): array + { + return array_merge([ + 'content_type' => 'application/json', + 'content_encoding' => 'utf8', + 'correlation_id' => Str::uuid(), + 'expiration' => 60000000, + 'app_id' => $this->app['config']['app_name'], + 'application_headers' => new AMQPTable($this->getHeaders()), + ], $userProps); + } + + public function header(string $key, $value): PublisherContract + { + $this->headers = Arr::add($this->headers, $key, $value); + + return $this; + } + + public function getHeaders(): array + { + $configHeaders = Arr::dot($this->app['config']->get('pigeon.headers')); + $mapped = $this->mapToValues($configHeaders); + + return array_merge($mapped, $this->headers); + } + + protected function mapToValues(array $headers) + { + $result = []; + foreach ($headers as $key => $value) { + $result[$key] = is_callable($value) ? call_user_func($value) : $value; + } + + return $result; + } +} \ No newline at end of file diff --git a/src/Publisher/PublisherContract.php b/src/Publisher/PublisherContract.php index d6bf6b1..a340468 100644 --- a/src/Publisher/PublisherContract.php +++ b/src/Publisher/PublisherContract.php @@ -11,9 +11,13 @@ public function __construct(Application $app, DriverContract $driver, string $ex public function routing(string $key): self; + public function getRoute(): ?string; + public function bind(string $queue): self; public function publish(array $message, array $properties = []); public function header(string $key, $value): self; + + public function getHeaders(): array; } diff --git a/src/Support/Testing/FakePublisher.php b/src/Support/Testing/FakePublisher.php new file mode 100644 index 0000000..f9909ce --- /dev/null +++ b/src/Support/Testing/FakePublisher.php @@ -0,0 +1,21 @@ +app, $this, $eventName); + + $publisher->header('category', $eventName); + foreach ($meta as $key => $value) { + $publisher->header($key, $value); + } + + DispatchingEvent::dispatch( + $publisher, + $eventName, + $event, + $meta + ); + $this->events->push([ 'event' => $eventName, 'data' => $event, ]); + + EventDispatched::dispatch( + $publisher, + $eventName, + $event, + $meta + ); } public function driver($driver = null) diff --git a/tests/Integration/Driver/DriverTest.php b/tests/Integration/Driver/DriverTest.php index 859a038..43105c4 100644 --- a/tests/Integration/Driver/DriverTest.php +++ b/tests/Integration/Driver/DriverTest.php @@ -3,8 +3,13 @@ namespace Convenia\Pigeon\Tests\Integration\Driver; use Convenia\Pigeon\Drivers\Driver; +use Convenia\Pigeon\Events\DispatchingEvent; +use Convenia\Pigeon\Events\EventDispatched; +use Convenia\Pigeon\Events\MessagePublished; +use Convenia\Pigeon\Events\PublishingMessage; use Convenia\Pigeon\Resolver\ResolverContract; use Convenia\Pigeon\Tests\Integration\TestCase; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Str; use PhpAmqpLib\Connection\AMQPStreamConnection; use PhpAmqpLib\Message\AMQPMessage; @@ -79,6 +84,52 @@ public function test_it_should_publish_event_with_meta() $this->assertEquals(array_merge($meta, ['category' => $event_name]), $event_meta->getNativeData()); } + public function test_it_should_dispatch_laravel_events_when_publishing_messages() + { + // setup + Event::fake(); + + $event_name = Str::random(7); + $event_content = [ + 'Golden' => 'Axe', + ]; + $event_meta = [ + 'Ax' => 'Battler', + ]; + $this->channel->exchange_declare(Driver::EVENT_EXCHANGE, Driver::EVENT_EXCHANGE_TYPE, false, true, false, false, false, new AMQPTable([ + 'x-dead-letter-exchange' => 'dead.letter', + ])); + $this->channel->queue_bind($this->queue, Driver::EVENT_EXCHANGE, $event_name); + + // act + $this->driver->dispatch($event_name, $event_content, $event_meta); + + sleep(1); + + // assert + $event = $this->channel->basic_get($this->queue); + $this->channel->exchange_delete(Driver::EVENT_EXCHANGE); + + Event::assertDispatched(function (DispatchingEvent $event) + use ($event_name, $event_content, $event_meta) { + return $event->publisher->getHeaders()['Ax'] === 'Battler' && + $event->eventName === $event_name && + $event->userData === $event_content && + $event->userMetaData === $event_meta; + }); + + Event::assertDispatched(function (EventDispatched $event) + use ($event_name, $event_content, $event_meta) { + return $event->publisher->getHeaders()['Ax'] === 'Battler' && + $event->eventName === $event_name && + $event->userData === $event_content && + $event->userMetaData === $event_meta; + }); + + Event::assertNotDispatched(PublishingMessage::class); + Event::assertNotDispatched(MessagePublished::class); + } + public function test_it_should_consume_event() { // setup diff --git a/tests/Integration/Publisher/PublisherTest.php b/tests/Integration/Publisher/PublisherTest.php index 052ed7c..7808e2c 100644 --- a/tests/Integration/Publisher/PublisherTest.php +++ b/tests/Integration/Publisher/PublisherTest.php @@ -4,6 +4,7 @@ use Convenia\Pigeon\Drivers\Driver; use Convenia\Pigeon\Tests\Integration\TestCase; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Str; use PhpAmqpLib\Message\AMQPMessage; use PhpAmqpLib\Wire\AMQPTable; @@ -18,6 +19,7 @@ class PublisherTest extends TestCase protected function setUp(): void { parent::setUp(); + Event::fake(); $this->pigeon = $this->app['pigeon']->driver('rabbit'); } diff --git a/tests/Unit/PigeonFakeTest.php b/tests/Unit/PigeonFakeTest.php index 1ace09e..cdeaaa5 100644 --- a/tests/Unit/PigeonFakeTest.php +++ b/tests/Unit/PigeonFakeTest.php @@ -3,11 +3,14 @@ namespace Convenia\Pigeon\Tests\Unit; use Convenia\Pigeon\Consumer\Consumer; +use Convenia\Pigeon\Events\DispatchingEvent; +use Convenia\Pigeon\Events\EventDispatched; use Convenia\Pigeon\Facade\Pigeon; use Convenia\Pigeon\Publisher\Publisher; use Convenia\Pigeon\Resolver\ResolverContract; use Convenia\Pigeon\Tests\TestCase; use Illuminate\Foundation\Testing\WithFaker; +use Illuminate\Support\Facades\Event; use PHPUnit\Framework\Constraint\ExceptionMessage; use PHPUnit\Framework\ExpectationFailedException; @@ -287,6 +290,38 @@ public function test_it_should_assert_event_emitted() $this->fake->assertDispatched($category, $data); } + public function test_it_should_assert_lifecycle_events() + { + Event::fake(); + + // setup + $category = 'some.event.category'; + $data = ['Golden' => 'Axe']; + $meta = ['Ax' => 'Battler']; + + // act + $this->fake->dispatch($category, $data, $meta); + + // assert + $this->fake->assertDispatched($category, $data, $meta); + + Event::assertDispatched(function (DispatchingEvent $event) + use ($category, $data, $meta) { + return $event->publisher->getHeaders()['Ax'] === 'Battler' && + $event->eventName === $category && + $event->userData === $data && + $event->userMetaData === $meta; + }); + + Event::assertDispatched(function (EventDispatched $event) + use ($category, $data, $meta) { + return $event->publisher->getHeaders()['Ax'] === 'Battler' && + $event->eventName === $category && + $event->userData === $data && + $event->userMetaData === $meta; + }); + } + public function test_it_should_assert_consuming_event() { // setup diff --git a/tests/Unit/PublisherTest.php b/tests/Unit/PublisherTest.php index 2a018eb..a586468 100644 --- a/tests/Unit/PublisherTest.php +++ b/tests/Unit/PublisherTest.php @@ -2,8 +2,11 @@ namespace Convenia\Pigeon\Tests\Unit; +use Convenia\Pigeon\Events\MessagePublished; +use Convenia\Pigeon\Events\PublishingMessage; use Convenia\Pigeon\Publisher\Publisher; use Convenia\Pigeon\Tests\TestCase; +use Illuminate\Support\Facades\Event; use Mockery; use PhpAmqpLib\Channel\AMQPChannel; use PhpAmqpLib\Message\AMQPMessage; @@ -35,6 +38,7 @@ public function test_it_should_publish_message_without_routing_key_and_merge_use 'priority' => 10, ]; $publisher = new Publisher($this->app, $this->driver, $exchange); + $publisher->disableEvents = true; // assert $this->channel->shouldReceive('basic_publish')->with( @@ -77,6 +81,47 @@ public function test_it_should_publish_message_with_routing_key_with_props() $publisher->routing($routing)->publish($data, $props); } + public function test_it_should_dispatch_lifecycle_events_when_publishing_events() + { + // setup + Event::fake(); + + $exchange = 'my.axe-is-powerful.exchange'; + $routing = 'my.axe-is-powerful.service'; + $data = ['Golden' => 'Axe']; + $props = ['Ax' => 'Battler']; + + $publisher = new Publisher($this->app, $this->driver, $exchange); + + $publisher->header('Ax', $props['Ax']); + + // assert + $this->channel->shouldReceive('basic_publish')->with( + Mockery::type(AMQPMessage::class), + $exchange, + $routing + )->once(); + + // act + $publisher->routing($routing)->publish($data, $props); + + Event::assertDispatched(function (PublishingMessage $event) + use ($routing, $data, $props) { + return $event->publisher->getHeaders()['Ax'] === $props['Ax'] && + $event->publisher->getRoute() === $routing && + $event->userData === $data && + $event->userMetaData === $props; + }); + + Event::assertDispatched(function (MessagePublished $event) + use ($routing, $data, $props) { + return $event->publisher->getHeaders()['Ax'] === $props['Ax'] && + $event->publisher->getRoute() === $routing && + $event->userData === $data && + $event->userMetaData === $props; + }); + } + public function test_it_should_create_new_publisher_on_new_routing() { // setup @@ -132,6 +177,7 @@ public function test_it_should_add_all_headers() ]); $publisher = new Publisher($this->app, $this->driver, $exchange); + $publisher->disableEvents = true; // assert $this->channel->shouldReceive('basic_publish')->with(