Failed Conditions
Push — startup ( 8a1b3f )
by Simon
06:20
created

PdoDatabase::getDatabaseConnection()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 38
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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