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.
Winter Boot offers two ways to manage transactions. Declarative transactions use the #[Transactional] attribute: the framework wraps the method in a transaction and commits or rolls back for you. Programmatic transactions use PlatformTransactionManager directly, which is useful when a transaction does not map to a single method call, or when the datasource is chosen at runtime (multi-tenant apps).
To use #[Transactional], add #[EnableTransactionManagement] to your application entry-point class. (Programmatic transactions work without it — a transaction manager bean is registered for every datasource either way.)
use dev\winterframework\stereotype\WinterBootApplication;use dev\winterframework\core\app\WinterWebSwooleApplication;use dev\winterframework\stereotype\txn\EnableTransactionManagement;
#[EnableTransactionManagement]#[WinterBootApplication( configDirectory: [__DIR__ . '/config'], scanNamespaces: [ ['com\\example\\myapp', __DIR__ . '/src'] ])]class MyApplication { public static function main(): void { (new WinterWebSwooleApplication())->run(MyApplication::class); }}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.
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. }}use dev\winterframework\stereotype\Service;use dev\winterframework\txn\stereotype\Transactional;
#[Service]class AdminService {
// Targets the "admindb" datasource's transaction manager #[Transactional("admindb-txn")] public function auditAction(string $action): void { // Runs inside a transaction on admindb }}use dev\winterframework\stereotype\Service;use dev\winterframework\txn\stereotype\Transactional;
#[Service]class WalletService {
#[Transactional(transactionManager: "myTxnMgr")] public function withdrawMoney(float $amount): void { // Runs under the custom transaction manager bean "myTxnMgr" }}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 behavior. 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. It is rolled back at the end instead of committed.
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. When one of them is thrown, the transaction is committed and the exception is re-thrown.
label array (default: [])
Arbitrary string labels to associate with the transaction for informational purposes.
| Constant | Value | Behavior |
|---|---|---|
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. Runs on a separate connection, so it commits or rolls back independently of the outer transaction |
PROPAGATION_NOT_SUPPORTED |
4 |
Always run non-transactionally, suspending any existing transaction. Runs on a separate auto-commit connection |
PROPAGATION_NEVER |
5 |
Run non-transactionally; throw an exception if a transaction exists |
PROPAGATION_NESTED |
6 |
Run within a savepoint of the current transaction — not supported by the PDO/OCI managers (throws) |
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:
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; } }}REQUIRES_NEW and NOT_SUPPORTED borrow a second connection from the pool for the duration of the call. The data source must implement IsolatedConnectionProvider to get that second connection (the built-in PDO and OCI data sources do). With a custom data source that does not implement it, the inner work runs on the outer connection (so it is not isolated) and a warning is logged once. The second connection counts against the pool’s maxConnections, but it never waits for a free slot: the request already holds one, and waiting could deadlock concurrent requests on each other. Under full load the pool can therefore briefly hold up to maxConnections extra connections, one per nested REQUIRES_NEW/NOT_SUPPORTED in flight; new requests wait until it is back under the cap.CannotGetConnectionException is thrown; the request never falls back to a shared connection.PDO does not natively support nested (savepoint-based) transactions. PROPAGATION_NESTED is defined in the Transaction interface, but the PDO/OCI transaction managers reject it (NestedTransactionNotSupportedException / SQLFeatureNotSupportedException).
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.