softonic / laravel-transactional-event-publisher
Softonic Laravel Transactional Event Publisher
Package info
github.com/softonic/laravel-transactional-event-publisher
pkg:composer/softonic/laravel-transactional-event-publisher
Requires
- php: >=8.5
- laravel/framework: ^12.0
- php-amqplib/php-amqplib: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- laravel/legacy-factories: ^1.0
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0
- php-mock/php-mock-mockery: ^1.3
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^12.0
- rector/rector: ^2.0
- squizlabs/php_codesniffer: ^3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- 11.1.0
- 11.0.1
- 11.0.0
- 10.2.0
- 10.1.0
- 10.0.0
- 9.4.2
- 9.4.1
- 9.4.0
- 9.3.0
- 9.2.0
- 9.1.1
- 9.1.0
- 9.0.0
- 8.0.1
- 8.0.0
- 7.0.7
- 7.0.6
- 7.0.5
- 7.0.4
- 7.0.3
- 7.0.2
- 7.0.1
- 7.0.0
- 6.0.0
- 5.2.0
- 5.1.0
- 5.0.3
- 5.0.2
- 5.0.1
- 5.0.0
- 4.2.0
- 4.1.1
- 4.1.0
- 4.0.0
- 3.0.0
- 3.0.0-alpha6
- 3.0.0-alpha5
- 3.0.0-alpha4
- 3.0.0-alpha3
- 3.0.0-alpha2
- 3.0.0-alpha
- 2.4.0
- 2.3.0
- 2.2.0
- 2.1.0
- 2.0.0
- 1.2.0
- 1.1.1
- 1.1.0
- 1.0.1
- 1.0.0
- dev-feature/DS-3717-fix-dangling-transaction
- dev-bugfix/log-event-store-failures
- dev-feature/update-laravel-12
- dev-bugfix/prevent-setup-on-serviceprovider
- dev-feature/update-laravel-11
- dev-feature/adapt-service-provider
- dev-Remove-model-relations-from-events-payload
- dev-feature/upgrade-laravel-amqp-version
- dev-feature/add-compatibility-to-laravel-10
- dev-feature/DS-974_remove_events_after_emmit
- dev-Send-events-in-batches
- dev-feature/upgrade_laravel_amqp
- dev-feature/add-event-publisher
This package is auto-updated.
Last update: 2026-10-29 18:00:29 UTC
README
Laravel package to handle atomicity between Eloquent model operations and domain event message generation.
Requirements:
- PHP >= 8.5
- Laravel 12.x
Main features
- Ensure every action has a domain event sent using an atomic transaction between Eloquent model operation, event generation and sent.
- Events sent to a AMQP system sync or async.
- Command to send all the events until now.
Installation
You can require the last version of the package using composer
composer require softonic/laravel-transactional-event-publisher
Configuration
It is possible to configure the basic AMQP information, you can check it in vendor/softonic/transactional-event-publisher/config/transactional-event-publisher.php
If you need further customization, you can publish the configuration.
php artisan vendor:publish --provider="Softonic\TransactionalEventPublisher\ServiceProvider" --tag=config
We provide Softonic\TransactionalEventPublisher\EventStoreMiddlewares\DatabaseMiddleware
and Softonic\TransactionalEventPublisher\EventStoreMiddlewares\AmqpMiddleware middlewares to store and send events.
Database middleware
This middleware just stores the events in a table in database. It can be useful if you want to expose the events as a REST endpoint or check your events history.
To configure this middleware you need to publish the migrations
php artisan vendor:publish --provider="Softonic\TransactionalEventPublisher\ServiceProvider" --tag=migrations
and execute them
php artisan migrate
Amqp middleware
This middleware publishes the events to an AMQP system. You just need to configure the AMQP connection using the configuration file or environmental variables. As you can see, in the configuration you won't be able to define a queue. This is because the library just publishes the message to an exchange and is the events collector responsibility to declare the needed queues with the needed bindings.
Publishing events in batches to improve performance
We provide a command to continuously publish events in batches.
You can find its signature in Softonic\TransactionalEventPublisher\Console\Commands\EmitEvents.
It will publish the events in batches of 100 by default, or you can change it with the option --batchSize.
You just need to create a job that will run indefinitely with the command php artisan event-sourcing:emit.
Sending all the events stored in database
By default, the command php artisan event-sourcing:emit will send all the events stored in database using a MySQL unbuffered connection.
Otherwise, a Mysql buffered Connection it is used to delete the events from database after they have been sent.
You can specify the connection to use with the option --dbConnection for the buffered connection and --dbConnectionUnbuffered for the unbuffered connection.
Unbuffered connection example from config/database.php
return [ 'connections' => [ 'mysql-unbuffered' => [ 'driver' => 'mysql', 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), 'database' => env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => '', 'strict' => true, 'engine' => null, 'options' => [ PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false, ], ], ] ];
Registering Models
To choose what models should send domain events, attach the \Softonic\TransactionalEventPublisher\Observers\ModelObserver
observer class and add the \Softonic\TransactionalEventPublisher\Concerns\PublishesDomainEvents trait to the models.
The observer stores the domain event of each insert, update and delete. The trait runs the write together with the storage of its event in the same transaction: if anything fails (the query, the event store or a listener halting the operation), only that transaction level is rolled back, so the caller's transaction is never left open. The trait alone does not publish anything: without the observer, the writes still run in their transaction but no event is stored.
Using the trait in a base model is enough to cover all the models extending it.
Example:
use Illuminate\Database\Eloquent\Model; use Softonic\TransactionalEventPublisher\Concerns\PublishesDomainEvents; abstract class BaseModel extends Model { use PublishesDomainEvents; }
use App\Models\Post as MyModel; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Softonic\TransactionalEventPublisher\Observers\ModelObserver; class EventServiceProvider extends ServiceProvider { public function boot() { parent::boot(); MyModel::observe(ModelObserver::class); } }
Upgrading to 11.1
Observing a model that does not use the trait is deprecated. It keeps working as before: the observer opens a
transaction level in the creating/updating/deleting events and closes it once the event is stored, so the level is
left open when the query fails. It also triggers an E_USER_DEPRECATED once per process. To migrate, add the
PublishesDomainEvents trait to the models (or to their base model) and keep the observe(ModelObserver::class) calls.
If the model overrides performInsert(), performUpdate() or delete(), the method of the model takes precedence over
the trait one. Import the trait method with an alias and call it from the override, so the write still runs in its
transaction:
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\UniqueConstraintViolationException; use Softonic\TransactionalEventPublisher\Concerns\PublishesDomainEvents; abstract class BaseModel extends Model { use PublishesDomainEvents { performInsert as transactionalPerformInsert; } protected function performInsert(Builder $query) { try { return $this->transactionalPerformInsert($query); } catch (UniqueConstraintViolationException $exception) { // The transaction level of the insert is already rolled back here. ... } } }
Disabling the publication of events
\Softonic\TransactionalEventPublisher\DomainEvents disables the storage of domain events for the whole process, for
example during a bulk synchronisation. It is checked each time an event is going to be stored, so it works regardless of
when the observers are registered. While it is disabled, the ModelObserver neither opens transaction levels nor stores
events, and the writes of models using the trait still run in their own transaction.
use Softonic\TransactionalEventPublisher\DomainEvents; DomainEvents::disable(); // ... DomainEvents::enable(); // Or only for a callback, restoring the previous state afterwards (even if it throws): DomainEvents::withoutPublishing(fn () => $this->synchronise());
The switch is static, so in long-running processes (queue workers, Octane) make sure it is enabled again when the work
that disabled it finishes. On models without the trait, do not change it in the middle of a write (between the
creating/updating/deleting and the created/updated/deleted events).
Custom middlewares
The middlewares should implement the Softonic\TransactionalEventPublisher\Interfaces\EventStoreMiddlewareInterface interface.
Its purpose is to store the domain event provided, so you can implement any storage for domain events.
Custom messages
The transactional-event.messageBuilder class must implement EventMessageBuilderInterface and transactional-event.middleware class must implement EventStoreMiddlewareInterface.
The builder should return a EventMessageInterface value object. It just needs to implement the toArray and jsonSerialize methods with all the attributes that you need.
Considerations
This package begins a database transaction in the following Eloquent Model events:
- creating
- updating
- deleting
And commit the database transaction when the event store middleware stores the event message successfully. On the other hand, if the event store couldn't store the event message would be a database rollback for the two operations (Eloquent model write + event message storing). Take into account if an error occurs between the event of creating/updating/deleting and created/updated/deleted the transaction would remain started until the connection had been closed.
Testing
softonic/laravel-transactional-event-publisher has a PHPUnit test suite and a coding style compliance test suite using PHP CS Fixer following PSR-12.
To run the tests, run the following command from the project folder:
$ docker compose run --rm tests
To run PHPUnit only:
$ docker compose run --rm phpunit
To run PHPStan static analysis:
$ docker compose run --rm phpstan
To check code style:
$ docker compose run --rm php composer run checkstyle
To fix code style issues:
$ docker compose run --rm fixcs
To open a terminal in the dev environment:
$ docker compose run --rm --entrypoint=bash php
For debugging with Xdebug:
$ docker compose run --rm debug
License
The Apache 2.0 license. Please see LICENSE for more information.