PHPackages                             aeatech/transaction-manager-core - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. aeatech/transaction-manager-core

ActiveLibrary[Database &amp; ORM](/categories/database)

aeatech/transaction-manager-core
================================

Transaction manager core

1.0.0(6mo ago)0375MITPHPPHP &gt;=8.2CI passing

Since Dec 24Pushed 5mo agoCompare

[ Source](https://github.com/AEATech/transaction-manager-core)[ Packagist](https://packagist.org/packages/aeatech/transaction-manager-core)[ RSS](/packages/aeatech-transaction-manager-core/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (5)Versions (2)Used By (5)

AEATech Transaction Manager (Core)
==================================

[](#aeatech-transaction-manager-core)

[![Code Coverage](.build/coverage_badge.svg)](.build/coverage_badge.svg)

A lightweight, DB-agnostic transaction manager designed for high-load environments, with first-class support for:

- retries
- exponential backoff
- transient/connection error classification
- commit-unknown detection
- strict idempotency contracts
- pure SQL transaction execution
- **deferred build** for dependent transactions

The core package contains **no DB-specific logic**.
MySQL and PostgreSQL integrations live in separate packages:

- `transaction-manager-doctrine-adapter` (bridge for Doctrine DBAL)
- `transaction-manager-mysql`
- `transaction-manager-postgresql`

---

Features
========

[](#features)

- Execute a sequence of SQL operations as a **single logical transaction**
- Automatically retry on transient errors (deadlocks, serialization failures)
- Automatically recovers from connection drops
- Handle `unknown commit outcome`
- IsolationLevel level control
- Pure, deterministic execution plan
- **Deferred Build**: support for dependent transactions (e.g., use generated IDs from previous steps)
- Backoff strategies (exponential with jitter)
- Pluggable error classifiers per-database
- Optional prepared statement reuse via `StatementReusePolicy` (disabled by default)
- No global state, no magic

---

If you need the full detailed contract describing all guarantees, responsibilities, idempotency rules, error semantics, and connection requirements, see:

👉 [CONTRACT.md](./CONTRACT.md)

Installation
============

[](#installation)

```
composer require aeatech/transaction-manager-core
```

Quick Example
=============

[](#quick-example)

```
use AEATech\TransactionManager\TransactionManager;
use AEATech\TransactionManager\TxOptions;
use AEATech\TransactionManager\RetryPolicy;
use AEATech\TransactionManager\ExponentialBackoff;
use AEATech\TransactionManager\SystemSleeper;
use AEATech\TransactionManager\ExecutionPlanBuilder;
use AEATech\TransactionManager\TransactionInterface;
use AEATech\TransactionManager\Query;
use AEATech\TransactionManager\GenericErrorClassifier;

// Assume $connectionAdapter implements AEATech\TransactionManager\ConnectionInterface
$connectionAdapter = '...';

// Assume $errorClassifier implements AEATech\TransactionManager\ErrorClassifierInterface
$errorClassifier = new GenericErrorClassifier($errorHeuristics);

$tm = new TransactionManager(
    new ExecutionPlanBuilder(),
    $connectionAdapter,
    $errorClassifier,
    RetryPolicy::noRetry(), // Default retry policy
    new SystemSleeper()
);

class InsertUser implements TransactionInterface
{
    public function __construct(
        private string $email,
        private string $name
    ) {}

    public function build(): Query
    {
        return new Query(
            sql: "INSERT INTO users (email, name) VALUES (?, ?)",
            params: [$this->email, $this->name],
            types: [\PDO::PARAM_STR, \PDO::PARAM_STR]
        );
    }

    public function isIdempotent(): bool
    {
        return false; // inserting the same user twice is not safe
    }
}

$result = $tm->run(new InsertUser('a@example.com', 'Alice'));

echo $result->affectedRows; // → 1
```

Idempotent Transaction Example
==============================

[](#idempotent-transaction-example)

```
class SetStock implements TransactionInterface
{
    public function __construct(
        private int $productId,
        private int $qty
    ) {}

    public function build(): Query
    {
        return new Query(
            sql: "UPDATE products SET stock = ? WHERE id = ?",
            params: [$this->qty, $this->productId],
            types: [\PDO::PARAM_INT, \PDO::PARAM_INT]
        );
    }

    public function isIdempotent(): bool
    {
        return true; // Setting a value twice results in the same state
    }
}
```

Executing Multiple Transactions as One
======================================

[](#executing-multiple-transactions-as-one)

```
$tm->run([
    new SetStock(10, 5),
    new SetStock(11, 0),
    new InsertUser("a@example.com", "Alice"),
]);
```

Deferred Build (Dependent Transactions)
=======================================

[](#deferred-build-dependent-transactions)

By default, `TransactionInterface::build()` is called **before** the database transaction starts. This ensures that the execution plan is deterministic and minimizes the database transaction's duration.

However, sometimes a transaction depends on data generated by a previous step in the same batch (e.g., using an `AUTO_INCREMENT` ID from a previous `INSERT`). In such cases, you can use the `#[DeferredBuild]` attribute.

### How it works:

[](#how-it-works)

1. When a transaction class is marked with `#[DeferredBuild]`, its `build()` method is **not** called during the initial plan construction.
2. Instead, `TransactionManager` calls `build()` **inside** the active database transaction, right before executing the query.
3. This allows you to perform `SELECT` queries (e.g., via a repository) to fetch IDs generated by previous steps and use them to build the current query.

### Example:

[](#example)

```
use AEATech\TransactionManager\Attribute\DeferredBuild;
use AEATech\TransactionManager\TransactionInterface;
use AEATech\TransactionManager\Query;

#[DeferredBuild]
class CreateProfile implements TransactionInterface
{
    public function __construct(
        private UserRepository $repository,
        private string $email,
        private array $profileData
    ) {}

    public function build(): Query
    {
        // This code runs INSIDE the DB transaction.
        // We can fetch the user ID that was inserted in the previous step.
        $user = $this->repository->findByEmail($this->email);

        return new Query(
            sql: "INSERT INTO profiles (user_id, bio) VALUES (?, ?)",
            params: [$user->id, $this->profileData['bio']]
        );
    }

    public function isIdempotent(): bool { return false; }
}

// Usage in a batch:
$tm->run([
    new InsertUser("a@example.com", "Alice"),
    new CreateProfile($userRepository, "a@example.com", ['bio' => 'Software Engineer'])
]);
```

Retry Options
=============

[](#retry-options)

Transaction Manager uses a **default retry policy** configured during instantiation. You can override this policy for specific transactions using `TxOptions`.

### 1. Default Retry Policy

[](#1-default-retry-policy)

When you create the `TransactionManager`, you must provide a default policy.

**Note on `maxRetries`**: The value specifies the number of *additional* attempts after the initial failure. For example, `maxRetries: 3` means the transaction can be executed up to **4 times** in total (1 initial attempt + 3 retries).

```
$defaultPolicy = new RetryPolicy(
    maxRetries: 3, // Total 4 attempts
    backoffStrategy: new ExponentialBackoff(baseDelayMs: 100)
);

$tm = new TransactionManager(
    $builder,
    $connection,
    $classifier,
    $defaultPolicy,
    $sleeper
);
```

### 2. Disabling Retries

[](#2-disabling-retries)

If you want to ensure a transaction is executed exactly once without any retries, use `RetryPolicy::noRetry()`:

```
// Globally (in constructor)
$tm = new TransactionManager(..., RetryPolicy::noRetry(), ...);

// Or for a specific call
$tm->run($tx, new TxOptions(retryPolicy: RetryPolicy::noRetry()));
```

### 3. Overriding Policy per Call

[](#3-overriding-policy-per-call)

```
use AEATech\TransactionManager\RetryPolicy;
use AEATech\TransactionManager\TxOptions;
use AEATech\TransactionManager\IsolationLevel;
use AEATech\TransactionManager\ExponentialBackoff;

$customPolicy = new RetryPolicy(
    maxRetries: 5, // Total 6 attempts
    backoffStrategy: new ExponentialBackoff(baseDelayMs: 50)
);

$options = new TxOptions(
    isolationLevel: IsolationLevel::RepeatableRead,
    retryPolicy: $customPolicy // Overrides the default policy
);

$tm->run($transaction, $options);
```

Isolation level (optional)
==========================

[](#isolation-level-optional)

`TxOptions::$isolationLevel` is optional.

- If `isolationLevel` is `null`, Transaction Manager does **not** issue `SET TRANSACTION ISOLATION LEVEL ...` and the effective isolation level is whatever is currently configured for the connection/database (server default, pool/session settings, etc.).
- If `isolationLevel` is set (e.g. `ReadCommitted`, `RepeatableRead`, `Serializable`), Transaction Manager will explicitly set it for the current transaction.

### Retry with explicit isolation level

[](#retry-with-explicit-isolation-level)

```
$options = new TxOptions(
    isolationLevel: IsolationLevel::RepeatableRead,
    retryPolicy: $customPolicy
);

$tm->run($transaction, $options);
```

### Retry without changing isolation level (use DB/connection default)

[](#retry-without-changing-isolation-level-use-dbconnection-default)

```
$options = new TxOptions(
    isolationLevel: null,
    retryPolicy: $customPolicy
);

$tm->run($transaction, $options);
```

Prepared Statement Reuse Hint
=============================

[](#prepared-statement-reuse-hint)

`StatementReusePolicy` is an optional, best‑effort hint that may help a connection adapter optimize execution of similar queries. It does not affect correctness and may be ignored by an implementation.

Options:

- `StatementReusePolicy::None` — no reuse (default).
- `StatementReusePolicy::PerTransaction` — attempt to reuse a prepared statement within a single transaction.
- `StatementReusePolicy::PerConnection` — attempt to reuse a prepared statement across transactions while the physical connection remains open.

Example usage in `TransactionInterface::build()`:

```
use AEATech\TransactionManager\Query;
use AEATech\TransactionManager\StatementReusePolicy;

class InsertUser implements TransactionInterface
{
    public function __construct(
        private string $email,
        private string $name
    ) {}

    public function build(): Query
    {
        return new Query(
            sql: "INSERT INTO users (email, name) VALUES (?, ?)",
            params: [$this->email, $this->name],
            types: [\PDO::PARAM_STR, \PDO::PARAM_STR],
            statementReusePolicy: StatementReusePolicy::PerTransaction
        );
    }

    public function isIdempotent(): bool { return false; }
}
```

Testing
=======

[](#testing)

The project is configured to run tests in Isolated Docker containers for different PHP versions (8.2, 8.3, 8.4).

1. Start the Environment
------------------------

[](#1-start-the-environment)

Make sure the Docker containers are up and running. From the project root:

```
docker-compose -p aeatech-transaction-manager-core -f docker/docker-compose.yml up -d --build
```

2. Install Dependencies
-----------------------

[](#2-install-dependencies)

Install composer dependencies inside the container (using PHP 8.2 as a base):

```
docker-compose -p aeatech-transaction-manager-core -f docker/docker-compose.yml exec php-cli-8.2 composer install
```

3. Run Tests
============

[](#3-run-tests)

PHP 8.2

```
docker-compose -p aeatech-transaction-manager-core -f docker/docker-compose.yml exec php-cli-8.2 vendor/bin/phpunit
```

PHP 8.3

```
docker-compose -p aeatech-transaction-manager-core -f docker/docker-compose.yml exec php-cli-8.3 vendor/bin/phpunit
```

PHP 8.4

```
docker-compose -p aeatech-transaction-manager-core -f docker/docker-compose.yml exec php-cli-8.4 vendor/bin/phpunit
```

Run All Tests (Bash Script)

```
for v in 8.2 8.3 8.4; do \
    echo "Testing PHP $v..."; \
    docker-compose -p aeatech-transaction-manager-core -f docker/docker-compose.yml exec php-cli-$v vendor/bin/phpunit || break; \
done
```

4. Run phpstan
--------------

[](#4-run-phpstan)

```
docker-compose -p aeatech-transaction-manager-core -f docker/docker-compose.yml exec php-cli-8.4 vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G
```

Stopping the Environment
------------------------

[](#stopping-the-environment)

```
docker-compose -p aeatech-transaction-manager-core -f docker/docker-compose.yml down -v
```

License
-------

[](#license)

This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance69

Regular maintenance activity

Popularity7

Limited adoption so far

Community14

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 50% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

190d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/249475575?v=4)[aeatech2025](/maintainers/aeatech2025)[@aeatech2025](https://github.com/aeatech2025)

---

Top Contributors

[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")[![Shahelm](https://avatars.githubusercontent.com/u/5507279?v=4)](https://github.com/Shahelm "Shahelm (1 commits)")

---

Tags

databasemysqlpostgresqltransactionaeatechtransaction-manager

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/aeatech-transaction-manager-core/health.svg)

```
[![Health](https://phpackages.com/badges/aeatech-transaction-manager-core/health.svg)](https://phpackages.com/packages/aeatech-transaction-manager-core)
```

###  Alternatives

[davmixcool/php-dbcloud

Easily backup PostgreSql or MySql database to the cloud

111.5k](/packages/davmixcool-php-dbcloud)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
