Passed
Push — master ( 455d8b...50583e )
by Gabor
09:52
created

ConnectorAdapter   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 488
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 53
lcom 1
cbo 0
dl 0
loc 488
ccs 88
cts 88
cp 1
rs 7.4757
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A init() 0 18 3
A __clone() 0 5 1
A getConnectorName() 0 4 1
A getDataDriver() 0 4 1
A setDataGroup() 0 6 1
A setIdKey() 0 6 1
A getTableDefinition() 0 15 2
A getData() 0 17 2
A getDataSet() 0 14 2
A getDataCardinality() 0 11 1
B getSelectQueryForExpression() 0 34 5
A getQueryGroup() 0 4 1
A getQueryHaving() 0 4 1
A getQueryOrder() 0 4 1
A getQueryLimit() 0 4 1
A getQueryOffset() 0 4 1
A getWhereExpression() 0 15 3
B setParamsAndBinds() 0 17 8
A getSimpleColumnCondition() 0 4 2
A getLikeColumnCondition() 0 8 2
A getInColumnCondition() 0 8 1
B saveData() 0 32 6
A bindValuesToStatement() 0 14 4
A deleteData() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like ConnectorAdapter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ConnectorAdapter, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * WebHemi.
4
 *
5
 * PHP version 7.1
6
 *
7
 * @copyright 2012 - 2018 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
    /**
29
     * @var string
30
     */
31
    protected $name;
32
    /**
33
     * @var PDO
34
     */
35
    protected $dataDriver;
36
    /**
37
     * @var string
38
     */
39
    protected $dataGroup = null;
40
    /**
41
     * @var string
42
     */
43
    protected $idKey = null;
44
45
    /**
46
     * ConnectorAdapter constructor.
47
     *
48
     * @param  string          $name
49
     * @param  DriverInterface $dataDriver
50
     * @throws InvalidArgumentException
51
     */
52 21
    public function __construct(string $name, DriverInterface $dataDriver)
53
    {
54 21
        $this->name = $name;
55 21
        $this->dataDriver = $dataDriver;
0 ignored issues
show
Documentation Bug introduced by
It seems like $dataDriver of type object<WebHemi\Data\DriverInterface> is incompatible with the declared type object<PDO> of property $dataDriver.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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