Passed
Push — master ( 5ff919...5bf459 )
by Gabor
05:53
created

ConnectorAdapter::saveData()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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