Test Failed
Push — master ( 539796...4ba9a1 )
by Gabor
04:03
created

ConnectorAdapter::getSelectQueryForExpression()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 34
ccs 16
cts 16
cp 1
rs 8.439
c 0
b 0
f 0
cc 5
eloc 20
nc 12
nop 3
crap 5
1
<?php
2
/**
3
 * WebHemi.
4
 *
5
 * PHP version 7.1
6
 *
7
 * @copyright 2012 - 2017 Gixx-web (http://www.gixx-web.com)
8
 * @license   https://opensource.org/licenses/MIT The MIT License (MIT)
9
 *
10
 * @link      http://www.gixx-web.com
11
 */
12
declare(strict_types = 1);
13
14
namespace WebHemi\Data\Connector\PDO\MySQL;
15
16
use InvalidArgumentException;
17
use PDO;
18
use PDOStatement;
19
use RuntimeException;
20
use WebHemi\Data\ConnectorInterface;
21
use WebHemi\Data\DriverInterface;
22
23
/**
24
 * Class ConnectorAdapter.
25
 */
26
class ConnectorAdapter implements ConnectorInterface
27
{
28
    /** @var string */
29
    protected $name;
30
    /** @var PDO */
31
    protected $dataDriver;
32
    /** @var string */
33
    protected $dataGroup = null;
34
    /** @var string */
35
    protected $idKey = null;
36
37
    /**
38
     * ConnectorAdapter constructor.
39
     *
40
     * @param string          $name
41
     * @param DriverInterface $dataDriver
42
     * @throws InvalidArgumentException
43
     */
44 21
    public function __construct(string $name, DriverInterface $dataDriver)
45
    {
46 21
        if (!$dataDriver instanceof DriverAdapter) {
47 1
            $type = gettype($dataDriver);
48
49 1
            if ($type == 'object') {
50 1
                $type = get_class($dataDriver);
51
            }
52
53 1
            $message = sprintf(
54 1
                'Can\'t create %s instance. The parameter must be an instance of MySQLDriver, %s given.',
55 1
                __CLASS__,
56 1
                $type
57
            );
58
59 1
            throw new InvalidArgumentException($message, 1001);
60
        }
61
62 21
        $this->name = $name;
63 21
        $this->dataDriver = $dataDriver;
64 21
    }
65
66
    /**
67
     * Stuffs to reset upon cloning.
68
     */
69 1
    public function __clone()
70
    {
71 1
        $this->idKey = null;
72 1
        $this->dataGroup = null;
73 1
    }
74
75
    /**
76
     * Returns the name of the connector.
77
     *
78
     * @return string
79
     */
80 3
    public function getConnectorName() : string
81
    {
82 3
        return $this->name;
83
    }
84
85
    /**
86
     * Returns the Data Storage instance.
87
     *
88
     * @return DriverInterface
89
     */
90 1
    public function getDataDriver() : DriverInterface
91
    {
92 1
        return $this->dataDriver;
93
    }
94
95
    /**
96
     * Set adapter data group. For Databases this can be the Tables.
97
     *
98
     * @param string $dataGroup
99
     * @return ConnectorInterface
100
     */
101 14
    public function setDataGroup(string $dataGroup) : ConnectorInterface
102
    {
103 14
        $this->dataGroup = $dataGroup;
104
105 14
        return $this;
106
    }
107
108
    /**
109
     * Set adapter ID key. For Databases this can be the Primary key. Only simple key is allowed.
110
     *
111
     * @param string $idKey
112
     * @return ConnectorInterface
113
     */
114 14
    public function setIdKey(string $idKey) : ConnectorInterface
115
    {
116 14
        $this->idKey = $idKey;
117
118 14
        return $this;
119
    }
120
121
    /**
122
     * Returns the CREATE TABLE statement.
123
     *
124
     * @param string $tableName
125
     * @return string
126
     *
127
     * @codeCoverageIgnore Don't test external library.
128
     */
129
    public function getTableDefinition(string $tableName) : string
130
    {
131
        $createStatement = '';
132
        /** @var PDO $driver */
133
        $driver = $this->getDataDriver();
134
        $result = $driver->query('SHOW CREATE TABLE '.$tableName);
135
136
        if ($result) {
137
            $createStatement = preg_replace('/(\sAUTO_INCREMENT\=\d+)/', '', $result->fetchColumn(1));
138
        }
139
140
        return $createStatement;
141
    }
142
143
    /**
144
     * Get exactly one "row" of data according to the expression.
145
     *
146
     * @param int $identifier
147
     * @return array
148
     *
149
     * @codeCoverageIgnore Don't test external library.
150
     */
151
    public function getData(int $identifier) : array
152
    {
153
        $queryBinds = [];
154
155
        $query = $this->getSelectQueryForExpression(
156
            [$this->idKey => $identifier],
157
            $queryBinds,
158
            [self::OPTION_LIMIT => 1, self::OPTION_OFFSET => 0]
159
        );
160
        $statement = $this->dataDriver->prepare($query);
161
        $this->bindValuesToStatement($statement, $queryBinds);
162
        $statement->execute();
163
164
        $data = $statement->fetch(PDO::FETCH_ASSOC);
165
166
        return $data ? $data : [];
167
    }
168
169
    /**
170
     * Get a set of data according to the expression and the chunk.
171
     *
172
     * @param array $expression
173
     * @param array $options
174
     * @return array
175
     *
176
     * @codeCoverageIgnore Don't test external library.
177
     */
178
    public function getDataSet(array $expression, array $options = []) : array
179
    {
180
        $queryBinds = [];
181
182
        $query = $this->getSelectQueryForExpression($expression, $queryBinds, $options);
183
        $statement = $this->dataDriver->prepare($query);
184
185
        $this->bindValuesToStatement($statement, $queryBinds);
186
        $statement->execute();
187
188
        $data = $statement->fetchAll(PDO::FETCH_ASSOC);
189
190
        return $data ? $data : [];
191
    }
192
193
    /**
194
     * Get the number of matched data in the set according to the expression.
195
     *
196
     * @param array $expression
197
     * @return int
198
     *
199
     * @codeCoverageIgnore Don't test external library.
200
     */
201
    public function getDataCardinality(array $expression) : int
202
    {
203
        $queryBinds = [];
204
205
        $query = $this->getSelectQueryForExpression($expression, $queryBinds, []);
206
        $statement = $this->dataDriver->prepare($query);
207
        $this->bindValuesToStatement($statement, $queryBinds);
208
        $statement->execute();
209
210
        return $statement->rowCount();
211
    }
212
213
    /**
214
     * Builds SQL query from the expression.
215
     *
216
     * @param array $expression
217
     * @param array $queryBinds
218
     * @param array $options
219
     * @return string
220
     */
221 12
    protected function getSelectQueryForExpression(
222
        array $expression,
223
        array&$queryBinds,
224
        array $options = []
225
    ) : string {
226 12
        $query = "SELECT * FROM {$this->dataGroup}";
227
228
        // Prepare WHERE expression.
229 12
        if (!empty($expression)) {
230 11
            $query .= $this->getWhereExpression($expression, $queryBinds);
231
        }
232
233 12
        $group = $this->getQueryGroup($options);
234 12
        $having = $this->getQueryHaving($options);
235
236 12
        if (!empty($group)) {
237 6
            $query .= " GROUP BY {$group}";
238
239 6
            if (!empty($having)) {
240 1
                $query .= " HAVING {$having}";
241
            }
242
        }
243
244 12
        $query .= " ORDER BY {$this->getQueryOrder($options)}";
245
246 12
        $limit = $this->getQueryLimit($options);
247
248 12
        if ($limit > 0) {
249 5
            $query .= " LIMIT {$limit}";
250 5
            $query .= " OFFSET {$this->getQueryOffset($options)}";
251
        }
252
253 12
        return $query;
254
    }
255
256
    /**
257
     * Gets the GROUP BY expression.
258
     *
259
     * @param array $options
260
     * @return string
261
     */
262 12
    protected function getQueryGroup(array $options) : string
263
    {
264 12
        return $options[self::OPTION_GROUP] ?? '';
265
    }
266
267
    /**
268
     * Gets the HAVING expression only when the GROUP BY option exists.
269
     *
270
     * @param array $options
271
     * @return string
272
     */
273 12
    protected function getQueryHaving(array $options) : string
274
    {
275 12
        return $options[self::OPTION_HAVING] ?? '';
276
    }
277
278
    /**
279
     * Gets the ORDER BY expression. The default value is the primary key.
280
     *
281
     * @param array $options
282
     * @return string
283
     */
284 12
    protected function getQueryOrder(array $options) : string
285
    {
286 12
        return $options[self::OPTION_ORDER] ?? $this->idKey;
287
    }
288
289
    /**
290
     * Gets the LIMIT expression.
291
     *
292
     * @param array $options
293
     * @return int
294
     */
295 12
    protected function getQueryLimit(array $options) : int
296
    {
297 12
        return $options[self::OPTION_LIMIT] ?? 0;
298
    }
299
300
    /**
301
     * Gets the OFFSET expression.
302
     *
303
     * @param array $options
304
     * @return int
305
     */
306 5
    protected function getQueryOffset(array $options) : int
307
    {
308 5
        return $options[self::OPTION_OFFSET] ?? 0;
309
    }
310
311
    /**
312
     * Creates a WHERE expression for the SQL query.
313
     *
314
     * @param array $expression
315
     * @param array $queryBinds
316
     * @return string
317
     */
318 15
    protected function getWhereExpression(array $expression, array&$queryBinds) : string
319
    {
320 15
        $whereExpression = '';
321 15
        $queryParams = [];
322
323 15
        foreach ($expression as $column => $value) {
324 14
            $this->setParamsAndBinds($column, $value, $queryParams, $queryBinds);
325
        }
326
327 15
        if (!empty($queryParams)) {
328 14
            $whereExpression = ' WHERE '.implode(' AND ', $queryParams);
329
        }
330
331 15
        return $whereExpression;
332
    }
333
334
    /**
335
     * Set the query params and quaery bindings according to the `column` and `value`.
336
     *
337
     * @param string $column
338
     * @param mixed  $value
339
     * @param array  $queryParams
340
     * @param array  $queryBinds
341
     */
342 14
    protected function setParamsAndBinds(string $column, $value, array&$queryParams, array&$queryBinds) : void
343
    {
344 14
        if (is_array($value)) {
345 5
            $queryParams[] = $this->getInColumnCondition($column, count($value));
346 5
            $queryBinds = array_merge($queryBinds, $value);
347 14
        } elseif (strpos($column, ' LIKE') !== false || (is_string($value) && strpos($value, '%') !== false)) {
348 4
            $queryParams[] = $this->getLikeColumnCondition($column);
349 4
            $queryBinds[] = $value;
350 14
        } elseif ($value === true) {
351 1
            $queryParams[] = "{$column} IS NOT NULL";
352 13
        } elseif (is_null($value) || $value === false) {
353 2
            $queryParams[] = "{$column} IS NULL";
354
        } else {
355 11
            $queryParams[] = $this->getSimpleColumnCondition($column);
356 11
            $queryBinds[] = $value;
357
        }
358 14
    }
359
360
    /**
361
     * Gets a simple condition for the column.
362
     *
363
     * @param string $column
364
     * @return string 'my_column = ?'
365
     */
366 11
    protected function getSimpleColumnCondition(string $column) : string
367
    {
368 11
        return strpos($column, '?') === false ? "{$column} = ?" : $column;
369
    }
370
371
    /**
372
     * Gets a 'LIKE' condition for the column.
373
     *
374
     * Allows special cases:
375
     * @example  ['my_column LIKE ?' => 'some value%']
376
     * @example  ['my_column NOT' => 'some value%']
377
     * @example  ['my_column' => 'some value%']
378
     *
379
     * @param string $column
380
     * @return string 'my_column LIKE ?' or 'my_column NOT LIKE ?'
381
     */
382 4
    protected function getLikeColumnCondition(string $column) : string
383
    {
384 4
        $like = strpos(' NOT ', $column) !== false ? ' NOT LIKE ' : ' LIKE ';
385
386 4
        list($columnNameOnly) = explode(' ', trim($column));
387
388 4
        return $columnNameOnly.$like.'?';
389
    }
390
391
    /**
392
     * Gets an 'IN' condition for the column.
393
     *
394
     * Allows special cases:
395
     * @example  ['my_column IN (?)' => [1,2,3]]
396
     * @example  ['my_column IN ?' => [1,2,3]]
397
     * @example  ['my_column IN' => [1,2,3]]
398
     * @example  ['my_column' => [1,2,3]]
399
     *
400
     * @param string $column
401
     * @param int    $parameterCount
402
     * @return string 'my_column IN (?,?,?)'
403
     */
404 5
    protected function getInColumnCondition(string $column, int $parameterCount = 1) : string
405
    {
406 5
        list($columnNameOnly) = explode(' ', $column);
407
408 5
        $inParameters = str_repeat('?,', $parameterCount - 1).'?';
409
410 5
        return $columnNameOnly.' IN ('.$inParameters.')';
411
    }
412
413
    /**
414
     * Insert or update entity in the storage.
415
     *
416
     * @param int   $identifier
417
     * @param array $data
418
     * @throws RuntimeException
419
     * @return int The ID of the saved entity in the storage
420
     *
421
     * @codeCoverageIgnore Don't test external library.
422
     */
423
    public function saveData(? int $identifier = null, array $data = []) : int
424
    {
425
        if (empty($identifier)) {
426
            $query = "INSERT INTO {$this->dataGroup}";
427
        } else {
428
            $query = "UPDATE {$this->dataGroup}";
429
        }
430
431
        $queryData = [];
432
        $queryBind = [];
433
434
        foreach ($data as $fieldName => $value) {
435
            $queryData[] = "{$fieldName} = ?";
436
            $queryBind[] = $value;
437
        }
438
439
        $query .= ' SET '.implode(', ', $queryData);
440
441
        if (!empty($identifier)) {
442
            $query .= " WHERE {$this->idKey} = ?";
443
            $queryBind[] = $identifier;
444
        }
445
446
        $statement = $this->dataDriver->prepare($query);
447
        if (!$statement) {
448
            throw new RuntimeException('Query error', 1002);
449
        }
450
        $this->bindValuesToStatement($statement, $queryBind);
451
        $statement->execute();
452
453
        return empty($identifier) ? (int) $this->dataDriver->lastInsertId() : $identifier;
454
    }
455
456
    /**
457
     * Binds values to the statement.
458
     *
459
     * @param PDOStatement $statement
460
     * @param array        $queryBind
461
     * @return void
462
     *
463
     * @codeCoverageIgnore Don't test external library.
464
     */
465
    protected function bindValuesToStatement(PDOStatement&$statement, array $queryBind) : void
466
    {
467
        foreach ($queryBind as $index => $data) {
468
            $paramType = PDO::PARAM_STR;
469
470
            if (is_null($data)) {
471
                $paramType = PDO::PARAM_NULL;
472
            } elseif (is_numeric($data)) {
473
                $paramType = PDO::PARAM_INT;
474
            }
475
476
            $statement->bindValue($index + 1, $data, $paramType);
477
        }
478
    }
479
480
    /**
481
     * Removes an entity from the storage.
482
     *
483
     * @param int $identifier
484
     * @return bool
485
     *
486
     * @codeCoverageIgnore Don't test external library.
487
     */
488
    public function deleteData(int $identifier) : bool
489
    {
490
        $statement = $this->dataDriver->prepare("DELETE FROM WHERE {$this->idKey} = ?");
491
492
        return $statement->execute([$identifier]);
493
    }
494
}
495