Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
Transaction
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
2 / 2
7
100.00% covered (success)
100.00%
1 / 1
 run
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 undo
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Util;
6
7use Celema\Quma\Database;
8use Throwable;
9
10/**
11 * Runs work atomically: in its own transaction, or in a savepoint when a
12 * transaction is already open, so that a failure undoes only this work
13 * and leaves the outer transaction usable.
14 *
15 * @internal
16 */
17final class Transaction
18{
19    /**
20     * Undoes the work and rethrows its exception if it throws.
21     *
22     * @template T
23     *
24     * @param callable(): T $work
25     * @return T
26     */
27    public static function run(Database $db, callable $work): mixed
28    {
29        $nested = $db->getConn()->inTransaction();
30
31        if ($nested) {
32            $db->transaction->savepoint()->run();
33        } else {
34            $db->begin();
35        }
36
37        try {
38            $result = $work();
39
40            if ($nested) {
41                $db->transaction->releaseSavepoint()->run();
42            } else {
43                $db->commit();
44            }
45        } catch (Throwable $e) {
46            self::undo($db, $nested);
47
48            throw $e;
49        }
50
51        return $result;
52    }
53
54    private static function undo(Database $db, bool $nested): void
55    {
56        try {
57            if ($nested) {
58                $db->transaction->rollbackSavepoint()->run();
59                $db->transaction->releaseSavepoint()->run();
60            } else {
61                $db->rollback();
62            }
63        } catch (Throwable) {
64            // @mago-expect lint:no-empty-catch-clause The work's exception is rethrown instead.
65            // Undoing fails when the connection was lost or the commit already
66            // ended the transaction. Either way the work's exception explains
67            // the failure, and Quma checks the connection before reusing it.
68        }
69    }
70}