diff --git a/README.md b/README.md
index 4d94c269c3..1021c6bf29 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,10 @@
[![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