cycle / transaction
Transaction abstraction for Cycle ORM: run DBAL operations and a scoped Entity Manager within a single database transaction.
Requires
- php: >=8.2
- cycle/database: ^2.20
- cycle/orm: ^2.18
Requires (Dev)
- cycle/skills: 1.x-dev
- infection/infection: >=0.32.6
- llm/skills: ^1.7
- testo/bridge-infection: ^0.1.6
- testo/testo: ^0.10.26
- vimeo/psalm: ^7
README
Transaction Abstraction for Cycle ORM
This library provides a small transaction abstraction for Cycle ORM. It opens a single database transaction and lets you run raw DBAL operations together with a scoped Entity Manager inside it.
Everything executed within the callback shares one transaction: if the callback throws, the transaction is rolled back; otherwise it is committed. The Entity Manager is scoped to a single database connection for the duration of the callback, so accidental cross-connection writes are rejected instead of silently splitting your work across transactions.
Installation
The preferred way to install this package is through Composer.
composer require cycle/transaction
Usage
Obtaining a Transaction
The package ships a single implementation of the Cycle\Transaction\Transaction interface. Build it
from your configured ORM and DBAL, or resolve Cycle\Transaction\Transaction from your framework's
container if it is registered there.
use Cycle\Transaction\Internal\TransactionImpl; use Cycle\Transaction\Transaction; /** * @var \Cycle\ORM\ORMInterface $orm * @var \Cycle\Database\DatabaseProviderInterface $dbal */ $transaction = new TransactionImpl($orm, $dbal);
Basic Example
The callback receives the scoped EntityManagerInterface and the DatabaseInterface of the resolved
connection. Persisted entities are flushed and committed automatically when the callback returns.
use Cycle\Database\DatabaseInterface; use Cycle\ORM\EntityManagerInterface; $transaction->transact(function (EntityManagerInterface $em, DatabaseInterface $db): void { $em->persist(new User('john@example.com')); // The same transaction is visible to raw DBAL operations. $db->table('audit')->insertOne(['event' => 'user.created']); });
If the callback throws, the whole transaction — both the Entity Manager changes and the raw DBAL operations — is rolled back.
$transaction->transact(function (EntityManagerInterface $em): void { $em->persist(new User('john@example.com')); throw new \DomainException('Something went wrong'); // Nothing is committed: the new user is rolled back. });
Returning a Value
transact() returns whatever the callback returns.
$user = $transaction->transact(function (EntityManagerInterface $em): User { $user = new User('john@example.com'); $em->persist($user); return $user; });
Flush Modes
The FlushMode enum controls when and how the scoped Entity Manager flushes its pending changes.
Pass it via the $flush argument (defaults to FlushMode::BeforeCommit).
| Mode | Behaviour |
|---|---|
FlushMode::OnWrite |
Flush every persist/persistState/delete to the database immediately. |
FlushMode::BeforeCommit |
Collect all changes and flush them once, right before the transaction is committed. (default) |
FlushMode::FailOnPending |
Do not flush automatically; throw a TransactionException if any changes are still pending. |
FlushMode::SkipPending |
Do not flush automatically; silently discard any pending changes (only DBAL operations are committed). |
use Cycle\Transaction\FlushMode; // Require the callback to flush explicitly; otherwise the transaction fails. $transaction->transact( callback: function (EntityManagerInterface $em): void { $em->persist(new User('john@example.com')); $em->run(); // explicit flush — without it a TransactionException is thrown }, flush: FlushMode::FailOnPending, );
Transaction Modes
The TransactionMode enum controls how the Entity Manager's Unit of Work interacts with the open
transaction. Pass it via the $emMode argument (defaults to TransactionMode::Current).
| Mode | Behaviour |
|---|---|
TransactionMode::Current |
Reuse the currently opened transaction. Throws if none is open. (default) |
TransactionMode::OpenNew |
Open a new inner transaction per driver connection and close it on finish. |
TransactionMode::Ignore |
Do not manage transactions for the Unit of Work. |
use Cycle\Transaction\TransactionMode; $transaction->transact( callback: function (EntityManagerInterface $em): void { $em->persist(new User('john@example.com')); }, emMode: TransactionMode::OpenNew, );
Selecting the Database
By default the transaction runs against the default database connection. Use the $source argument
to pick another connection — either by its name or by an entity class mapped to it. The scoped Entity
Manager then rejects entities that belong to a different connection.
// By connection name. $transaction->transact( callback: fn(EntityManagerInterface $em, DatabaseInterface $db) => $db->getName(), source: 'reporting', ); // By entity class — resolves the connection the entity is mapped to. $transaction->transact( callback: function (EntityManagerInterface $em): void { $em->persist(new User('john@example.com')); }, source: User::class, );