PdoDriver   A
last analyzed

Complexity

Total Complexity 29

Size/Duplication

Total Lines 524
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 96
c 3
b 1
f 0
dl 0
loc 524
ccs 101
cts 101
cp 1
rs 10
wmc 29

19 Methods

Rating   Name   Duplication   Size   Complexity  
A createResultReader() 0 6 1
A appendMiddleware() 0 4 1
A fetchFirst() 0 10 1
A fetchAll() 0 9 1
A fetch() 0 8 1
A fetchReader() 0 7 1
A getLastInsertedId() 0 3 1
A configure() 0 15 2
A create() 0 6 1
A run() 0 9 1
A beginTransaction() 0 3 1
A open() 0 9 2
A runMiddleware() 0 7 2
A executeMiddlewareChain() 0 10 2
A getPdo() 0 4 1
A close() 0 8 2
A executeStatement() 0 7 1
A useMiddleware() 0 4 1
A prepareStatement() 0 30 6
1
<?php
2
/**
3
 * Copyright 2021 Aleksandar Panic
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *   http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
18
namespace ArekX\PQL\Drivers\Pdo;
19
20
use ArekX\PQL\Contracts\Driver;
21
use ArekX\PQL\Contracts\RawQuery;
22
use ArekX\PQL\Contracts\ResultBuilder;
23
use ArekX\PQL\Contracts\ResultReader;
24
use ArekX\PQL\QueryResultBuilder;
25
use PDO;
26
use PDOStatement;
27
28
/**
29
 * Wrapping driver for PDO connections.
30
 */
31
abstract class PdoDriver implements Driver
32
{
33
    /**
34
     * Middleware step after a connection is open.
35
     *
36
     * Function signature for event:
37
     * ```php
38
     * function($result, $params, $next) {
39
     *    // Implementation
40
     *
41
     *    // $result is a PDO instance
42
     *
43
     *    $next($result)
44
     * }
45
     */
46
    const STEP_OPEN = 'open';
47
48
    /**
49
     * Middleware step after a connection is closed
50
     *
51
     * Function signature for event:
52
     * ```php
53
     * function($result, $params, $next) {
54
     *    // Implementation
55
     *
56
     *    // $result is null
57
     *
58
     *    $next($result)
59
     * }
60
     */
61
    const STEP_CLOSE = 'close';
62
63
    /**
64
     * Middleware step before a query is run.
65
     *
66
     * Function signature for event:
67
     * ```php
68
     * function($result, $params, $next) {
69
     *    // Implementation
70
     *
71
     *    // $result is a RawQuery
72
     *    // $params[0] is the type of the method used 'run', 'first', 'all', etc.
73
     *
74
     *    $next($result)
75
     * }
76
     */
77
    const STEP_BEFORE_RUN = 'before-run';
78
79
    /**
80
     * Middleware step before a statement is fully prepared.
81
     *
82
     * Function signature for event:
83
     * ```php
84
     * function($result, $params, $next) {
85
     *    // Implementation
86
     *
87
     *    // $result is a PdoStatement
88
     *    // $params[0] is RawQuery used
89
     *
90
     *    $next($result)
91
     * }
92
     */
93
    const STEP_BEFORE_PREPARE = 'before-prepare';
94
95
    /**
96
     * Middleware step after a statement is fully prepared.
97
     *
98
     * Function signature for event:
99
     * ```php
100
     * function($result, $params, $next) {
101
     *    // Implementation
102
     *
103
     *    // $result is a PdoStatement
104
     *    // $params[0] is RawQuery used
105
     *
106
     *    $next($result)
107
     * }
108
     */
109
    const STEP_AFTER_PREPARE = 'after-prepare';
110
111
    /**
112
     * Middleware step after a query is run.
113
     *
114
     * Function signature for event:
115
     * ```php
116
     * function($result, $params, $next) {
117
     *    // Implementation
118
     *
119
     *    // $result is a number of affected rows
120
     *    // $params[0] is RawQuery used
121
     *    // $params[1] is the type of the method used 'run', 'first', 'all', etc.
122
     *
123
     *    $next($result)
124
     * }
125
     */
126
    const STEP_AFTER_RUN = 'after-run';
127
128
    /**
129
     * Mode how to fetch the result.
130
     *
131
     * @var int
132
     */
133
    public int $fetchMode = PDO::FETCH_ASSOC;
134
135
    /**
136
     * Data source name (connection string) of the database
137
     * the user is trying to connect to.
138
     *
139
     * @var string
140
     */
141
    public string $dsn;
142
143
    /**
144
     * Username to be used to connect.
145
     *
146
     * @var string
147
     */
148
    public string $username;
149
150
    /**
151
     * Password to be used to connect.
152
     *
153
     * @var string
154
     */
155
    public string $password;
156
157
    /**
158
     * Additional connection options to be passed
159
     * to the PDO driver.
160
     *
161
     * @var array
162
     */
163
    public array $options = [];
164
165
    /**
166
     * List of middleware methods to be attached
167
     * to this connection.
168
     *
169
     * Format is:
170
     * ```php
171
     * [
172
     *    PdoDriver::STEP_OPEN => [
173
     *        function(mixed $result, array $params, callable $next) {
174
     *           // Implementation
175
     *
176
     *           $next($result); // Call next middleware in chain
177
     *        }
178
     *    ]
179
     * ]
180
     * ```
181
     *
182
     * @var array
183
     */
184
    protected array $middlewares = [];
185
186
    /**
187
     * Instance of PDO used.
188
     *
189
     * @var PDO|null
190
     */
191
    protected ?PDO $pdo = null;
192
193
    /**
194
     * Creates new instance of this PDO driver.
195
     *
196
     * @param array $config List of configuration to be passed to configure method.
197
     * @return static
198
     * @see PdoDriver::configure() for information about what to pass with $config
199
     */
200 18
    public static function create(array $config = []): static
201
    {
202 18
        $instance = new static();
203 18
        $instance->configure($config);
204
205 18
        return $instance;
206
    }
207
208
    /**
209
     * Configures the PDO driver from an array
210
     *
211
     * Array is in format:
212
     * ```php
213
     * [
214
     *    'fetchMode' => PDO::FETCH_ASSOC,
215
     *    'dsn' => 'connection string',
216
     *    'username' => 'user',
217
     *    'password' => 'pass',
218
     *    'options' => [
219
     *       // PDO options
220
     *    ],
221
     *    'middleware' => [
222
     *       // List of middleware events and methods to apply.
223
     *    ]
224
     * ]
225
     * ```
226
     *
227
     * Other configuration is passed to extendConfigure
228
     *
229
     * @param array $config
230
     * @return void
231
     * @see PdoDriver::extendConfigure()
232
     */
233 18
    public function configure(array $config): void
234
    {
235 18
        $this->fetchMode ??= $config['fetchMode'];
236 18
        $this->dsn ??= $config['dsn'];
237 18
        $this->username ??= $config['username'];
238 18
        $this->password ??= $config['password'];
239 18
        $this->options ??= $config['options'];
240
241 18
        $middlewares = $config['middleware'] ?? [];
242
243 18
        foreach ($middlewares as $step => $middlewareList) {
244 2
            $this->useMiddleware($step, $middlewareList);
245
        }
246
247 18
        $this->extendConfigure($config);
248
    }
249
250
    /**
251
     * Apply list of middlewares to a specific step.
252
     *
253
     * See STEP_ constants for available steps and method signatures.
254
     *
255
     * @param string $step Step to be used.
256
     * @param array $middlewareList List of middleware functions to apply.
257
     * @return $this
258
     */
259 2
    public function useMiddleware(string $step, array $middlewareList): static
260
    {
261 2
        $this->middlewares[$step] = array_values($middlewareList);
262 2
        return $this;
263
    }
264
265
    /**
266
     * Template methods which allows further configuring
267
     * a class which inherits this PDO driver.
268
     *
269
     * Format of the config depends on the class inheriting this.
270
     *
271
     * @param array $config Config to be processed
272
     * @return void
273
     */
274
    abstract protected function extendConfigure(array $config): void;
275
276
    /**
277
     * Closes the connection to the PDO driver.
278
     *
279
     * @return void
280
     */
281 2
    public function close(): void
282
    {
283 2
        if ($this->pdo === null) {
284 2
            return;
285
        }
286
287 1
        $this->pdo = null;
288 1
        $this->runMiddleware(self::STEP_CLOSE);
289
    }
290
291
    /**
292
     * Runs a specific middleware methods based on a step passed.
293
     *
294
     * See STEP_ for available steps and method signatures.
295
     *
296
     * @param string $step
297
     * @param mixed $result
298
     * @param mixed ...$params
299
     * @return mixed
300
     */
301 18
    protected function runMiddleware(string $step, mixed $result = null, ...$params): mixed
302
    {
303 18
        if (empty($this->middlewares[$step])) {
304 18
            return $result;
305
        }
306
307 3
        return $this->executeMiddlewareChain($step, 0, $result, $params);
308
    }
309
310
    /**
311
     * Executes a method in middleware chain and allows
312
     * that method to control further execution via $next call
313
     *
314
     * @param string $step Step to be used
315
     * @param int $index Position of the middleware in the list.
316
     * @param mixed $result Result to pass to the middleware
317
     * @param array $params Params to pass into the middleware
318
     * @return mixed Result from that middleware
319
     */
320 3
    protected function executeMiddlewareChain(string $step, int $index, mixed $result, array $params): mixed
321
    {
322 3
        if (empty($this->middlewares[$step][$index])) {
323 3
            return $result;
324
        }
325
326 3
        return $this->middlewares[$step][$index](
327 3
            $result,
328 3
            $params,
329 3
            fn($result) => $this->executeMiddlewareChain($step, $index + 1, $result, $params)
330 3
        );
331
    }
332
333
    /**
334
     * Initiate transaction handler for handling multiple
335
     * queries on the database in an atomic way.
336
     *
337
     * @return PdoTransaction
338
     */
339 5
    public function beginTransaction(): PdoTransaction
340
    {
341 5
        return PdoTransaction::create($this);
342
    }
343
344
    /**
345
     * Returns last inserted ID of a record
346
     *
347
     * @param string|null $sequenceName Name of the sequence to return from, if needed
348
     * @return false|string Last inserted ID or false if nothing was inserted
349
     */
350 1
    public function getLastInsertedId(string|null $sequenceName = null): false|string
351
    {
352 1
        return $this->getPdo()->lastInsertId($sequenceName);
353
    }
354
355
    /**
356
     * Returns an instance of a PDO used for underlying connection.
357
     *
358
     * @return PDO
359
     */
360 18
    public function getPdo(): PDO
361
    {
362 18
        $this->open();
363 18
        return $this->pdo;
364
    }
365
366
    /**
367
     * Open a connection to the database
368
     *
369
     * @return void
370
     */
371 18
    public function open(): void
372
    {
373 18
        if ($this->pdo) {
374 18
            return;
375
        }
376
377 18
        $this->pdo = $this->createPdoInstance();
378 18
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
379 18
        $this->runMiddleware(self::STEP_OPEN, $this->pdo);
380
    }
381
382
    /**
383
     * Create a new instance of the PDO for underlying connection.
384
     *
385
     * @return PDO
386
     */
387
    abstract protected function createPdoInstance(): PDO;
388
389
    /**
390
     * Executes a Raw Query
391
     *
392
     * @param RawQuery $query
393
     * @return int Returns number of affected rows
394
     */
395 18
    public function run(RawQuery $query): int
396
    {
397 18
        $query = $this->runMiddleware(self::STEP_BEFORE_RUN, $query, 'run');
398
399 18
        $statement = $this->executeStatement($query);
400 18
        $affected = $statement->rowCount();
401 18
        $statement->closeCursor();
402
403 18
        return $this->runMiddleware(self::STEP_AFTER_RUN, $affected, $query, 'run');
404
    }
405
406
    /**
407
     * Prepares and executes a PDO statement from a RawQuery
408
     *
409
     * @param RawQuery $query RawQuery to transform into a PDO statement
410
     * @return PDOStatement PDO statement which was executed
411
     */
412 18
    protected function executeStatement(RawQuery $query): PDOStatement
413
    {
414 18
        $statement = $this->prepareStatement($query);
415
416 18
        $statement->execute();
417
418 18
        return $statement;
419
    }
420
421
    /**
422
     * Prepares RawQuery as a PDO statement.
423
     *
424
     * @param RawQuery $query Raw query to prepare
425
     * @return PDOStatement Prepared PDO statement.
426
     */
427 18
    protected function prepareStatement(RawQuery $query): PDOStatement
428
    {
429 18
        $this->open();
430
431 18
        $statement = $this->getPdo()->prepare($query->getQuery(), $query->getConfig() ?? []);
432
433
        /** @var PDOStatement $statement */
434 18
        $statement = $this->runMiddleware(self::STEP_BEFORE_PREPARE, $statement, $query);
435
436
437 18
        $params = $query->getParams() ?? [];
438
439 18
        foreach ($params as $paramName => [$value, $type]) {
440 18
            if ($type === null) {
441 18
                if (is_int($value)) {
442 18
                    $type = PDO::PARAM_INT;
443 18
                } elseif (is_null($value)) {
444 18
                    $type = PDO::PARAM_NULL;
445 18
                } elseif (is_bool($value)) {
446 1
                    $type = PDO::PARAM_BOOL;
447
                } else {
448 18
                    $type = PDO::PARAM_STR;
449
                }
450
            }
451
452 18
            $statement->bindValue($paramName, $value, $type);
453
        }
454
455
456 18
        return $this->runMiddleware(self::STEP_AFTER_PREPARE, $statement, $query);
457
    }
458
459
    /**
460
     * Run a RawQuery and return the first record in the result.
461
     *
462
     * Type of the result depends on fetchMode.
463
     *
464
     * @param RawQuery $query Raw query to be run.
465
     * @return mixed Result of a first record from the query.
466
     * @see PdoDriver::$fetchMode
467
     */
468 4
    public function fetchFirst(RawQuery $query): mixed
469
    {
470
471 4
        $query = $this->runMiddleware(self::STEP_BEFORE_RUN, $query, 'first');
472
473 4
        $statement = $this->executeStatement($query);
474 4
        $result = $statement->fetch($this->fetchMode);
475 4
        $statement->closeCursor();
476
477 4
        return $this->runMiddleware(self::STEP_AFTER_RUN, $result, $query, 'first');
478
    }
479
480
    /**
481
     * Run a RawQuery and return all records in the result.
482
     *
483
     * Type of each record depends on the fetchMode
484
     *
485
     * @param RawQuery $query Raw query to be run.
486
     * @return array All results from the query
487
     * @see PdoDriver::$fetchMode
488
     */
489 1
    public function fetchAll(RawQuery $query): array
490
    {
491 1
        $query = $this->runMiddleware(self::STEP_BEFORE_RUN, $query, 'fetchAll');
492
493 1
        $statement = $this->executeStatement($query);
494 1
        $result = $statement->fetchAll($this->fetchMode);
495 1
        $statement->closeCursor();
496
497 1
        return $this->runMiddleware(self::STEP_AFTER_RUN, $result, $query, 'fetchAll');
498
    }
499
500
    /**
501
     * Run a RawQuery and return a result builder to further scope
502
     * down the result.
503
     *
504
     * @param RawQuery $query Raw query to process
505
     * @return ResultBuilder Instance of a result builder to process the result.
506
     */
507 8
    public function fetch(RawQuery $query): ResultBuilder
508
    {
509 8
        $query = $this->runMiddleware(self::STEP_BEFORE_RUN, $query, 'builder');
510
511 8
        $result = QueryResultBuilder::create()
512 8
            ->useReader($this->createResultReader($query));
513
514 8
        return $this->runMiddleware(self::STEP_AFTER_RUN, $result, $query, 'builder');
515
    }
516
517
    /**
518
     * Executes a RawQuery and returns a PDO specific reader.
519
     *
520
     * @param RawQuery $query Raw query to be processed
521
     * @return PdoResultReader PDO specific result reader.
522
     * @see PdoDriver::fetchReader()
523
     */
524 11
    protected function createResultReader(RawQuery $query): PdoResultReader
525
    {
526 11
        $result = PdoResultReader::create($this->executeStatement($query));
527 11
        $result->fetchMode = $this->fetchMode;
528
529 11
        return $result;
530
    }
531
532
    /**
533
     * @inheritDoc
534
     */
535 3
    public function fetchReader(RawQuery $query): ResultReader
536
    {
537 3
        $query = $this->runMiddleware(self::STEP_BEFORE_RUN, $query, 'reader');
538
539 3
        $result = $this->createResultReader($query);
540
541 3
        return $this->runMiddleware(self::STEP_AFTER_RUN, $result, $query, 'reader');
542
    }
543
544
    /**
545
     * Appends a middleware at the end for a specific step.
546
     *
547
     * @param string $step Step to add middleware to
548
     * @param callable $middleware Middleware to be used.
549
     * @return $this
550
     */
551 1
    public function appendMiddleware(string $step, callable $middleware): static
552
    {
553 1
        $this->middlewares[$step][] = $middleware;
554 1
        return $this;
555
    }
556
}
557