PdoDatabase   A
last analyzed

Complexity

Total Complexity 13

Size/Duplication

Total Lines 120
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 13
eloc 45
dl 0
loc 120
ccs 0
cts 34
cp 0
rs 10
c 1
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A commit() 0 5 2
A getDatabaseConnection() 0 38 3
A rollBack() 0 5 2
A beginTransaction() 0 32 5
A hasActiveTransaction() 0 3 1
1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 * ACC Development Team. Please see team.json for a list of contributors.     *
5
 *                                                                            *
6
 * This is free and unencumbered software released into the public domain.    *
7
 * Please see LICENSE.md for the full licencing statement.                    *
8
 ******************************************************************************/
9
10
namespace Waca;
11
12
use Exception;
13
use PDO;
14
use PDOException;
15
use Waca\Exceptions\EnvironmentException;
16
17
class PdoDatabase extends PDO
18
{
19
    public const ISOLATION_SERIALIZABLE = 'SERIALIZABLE';
20
    public const ISOLATION_READ_COMMITTED = 'READ COMMITTED';
21
    public const ISOLATION_READ_ONLY = 'READ ONLY';
22
23
    private static PdoDatabase $connection;
24
    /**
25
     * @var bool True if a transaction is active
26
     */
27
    protected bool $hasActiveTransaction = false;
28
29
    /**
30
     * Unless you're doing low-level work, this is not the function you want.
31
     *
32
     * @throws Exception
33
     */
34
    public static function getDatabaseConnection(SiteConfiguration $configuration): PdoDatabase
35
    {
36
        if (!isset(self::$connection)) {
37
            $dbConfig = $configuration->getDatabaseConfig();
38
39
            try {
40
                $databaseObject = new PdoDatabase(
41
                    $dbConfig['datasource'],
42
                    $dbConfig['username'],
43
                    $dbConfig['password'],
44
                    [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_520_ci']
45
                );
46
            }
47
            catch (PDOException $ex) {
48
                // wrap around any potential stack traces which may include passwords
49
                throw new EnvironmentException('Error connecting to database: ' . $ex->getMessage());
50
            }
51
52
            $databaseObject->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
53
54
            // emulating prepared statements gives a performance boost on MySQL.
55
            //
56
            // however, our version of PDO doesn't seem to understand parameter types when emulating
57
            // the prepared statements, so we're forced to turn this off for now.
58
            // -- stw 2014-02-11
59
            //
60
            // and that's not the only problem with emulated prepares. We've now got code that relies
61
            // on real prepares.
62
            // -- stw 2023-09-30
63
            $databaseObject->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
64
65
            // Set the default transaction mode
66
            $databaseObject->exec("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;");
67
68
            self::$connection = $databaseObject;
69
        }
70
71
        return self::$connection;
72
    }
73
74
    /**
75
     * Determines if this connection has a transaction in progress or not
76
     * @return boolean true if there is a transaction in progress.
77
     */
78
    public function hasActiveTransaction(): bool
79
    {
80
        return $this->hasActiveTransaction;
81
    }
82
83
    public function beginTransaction(string $isolationLevel = self::ISOLATION_READ_COMMITTED): bool
84
    {
85
        // Override the pre-existing method, which doesn't stop you from
86
        // starting transactions within transactions - which doesn't work and
87
        // will throw an exception. This eliminates the need to catch exceptions
88
        // all over the rest of the code
89
        if ($this->hasActiveTransaction) {
90
            return false;
91
        }
92
        else {
93
            $accessMode = 'READ WRITE';
94
95
            switch ($isolationLevel) {
96
                case self::ISOLATION_SERIALIZABLE:
97
                case self::ISOLATION_READ_COMMITTED:
98
                    break;
99
                case self::ISOLATION_READ_ONLY:
100
                    $isolationLevel = self::ISOLATION_READ_COMMITTED;
101
                    $accessMode = 'READ ONLY';
102
                    break;
103
                default:
104
                    throw new Exception("Invalid transaction isolation level");
105
            }
106
107
            // set the transaction isolation level for every transaction.
108
            // string substitution is safe here; values can only be one of the above constants
109
            parent::exec("SET TRANSACTION ISOLATION LEVEL ${isolationLevel}, ${accessMode};");
110
111
            // start a new transaction, and return whether the start was successful
112
            $this->hasActiveTransaction = parent::beginTransaction();
113
114
            return $this->hasActiveTransaction;
115
        }
116
    }
117
118
    /**
119
     * Commits the active transaction
120
     */
121
    public function commit(): void
122
    {
123
        if ($this->hasActiveTransaction) {
124
            parent::commit();
125
            $this->hasActiveTransaction = false;
126
        }
127
    }
128
129
    /**
130
     * Rolls back a transaction
131
     */
132
    public function rollBack(): void
133
    {
134
        if ($this->hasActiveTransaction) {
135
            parent::rollback();
136
            $this->hasActiveTransaction = false;
137
        }
138
    }
139
}
140