Passed
Pull Request — main (#143)
by Andreas
01:39
created

PDO::__construct()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 44
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 13.2607

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 24
nc 8
nop 4
dl 0
loc 44
ccs 13
cts 23
cp 0.5652
crap 13.2607
rs 8.4444
c 1
b 0
f 0
1
<?php
2
/**
3
 * Licensed to CRATE Technology GmbH("Crate") under one or more contributor
4
 * license agreements.  See the NOTICE file distributed with this work for
5
 * additional information regarding copyright ownership.  Crate licenses
6
 * this file to you under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.  You may
8
 * obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
15
 * License for the specific language governing permissions and limitations
16
 * under the License.
17
 *
18
 * However, if you have executed another commercial license agreement
19
 * with Crate these terms will supersede the license and you may use the
20
 * software solely pursuant to the terms of the relevant commercial agreement.
21
 */
22
23
declare(strict_types=1);
24
25
namespace Crate\PDO;
26
27
use Crate\PDO\Exception\InvalidArgumentException;
28
use Crate\PDO\Exception\PDOException;
29
use Crate\PDO\Http\ServerInterface;
30
use Crate\PDO\Http\ServerPool;
31
use Crate\Stdlib\ArrayUtils;
32
use PDO as BasePDO;
33
34
class PDO extends BasePDO implements PDOInterface
35
{
36
    use PDOImplementation;
37
38
    public const VERSION     = '2.1.4';
39
    public const DRIVER_NAME = 'crate';
40
41
    public const DSN_REGEX = '/^(?:crate:)(?:((?:[\w\.-]+:\d+\,?)+))\/?([\w]+)?$/';
42
43
    public const CRATE_ATTR_HTTP_BASIC_AUTH = 1000;
44
    public const CRATE_ATTR_DEFAULT_SCHEMA  = 1001;
45
    public const CRATE_ATTR_BULK_MODE       = 1009;
46
47
    public const CRATE_ATTR_SSL_MODE                                       = 1008;
48
    public const CRATE_ATTR_SSL_MODE_DISABLED                              = 1;
49
    public const CRATE_ATTR_SSL_MODE_ENABLED_BUT_WITHOUT_HOST_VERIFICATION = 2;
50
    public const CRATE_ATTR_SSL_MODE_REQUIRED                              = 3;
51
52
    public const CRATE_ATTR_SSL_KEY_PATH      = 1002;
53
    public const CRATE_ATTR_SSL_KEY_PASSWORD  = 1003;
54
    public const CRATE_ATTR_SSL_CERT_PATH     = 1004;
55
    public const CRATE_ATTR_SSL_CERT_PASSWORD = 1005;
56
    public const CRATE_ATTR_SSL_CA_PATH       = 1006;
57
    public const CRATE_ATTR_SSL_CA_PASSWORD   = 1007;
58
59
    public const PARAM_FLOAT     = 6;
60
    public const PARAM_DOUBLE    = 7;
61
    public const PARAM_LONG      = 8;
62
    public const PARAM_ARRAY     = 9;
63
    public const PARAM_OBJECT    = 10;
64
    public const PARAM_TIMESTAMP = 11;
65
    public const PARAM_IP        = 12;
66
67
    /**
68
     * @var array
69
     */
70
    private $attributes = [
71
        'defaultFetchMode' => self::FETCH_BOTH,
72
        'errorMode'        => self::ERRMODE_SILENT,
73
        'sslMode'          => self::CRATE_ATTR_SSL_MODE_DISABLED,
74
        'statementClass'   => PDOStatement::class,
75
        'timeout'          => 0.0,
76
        'auth'             => [],
77
        'defaultSchema'    => 'doc',
78
        'bulkMode'         => false,
79
    ];
80
81
    /**
82
     * @var Http\ServerInterface
83
     */
84
    private $server;
85
86
    /**
87
     * @var PDOStatement|null
88
     */
89
    private $lastStatement;
90
91
    /**
92
     * @var callable
93
     */
94
    private $request;
95
96
    /**
97
     * {@inheritDoc}
98
     *
99
     * @param string     $dsn      The HTTP endpoint to call
100
     * @param null       $username Username for basic auth
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $username is correct as it would always require null to be passed?
Loading history...
101
     * @param null       $passwd   Password for basic auth
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $passwd is correct as it would always require null to be passed?
Loading history...
102
     * @param null|array $options  Attributes to set on the PDO
103
     */
104 34
    public function __construct($dsn, $username = null, $passwd = null, $options = [])
105
    {
106 34
        $dsnParts = self::parseDSN($dsn);
107 34
        $servers  = self::serversFromDsnParts($dsnParts);
108
109 34
        $this->setServer(new ServerPool($servers));
110
111 34
        foreach ((array)$options as $attribute => $value) {
112 2
            $this->setAttribute($attribute, $value);
113
        }
114
115 34
        if (!empty($username)) {
116 4
            $this->setAttribute(self::CRATE_ATTR_HTTP_BASIC_AUTH, [$username, $passwd]);
117
        }
118
119 34
        if (!empty($dsnParts[1])) {
120
            $this->setAttribute(self::CRATE_ATTR_DEFAULT_SCHEMA, $dsnParts[1]);
121
        }
122
123
        // Define a callback that will be used in the PDOStatements
124
        // This way we don't expose this as a public api to the end users.
125 34
        $this->request = function (PDOStatement $statement, $sql, array $parameters) {
126
127 2
            $this->lastStatement = $statement;
128
129
            try {
130 2
                if ($this->getAttribute(self::CRATE_ATTR_BULK_MODE)) {
131
                    return $this->server->executeBulk($sql, $parameters);
132
                } else {
133 2
                    return $this->server->execute($sql, $parameters);
134
                }
135
            } catch (Exception\RuntimeException $e) {
136
                if ($this->getAttribute(self::ATTR_ERRMODE) === self::ERRMODE_EXCEPTION) {
137
                    throw new Exception\PDOException($e->getMessage(), $e->getCode());
138
                }
139
140
                if ($this->getAttribute(self::ATTR_ERRMODE) === self::ERRMODE_WARNING) {
141
                    trigger_error(sprintf('[%d] %s', $e->getCode(), $e->getMessage()), E_USER_WARNING);
142
                }
143
144
                // should probably wrap this in a error object ?
145
                return [
146
                    'code'    => $e->getCode(),
147
                    'message' => $e->getMessage(),
148
                ];
149
            }
150 34
        };
151
    }
152
153
    /**
154
     * Change the server implementation
155
     *
156
     * @param ServerInterface $server
157
     */
158 28
    public function setServer(ServerInterface $server): void
159
    {
160 28
        $this->server = $server;
161 28
        $this->server->configure($this);
162
    }
163
164
    /**
165
     * Extract servers and optional custom schema from DSN string
166
     *
167
     * @param string $dsn The DSN string
168
     *
169
     * @throws \Crate\PDO\Exception\PDOException on an invalid DSN string
170
     *
171
     * @return array An array of ['host:post,host:port,...', 'schema']
172
     */
173 112
    private static function parseDSN($dsn)
174
    {
175 112
        $matches = [];
176
177 112
        if (!preg_match(static::DSN_REGEX, $dsn, $matches)) {
178 8
            throw new PDOException(sprintf('Invalid DSN %s', $dsn));
179
        }
180
181 104
        return array_slice($matches, 1);
182
    }
183
184
    /**
185
     * Extract host:port pairs out of the DSN parts
186
     *
187
     * @param array $dsnParts The parts of the parsed DSN string
188
     *
189
     * @return array An array of host:port strings
190
     */
191 102
    private static function serversFromDsnParts($dsnParts)
192
    {
193 102
        return explode(',', trim($dsnParts[0], ','));
194
    }
195
196
    /**
197
     * {@inheritDoc}
198
     */
199 4
    public function prepare($statement, $options = null)
200
    {
201 4
        $options = ArrayUtils::toArray($options);
202
203 4
        if (isset($options[self::ATTR_CURSOR])) {
204
            trigger_error(sprintf('%s not supported', __METHOD__), E_USER_WARNING);
205
206
            return true;
207
        }
208
209 4
        $className = $this->attributes['statementClass'];
210
211 4
        return new $className($this, $this->request, $statement, $options);
212
    }
213
214
    /**
215
     * {@inheritDoc}
216
     */
217 2
    public function beginTransaction()
218
    {
219 2
        return true;
220
    }
221
222
    /**
223
     * {@inheritDoc}
224
     */
225 2
    public function commit()
226
    {
227 2
        return true;
228
    }
229
230
    /**
231
     * {@inheritDoc}
232
     */
233 2
    public function rollBack()
234
    {
235 2
        throw new Exception\UnsupportedException;
236
    }
237
238
    /**
239
     * {@inheritDoc}
240
     */
241 2
    public function inTransaction()
242
    {
243 2
        return false;
244
    }
245
246
    /**
247
     * {@inheritDoc}
248
     */
249
    public function exec($statement)
250
    {
251
        $statement = $this->prepare($statement);
252
        $result    = $statement->execute();
253
254
        return $result === false ? false : $statement->rowCount();
255
    }
256
257
    /**
258
     * {@inheritDoc}
259
     */
260 2
    public function doQuery($statement)
261
    {
262 2
        $statement = $this->prepare($statement);
263 2
        $result    = $statement->execute();
264
265 2
        return $result === false ? false : $statement;
266
    }
267
268
    /**
269
     * {@inheritDoc}
270
     */
271 2
    public function lastInsertId($name = null)
272
    {
273 2
        throw new Exception\UnsupportedException;
274
    }
275
276
    /**
277
     * {@inheritDoc}
278
     */
279
    public function errorCode()
280
    {
281
        return $this->lastStatement === null ? null : $this->lastStatement->errorCode();
282
    }
283
284
    /**
285
     * {@inheritDoc}
286
     */
287
    public function errorInfo()
288
    {
289
        return $this->lastStatement === null ? null : $this->lastStatement->errorInfo();
290
    }
291
292
    /**
293
     * {@inheritDoc}
294
     *
295
     * @throws \Crate\PDO\Exception\PDOException
296
     * @throws \Crate\PDO\Exception\InvalidArgumentException
297
     */
298 36
    public function setAttribute($attribute, $value)
299
    {
300
        switch ($attribute) {
301 36
            case self::ATTR_DEFAULT_FETCH_MODE:
302 2
                $this->attributes['defaultFetchMode'] = $value;
303 2
                break;
304
305 34
            case self::ATTR_ERRMODE:
306 2
                $this->attributes['errorMode'] = $value;
307 2
                break;
308
309 32
            case self::ATTR_STATEMENT_CLASS:
310 2
                $this->attributes['statementClass'] = $value;
311 2
                break;
312
313 30
            case self::ATTR_TIMEOUT:
314 4
                $this->attributes['timeout'] = (int)$value;
315 4
                break;
316
317 28
            case self::CRATE_ATTR_HTTP_BASIC_AUTH:
318 4
                if (!is_array($value) && $value !== null) {
319
                    throw new InvalidArgumentException(
320
                        'Value probided to CRATE_ATTR_HTTP_BASIC_AUTH must be null or an array'
321
                    );
322
                }
323
324 4
                $this->attributes['auth'] = $value;
325 4
                break;
326
327 28
            case self::CRATE_ATTR_DEFAULT_SCHEMA:
328 4
                $this->attributes['defaultSchema'] = $value;
329 4
                break;
330
331 24
            case self::CRATE_ATTR_BULK_MODE:
332 4
                $this->attributes['bulkMode'] = $value;
333 4
                break;
334
335 20
            case self::CRATE_ATTR_SSL_MODE:
336 18
                $this->attributes['sslMode'] = $value;
337 18
                break;
338
339 14
            case self::CRATE_ATTR_SSL_CA_PATH:
340 4
                $this->attributes['sslCa'] = $value;
341 4
                break;
342
343 12
            case self::CRATE_ATTR_SSL_CA_PASSWORD:
344 2
                $this->attributes['sslCaPassword'] = $value;
345 2
                break;
346
347 10
            case self::CRATE_ATTR_SSL_CERT_PATH:
348 4
                $this->attributes['sslCert'] = $value;
349 4
                break;
350
351 8
            case self::CRATE_ATTR_SSL_CERT_PASSWORD:
352 2
                $this->attributes['sslCertPassword'] = $value;
353 2
                break;
354
355 6
            case self::CRATE_ATTR_SSL_KEY_PATH:
356 4
                $this->attributes['sslKey'] = $value;
357 4
                break;
358
359 4
            case self::CRATE_ATTR_SSL_KEY_PASSWORD:
360 2
                $this->attributes['sslKeyPassword'] = $value;
361 2
                break;
362
363
            default:
364 2
                throw new Exception\PDOException('Unsupported driver attribute');
365
        }
366
367
        // A setting changed so we need to reconfigure the server pool
368 34
        $this->server->configure($this);
369
    }
370
371
    /**
372
     * {@inheritDoc}
373
     *
374
     * @throws \Crate\PDO\Exception\PDOException
375
     */
376 52
    public function getAttribute($attribute)
377
    {
378
        switch ($attribute) {
379 52
            case self::ATTR_PERSISTENT:
380 2
                return false;
381
382 52
            case self::ATTR_PREFETCH:
383 2
                return false;
384
385 52
            case self::ATTR_CLIENT_VERSION:
386 2
                return self::VERSION;
387
388 52
            case self::ATTR_SERVER_VERSION:
389
                return $this->server->getServerVersion();
390
391 52
            case self::ATTR_SERVER_INFO:
392
                return $this->server->getServerInfo();
393
394 52
            case self::ATTR_TIMEOUT:
395 52
                return $this->attributes['timeout'];
396
397 52
            case self::CRATE_ATTR_HTTP_BASIC_AUTH:
398 52
                return $this->attributes['auth'];
399
400 52
            case self::ATTR_DEFAULT_FETCH_MODE:
401 4
                return $this->attributes['defaultFetchMode'];
402
403 52
            case self::ATTR_ERRMODE:
404 2
                return $this->attributes['errorMode'];
405
406 52
            case self::ATTR_DRIVER_NAME:
407 2
                return static::DRIVER_NAME;
408
409 52
            case self::ATTR_STATEMENT_CLASS:
410 2
                return [$this->attributes['statementClass']];
411
412 52
            case self::CRATE_ATTR_DEFAULT_SCHEMA:
413 52
                return $this->attributes['defaultSchema'];
414
415 52
            case self::CRATE_ATTR_BULK_MODE:
416 4
                return $this->attributes['bulkMode'];
417
418 52
            case self::CRATE_ATTR_SSL_MODE:
419 52
                return $this->attributes['sslMode'];
420
421 52
            case self::CRATE_ATTR_SSL_CA_PATH:
422 52
                return $this->attributes['sslCa'] ?? null;
423
424 52
            case self::CRATE_ATTR_SSL_CA_PASSWORD:
425 52
                return $this->attributes['sslCaPassword'] ?? null;
426
427 52
            case self::CRATE_ATTR_SSL_CERT_PATH:
428 52
                return $this->attributes['sslCert'] ?? null;
429
430 52
            case self::CRATE_ATTR_SSL_CERT_PASSWORD:
431 52
                return $this->attributes['sslCertPassword'] ?? null;
432
433 52
            case self::CRATE_ATTR_SSL_KEY_PATH:
434 52
                return $this->attributes['sslKey'] ?? null;
435
436 52
            case self::CRATE_ATTR_SSL_KEY_PASSWORD:
437 52
                return $this->attributes['sslKeyPassword'] ?? null;
438
439
            default:
440
                // PHP Switch is a lose comparison
441 4
                if ($attribute === self::ATTR_AUTOCOMMIT) {
442 2
                    return true;
443
                }
444
445 2
                throw new Exception\PDOException(sprintf('Unsupported driver attribute: %s', $attribute));
446
        }
447
    }
448
449
    /**
450
     * {@inheritDoc}
451
     */
452 8
    public function quote($string, $parameter_type = self::PARAM_STR)
453
    {
454
        switch ($parameter_type) {
455 8
            case self::PARAM_INT:
456 2
                return (int)$string;
457
458 8
            case self::PARAM_BOOL:
459 2
                return (bool)$string;
460
461 8
            case self::PARAM_NULL:
462 2
                return null;
463
464 6
            case self::PARAM_LOB:
465 2
                throw new Exception\UnsupportedException('This is not supported by crate.io');
466
467 4
            case self::PARAM_STR:
468 2
                throw new Exception\UnsupportedException('This is not supported, please use prepared statements.');
469
470
            default:
471 2
                throw new Exception\InvalidArgumentException('Unknown param type');
472
        }
473
    }
474
475
    /**
476
     * {@inheritDoc}
477
     */
478 2
    public static function getAvailableDrivers()
479
    {
480 2
        return array_merge(parent::getAvailableDrivers(), [static::DRIVER_NAME]);
481
    }
482
483
    public function getServerVersion()
484
    {
485
        return $this->server->getServerVersion();
486
    }
487
488
    public function getServerInfo()
489
    {
490
        return $this->getServerVersion();
491
    }
492
}
493