Skip to content

Managing Database Transactions in Winter Boot Services

Winter Boot supports two complementary approaches to transaction management. Declarative transactions use the #[Transactional] method annotation — the framework wraps the method in a transaction automatically, handling commit and rollback with no extra code in your business logic. Programmatic transactions give you fine-grained control via PlatformTransactionManager, which is useful when transaction boundaries cannot be expressed as a single method call or when you need to interact with dynamically-resolved data sources such as in multi-tenant scenarios.

Before using either approach, add #[EnableTransactionManagement] to your application entry-point class. Without this annotation, the transaction infrastructure is not bootstrapped.

MyApplication.php
use dev\winterframework\stereotype\WinterBootApplication;
use dev\winterframework\core\app\WinterWebSwooleApplication;
use dev\winterframework\txn\stereotype\EnableTransactionManagement;
#[EnableTransactionManagement]
#[WinterBootApplication(
configDirectory: [__DIR__ . '/config'],
scanNamespaces: [
['com\\example\\myapp', __DIR__ . '/src']
]
)]
class MyApplication {
public static function main(): void {
(new WinterWebSwooleApplication())->run(MyApplication::class);
}
}

Declarative Transactions with #[Transactional]

Section titled “Declarative Transactions with #[Transactional]”

Annotate any service or component method with #[Transactional] to have the framework begin a transaction before the method runs, commit it on success, or roll it back on any uncaught exception.

PaymentService.php
use dev\winterframework\stereotype\Service;
use dev\winterframework\txn\stereotype\Transactional;
#[Service]
class PaymentService {
#[Transactional]
public function processPayment(int $orderId, float $amount): void {
// All database operations here run inside a single transaction.
// Any uncaught exception triggers an automatic rollback.
}
}
  • transactionManager string (default: default) Bean name of the PlatformTransactionManager to use. Defaults to the primary datasource’s transaction manager.

  • propagation int (default: PROPAGATION_REQUIRED) Transaction propagation behaviour. See the propagation constants table below.

  • isolation int (default: ISOLATION_DEFAULT) Database isolation level to request from the driver.

  • timeout int (default: TIMEOUT_DEFAULT) Transaction timeout in seconds. -1 means use the driver default.

  • readOnly bool (default: false) Mark the transaction as read-only. A rollback is performed at the end instead of a commit.

  • rollbackFor array (default: all exceptions) Array of exception class names that must trigger a rollback.

  • noRollbackFor array (default: none) Array of exception class names that must not trigger a rollback.

  • label array (default: []) Arbitrary string labels to associate with the transaction for informational purposes.

Constant Value Behaviour
PROPAGATION_REQUIRED 0 Use the current transaction, or create a new one if none exists (default)
PROPAGATION_SUPPORTS 1 Use the current transaction if one exists; otherwise run non-transactionally
PROPAGATION_MANDATORY 2 Use the current transaction; throw an exception if none exists
PROPAGATION_REQUIRES_NEW 3 Always create a new transaction, suspending any existing one
PROPAGATION_NOT_SUPPORTED 4 Always run non-transactionally, suspending any existing transaction
PROPAGATION_NEVER 5 Run non-transactionally; throw an exception if a transaction exists
PROPAGATION_NESTED 6 Run within a nested transaction if one exists; falls back to PROPAGATION_REQUIREDnot supported by PDO drivers

PlatformTransactionManager gives you direct control over the transaction lifecycle. Inject it with #[Autowired] for the primary datasource, or by bean name for a named datasource, then call getTransaction(), commit(), and rollback() yourself.

  • getTransaction(TransactionDefinition $definition) TransactionStatus Begin or join a transaction according to the given definition.

  • commit(TransactionStatus $status) void Commit the transaction represented by $status.

  • rollback(TransactionStatus $status) void Roll back the transaction represented by $status.

DefaultTransactionDefinition is the standard implementation of TransactionDefinition. Configure it before calling getTransaction():

use dev\winterframework\txn\support\DefaultTransactionDefinition;
use dev\winterframework\txn\Transaction;
$def = new DefaultTransactionDefinition();
$def->setPropagationBehavior(Transaction::PROPAGATION_REQUIRES_NEW);
$def->setIsolationLevel(Transaction::ISOLATION_READ_COMMITTED);
$def->setReadOnly(false);
$def->setTimeout(30);

The following service shows the full try/commit/catch/rollback pattern using an injected PlatformTransactionManager:

UserService.php
use dev\winterframework\pdbc\ex\EmptyResultDataAccessException;
use dev\winterframework\pdbc\PdbcTemplate;
use dev\winterframework\stereotype\Autowired;
use dev\winterframework\stereotype\Service;
use dev\winterframework\txn\PlatformTransactionManager;
use dev\winterframework\txn\support\DefaultTransactionDefinition;
use dev\winterframework\util\log\Wlf4p;
#[Service]
class UserService {
use Wlf4p;
#[Autowired]
private PdbcTemplate $pdbc;
#[Autowired]
private PlatformTransactionManager $txnMgr;
public function createUser(User $user): User {
$status = $this->txnMgr->getTransaction(new DefaultTransactionDefinition());
try {
$sql = "INSERT INTO users (name, email, age) VALUES (:name, :email, :age) RETURNING id";
$ret = [];
$result = $this->pdbc->update($sql, [
'name' => $user->getName(),
'email' => $user->getEmail(),
'age' => $user->getAge(),
], [], $ret);
if ($result) {
$user->setId(intval($ret['id']));
}
$this->txnMgr->commit($status);
return $user;
} catch (\Throwable $e) {
$this->txnMgr->rollback($status);
throw $e;
}
}
public function findById(int $id): ?User {
try {
return $this->pdbc->queryForObject(
"SELECT * FROM users WHERE id = :id",
['id' => $id],
User::class
);
} catch (EmptyResultDataAccessException) {
return null;
}
}
}

PDO does not natively support nested (savepoint-based) transactions. PROPAGATION_NESTED is defined in the Transaction interface but falls back to PROPAGATION_REQUIRED behaviour when used with PDO-backed data sources.


Declarative (#[Transactional])

Best for service-layer methods with straightforward, single-method transaction boundaries. Zero boilerplate — commit and rollback happen automatically based on whether the method throws.

Programmatic (PlatformTransactionManager)

Best when transaction scope spans multiple methods, when you need conditional commits, or when working with multi-tenant dynamic data sources where the transaction manager is resolved at runtime.