inneair/transaction-bundle
inneair/transaction-bundle is a Symfony bundle for managing database transactions. It provides reusable services and integration points to wrap application operations in transactional boundaries, helping keep persistence logic consistent and easier to maintain in Symfony projects.
The first thing to establish: inneair/transaction-bundle is a Symfony bundle, not a Laravel package. There is no TransactionBundleServiceProvider, no facade, no config stub published by php artisan vendor:publish. It targets Symfony 2.x/3.x-era applications on top of Doctrine, its last tagged release is from July 2017, and the repository is archived. So the realistic "getting started" is one of two paths:
composer show inneair/transaction-bundle to confirm which version is locked, then look at the bundle's Resources/config/ directory first. That is where the service definitions live, and it is the fastest way to discover the actual service id and constructor signature of the transaction manager (naming has drifted between versions, so trust the YAML over the README). Register the bundle in app/AppKernel.php (or config/bundles.php on Symfony 4+) and check DependencyInjection/Configuration.php for any tunable options such as retry counts or connection names.composer.json will constrain PHP and symfony/* to versions that conflict with any current Laravel skeleton. Instead read the source as a design reference: the interesting part is a thin service that wraps a unit of work in beginTransaction / commit / rollback and lets you nest. You can reproduce that in ~40 lines on top of Illuminate\Database\Connection.First use case either way: take one multi-write application service (create order + decrement stock + write audit row) and put the whole call behind the transaction manager, rather than scattering beginTransaction calls through repositories. That is the pattern the bundle exists to encourage, and it is the pattern worth copying.
Wrap the unit of work, not the query. The bundle's model is that an application/domain service is the transaction boundary. In Laravel terms this is:
DB::transaction(function () use ($dto) {
$order = $this->orders->create($dto);
$this->stock->decrement($dto->sku, $dto->qty);
$this->audit->record($order);
}, attempts: 3);
The attempts argument is the Laravel equivalent of the retry wrapper a transaction bundle provides — it re-runs the closure on deadlock/serialization failure. If you port the bundle's idea, make retry a first-class option rather than something each caller remembers.
Nesting via savepoints. Doctrine emulates nested transactions with savepoints; Laravel does the same through nested DB::transaction() calls. Use nesting for composability — a PlaceOrder action calling a ReserveStock action should not need to know whether it is already inside a transaction. Keep nesting shallow (two levels is plenty); deep nesting makes rollback semantics hard to reason about.
Keep the boundary above the repository layer. Repositories that open their own transactions defeat the purpose. Inject the transaction manager (or just use the DB facade) in actions/handlers/jobs, and let repositories be dumb persistence.
Do not cross I/O boundaries inside the transaction. No HTTP calls, no queue pushes, no mail. If you need side effects, use Laravel's DB::afterCommit() (or the bundle's post-commit hook if it exposes one) so the effect only fires on successful commit. This is the single most valuable habit the pattern enforces.
Integration tip for jobs and console commands: a queued job that fails mid-transaction should be allowed to release back to the queue rather than swallow the exception. Let the transaction helper rethrow after rollback; catch at the job level, not inside the closure.
--ignore-platform-reqs, and ignoring those flags on a transaction package is a good way to ship a runtime fatal in production.commit() throws — even if your code caught the inner exception and continued. Laravel's savepoint handling has a similar trap: catching an exception inside the closure and returning normally will still commit whatever partial state exists at that savepoint. Never swallow exceptions inside the transaction body.CREATE TABLE/ALTER TABLE; migrations are not transactional on that engine. Do not assume the wrapper protects schema work.wait_timeout/innodb_lock_wait_timeout deliberately, and log transaction duration if you own the wrapper — it is the cheapest early warning you will get.START TRANSACTION / SAVEPOINT / ROLLBACK around the failing request. On Doctrine, getTransactionNestingLevel() is the quickest way to spot a leaked beginTransaction that never committed — the classic cause of "connection pool exhausted" after a deploy.TransactionManager) in the container rather than referencing a concrete class, so you can later swap in a decorator that adds metrics, retry, or after-commit callbacks without touching call sites. In Laravel, that is a one-line binding in a service provider and is worth doing on day one.How can I help you explore Laravel packages today?