Passed
Push — master ( b85cf8...7514fb )
by Aleksandar
02:26
created

PdoDriver::runMiddleware()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
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 Exception;
26
use PDO;
27
use PDOStatement;
28
29
/**
30
 * Wrapping driver for PDO connections.
31
 */
32
abstract class PdoDriver implements Driver
33
{
34
    /**
35
     * Middleware step after a connection is open.
36
     *
37
     * Function signature for event:
38
     * ```php
39
     * function($result, $params, $next) {
40
     *    // Implementation
41
     *
42
     *    // $result is a PDO instance
43
     *
44
     *    $next($result)
45
     * }
46
     */
47
    const STEP_OPEN = 'open';
48
49
    /**
50
     * Middleware step after a connection is closed
51
     *
52
     * Function signature for event:
53
     * ```php
54
     * function($result, $params, $next) {
55
     *    // Implementation
56
     *
57
     *    // $result is null
58
     *
59
     *    $next($result)
60
     * }
61
     */
62
    const STEP_CLOSE = 'close';
63
64
    /**
65
     * Middleware step before a query is run.
66
     *
67
     * Function signature for event:
68
     * ```php
69
     * function($result, $params, $next) {
70
     *    // Implementation
71
     *
72
     *    // $result is a RawQuery
73
     *    // $params[0] is the type of the method used 'run', 'first', 'all', etc.
74
     *
75
     *    $next($result)
76
     * }
77
     */
78
    const STEP_BEFORE_RUN = 'before-run';
79
80
    /**
81
     * Middleware step before a statement is fully prepared.
82
     *
83
     * Function signature for event:
84
     * ```php
85
     * function($result, $params, $next) {
86
     *    // Implementation
87
     *
88
     *    // $result is a PdoStatement
89
     *    // $params[0] is RawQuery used
90
     *
91
     *    $next($result)
92
     * }
93
     */
94
    const STEP_BEFORE_PREPARE = 'before-prepare';
95
96
    /**
97
     * Middleware step after a statement is fully prepared.
98
     *
99
     * Function signature for event:
100
     * ```php
101
     * function($result, $params, $next) {
102
     *    // Implementation
103
     *
104
     *    // $result is a PdoStatement
105
     *    // $params[0] is RawQuery used
106
     *
107
     *    $next($result)
108
     * }
109
     */
110
    const STEP_AFTER_PREPARE = 'after-prepare';
111
112
    /**
113
     * Middleware step after a query is run.
114
     *
115
     * Function signature for event:
116
     * ```php
117
     * function($result, $params, $next) {
118
     *    // Implementation
119
     *
120
     *    // $result is a number of affected rows
121
     *    // $params[0] is RawQuery used
122
     *    // $params[1] is the type of the method used 'run', 'first', 'all', etc.
123
     *
124
     *    $next($result)
125
     * }
126
     */
127
    const STEP_AFTER_RUN = 'after-run';
128
129
    /**
130
     * Mode how to fetch the result.
131
     *
132
     * @var int
133
     */
134
    public int $fetchMode = PDO::FETCH_ASSOC;
135
136
    /**
137
     * Data source name (connection string) of the database
138
     * the user is trying to connect to.
139
     *
140
     * @var string
141
     */
142
    public string $dsn;
143
144
    /**
145
     * Username to be used to connect.
146
     *
147
     * @var string
148
     */
149
    public string $username;
150
151
    /**
152
     * Password to be used to connect.
153
     *
154
     * @var string
155
     */
156
    public string $password;
157
158
    /**
159
     * Additional connection options to be passed
160
     * to the PDO driver.
161
     *
162
     * @var array
163
     */
164
    public array $options = [];
165
166
    /**
167
     * List of middleware methods to be attached
168
     * to this connection.
169
     *
170
     * Format is:
171
     * ```php
172
     * [
173
     *    PdoDriver::STEP_OPEN => [
174
     *        function(mixed $result, array $params, callable $next) {
175
     *           // Implementation
176
     *
177
     *           $next($result); // Call next middleware in chain
178
     *        }
179
     *    ]
180
     * ]
181
     * ```
182
     *
183
     * @var array
184
     */
185
    protected array $middlewares = [];
186
187
    /**
188
     * Instance of PDO used.
189
     *
190
     * @var PDO|null
191
     */
192
    protected ?PDO $pdo = null;
193
194
    /**
195
     * Creates new instance of this PDO driver.
196
     *
197
     * @param array $config List of configuration to be passed to configure method.
198
     * @return static
199
     * @see PdoDriver::configure() for information about what to pass with $config
200
     */
201 16
    public static function create(array $config = []): static
202
    {
203 16
        $instance = new static();
204 16
        $instance->configure($config);
205
206 16
        return $instance;
207
    }
208
209
    /**
210
     * Configures the PDO driver from an array
211
     *
212
     * Array is in format:
213
     * ```php
214
     * [
215
     *    'fetchMode' => PDO::FETCH_ASSOC,
216
     *    'dsn' => 'connection string',
217
     *    'username' => 'user',
218
     *    'password' => 'pass',
219
     *    'options' => [
220
     *       // PDO options
221
     *    ],
222
     *    'middleware' => [
223
     *       // List of middleware events and methods to apply.
224
     *    ]
225
     * ]
226
     * ```
227
     *
228
     * Other configuration is passed to extendConfigure
229
     *
230
     * @param array $config
231
     * @return void
232
     * @see PdoDriver::extendConfigure()
233
     */
234 16
    public function configure(array $config): void
235
    {
236 16
        $this->fetchMode ??= $config['fetchMode'];
237 16
        $this->dsn ??= $config['dsn'];
238 16
        $this->username ??= $config['username'];
239 16
        $this->password ??= $config['password'];
240 16
        $this->options ??= $config['options'];
241
242 16
        $middlewares = $config['middleware'] ?? [];
243
244 16
        foreach ($middlewares as $step => $middlewareList) {
245 2
            $this->useMiddleware($step, $middlewareList);
246
        }
247
248 16
        $this->extendConfigure($config);
249
    }
250
251
    /**
252
     * Apply list of middlewares to a specific step.
253
     *
254
     * See STEP_ constants for available steps and method signatures.
255
     *
256
     * @param string $step Step to be used.
257
     * @param array $middlewareList List of middleware functions to apply.
258
     * @return $this
259
     */
260 2
    public function useMiddleware(string $step, array $middlewareList): static
261
    {
262 2
        $this->middlewares[$step] = array_values($middlewareList);
263 2
        return $this;
264
    }
265
266
    /**
267
     * Template methods which allows further configuring
268
     * a class which inherits this PDO driver.
269
     *
270
     * Format of the config depends on the class inheriting this.
271
     *
272
     * @param array $config Config to be processed
273
     * @return void
274
     */
275
    abstract protected function extendConfigure(array $config): void;
276
277
    /**
278
     * Closes the connection to the PDO driver.
279
     *
280
     * @return void
281
     */
282 2
    public function close(): void
283
    {
284 2
        if ($this->pdo === null) {
285 2
            return;
286
        }
287
288 1
        $this->pdo = null;
289 1
        $this->runMiddleware(self::STEP_CLOSE);
290
    }
291
292
    /**
293
     * Runs a specific middleware methods based on a step passed.
294
     *
295
     * See STEP_ for available steps and method signatures.
296
     *
297
     * @param string $step
298
     * @param mixed $result
299
     * @param mixed ...$params
300
     * @return mixed
301
     */
302 16
    protected function runMiddleware(string $step, mixed $result = null, ...$params): mixed
303
    {
304 16
        if (empty($this->middlewares[$step])) {
305 16
            return $result;
306
        }
307
308 3
        return $this->executeMiddlewareChain($step, 0, $result, $params);
309
    }
310
311
    /**
312
     * Executes a method in middleware chain and allows
313
     * that method to control further execution via $next call
314
     *
315
     * @param string $step Step to be used
316
     * @param int $index Position of the middleware in the list.
317
     * @param mixed $result Result to pass to the middleware
318
     * @param array $params Params to pass into the middleware
319
     * @return mixed Result from that middleware
320
     */
321 3
    protected function executeMiddlewareChain(string $step, int $index, mixed $result, array $params): mixed
322
    {
323 3
        if (empty($this->middlewares[$step][$index])) {
324 3
            return $result;
325
        }
326
327 3
        return $this->middlewares[$step][$index](
328 3
            $result,
329 3
            $params,
330 3
            fn($result) => $this->executeMiddlewareChain($step, $index + 1, $result, $params)
331 3
        );
332
    }
333
334
    /**
335
     * Initiate transaction handler for handling multiple
336
     * queries on the database in an atomic way.
337
     *
338
     * @return PdoTransaction
339
     */
340 3
    public function beginTransaction(): PdoTransaction
341
    {
342 3
        return PdoTransaction::create($this);
343
    }
344
345
    /**
346
     * Returns last inserted ID of a record
347
     *
348
     * @param string|null $sequenceName Name of the sequence to return from, if needed
349
     * @return false|string Last inserted ID or false if nothing was inserted
350
     */
351 1
    public function getLastInsertedId(string|null $sequenceName = null): false|string
352
    {
353 1
        return $this->getPdo()->lastInsertId($sequenceName);
354
    }
355
356
    /**
357
     * Returns an instance of a PDO used for underlying connection.
358
     *
359
     * @return PDO
360
     */
361 16
    public function getPdo(): PDO
362
    {
363 16
        $this->open();
364 16
        return $this->pdo;
365
    }
366
367
    /**
368
     * Open a connection to the database
369
     *
370
     * @return void
371
     */
372 16
    public function open(): void
373
    {
374 16
        if ($this->pdo) {
375 16
            return;
376
        }
377
378 16
        $this->pdo = $this->createPdoInstance();
379 16
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
380 16
        $this->runMiddleware(self::STEP_OPEN, $this->pdo);
381
    }
382
383
    /**
384
     * Create a new instance of the PDO for underlying connection.
385
     *
386
     * @return PDO
387
     */
388
    abstract protected function createPdoInstance(): PDO;
389
390
    /**
391
     * Executes a Raw Query
392
     *
393
     * @param RawQuery $query
394
     * @return int Returns number of affected rows
395
     */
396 16
    public function run(RawQuery $query): int
397
    {
398 16
        $query = $this->runMiddleware(self::STEP_BEFORE_RUN, $query, 'run');
399
400 16
        $statement = $this->executeStatement($query);
401 16
        $affected = $statement->rowCount();
402 16
        $statement->closeCursor();
403
404 16
        return $this->runMiddleware(self::STEP_AFTER_RUN, $affected, $query, 'run');
405
    }
406
407
    /**
408
     * Prepares and executes a PDO statement from a RawQuery
409
     *
410
     * @param RawQuery $query RawQuery to transform into a PDO statement
411
     * @return PDOStatement PDO statement which was executed
412
     */
413 16
    protected function executeStatement(RawQuery $query): PDOStatement
414
    {
415 16
        $statement = $this->prepareStatement($query);
416
417 16
        $statement->execute();
418
419 16
        return $statement;
420
    }
421
422
    /**
423
     * Prepares RawQuery as a PDO statement.
424
     *
425
     * @param RawQuery $query Raw query to prepare
426
     * @return PDOStatement Prepared PDO statement.
427
     */
428 16
    protected function prepareStatement(RawQuery $query): PDOStatement
429
    {
430 16
        $this->open();
431
432 16
        $statement = $this->getPdo()->prepare($query->getQuery(), $query->getConfig() ?? []);
433
434
        /** @var PDOStatement $statement */
435 16
        $statement = $this->runMiddleware(self::STEP_BEFORE_PREPARE, $statement, $query);
436
437 16
        $params = $query->getParams() ?? [];
438
439 16
        foreach ($params as $paramName => [$value, $type]) {
440 16
            if ($type === null) {
441 16
                if (is_int($value)) {
442 16
                    $type = PDO::PARAM_INT;
443 16
                } elseif (is_null($value)) {
444 16
                    $type = PDO::PARAM_NULL;
445 16
                } elseif (is_bool($value)) {
446 1
                    $type = PDO::PARAM_BOOL;
447
                } else {
448 16
                    $type = PDO::PARAM_STR;
449
                }
450
            }
451
452 16
            $statement->bindValue($paramName, $value, $type);
453
        }
454
455
456 16
        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 6
    public function fetch(RawQuery $query): ResultBuilder
508
    {
509 6
        $query = $this->runMiddleware(self::STEP_BEFORE_RUN, $query, 'builder');
510
511 6
        $result = QueryResultBuilder::create()
512 6
            ->useReader($this->createResultReader($query));
513
514 6
        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 9
    protected function createResultReader(RawQuery $query): PdoResultReader
525
    {
526 9
        $result = PdoResultReader::create($this->executeStatement($query));
527 9
        $result->fetchMode = $this->fetchMode;
528
529 9
        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