Completed
Pull Request — master (#3759)
by Benjamin
131:22 queued 67:31
created

MysqliConnection::quote()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 3
cp 0.6667
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1.037
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Driver\Mysqli;
6
7
use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionError;
8
use Doctrine\DBAL\Driver\PingableConnection;
9
use Doctrine\DBAL\Driver\ResultStatement;
10
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
11
use Doctrine\DBAL\Driver\Statement as DriverStatement;
12
use mysqli;
13
use const MYSQLI_INIT_COMMAND;
14
use const MYSQLI_OPT_CONNECT_TIMEOUT;
15
use const MYSQLI_OPT_LOCAL_INFILE;
16
use const MYSQLI_READ_DEFAULT_FILE;
17
use const MYSQLI_READ_DEFAULT_GROUP;
18
use const MYSQLI_SERVER_PUBLIC_KEY;
19
use function defined;
20
use function floor;
21
use function in_array;
22
use function ini_get;
23
use function mysqli_init;
24
use function mysqli_options;
25
use function restore_error_handler;
26
use function set_error_handler;
27
use function sprintf;
28
use function stripos;
29
30
class MysqliConnection implements PingableConnection, ServerInfoAwareConnection
31
{
32
    /**
33
     * Name of the option to set connection flags
34
     */
35
    public const OPTION_FLAGS = 'flags';
36
37
    /** @var mysqli */
38
    private $conn;
39
40
    /**
41
     * @param array<string, mixed> $params
42
     * @param array<int, mixed>    $driverOptions
43
     *
44
     * @throws MysqliException
45
     */
46
    public function __construct(array $params, string $username, string $password, array $driverOptions = [])
47
    {
48 1990
        $port = $params['port'] ?? (int) ini_get('mysqli.default_port');
49
50 1990
        // Fallback to default MySQL port if not given.
51
        if (! $port) {
52
            $port = 3306;
53 1990
        }
54
55
        $socket = $params['unix_socket'] ?? ini_get('mysqli.default_socket');
56
        $dbname = $params['dbname'] ?? '';
57 1990
        $host   = $params['host'];
58 1990
59
        if (! empty($params['persistent'])) {
60 1990
            $host = 'p:' . $host;
61
        }
62 1990
63
        $flags = $driverOptions[static::OPTION_FLAGS] ?? 0;
64 1990
65 1990
        $this->conn = mysqli_init();
66
67
        $this->setSecureConnection($params);
68 1990
        $this->setDriverOptions($driverOptions);
69
70 1990
        set_error_handler(static function () : bool {
71 1983
            return true;
72
        });
73 1983
        try {
74 1990
            if (! $this->conn->real_connect($host, $username, $password, $dbname, $port, $socket, $flags)) {
75
                throw ConnectionError::new($this->conn);
76
            }
77 1983
        } finally {
78 1983
            restore_error_handler();
79
        }
80
81 1312
        if (! isset($params['charset'])) {
82 1312
            return;
83
        }
84
85
        $this->conn->set_charset($params['charset']);
86
    }
87
88
    /**
89
     * Retrieves mysqli native resource handle.
90
     *
91
     * Could be used if part of your application is not using DBAL.
92
     */
93
    public function getWrappedResourceHandle() : mysqli
94
    {
95
        return $this->conn;
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     *
101
     * The server version detection includes a special case for MariaDB
102
     * to support '5.5.5-' prefixed versions introduced in Maria 10+
103
     *
104 1983
     * @link https://jira.mariadb.org/browse/MDEV-4088
105
     */
106 1983
    public function getServerVersion() : string
107 1983
    {
108 1130
        $serverInfos = $this->conn->get_server_info();
109
        if (stripos($serverInfos, 'mariadb') !== false) {
110
            return $serverInfos;
111 853
        }
112 853
113 853
        $majorVersion = floor($this->conn->server_version / 10000);
114
        $minorVersion = floor(($this->conn->server_version - $majorVersion * 10000) / 100);
115 853
        $patchVersion = floor($this->conn->server_version - $majorVersion * 10000 - $minorVersion * 100);
116
117
        return $majorVersion . '.' . $minorVersion . '.' . $patchVersion;
118
    }
119
120
    /**
121 1997
     * {@inheritdoc}
122
     */
123 1997
    public function requiresQueryForServerVersion() : bool
124
    {
125
        return false;
126
    }
127
128
    /**
129 1969
     * {@inheritdoc}
130
     */
131 1969
    public function prepare(string $sql) : DriverStatement
132
    {
133
        return new MysqliStatement($this->conn, $sql);
134
    }
135
136
    /**
137 1962
     * {@inheritdoc}
138
     */
139 1962
    public function query(string $sql) : ResultStatement
140 1962
    {
141 1962
        $stmt = $this->prepare($sql);
142 1962
        $stmt->execute();
143
144 1962
        return $stmt;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 1850
    public function quote(string $input) : string
151
    {
152 1850
        return "'" . $this->conn->escape_string($input) . "'";
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 1983
    public function exec(string $statement) : int
159
    {
160 1983
        if ($this->conn->query($statement) === false) {
161 1969
            throw ConnectionError::new($this->conn);
162
        }
163
164 1983
        return $this->conn->affected_rows;
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170 49
    public function lastInsertId() : string
171
    {
172 49
        $lastInsertId = (string) $this->conn->insert_id;
173
174
        if ($lastInsertId === '0') {
175
            throw new MysqliException('No identity value was generated by the last statement.');
176
        }
177
178 1920
        return $lastInsertId;
179
    }
180 1920
181
    /**
182 1920
     * {@inheritdoc}
183
     */
184
    public function getSequenceNumber(string $name) : string
185
    {
186
        throw new MysqliException('MySQL does not support sequences.');
187
    }
188 1899
189
    /**
190 1899
     * {@inheritdoc}
191
     */
192
    public function beginTransaction() : void
193
    {
194
        $this->conn->query('START TRANSACTION');
195
    }
196 1920
197
    /**
198 1920
     * {@inheritdoc}
199
     */
200
    public function commit() : void
201
    {
202
        if (! $this->conn->commit()) {
203
            throw ConnectionError::new($this->conn);
204
        }
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function rollBack() : void
211
    {
212
        if (! $this->conn->rollback()) {
213
            throw ConnectionError::new($this->conn);
214
        }
215
    }
216
217
    /**
218
     * Apply the driver options to the connection.
219
     *
220
     * @param array<int, mixed> $driverOptions
221
     *
222
     * @throws MysqliException When one of of the options is not supported.
223
     * @throws MysqliException When applying doesn't work - e.g. due to incorrect value.
224
     */
225 1990
    private function setDriverOptions(array $driverOptions = []) : void
226
    {
227
        $supportedDriverOptions = [
228 1990
            MYSQLI_OPT_CONNECT_TIMEOUT,
229
            MYSQLI_OPT_LOCAL_INFILE,
230
            MYSQLI_INIT_COMMAND,
231
            MYSQLI_READ_DEFAULT_FILE,
232
            MYSQLI_READ_DEFAULT_GROUP,
233
        ];
234
235 1990
        if (defined('MYSQLI_SERVER_PUBLIC_KEY')) {
236 1990
            $supportedDriverOptions[] = MYSQLI_SERVER_PUBLIC_KEY;
237
        }
238
239 1990
        $exceptionMsg = "%s option '%s' with value '%s'";
240
241 1990
        foreach ($driverOptions as $option => $value) {
242 1472
            if ($option === static::OPTION_FLAGS) {
243
                continue;
244
            }
245
246 1472
            if (! in_array($option, $supportedDriverOptions, true)) {
247 1465
                throw new MysqliException(
248 1465
                    sprintf($exceptionMsg, 'Unsupported', $option, $value)
249
                );
250
            }
251
252 1472
            if (@mysqli_options($this->conn, $option, $value)) {
253 1472
                continue;
254
            }
255
256
            throw ConnectionError::new($this->conn);
257
        }
258
    }
259
260
    /**
261
     * Pings the server and re-connects when `mysqli.reconnect = 1`
262
     *
263
     * {@inheritDoc}
264
     */
265 1990
    public function ping() : void
266
    {
267
        if (! $this->conn->ping()) {
268
            throw new MysqliException($this->conn->error, $this->conn->sqlstate, $this->conn->errno);
269
        }
270
    }
271
272 1843
    /**
273
     * Establish a secure connection
274 1843
     *
275
     * @param array<string, mixed> $params
276
     *
277
     * @throws MysqliException
278
     */
279
    private function setSecureConnection(array $params) : void
280
    {
281
        if (! isset($params['ssl_key']) &&
282
            ! isset($params['ssl_cert']) &&
283
            ! isset($params['ssl_ca']) &&
284 1990
            ! isset($params['ssl_capath']) &&
285
            ! isset($params['ssl_cipher'])
286 1990
        ) {
287 1990
            return;
288 1990
        }
289 1990
290 1990
        $this->conn->ssl_set(
291
            $params['ssl_key']    ?? null,
292 1990
            $params['ssl_cert']   ?? null,
293
            $params['ssl_ca']     ?? null,
294
            $params['ssl_capath'] ?? null,
295
            $params['ssl_cipher'] ?? null
296
        );
297
    }
298
}
299