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