Passed
Push — main ( a01029...30b22a )
by Andreas
02:09
created

PDOCrateDB::setAttribute()   D

Complexity

Conditions 20
Paths 18

Size

Total Lines 90
Code Lines 61

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 22.898

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 20
eloc 61
c 1
b 0
f 0
nc 18
nop 2
dl 0
loc 90
ccs 50
cts 62
cp 0.8065
crap 22.898
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
use const PHP_VERSION_ID;
35
36
class PDOCrateDB extends BasePDO implements PDOInterface
37
{
38
    use PDOImplementation;
39
40
    public const VERSION     = '2.2.3';
41
    public const DRIVER_NAME = 'crate';
42
43
    public const DSN_REGEX = '/^(?:crate:)(?:((?:[\w\.-]+:\d+\,?)+))\/?([\w]+)?$/';
44
45
    public const CRATE_ATTR_HTTP_BASIC_AUTH = 1000;
46
    public const CRATE_ATTR_DEFAULT_SCHEMA  = 1001;
47
48
    public const CRATE_ATTR_SSL_MODE                                       = 1008;
49
    public const CRATE_ATTR_SSL_MODE_DISABLED                              = 1;
50
    public const CRATE_ATTR_SSL_MODE_ENABLED_BUT_WITHOUT_HOST_VERIFICATION = 2;
51
    public const CRATE_ATTR_SSL_MODE_REQUIRED                              = 3;
52
53
    public const CRATE_ATTR_SSL_KEY_PATH      = 1002;
54
    public const CRATE_ATTR_SSL_KEY_PASSWORD  = 1003;
55
    public const CRATE_ATTR_SSL_CERT_PATH     = 1004;
56
    public const CRATE_ATTR_SSL_CERT_PASSWORD = 1005;
57
    public const CRATE_ATTR_SSL_CA_PATH       = 1006;
58
    public const CRATE_ATTR_SSL_CA_PASSWORD   = 1007;
59
60
    public const PARAM_FLOAT     = 6;
61
    public const PARAM_DOUBLE    = 7;
62
    public const PARAM_LONG      = 8;
63
    public const PARAM_ARRAY     = 9;
64
    public const PARAM_OBJECT    = 10;
65
    public const PARAM_TIMESTAMP = 11;
66
    public const PARAM_IP        = 12;
67
68
    /**
69
     * @var array
70
     */
71
    private $attributes = [
72
        'defaultFetchMode' => self::FETCH_BOTH,
73
        'errorMode'        => self::ERRMODE_SILENT,
74
        'sslMode'          => self::CRATE_ATTR_SSL_MODE_DISABLED,
75
        'statementClass'   => [PDOStatement::class, []],
76
        'timeout'          => 0.0,
77
        'auth'             => [],
78
        'defaultSchema'    => 'doc',
79
        'bulkMode'         => false,
80
    ];
81
82
    /**
83
     * @var Http\ServerInterface
84
     */
85
    private $server;
86
87
    /**
88
     * @var PDOStatement|null
89
     */
90
    private $lastStatement;
91
92
    /**
93
     * @var callable
94
     */
95
    private $request;
96
97
    /**
98
     * {@inheritDoc}
99
     *
100
     * @param string     $dsn      The HTTP endpoint to call
101
     * @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...
102
     * @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...
103
     * @param null|array $options  Attributes to set on the PDO
104
     */
105 36
    public function __construct($dsn, $username = null, $passwd = null, $options = [])
106
    {
107
108 36
        if (PHP_VERSION_ID < 80000) {
109
            trigger_error(
110
                "`crate/crate-pdo` will stop supporting PHP7 on one of the upcoming " .
111
                "releases. Please upgrade to PHP8.",
112
                E_USER_DEPRECATED
113
            );
114
        }
115
116 36
        $dsnParts = self::parseDSN($dsn);
117 36
        $servers  = self::serversFromDsnParts($dsnParts);
118
119 36
        $this->setServer(new ServerPool($servers));
120
121 36
        foreach ((array)$options as $attribute => $value) {
122 2
            $this->setAttribute($attribute, $value);
123
        }
124
125 36
        if (!empty($username)) {
126 4
            $this->setAttribute(self::CRATE_ATTR_HTTP_BASIC_AUTH, [$username, $passwd]);
127
        }
128
129 36
        if (!empty($dsnParts[1])) {
130
            $this->setAttribute(self::CRATE_ATTR_DEFAULT_SCHEMA, $dsnParts[1]);
131
        }
132
133
        // Define a callback that will be used in the PDOStatements
134
        // This way we don't expose this as a public api to the end users.
135 36
        $this->request = function (PDOStatement $statement, $sql, array $parameters) {
136
137 2
            $this->lastStatement = $statement;
138
139
            try {
140 2
                if ($statement->isBulkMode()) {
141
                    return $this->server->executeBulk($sql, $parameters);
142
                } else {
143 2
                    return $this->server->execute($sql, $parameters);
144
                }
145
            } catch (Exception\RuntimeException $e) {
146
                if ($this->getAttribute(self::ATTR_ERRMODE) === self::ERRMODE_EXCEPTION) {
147
                    throw new Exception\PDOException($e->getMessage(), $e->getCode());
148
                }
149
150
                if ($this->getAttribute(self::ATTR_ERRMODE) === self::ERRMODE_WARNING) {
151
                    trigger_error(sprintf('[%d] %s', $e->getCode(), $e->getMessage()), E_USER_WARNING);
152
                }
153
154
                // should probably wrap this in a error object ?
155
                return [
156
                    'code'    => $e->getCode(),
157
                    'message' => $e->getMessage(),
158
                ];
159
            }
160 36
        };
161
    }
162
163
    /**
164
     * Change the server implementation
165
     *
166
     * @param ServerInterface $server
167
     */
168 30
    public function setServer(ServerInterface $server): void
169
    {
170 30
        $this->server = $server;
171 30
        $this->server->configure($this);
172
    }
173
174
    /**
175
     * Extract servers and optional custom schema from DSN string
176
     *
177
     * @param string $dsn The DSN string
178
     *
179
     * @throws \Crate\PDO\Exception\PDOException on an invalid DSN string
180
     *
181
     * @return array An array of ['host:post,host:port,...', 'schema']
182
     */
183 110
    private static function parseDSN($dsn)
184
    {
185 110
        $matches = [];
186
187 110
        if (!preg_match(static::DSN_REGEX, $dsn, $matches)) {
188 8
            throw new PDOException(sprintf('Invalid DSN %s', $dsn));
189
        }
190
191 102
        return array_slice($matches, 1);
192
    }
193
194
    /**
195
     * Extract host:port pairs out of the DSN parts
196
     *
197
     * @param array $dsnParts The parts of the parsed DSN string
198
     *
199
     * @return array An array of host:port strings
200
     */
201 100
    private static function serversFromDsnParts($dsnParts)
202
    {
203 100
        return explode(',', trim($dsnParts[0], ','));
204
    }
205
206
    /**
207
     * {@inheritDoc}
208
     */
209 4
    #[\ReturnTypeWillChange]
210
    public function prepare($statement, $options = null)
211
    {
212 4
        $options = ArrayUtils::toArray($options);
213
214 4
        if (isset($options[self::ATTR_CURSOR])) {
215
            trigger_error(sprintf('%s not supported', __METHOD__), E_USER_WARNING);
216
217
            return true;
218
        }
219
220 4
        $statementClass = $this->attributes['statementClass'];
221 4
        if (is_string($statementClass)) {
222
            trigger_error(
223
                "Using bare class strings with `statementClass` is deprecated, " .
224
                "see also https://github.com/crate/crate-pdo/issues/191.",
225
                E_USER_DEPRECATED
226
            );
227
            $className = $statementClass;
228
            $constructorArgs = [];
229 4
        } elseif (is_array($statementClass)) {
230 4
            $className = $statementClass[0];
231 4
            $constructorArgs = $statementClass[1] ?? [];
232
        } else {
233
            throw new InvalidArgumentException(
234
                'Value provided to statementClass has invalid type'
235
            );
236
        }
237 4
        if ($className === PDOStatement::class) {
238 4
            $constructorArgs = [$this, $this->request, $statement, $options];
239
        }
240
241 4
        return new $className(...$constructorArgs);
242
    }
243
244
    /**
245
     * {@inheritDoc}
246
     */
247 2
    public function beginTransaction(): bool
248
    {
249 2
        return true;
250
    }
251
252
    /**
253
     * {@inheritDoc}
254
     */
255 2
    public function commit(): bool
256
    {
257 2
        return true;
258
    }
259
260
    /**
261
     * {@inheritDoc}
262
     */
263 2
    public function rollBack(): bool
264
    {
265 2
        throw new Exception\UnsupportedException;
266
    }
267
268
    /**
269
     * {@inheritDoc}
270
     */
271 2
    public function inTransaction(): bool
272
    {
273 2
        return false;
274
    }
275
276
    /**
277
     * {@inheritDoc}
278
     */
279
    #[\ReturnTypeWillChange]
280
    public function exec($statement)
281
    {
282
        $statement = $this->prepare($statement);
283
        $result    = $statement->execute();
284
285
        return $result === false ? false : $statement->rowCount();
286
    }
287
288
    /**
289
     * {@inheritDoc}
290
     */
291 2
    public function doQuery($statement, ?int $fetchMode = null, ...$fetchModeArgs)
292
    {
293 2
        $statement = $this->prepare($statement);
294 2
        if ($fetchMode !== null) {
295
            $statement->setFetchMode($fetchMode, ...$fetchModeArgs);
296
        }
297
298 2
        $result    = $statement->execute();
299
300 2
        return $result === false ? false : $statement;
301
    }
302
303
    /**
304
     * {@inheritDoc}
305
     */
306 2
    public function lastInsertId($name = null): string
307
    {
308 2
        throw new Exception\UnsupportedException;
309
    }
310
311
    /**
312
     * {@inheritDoc}
313
     */
314
    public function errorCode(): ?string
315
    {
316
        return $this->lastStatement === null ? null : $this->lastStatement->errorCode();
317
    }
318
319
    /**
320
     * {@inheritDoc}
321
     */
322
    public function errorInfo(): array
323
    {
324
        return $this->lastStatement === null ? ["00000", null, null] : $this->lastStatement->errorInfo();
325
    }
326
327
    /**
328
     * {@inheritDoc}
329
     *
330
     * @throws \Crate\PDO\Exception\PDOException
331
     * @throws \Crate\PDO\Exception\InvalidArgumentException
332
     */
333 32
    #[\ReturnTypeWillChange]
334
    public function setAttribute($attribute, $value)
335
    {
336
        switch ($attribute) {
337 32
            case self::ATTR_DEFAULT_FETCH_MODE:
338 2
                $this->attributes['defaultFetchMode'] = $value;
339 2
                break;
340
341 30
            case self::ATTR_ERRMODE:
342 2
                $this->attributes['errorMode'] = $value;
343 2
                break;
344
345 28
            case self::ATTR_STATEMENT_CLASS:
346
                // Previous versions accepted bare class strings on this mode's argument value,
347
                // while the PDO standard format is `[ClassName::class, [constructor_args]]`.
348
                // Let's modernize, upcycle, and propagate accordingly.
349 2
                if (is_string($value)) {
350 2
                    trigger_error(
351 2
                        "Using bare class strings with `ATTR_STATEMENT_CLASS` is deprecated, " .
352 2
                        "see also https://github.com/crate/crate-pdo/issues/191.",
353 2
                        E_USER_DEPRECATED
354 2
                    );
355 2
                    $this->attributes['statementClass'] = [$value, []];
356
                } elseif (is_array($value)) {
357
                    if (empty($value) || !is_string($value[0])) {
358
                        throw new InvalidArgumentException(
359
                            'ATTR_STATEMENT_CLASS array must contain a class name as first element'
360
                        );
361
                    }
362
                    $this->attributes['statementClass'] = $value;
363
                } else {
364
                    throw new InvalidArgumentException(
365
                        'Value provided to ATTR_STATEMENT_CLASS has invalid type'
366
                    );
367
                }
368 2
                break;
369
370 26
            case self::ATTR_TIMEOUT:
371 4
                $this->attributes['timeout'] = (int)$value;
372 4
                break;
373
374 24
            case self::CRATE_ATTR_HTTP_BASIC_AUTH:
375 4
                if (!is_array($value) && $value !== null) {
376
                    throw new InvalidArgumentException(
377
                        'Value provided to CRATE_ATTR_HTTP_BASIC_AUTH must be null or an array'
378
                    );
379
                }
380
381 4
                $this->attributes['auth'] = $value;
382 4
                break;
383
384 24
            case self::CRATE_ATTR_DEFAULT_SCHEMA:
385 4
                $this->attributes['defaultSchema'] = $value;
386 4
                break;
387
388 20
            case self::CRATE_ATTR_SSL_MODE:
389 18
                $this->attributes['sslMode'] = $value;
390 18
                break;
391
392 14
            case self::CRATE_ATTR_SSL_CA_PATH:
393 4
                $this->attributes['sslCa'] = $value;
394 4
                break;
395
396 12
            case self::CRATE_ATTR_SSL_CA_PASSWORD:
397 2
                $this->attributes['sslCaPassword'] = $value;
398 2
                break;
399
400 10
            case self::CRATE_ATTR_SSL_CERT_PATH:
401 4
                $this->attributes['sslCert'] = $value;
402 4
                break;
403
404 8
            case self::CRATE_ATTR_SSL_CERT_PASSWORD:
405 2
                $this->attributes['sslCertPassword'] = $value;
406 2
                break;
407
408 6
            case self::CRATE_ATTR_SSL_KEY_PATH:
409 4
                $this->attributes['sslKey'] = $value;
410 4
                break;
411
412 4
            case self::CRATE_ATTR_SSL_KEY_PASSWORD:
413 2
                $this->attributes['sslKeyPassword'] = $value;
414 2
                break;
415
416
            default:
417 2
                throw new Exception\PDOException('Unsupported driver attribute');
418
        }
419
420
        // A setting changed so we need to reconfigure the server pool
421 30
        $this->server->configure($this);
422 30
        return true;
423
    }
424
425
    /**
426
     * {@inheritDoc}
427
     *
428
     * @throws \Crate\PDO\Exception\PDOException
429
     */
430 52
    #[\ReturnTypeWillChange]
431
    public function getAttribute($attribute)
432
    {
433
        switch ($attribute) {
434 52
            case self::ATTR_PREFETCH:
435 52
            case self::ATTR_PERSISTENT:
436 4
                return false;
437
438 52
            case self::ATTR_CLIENT_VERSION:
439 2
                return self::VERSION;
440
441 52
            case self::ATTR_SERVER_VERSION:
442
                return $this->server->getServerVersion();
443
444 52
            case self::ATTR_SERVER_INFO:
445
                return $this->server->getServerInfo();
446
447 52
            case self::ATTR_TIMEOUT:
448 52
                return $this->attributes['timeout'];
449
450 52
            case self::CRATE_ATTR_HTTP_BASIC_AUTH:
451 52
                return $this->attributes['auth'];
452
453 52
            case self::ATTR_DEFAULT_FETCH_MODE:
454 4
                return $this->attributes['defaultFetchMode'];
455
456 52
            case self::ATTR_ERRMODE:
457 2
                return $this->attributes['errorMode'];
458
459 52
            case self::ATTR_DRIVER_NAME:
460 2
                return static::DRIVER_NAME;
461
462 52
            case self::ATTR_STATEMENT_CLASS:
463 2
                return $this->attributes['statementClass'];
464
465 52
            case self::CRATE_ATTR_DEFAULT_SCHEMA:
466 52
                return $this->attributes['defaultSchema'];
467
468 52
            case self::CRATE_ATTR_SSL_MODE:
469 52
                return $this->attributes['sslMode'];
470
471 52
            case self::CRATE_ATTR_SSL_CA_PATH:
472 52
                return $this->attributes['sslCa'] ?? null;
473
474 52
            case self::CRATE_ATTR_SSL_CA_PASSWORD:
475 52
                return $this->attributes['sslCaPassword'] ?? null;
476
477 52
            case self::CRATE_ATTR_SSL_CERT_PATH:
478 52
                return $this->attributes['sslCert'] ?? null;
479
480 52
            case self::CRATE_ATTR_SSL_CERT_PASSWORD:
481 52
                return $this->attributes['sslCertPassword'] ?? null;
482
483 52
            case self::CRATE_ATTR_SSL_KEY_PATH:
484 52
                return $this->attributes['sslKey'] ?? null;
485
486 52
            case self::CRATE_ATTR_SSL_KEY_PASSWORD:
487 52
                return $this->attributes['sslKeyPassword'] ?? null;
488
489
            default:
490
                // PHP Switch is a lose comparison
491 4
                if ($attribute === self::ATTR_AUTOCOMMIT) {
492 2
                    return true;
493
                }
494
495 2
                throw new Exception\PDOException(sprintf('Unsupported driver attribute: %s', $attribute));
496
        }
497
    }
498
499
    /**
500
     * {@inheritDoc}
501
     */
502 6
    #[\ReturnTypeWillChange]
503
    public function quote($string, $parameter_type = self::PARAM_STR)
504
    {
505
        switch ($parameter_type) {
506 6
            case self::PARAM_INT:
507 2
                return (int)$string;
508
509 6
            case self::PARAM_BOOL:
510 2
                return (bool)$string;
511
512 6
            case self::PARAM_NULL:
513 2
                return null;
514
515 6
            case self::PARAM_LOB:
516 2
                throw new Exception\UnsupportedException('This is not supported by crate.io');
517
518 4
            case self::PARAM_STR:
519 2
                trigger_error(
520 2
                    "Strongly consider using prepared statements (secure) " .
521 2
                    "instead of quoting strings manually (insecure), " .
522 2
                    "see also https://www.php.net/manual/en/pdo.quote.php.",
523 2
                    E_USER_DEPRECATED
524 2
                );
525 2
                return $this->quotePostgresql((string)$string);
526
527
            default:
528 2
                throw new Exception\InvalidArgumentException('Unknown param type');
529
        }
530
    }
531
532
    /**
533
     * Escape/quote strings for PostgreSQL when using prepared statements is not possible.
534
     *
535
     * https://github.com/ADOdb/ADOdb/blob/v5.22.10/adodb.inc.php
536
     * https://github.com/ADOdb/ADOdb/blob/v5.22.10/drivers/adodb-postgres64.inc.php
537
     *
538
     * @param string $string
539
     * @return string
540
     */
541 2
    private function quotePostgresql($value): string
542
    {
543 2
        if (is_bool($value)) {
544
            return $value ? 'true' : 'false';
545
        }
546 2
        $value = str_replace(
547 2
            array('\\', "\0"),
548 2
            array('\\\\', "\\\0"),
549 2
            $value
550 2
        );
551
        // CrateDB uses quote-doubling instead of backslash escaping.
552 2
        return str_replace("'", "''", $value);
553
    }
554
555
    /**
556
     * {@inheritDoc}
557
     */
558 2
    public static function getAvailableDrivers(): array
559
    {
560 2
        return array_merge(parent::getAvailableDrivers(), [static::DRIVER_NAME]);
561
    }
562
563
    public function getServerVersion(): string
564
    {
565
        return $this->server->getServerVersion();
566
    }
567
568
    public function getServerInfo(): string
569
    {
570
        return $this->getServerVersion();
571
    }
572
}
573