cycle/transaction

Transaction abstraction for Cycle ORM: run DBAL operations and a scoped Entity Manager within a single database transaction.

Maintainers

Package info

github.com/cycle/transaction

pkg:composer/cycle/transaction

Statistics

Installs: 231

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-06-24 11:21 UTC

This package is auto-updated.

Last update: 2026-06-24 12:41:51 UTC


README


CycleORM Logo

Build Status Codecov Coverage Mutation testing badge

Discord Follow on Twitter (X)

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

PHP Version Require Latest Stable Version License Total Downloads


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,
);