Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.89% covered (success)
88.89%
32 / 36
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Manager
88.89% covered (success)
88.89%
32 / 36
60.00% covered (warning)
60.00%
3 / 5
24.79
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isTransaction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTransactionDepth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 enter
78.57% covered (success)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
7.48
 leave
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
14.03
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15
16namespace Pop\Db\Adapter\Transaction;
17
18/**
19 * Nested transaction manager for use in AdapaterInterface implementations
20 *
21 * @category   Pop
22 * @package    Pop\Db
23 * @author     Nick Sagona, III <nick@popphp.org>
24 * @author     Martok <martok@martoks-place.de>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    7.0.0
28 */
29class Manager extends AbstractManager
30{
31
32    /**
33     * Transaction state flag
34     * @var int
35     */
36    private int $transactionState = 0;
37    private const TS_NONE = 0;
38    private const TS_OPEN = 1;
39    private const TS_ROLLED_BACK = -1;
40
41    /**
42     * Transaction depth
43     * @var int
44     */
45    private int $transactionDepth = 0;
46
47    /**
48     * Use savepoints or simulated nested transactions (SNTs)
49     * @var bool
50     */
51    private bool $useSavepoints;
52
53    /**
54     * Names of active savepoints. Count is always one less than $transactionDepth.
55     * @var string[]
56     */
57    private array $savepoints = [];
58    private int $savepointName = 0;
59
60    /**
61     * Constructor
62     *
63     * Instantiate the transaction manager object
64     *
65     * @param bool $useSavepoints Enable the use of savepoints by default
66     */
67    public function __construct(bool $useSavepoints = true)
68    {
69        $this->useSavepoints = $useSavepoints;
70    }
71
72    /**
73     * Check if adapter is in the middle of an open transaction
74     *
75     * @return bool
76     */
77    public function isTransaction(): bool
78    {
79        return $this->transactionState !== self::TS_NONE;
80    }
81
82    /**
83     * Get transaction depth
84     *
85     * @return int
86     */
87    public function getTransactionDepth(): int
88    {
89        return $this->transactionDepth;
90    }
91
92    /**
93     * Enter a new transaction or increase nesting level
94     *
95     * @param ?callable $beginFunc Called when a new top-level transaction must be started
96     * @param ?callable $savepointFunc Called when a named savepoint is created
97     * @return bool
98     */
99    public function enter(?callable $beginFunc = null, ?callable $savepointFunc = null): bool
100    {
101        $this->transactionDepth++;
102
103        // an already rolled back SNT can never turn back into a normal one
104        if ($this->transactionState == self::TS_ROLLED_BACK)
105            return false;
106
107        if ($this->transactionDepth == 1) {
108            // BEGIN a new transaction
109            if (is_callable($beginFunc)) {
110                $beginFunc();
111            }
112            $this->transactionState = self::TS_OPEN;
113        } else {
114            // increase nesting level
115            if ($this->useSavepoints && is_callable($savepointFunc)) {
116                try {
117                    $sp = 'PopDbTxn_' . $this->savepointName++;
118                    $savepointFunc($sp);
119                    $this->savepoints[] = $sp;
120                } catch (\Exception $e) {
121                    // if this failed, assume this Adapter doesn't actually support savepoints
122                    $this->useSavepoints = false;
123                }
124            }
125        }
126        return true;
127    }
128
129    /**
130     * Leave a transaction or reduce nesting level
131     *
132     * @param bool $doCommit If true, perform a commit. Rollback otherwise.
133     * @param ?callable $commitFunc Called when a top-level commit must be performed
134     * @param ?callable $rollbackFunc Called when a top-level rollback must be performed
135     * @param ?callable $savepointReleaseFunc Called when a savepoint is released (like commit)
136     * @param ?callable $savepointRollbackFunc Called when the transaction is rolled back to a savepoint
137     * @return bool
138     */
139    public function leave(bool      $doCommit,
140                          ?callable $commitFunc = null, ?callable $rollbackFunc = null,
141                          ?callable $savepointReleaseFunc = null, ?callable $savepointRollbackFunc = null): bool
142    {
143        if ($this->transactionDepth <= 0 || $this->transactionState == self::TS_NONE)
144            return false;
145
146        $this->transactionDepth--;
147
148        // Leaving the outermost transaction always commits/rolls back the transaction.
149        // If savepoints are enabled, leaving a nested transaction requires the rollback/release of the savepoint.
150        // Without savepoints, only the outermost transaction is real, becoming an automatic rollback if
151        // any nested transaction was a rollback.
152        if ($this->useSavepoints && $this->transactionDepth > 0) {
153            $sp = array_pop($this->savepoints);
154            if ($doCommit) {
155                if (is_callable($savepointReleaseFunc)) {
156                    $savepointReleaseFunc($sp);
157                }
158            } else {
159                if (is_callable($savepointRollbackFunc)) {
160                    $savepointRollbackFunc($sp);
161                }
162            }
163        } else {
164            if (!$doCommit)
165                $this->transactionState = self::TS_ROLLED_BACK;
166        }
167
168        if ($this->transactionDepth == 0) {
169            if ($this->transactionState == self::TS_OPEN && is_callable($commitFunc))
170                $commitFunc();
171            elseif ($this->transactionState == self::TS_ROLLED_BACK && is_callable($rollbackFunc))
172                $rollbackFunc();
173            $this->transactionState = self::TS_NONE;
174        }
175
176        return true;
177    }
178
179}