Skip to content

Commit 6fe22a4

Browse files
author
andrei.korsak
committed
initial
0 parents  commit 6fe22a4

13 files changed

+1275
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor/
2+
report/
3+
composer.lock
4+
/.idea

.travis.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
language: php
2+
3+
php:
4+
- 7.1
5+
- 7.2
6+
- 7.3
7+
8+
cache:
9+
directories:
10+
- ./vendor
11+
- $HOME/.composer/cache
12+
13+
env:
14+
- LARAVEL_VERSION=5.8.* TESTBENCH_VERSION=3.8.*
15+
16+
before_script:
17+
- composer self-update
18+
- composer require "laravel/framework:${LARAVEL_VERSION}" "orchestra/testbench:${TESTBENCH_VERSION}" --no-update
19+
- if [ "$PHPUNIT_VERSION" != "" ]; then composer require "phpunit/phpunit:${PHPUNIT_VERSION}" --no-update; fi;
20+
- composer update
21+
- mkdir -p build/logs
22+
23+
script: vendor/bin/phpunit

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2017 Francisco Neves
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 all
13+
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 THE
21+
SOFTWARE.

README.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Transaction-aware Event Dispatcher for Laravel (and Lumen)
2+
3+
This package introduces a transactional layer to the Laravel Event Dispatcher. Its purpose is to ensure, without changing a single line of code, consistency between events dispatched and database transactions. This behavior is also applicable to Eloquent events, such as `saved` and `created`.
4+
5+
* [Introduction](#introduction)
6+
* [Installation](#installation)
7+
* [Laravel](#laravel) (5.5+)
8+
* [Lumen](#lumen) (5.5+)
9+
* [Usage](#usage)
10+
* [Configuration](#configuration)
11+
12+
## Introduction
13+
14+
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.
15+
16+
```php
17+
DB::transaction(function() {
18+
$user = User::find(...);
19+
$concert = Concert::find(...);
20+
$order = $concert->orderTickets($user, 3);
21+
event(new OrderWasProcessed($order));
22+
PaymentService::registerOrder($order);
23+
});
24+
```
25+
26+
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.
27+
28+
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.
29+
30+
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.
31+
32+
Please note that events dispatched out of transactions will bypass the transactional layer, meaning that they will be handled by the default Event Dispatcher. This is true also for events in which the `$halt` parameter is set to `true`.
33+
34+
## Installation
35+
36+
Laravel | Package
37+
:---------|:----------
38+
5.8.x | 1.0.x
39+
40+
* [Laravel](#laravel) (5.5+)
41+
* [Lumen](#lumen) (5.5+)
42+
43+
### Laravel
44+
The installation of this package in Laravel is automatic thanks to the _Package Auto-Discovery_ feature of Laravel 5.5+.
45+
Just add this package to the `composer.json` file and it will be ready for your application.
46+
47+
```
48+
composer require merkeleon/laravel-transactional-events
49+
```
50+
51+
A configuration file is also available for this package. Run the following command to copy the provided configuration file `transactional-events.php` your `config` folder.
52+
53+
```
54+
php artisan vendor:publish --provider="Merkeleon\Events\EventServiceProvider"
55+
```
56+
57+
### Lumen
58+
59+
As Lumen is built on top of Laravel packages, this package should also work smoothly on this micro-web framework.
60+
Run the following command to install this package:
61+
62+
``` bash
63+
composer require merkeleon/laravel-transactional-events
64+
```
65+
66+
In order to configure the behavior of this package, copy the configuration files:
67+
68+
```bash
69+
cp vendor/merkeleon/laravel-transactional-events/src/config/transactional-events.php config/transactional-events.php
70+
```
71+
72+
Then, in `bootstrap/app.php`, register the configuration and the service provider:<br/>
73+
*Note:* This package must be registered _after_ the default EventServiceProvider, so your event listeners are not overriden.
74+
75+
```php
76+
// The default EventServiceProvider must be registered.
77+
$app->register(App\Providers\EventServiceProvider::class);
78+
79+
...
80+
81+
$app->configure('transactional-events');
82+
$app->register(Merkeleon\Events\EventServiceProvider::class);
83+
```
84+
85+
## Usage
86+
87+
The transactional layer is enabled by default for the events placed under the `App\Events` namespace.
88+
89+
However, the easiest way to make your events behave as transactional events is by implementing the contract `Merkeleon\Events\Contracts\TransactionalEvent`.<br/>
90+
*Note that events that implement it will behave as transactional events even when excluded in config.*
91+
92+
```php
93+
namespace App\Events;
94+
95+
use Illuminate\Queue\SerializesModels;
96+
use Illuminate\Foundation\Events\Dispatchable;
97+
...
98+
use Merkeleon\Events\Contracts\TransactionalEvent;
99+
100+
class TicketsOrdered implements TransactionalEvent
101+
{
102+
use Dispatchable, InteractsWithSockets, SerializesModels;
103+
104+
...
105+
}
106+
```
107+
108+
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:
109+
110+
```php
111+
Event::dispatch(new App\Event\TicketsOrdered) // Using Event facade
112+
event(new App\Event\TicketsOrdered) // Using event() helper method
113+
broadcast(new App\Event\TicketsOrdered) // Using broadcast() helper method
114+
```
115+
116+
Even if you are using queues, they will still work smothly because this package does not change the core behavior of the event dispatcher. They will be enqueued as soon as the active transaction succeeds. Otherwise, they will be discarded.
117+
118+
**Reminder:** Events are considered transactional when they are dispatched within transactions. When an event is dispatched out of transactions, it bypasses the transactional layer.
119+
120+
121+
## Configuration
122+
123+
The following keys are present in the configuration file:
124+
125+
Enable or disable the transactional behavior by changing the following property:
126+
```php
127+
'enable' => true
128+
```
129+
130+
By default, the transactional behavior will be applied to events on `App\Events` namespace. Feel free to use patterns and namespaces.
131+
132+
```php
133+
'transactional' => ['App\Events']
134+
```
135+
136+
Choose the events that should always bypass the transactional layer, i.e., should be handled by the default event dispatcher. By default, all `*ed` Eloquent events are excluded.
137+
138+
```php
139+
'excluded' => [
140+
// 'eloquent.*',
141+
'eloquent.booted',
142+
'eloquent.retrieved',
143+
'eloquent.saved',
144+
'eloquent.updated',
145+
'eloquent.created',
146+
'eloquent.deleted',
147+
'eloquent.restored',
148+
],
149+
```
150+
151+
## Known issues
152+
153+
> Transactional events are not dispatched in tests.
154+
155+
**This issue is fixed for Laravel 5.6.16+ (see [#23832](https://github.com/laravel/framework/pull/23832)).**<br/>
156+
For previous versions, it is associated with the `RefreshDatabase` trait, namely when it uses database transactions to reset database after each test.
157+
This package relies on events dispached when transactions begin/commit/rollback and as each is executed within a transaction that rollbacks when test finishes, the dispatched application events are never actually dispatched. In order to get the expected behavior, use the `Merkeleon\Testing\RefreshDatabase` trait in your tests instead of the one originally provided by Laravel.
158+
159+
## License
160+
This package is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT).

composer.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "merkeleon/laravel-transactional-events",
3+
"description": "Transactional layer for Laravel Event Dispatcher",
4+
"type": "library",
5+
"license": "MIT",
6+
"require": {
7+
"illuminate/database": "~5.8.0",
8+
"illuminate/events": "~5.8.0",
9+
"illuminate/support": "~5.8.0"
10+
},
11+
"authors": [
12+
{
13+
"name": "Francisco Neves",
14+
"email": "hi@francisconeves.me"
15+
}
16+
],
17+
"autoload": {
18+
"psr-0": {
19+
"Merkeleon\\Events\\": "src/"
20+
}
21+
},
22+
"extra": {
23+
"laravel": {
24+
"providers": [
25+
"Merkeleon\\Events\\EventServiceProvider"
26+
]
27+
}
28+
},
29+
"minimum-stability": "dev",
30+
"prefer-stable": true,
31+
"config": {
32+
"sort-packages": true
33+
},
34+
"require-dev": {
35+
"orchestra/testbench": "~3.8.0"
36+
}
37+
}

phpunit.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit backupGlobals="false"
3+
backupStaticAttributes="false"
4+
bootstrap="vendor/autoload.php"
5+
colors="true"
6+
convertErrorsToExceptions="true"
7+
convertNoticesToExceptions="true"
8+
convertWarningsToExceptions="true"
9+
processIsolation="false"
10+
stopOnFailure="false"
11+
syntaxCheck="false"
12+
>
13+
<testsuites>
14+
<testsuite name="Package Test Suite">
15+
<directory suffix=".php">./tests/</directory>
16+
</testsuite>
17+
</testsuites>
18+
</phpunit>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Merkeleon\Events\Contracts;
4+
5+
interface TransactionalEvent
6+
{
7+
//
8+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Merkeleon\Events;
4+
5+
use Illuminate\Support\ServiceProvider;
6+
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
7+
8+
class EventServiceProvider extends ServiceProvider
9+
{
10+
/**
11+
* Register the service provider.
12+
*
13+
* @return void
14+
*/
15+
public function register()
16+
{
17+
$this->mergeConfigFrom(
18+
__DIR__ . '/../../config/transactional-events.php',
19+
'transactional-events'
20+
);
21+
22+
if (! $this->app['config']->get('transactional-events.enable')) {
23+
return;
24+
}
25+
26+
$this->app->afterResolving('db', function ($connectionResolver) {
27+
$eventDispatcher = $this->app->make(EventDispatcher::class);
28+
$this->app->extend('events', function () use ($connectionResolver, $eventDispatcher) {
29+
$dispatcher = new TransactionalDispatcher($connectionResolver, $eventDispatcher);
30+
$dispatcher->setTransactionalEvents($this->app['config']->get('transactional-events.transactional'));
31+
$dispatcher->setExcludedEvents($this->app['config']->get('transactional-events.excluded'));
32+
33+
return $dispatcher;
34+
});
35+
});
36+
}
37+
38+
/**
39+
* Bootstrap the application events.
40+
*
41+
* @return void
42+
*/
43+
public function boot()
44+
{
45+
$configPath = $this->app->basePath().'/config';
46+
47+
$this->publishes([
48+
__DIR__ . '/../../config/transactional-events.php' => $configPath.'/transactional-events.php',
49+
]);
50+
}
51+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Merkeleon\Events\Testing;
4+
5+
use Illuminate\Foundation\Testing\RefreshDatabase as BaseRefreshDatabase;
6+
7+
trait RefreshDatabase
8+
{
9+
use BaseRefreshDatabase;
10+
11+
/**
12+
* Begin a database transaction on the testing database.
13+
*
14+
* @return void
15+
*/
16+
public function beginDatabaseTransaction()
17+
{
18+
$emptyDispatcher = new \Illuminate\Events\Dispatcher;
19+
$database = $this->app->make('db');
20+
21+
foreach ($this->connectionsToTransact() as $name) {
22+
$connection = $database->connection($name);
23+
24+
$currentDispatcher = $connection->getEventDispatcher();
25+
$connection->setEventDispatcher($emptyDispatcher);
26+
$connection->beginTransaction();
27+
$connection->setEventDispatcher($currentDispatcher);
28+
}
29+
30+
$this->beforeApplicationDestroyed(function () use ($database, $emptyDispatcher) {
31+
foreach ($this->connectionsToTransact() as $name) {
32+
$connection = $database->connection($name);
33+
34+
$currentDispatcher = $connection->getEventDispatcher();
35+
$connection->setEventDispatcher($emptyDispatcher);
36+
$connection->rollBack();
37+
$connection->setEventDispatcher($currentDispatcher);
38+
39+
$connection->disconnect();
40+
}
41+
});
42+
}
43+
}

0 commit comments

Comments
 (0)