Passed
Push — master ( 5c25b8...502eee )
by Gabor
05:16
created

ConnectorAdapter::getTableDefinition()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 0
cts 7
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
crap 6
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 19
    public function __construct(string $name, DriverInterface $dataDriver)
45
    {
46 19
        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
                $type
57
            );
58
59 1
            throw new InvalidArgumentException($message, 1001);
60
        }
61
62 19
        $this->name = $name;
63 19
        $this->dataDriver = $dataDriver;
64 19
    }
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 12
    public function setDataGroup(string $dataGroup) : ConnectorInterface
102
    {
103 12
        $this->dataGroup = $dataGroup;
104
105 12
        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 12
    public function setIdKey(string $idKey) : ConnectorInterface
115
    {
116 12
        $this->idKey = $idKey;
117
118 12
        return $this;
119
    }
120
121
    /**
122
     * Returns the CREATE TABLE statement.
123
     *
124
     * @param string $tableName
125
     * @return string
126
     */
127
    public function getTableDefinition(string $tableName) : string
128
    {
129
        $createStatement = '';
130
        /** @var PDO $driver */
131
        $driver = $this->getDataDriver();
132
        $result = $driver->query('SHOW CREATE TABLE '.$tableName);
133
134
        if ($result) {
135
            $createStatement = preg_replace('/(\sAUTO_INCREMENT\=\d+)/', '', $result->fetchColumn(1));
136
        }
137
138
        return $createStatement;
139
    }
140
141
    /**
142
     * Get exactly one "row" of data according to the expression.
143
     *
144
     * @param int $identifier
145
     * @return array
146
     *
147
     * @codeCoverageIgnore Don't test external library.
148
     */
149
    public function getData(int $identifier) : array
150
    {
151
        $queryBinds = [];
152
153
        $query = $this->getSelectQueryForExpression(
154
            [$this->idKey => $identifier],
155
            $queryBinds,
156
            [self::OPTION_LIMIT => 1, self::OPTION_OFFSET => 0]
157
        );
158
        $statement = $this->dataDriver->prepare($query);
159
        $this->bindValuesToStatement($statement, $queryBinds);
160
        $statement->execute();
161
162
        $data = $statement->fetch(PDO::FETCH_ASSOC);
163
164
        return $data ? $data : [];
165
    }
166
167
    /**
168
     * Get a set of data according to the expression and the chunk.
169
     *
170
     * @param array $expression
171
     * @param array $options
172
     * @return array
173
     *
174
     * @codeCoverageIgnore Don't test external library.
175
     */
176
    public function getDataSet(array $expression, array $options = []) : array
177
    {
178
        $queryBinds = [];
179
180
        $query = $this->getSelectQueryForExpression($expression, $queryBinds, $options);
181
        $statement = $this->dataDriver->prepare($query);
182
183
        $this->bindValuesToStatement($statement, $queryBinds);
184
        $statement->execute();
185
186
        $data = $statement->fetchAll(PDO::FETCH_ASSOC);
187
188
        return $data ? $data : [];
189
    }
190
191
    /**
192
     * Get the number of matched data in the set according to the expression.
193
     *
194
     * @param array $expression
195
     * @return int
196
     *
197
     * @codeCoverageIgnore Don't test external library.
198
     */
199
    public function getDataCardinality(array $expression) : int
200
    {
201
        $queryBinds = [];
202
203
        $query = $this->getSelectQueryForExpression($expression, $queryBinds, []);
204
        $statement = $this->dataDriver->prepare($query);
205
        $this->bindValuesToStatement($statement, $queryBinds);
206
        $statement->execute();
207
208
        return $statement->rowCount();
209
    }
210
211
    /**
212
     * Builds SQL query from the expression.
213
     *
214
     * @param array $expression
215
     * @param array $queryBinds
216
     * @param array $options
217
     * @return string
218
     */
219 10
    protected function getSelectQueryForExpression(
220
        array $expression,
221
        array&$queryBinds,
222
        array $options = []
223
    ) : string {
224 10
        $query = "SELECT * FROM {$this->dataGroup}";
225
226
        // Prepare WHERE expression.
227 10
        if (!empty($expression)) {
228 9
            $query .= $this->getWhereExpression($expression, $queryBinds);
229
        }
230
231 10
        $group = $this->getQueryGroup($options);
232 10
        $having = $this->getQueryHaving($options);
233
234 10
        if (!empty($group)) {
235 4
            $query .= " GROUP BY {$group}";
236
237 4
            if (!empty($having)) {
238 1
                $query .= " HAVING {$having}";
239
            }
240
        }
241
242 10
        $query .= " ORDER BY {$this->getQueryOrder($options)}";
243
244 10
        $limit = $this->getQueryLimit($options);
245
246 10
        if ($limit > 0) {
247 5
            $query .= " LIMIT {$limit}";
248 5
            $query .= " OFFSET {$this->getQueryOffset($options)}";
249
        }
250
251 10
        return $query;
252
    }
253
254
    /**
255
     * Gets the GROUP BY expression.
256
     *
257
     * @param array $options
258
     * @return string
259
     */
260 10
    protected function getQueryGroup(array $options) : string
261
    {
262 10
        return $options[self::OPTION_GROUP] ?? '';
263
    }
264
265
    /**
266
     * Gets the HAVING expression only when the GROUP BY option exists.
267
     *
268
     * @param array $options
269
     * @return string
270
     */
271 10
    protected function getQueryHaving(array $options) : string
272
    {
273 10
        return isset($options[self::OPTION_GROUP]) ? $options[self::OPTION_HAVING] : '';
274
    }
275
276
    /**
277
     * Gets the ORDER BY expression. The default value is the primary key.
278
     *
279
     * @param array $options
280
     * @return string
281
     */
282 10
    protected function getQueryOrder(array $options) : string
283
    {
284 10
        return $options[self::OPTION_ORDER] ?? $this->idKey;
285
    }
286
287
    /**
288
     * Gets the LIMIT expression.
289
     *
290
     * @param array $options
291
     * @return int
292
     */
293 10
    protected function getQueryLimit(array $options) : int
294
    {
295 10
        return $options[self::OPTION_LIMIT] ?? 0;
296
    }
297
298
    /**
299
     * Gets the OFFSET expression.
300
     *
301
     * @param array $options
302
     * @return int
303
     */
304 5
    protected function getQueryOffset(array $options) : int
305
    {
306 5
        return $options[self::OPTION_OFFSET] ?? 0;
307
    }
308
309
    /**
310
     * Creates a WHERE expression for the SQL query.
311
     *
312
     * @param array $expression
313
     * @param array $queryBinds
314
     * @return string
315
     */
316 13
    protected function getWhereExpression(array $expression, array&$queryBinds) : string
317
    {
318 13
        $whereExpression = '';
319 13
        $queryParams = [];
320
321 13
        foreach ($expression as $column => $value) {
322 12
            $this->setParamsAndBinds($column, $value, $queryParams, $queryBinds);
323
        }
324
325 13
        if (!empty($queryParams)) {
326 12
            $whereExpression = ' WHERE '.implode(' AND ', $queryParams);
327
        }
328
329 13
        return $whereExpression;
330
    }
331
332
    /**
333
     * Set the query params and quaery bindings according to the `column` and `value`.
334
     *
335
     * @param string $column
336
     * @param mixed  $value
337
     * @param array  $queryParams
338
     * @param array  $queryBinds
339
     */
340 12
    protected function setParamsAndBinds(string $column, $value, array&$queryParams, array&$queryBinds) : void
341
    {
342 12
        if (is_array($value)) {
343 5
            $queryParams[] = $this->getInColumnCondition($column, count($value));
344 5
            $queryBinds = array_merge($queryBinds, $value);
345 12
        } elseif (strpos($column, ' LIKE') !== false || (is_string($value) && strpos($value, '%') !== false)) {
346 4
            $queryParams[] = $this->getLikeColumnCondition($column);
347 4
            $queryBinds[] = $value;
348 12
        } elseif (is_null($value)) {
349 1
            $queryParams[] = "{$column} IS NULL";
350
        } else {
351 11
            $queryParams[] = $this->getSimpleColumnCondition($column);
352 11
            $queryBinds[] = $value;
353
        }
354 12
    }
355
356
    /**
357
     * Gets a simple condition for the column.
358
     *
359
     * @param string $column
360
     * @return string 'my_column = ?'
361
     */
362 11
    protected function getSimpleColumnCondition(string $column) : string
363
    {
364 11
        return strpos($column, '?') === false ? "{$column} = ?" : $column;
365
    }
366
367
    /**
368
     * Gets a 'LIKE' condition for the column.
369
     *
370
     * Allows special cases:
371
     * @example  ['my_column LIKE ?' => 'some value%']
372
     * @example  ['my_column LIKE' => 'some value%']
373
     * @example  ['my_column' => 'some value%']
374
     *
375
     * @param string $column
376
     * @return string 'my_column LIKE ?'
377
     */
378 4
    protected function getLikeColumnCondition(string $column) : string
379
    {
380 4
        list($columnNameOnly) = explode(' ', $column);
381
382 4
        return $columnNameOnly.' LIKE ?';
383
    }
384
385
    /**
386
     * Gets an 'IN' condition for the column.
387
     *
388
     * Allows special cases:
389
     * @example  ['my_column IN (?)' => [1,2,3]]
390
     * @example  ['my_column IN ?' => [1,2,3]]
391
     * @example  ['my_column IN' => [1,2,3]]
392
     * @example  ['my_column' => [1,2,3]]
393
     *
394
     * @param string $column
395
     * @param int    $parameterCount
396
     * @return string 'my_column IN (?,?,?)'
397
     */
398 5
    protected function getInColumnCondition(string $column, int $parameterCount = 1) : string
399
    {
400 5
        list($columnNameOnly) = explode(' ', $column);
401
402 5
        $inParameters = str_repeat('?,', $parameterCount - 1).'?';
403
404 5
        return $columnNameOnly.' IN ('.$inParameters.')';
405
    }
406
407
    /**
408
     * Insert or update entity in the storage.
409
     *
410
     * @param int   $identifier
411
     * @param array $data
412
     * @throws RuntimeException
413
     * @return int The ID of the saved entity in the storage
414
     *
415
     * @codeCoverageIgnore Don't test external library.
416
     */
417
    public function saveData(? int $identifier = null, array $data = []) : int
418
    {
419
        if (empty($identifier)) {
420
            $query = "INSERT INTO {$this->dataGroup}";
421
        } else {
422
            $query = "UPDATE {$this->dataGroup}";
423
        }
424
425
        $queryData = [];
426
        $queryBind = [];
427
428
        foreach ($data as $fieldName => $value) {
429
            $queryData[] = "{$fieldName} = ?";
430
            $queryBind[] = $value;
431
        }
432
433
        $query .= ' SET '.implode(', ', $queryData);
434
435
        if (!empty($identifier)) {
436
            $query .= " WHERE {$this->idKey} = ?";
437
            $queryBind[] = $identifier;
438
        }
439
440
        $statement = $this->dataDriver->prepare($query);
441
        if (!$statement) {
442
            throw new RuntimeException('Query error', 1002);
443
        }
444
        $this->bindValuesToStatement($statement, $queryBind);
445
        $statement->execute();
446
447
        return empty($identifier) ? (int) $this->dataDriver->lastInsertId() : $identifier;
448
    }
449
450
    /**
451
     * Binds values to the statement.
452
     *
453
     * @param PDOStatement $statement
454
     * @param array        $queryBind
455
     * @return void
456
     *
457
     * @codeCoverageIgnore Don't test external library.
458
     */
459
    protected function bindValuesToStatement(PDOStatement&$statement, array $queryBind) : void
460
    {
461
        foreach ($queryBind as $index => $data) {
462
            $paramType = PDO::PARAM_STR;
463
464
            if (is_null($data)) {
465
                $paramType = PDO::PARAM_NULL;
466
            } elseif (is_numeric($data)) {
467
                $paramType = PDO::PARAM_INT;
468
            }
469
470
            $statement->bindValue($index + 1, $data, $paramType);
471
        }
472
    }
473
474
    /**
475
     * Removes an entity from the storage.
476
     *
477
     * @param int $identifier
478
     * @return bool
479
     *
480
     * @codeCoverageIgnore Don't test external library.
481
     */
482
    public function deleteData(int $identifier) : bool
483
    {
484
        $statement = $this->dataDriver->prepare("DELETE FROM WHERE {$this->idKey} = ?");
485
486
        return $statement->execute([$identifier]);
487
    }
488
}
489