diff --git a/README.md b/README.md index 4d94c269c3..1021c6bf29 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,10 @@ Scrutinizer Code Quality [![Total Downloads](https://poser.pugx.org/fntneves/laravel-transactional-events/downloads)](https://packagist.org/packages/fntneves/laravel-transactional-events) -This package introduces a transactional layer to Laravel Event Dispatcher.
-Out of the box, it ensures consistency between events dispatched and database transactions. + +This package adds a transaction-aware layer on top of the Laravel Event Dispatcher.
+Basically, it holds events dispatched in a database transaction until the transaction successfully commits.
+In case of transaction failure, the events are discarded and never dispatched. * [Introduction](#introduction) * [Installation](#installation) @@ -14,10 +16,13 @@ Out of the box, it ensures consistency between events dispatched and database tr * [Lumen](#lumen) * [Usage](#usage) * [Configuration](#configuration) +* [F.A.Q.](#frequently-asked-questions) +* [Known Issues](#known-issues) ## Introduction -Let's start with a simple example of ordering tickets. Assume that it involves database changes and a payment registration and that the custom event `OrderWasProcessed` is dispatched immediately after the order is processed in the database. +Consider the following example of ordering tickets that involves database changes and payment operation. +The custom event `OrderWasProcessed` is dispatched immediately after the order is processed in the database. ```php DB::transaction(function() { @@ -29,9 +34,11 @@ DB::transaction(function() { }); ``` -The transaction in the above example may fail for several reasons: an exception may occur in the `orderTickets` method or in the payment service or simply due to a deadlock. +The transaction in the above example may fail due to several reasons. For instance, due to an exception in the `orderTickets` method or cause by the Payment Service package or simply due to a deadlock. -A failure will rollback the database changes made during the transaction. However, this is not true for the `OrderWasProcessed` event, which is actually dispatched and eventually executed. Considering that this event may result in sending an e-mail with the order confirmation, managing it the right way becomes mandatory. +The failed transaction will undo the database changes performed during the transaction. +This is not true however for the `OrderWasProcessed` event, which was dispatched and eventually executed or enqueued. +Preventing the event to be dispatch may prevent embarrassing situations like confirmation emails sent after orders failure. The purpose of this package is to actually dispatch events **if and only if** the transaction in which they were dispatched commits. For instance, in the above example the `OrderWasProcessed` event would not be dispatched if the transaction fails. @@ -93,9 +100,10 @@ $app->register(Neves\Events\EventServiceProvider::class); The transactional layer is enabled out of the box for the events placed under the `App\Events` namespace. -Additionally, this package offers two ways to mark events as transactional: -- Implement the `Neves\Events\Contracts\TransactionalEvent` contract (recommended) -- Change the [configuration file](#configuration) provided by this package +Additionally, this package offers three distinct ways to execute transactional-aware events or custom behavior: +- Implement the `Neves\Events\Contracts\TransactionalEvent` contract +- Use the `transactional` helper to pass a custom closure to be executed once transaction commits +- Change the [configuration file](#configuration) provided by this package (not recommended) #### Use the contract, Luke: @@ -117,7 +125,7 @@ class TicketsOrdered implements TransactionalEvent ... } ``` -As this package does not require any changes in your code, you are still able to use the `Event` facade and call the `event()` or `broadcast()` helper to dispatch an event: +As this package does not require any changes in your code, you are to use `Event` facade, call the `event()` or `broadcast()` helper to dispatch an event as follows: ```php Event::dispatch(new App\Event\TicketsOrdered) // Using Event facade @@ -129,6 +137,27 @@ Even if you are using queues, they will still work smothly because this package **Reminder:** Events are considered transactional when they are dispatched within transactions. When an event is dispatched out of transactions, it bypasses the transactional layer. +#### What about Jobs? + +In version **1.8.8**, this package introduced the `transactional` helper for applying the same behavior to custom instructions without the need to create a specific event. + +This helper can be used to ensure that Jobs are dispatched only after the transaction successfully commits: + +```php +DB::transaction(function () { + ... + + transactional(function () { + // Job will be dispatched only if the transaction commits. + ProcessOrderShippingJob::dispatch($order); + }); + + ... +}); +``` + +Under the hood, it creates a *TransactionalClosureEvent* event provided by this package. + ## Configuration @@ -162,6 +191,12 @@ Choose the events that should always bypass the transactional layer, i.e., shoul ], ``` +## Frequently Asked Questions + +#### Can I use it for Jobs? + +Yes. From version **1.8.8**, as mentioned in [Usage](#usage) section, you can use the `transactional(Closure $callable)` helper to trigger jobs only after the transaction commits. + ## Known issues #### Transactional events are not dispatched in tests. diff --git a/composer.json b/composer.json index 5a899f4d9e..42b958ecbf 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,10 @@ "autoload": { "psr-0": { "Neves\\": "src/" - } + }, + "files": [ + "src/helpers.php" + ] }, "extra": { "laravel": { diff --git a/src/Neves/Events/EventServiceProvider.php b/src/Neves/Events/EventServiceProvider.php index 2d1e3db445..1f8a5f87af 100644 --- a/src/Neves/Events/EventServiceProvider.php +++ b/src/Neves/Events/EventServiceProvider.php @@ -27,6 +27,9 @@ public function register() $eventDispatcher = $this->app->make(EventDispatcher::class); $this->app->extend('events', function () use ($eventDispatcher) { $dispatcher = new TransactionalDispatcher($eventDispatcher); + $dispatcher->listen(TransactionalClosureEvent::class, function(TransactionalClosureEvent $event) { + ($event->getClosure())(); + }); $dispatcher->setTransactionalEvents($this->app['config']->get('transactional-events.transactional')); $dispatcher->setExcludedEvents($this->app['config']->get('transactional-events.excluded')); diff --git a/src/Neves/Events/TransactionalClosureEvent.php b/src/Neves/Events/TransactionalClosureEvent.php new file mode 100644 index 0000000000..6bd4272209 --- /dev/null +++ b/src/Neves/Events/TransactionalClosureEvent.php @@ -0,0 +1,22 @@ +closure = $closure; + } + + public function getClosure(): Closure { + return $this->closure; + } +} \ No newline at end of file diff --git a/src/helpers.php b/src/helpers.php new file mode 100644 index 0000000000..17c819a8be --- /dev/null +++ b/src/helpers.php @@ -0,0 +1,19 @@ +dispatch(new TransactionalClosureEvent($callable)); + } +} \ No newline at end of file diff --git a/tests/TransactionalDispatcherTest.php b/tests/TransactionalDispatcherTest.php index 410b76f046..ec3c53ba4e 100644 --- a/tests/TransactionalDispatcherTest.php +++ b/tests/TransactionalDispatcherTest.php @@ -2,6 +2,7 @@ use Neves\Events\Contracts\TransactionalEvent; use Neves\Events\EventServiceProvider; +use Neves\Events\TransactionalClosureEvent; use Neves\Events\TransactionalDispatcher; use Orchestra\Testbench\TestCase; @@ -268,6 +269,34 @@ public function it_immediately_dispatches_specific_events_excluded_on_a_pattern( }); } + /** @test */ + public function it_provides_transactional_behavior_of_custom_closures() + { + DB::transaction(function () { + $this->dispatcher->dispatch(new TransactionalClosureEvent(function() { + $_SERVER['__events'] = 'bar'; + })); + + $this->assertArrayNotHasKey('__events', $_SERVER); + }); + + $this->assertEquals('bar', $_SERVER['__events']); + } + + /** @test */ + public function it_provides_transactional_behavior_of_custom_closures_using_transactional_helper() + { + DB::transaction(function () { + transactional(function() { + $_SERVER['__events'] = 'bar'; + }); + + $this->assertArrayNotHasKey('__events', $_SERVER); + }); + + $this->assertEquals('bar', $_SERVER['__events']); + } + /** * Regression test: Fix infinite loop caused by TransactionCommitted (#12). * @test