ReadQuery   D
last analyzed

Complexity

Total Complexity 82

Size/Duplication

Total Lines 570
Duplicated Lines 4.56 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 25
Bugs 3 Features 3
Metric Value
wmc 82
c 25
b 3
f 3
lcom 1
cbo 11
dl 26
loc 570
rs 4.5142

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 7 7 1
A setQuery() 0 6 1
A fields() 19 19 4
A field() 0 6 1
A assignField() 0 16 2
A where() 0 14 2
A condition() 0 11 2
A buildSingularFieldCondition() 0 10 2
A buildMultipleFieldsCondition() 0 20 3
C buildConditionString() 0 23 7
D normalizeComparison() 0 40 24
B normalizeLogical() 0 13 5
A bindValues() 0 12 3
A order() 0 9 1
A normalizeOrder() 0 12 3
A limit() 0 10 2
A count() 0 12 2
A query() 0 7 1
A execute() 0 16 3
A executeQuery() 0 8 2
A fetchAsAssoc() 0 6 1
A fetchAsObject() 0 12 2
B restoreObject() 0 31 6
A convertToPHPValue() 0 4 1
A reset() 0 14 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ReadQuery 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 ReadQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
* This file is part of the moss-storage package
5
*
6
* (c) Michal Wachowski <[email protected]>
7
*
8
* For the full copyright and license information, please view the LICENSE
9
* file that was distributed with this source code.
10
*/
11
12
namespace Moss\Storage\Query;
13
14
use Doctrine\DBAL\Connection;
15
use Doctrine\DBAL\Driver\Statement;
16
use Doctrine\DBAL\Types\Type;
17
use Moss\Storage\GetTypeTrait;
18
use Moss\Storage\Model\Definition\FieldInterface;
19
use Moss\Storage\Model\ModelInterface;
20
use Moss\Storage\Query\Accessor\AccessorInterface;
21
use Moss\Storage\Query\EventDispatcher\EventDispatcherInterface;
22
use Moss\Storage\Query\Relation\RelationFactoryInterface;
23
24
/**
25
 * Query used to read data from table
26
 *
27
 * @author  Michal Wachowski <[email protected]>
28
 * @package Moss\Storage
29
 */
30
class ReadQuery extends AbstractQuery implements ReadQueryInterface
31
{
32
    const EVENT_BEFORE = 'read.before';
33
    const EVENT_AFTER = 'read.after';
34
35
    use GetTypeTrait;
36
37
    protected $queryString;
38
    protected $queryParams = [];
39
40
    /**
41
     * @var array
42
     */
43
    protected $casts = [];
44
45
    /**
46
     * Constructor
47
     *
48
     * @param Connection               $connection
49
     * @param ModelInterface           $model
50
     * @param RelationFactoryInterface $factory
51
     * @param AccessorInterface        $accessor
52
     * @param EventDispatcherInterface $dispatcher
53
     */
54 View Code Duplication
    public function __construct(Connection $connection, ModelInterface $model, RelationFactoryInterface $factory, AccessorInterface $accessor, EventDispatcherInterface $dispatcher)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
55
    {
56
        parent::__construct($connection, $model, $factory, $accessor, $dispatcher);
57
58
        $this->setQuery();
59
        $this->fields();
60
    }
61
62
    /**
63
     * Sets query instance with delete operation and table
64
     */
65
    protected function setQuery()
66
    {
67
        $this->builder = $this->connection->createQueryBuilder();
68
        $this->builder->select([]);
69
        $this->builder->from($this->connection->quoteIdentifier($this->model->table()));
70
    }
71
72
    /**
73
     * Sets field names which will be read
74
     *
75
     * @param array $fields
76
     *
77
     * @return $this
78
     */
79 View Code Duplication
    public function fields($fields = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
80
    {
81
        $this->builder->select([]);
82
        $this->casts = [];
83
84
        if (empty($fields)) {
85
            foreach ($this->model->fields() as $field) {
86
                $this->assignField($field);
87
            }
88
89
            return $this;
90
        }
91
92
        foreach ($fields as $field) {
93
            $this->assignField($this->model->field($field));
94
        }
95
96
        return $this;
97
    }
98
99
    /**
100
     * Adds field to query
101
     *
102
     * @param string $field
103
     *
104
     * @return $this
105
     */
106
    public function field($field)
107
    {
108
        $this->assignField($this->model->field($field));
109
110
        return $this;
111
    }
112
113
    /**
114
     * Assigns field to query
115
     *
116
     * @param FieldInterface $field
117
     */
118
    protected function assignField(FieldInterface $field)
119
    {
120
        if ($field->mapping() !== null) {
121
            $this->builder->addSelect(
122
                sprintf(
123
                    '%s AS %s',
124
                    $this->connection->quoteIdentifier($field->mapping()),
125
                    $this->connection->quoteIdentifier($field->name())
126
                )
127
            );
128
        } else {
129
            $this->builder->addSelect($this->connection->quoteIdentifier($field->name()));
130
        }
131
132
        $this->casts[$field->name()] = $field->type();
133
    }
134
135
    /**
136
     * Adds where condition to query
137
     *
138
     * @param string|array $field
139
     * @param mixed        $value
140
     * @param string       $comparison
141
     * @param string       $logical
142
     *
143
     * @return $this
144
     * @throws QueryException
145
     */
146
    public function where($field, $value, $comparison = '=', $logical = 'and')
147
    {
148
        $condition = $this->condition($field, $value, $comparison, $logical);
149
150
        if ($this->normalizeLogical($logical) === 'or') {
151
            $this->builder()->orWhere($condition);
152
153
            return $this;
154
        }
155
156
        $this->builder()->andWhere($condition);
157
158
        return $this;
159
    }
160
161
    /**
162
     * Adds where condition to query
163
     *
164
     * @param string|array $field
165
     * @param mixed        $value
166
     * @param string       $comparison
167
     * @param string       $logical
168
     *
169
     * @return string
170
     * @throws QueryException
171
     */
172
    public function condition($field, $value, $comparison, $logical)
173
    {
174
        $comparison = $this->normalizeComparison($comparison);
175
        $logical = $this->normalizeLogical($logical);
176
177
        if (!is_array($field)) {
178
            return $this->buildSingularFieldCondition($field, $value, $comparison);
179
        }
180
181
        return $this->buildMultipleFieldsCondition($field, $value, $comparison, $logical);
182
    }
183
184
    /**
185
     * Builds condition for singular field
186
     *
187
     * @param string $field
188
     * @param mixed  $value
189
     * @param string $comparison
190
     *
191
     * @return array
192
     */
193
    protected function buildSingularFieldCondition($field, $value, $comparison)
194
    {
195
        $field = $this->model()->field($field);
196
197
        return $this->buildConditionString(
198
            $this->connection()->quoteIdentifier($field->mappedName()),
199
            $value === null ? null : $this->bindValues($field->mappedName(), $field->type(), $value),
200
            $comparison
201
        );
202
    }
203
204
    /**
205
     * Builds conditions for multiple fields
206
     *
207
     * @param array  $fields
208
     * @param mixed  $value
209
     * @param string $comparison
210
     * @param string $logical
211
     *
212
     * @return array
213
     */
214
    protected function buildMultipleFieldsCondition($fields, $value, $comparison, $logical)
215
    {
216
        $conditions = [];
217
        foreach ((array) $fields as $field) {
218
            $field = $this->model()->field($field);
219
220
            $fieldName = $field->mappedName();
221
            $conditions[] = $this->buildConditionString(
222
                $this->connection()->quoteIdentifier($fieldName),
223
                $value === null ? null : $this->bindValues($fieldName, $field->type(), $value),
224
                $comparison
225
            );
226
227
            $conditions[] = $logical;
228
        }
229
230
        array_pop($conditions);
231
232
        return '(' . implode(' ', $conditions) . ')';
233
    }
234
235
    /**
236
     * Builds condition string
237
     *
238
     * @param string       $field
239
     * @param string|array $bind
240
     * @param string       $operator
241
     *
242
     * @return string
243
     */
244
    protected function buildConditionString($field, $bind, $operator)
245
    {
246
        if (is_array($bind)) {
247
            foreach ($bind as &$val) {
248
                $val = $this->buildConditionString($field, $val, $operator);
249
                unset($val);
250
            }
251
252
            $logical = $operator === '!=' ? ' and ' : ' or ';
253
254
            return '(' . implode($logical, $bind) . ')';
255
        }
256
257
        if ($bind === null) {
258
            return $field . ' ' . ($operator == '!=' ? 'IS NOT NULL' : 'IS NULL');
259
        }
260
261
        if ($operator === 'regexp') {
262
            return sprintf('%s regexp %s', $field, $bind);
263
        }
264
265
        return $field . ' ' . $operator . ' ' . $bind;
266
    }
267
268
    /**
269
     * Asserts correct comparison operator
270
     *
271
     * @param string $operator
272
     *
273
     * @return string
274
     * @throws QueryException
275
     */
276
    protected function normalizeComparison($operator)
277
    {
278
        switch (strtolower($operator)) {
279
            case '<':
280
            case 'lt':
281
                return '<';
282
            case '<=':
283
            case 'lte':
284
                return '<=';
285
            case '>':
286
            case 'gt':
287
                return '>';
288
            case '>=':
289
            case 'gte':
290
                return '>=';
291
            case '~':
292
            case '~=':
293
            case '=~':
294
            case 'regex':
295
            case 'regexp':
296
                return "regexp";
297
            // LIKE
298
            case 'like':
299
                return "like";
300
            case '||':
301
            case 'fulltext':
302
            case 'fulltext_boolean':
303
                return 'fulltext';
304
            case '<>':
305
            case '!=':
306
            case 'ne':
307
            case 'not':
308
                return '!=';
309
            case '=':
310
            case 'eq':
311
                return '=';
312
            default:
313
                throw new QueryException(sprintf('Query does not supports comparison operator "%s" in query "%s"', $operator, $this->model()->entity()));
314
        }
315
    }
316
317
    /**
318
     * Asserts correct logical operation
319
     *
320
     * @param string $operator
321
     *
322
     * @return string
323
     * @throws QueryException
324
     */
325
    protected function normalizeLogical($operator)
326
    {
327
        switch (strtolower($operator)) {
328
            case '&&':
329
            case 'and':
330
                return 'and';
331
            case '||':
332
            case 'or':
333
                return 'or';
334
            default:
335
                throw new QueryException(sprintf('Query does not supports logical operator "%s" in query "%s"', $operator, $this->model()->entity()));
336
        }
337
    }
338
339
    /**
340
     * Binds condition value to key
341
     *
342
     * @param string $name
343
     * @param string $type
344
     * @param mixed  $values
345
     *
346
     * @return string
347
     */
348
    protected function bindValues($name, $type, $values)
349
    {
350
        if (!is_array($values)) {
351
            return $this->builder->createNamedParameter($values, $type);
352
        }
353
354
        foreach ($values as $key => $value) {
355
            $values[$key] = $this->bindValues($name, $type, $value);
356
        }
357
358
        return $values;
359
    }
360
361
    /**
362
     * Adds sorting to query
363
     *
364
     * @param string $field
365
     * @param string $order
366
     *
367
     * @return $this
368
     */
369
    public function order($field, $order = 'desc')
370
    {
371
        $field = $this->model->field($field);
372
        $order = $this->normalizeOrder($order);
373
374
        $this->builder->addOrderBy($this->connection->quoteIdentifier($field->mappedName()), $order);
375
376
        return $this;
377
    }
378
379
    /**
380
     * Asserts correct order
381
     *
382
     * @param string $order
383
     *
384
     * @return string
385
     * @throws QueryException
386
     */
387
    protected function normalizeOrder($order)
388
    {
389
        switch (strtolower($order)) {
390
            case 'asc':
391
                return 'asc';
392
            case 'desc':
393
                return 'desc';
394
            default:
395
                throw new QueryException(sprintf('Unsupported sorting method "%s" in query "%s"', $this->getType($order), $this->model->entity()));
396
397
        }
398
    }
399
400
    /**
401
     * Sets limits to query
402
     *
403
     * @param int      $limit
404
     * @param null|int $offset
405
     *
406
     * @return $this
407
     */
408
    public function limit($limit, $offset = null)
409
    {
410
        if ($offset !== null) {
411
            $this->builder->setFirstResult((int) $offset);
412
        }
413
414
        $this->builder->setMaxResults((int) $limit);
415
416
        return $this;
417
    }
418
419
    /**
420
     * Returns number of entities that will be read
421
     *
422
     * @return int
423
     */
424
    public function count()
425
    {
426
        if (empty($this->queryString)) {
427
            $builder = clone $this->builder;
428
            $builder->resetQueryPart('orderBy');
429
            $stmt = $builder->execute();
430
        } else {
431
            $stmt = $this->connection->executeQuery($this->queryString, $this->queryParams);
432
        }
433
434
        return (int) $stmt->rowCount();
435
    }
436
437
    /**
438
     * Sets custom query to be executed instead of one based on entity structure
439
     *
440
     * @param string $query
441
     * @param array  $params
442
     *
443
     * @return $this
444
     */
445
    public function query($query, array $params = [])
446
    {
447
        $this->queryString = $query;
448
        $this->queryParams = $params;
449
450
        return $this;
451
    }
452
453
    /**
454
     * Executes query
455
     * After execution query is reset
456
     *
457
     * @return mixed
458
     */
459
    public function execute()
460
    {
461
        $this->dispatcher->fire(self::EVENT_BEFORE);
462
463
        $stmt = $this->executeQuery();
464
        $result = $this->model->entity() ? $this->fetchAsObject($stmt) : $this->fetchAsAssoc($stmt);
465
466
        $this->dispatcher->fire(self::EVENT_AFTER, $result);
467
468
        foreach ($this->relations as $relation) {
469
            $result = $relation->read($result);
470
        }
471
472
473
        return $result;
474
    }
475
476
    /**
477
     * Executes query - from builder or custom
478
     *
479
     * @return Statement
480
     * @throws \Doctrine\DBAL\DBALException
481
     */
482
    protected function executeQuery()
483
    {
484
        if (empty($this->queryString)) {
485
            return $this->builder->execute();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->builder->execute(); of type Doctrine\DBAL\Driver\Statement|integer adds the type integer to the return on line 485 which is incompatible with the return type documented by Moss\Storage\Query\ReadQuery::executeQuery of type Doctrine\DBAL\Driver\Statement.
Loading history...
486
        }
487
488
        return $this->connection->executeQuery($this->queryString, $this->queryParams);
489
    }
490
491
    /**
492
     * Fetches result as associative array, mostly for pivot tables
493
     *
494
     * @param Statement $stmt
495
     *
496
     * @return array
497
     */
498
    protected function fetchAsAssoc(Statement $stmt)
499
    {
500
        $stmt->setFetchMode(\PDO::FETCH_ASSOC);
501
502
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
503
    }
504
505
    /**
506
     * Fetches result as entity object
507
     *
508
     * @param Statement $stmt
509
     *
510
     * @return array
511
     */
512
    protected function fetchAsObject(Statement $stmt)
513
    {
514
        $stmt->setFetchMode(\PDO::FETCH_CLASS, $this->model->entity());
515
        $result = $stmt->fetchAll();
516
517
        $ref = new \ReflectionClass($this->model->entity());
518
        foreach ($result as $entity) {
519
            $this->restoreObject($entity, $this->casts, $ref);
520
        }
521
522
        return $result;
523
    }
524
525
    /**
526
     * Restores entity values from their stored representation
527
     *
528
     * @param object           $entity
529
     * @param array            $restore
530
     * @param \ReflectionClass $ref
531
     *
532
     * @return mixed
533
     */
534
    protected function restoreObject($entity, array $restore, \ReflectionClass $ref)
535
    {
536
        foreach ($restore as $field => $type) {
537
            if (is_array($entity)) {
538
                if (!isset($entity[$field])) {
539
                    continue;
540
                }
541
542
                $entity[$field] = $this->convertToPHPValue($entity[$field], $type);
543
                continue;
544
            }
545
546
            if (!$ref->hasProperty($field)) {
547
                if (!isset($entity->$field)) {
548
                    continue;
549
                }
550
551
                $entity->$field = $this->convertToPHPValue($entity->$field, $type);
552
                continue;
553
            }
554
555
            $prop = $ref->getProperty($field);
556
            $prop->setAccessible(true);
557
558
            $value = $prop->getValue($entity);
559
            $value = $this->convertToPHPValue($value, $type);
560
            $prop->setValue($entity, $value);
561
        }
562
563
        return $entity;
564
    }
565
566
    /**
567
     * Converts read value to its php representation
568
     *
569
     * @param mixed  $value
570
     * @param string $type
571
     *
572
     * @return mixed
573
     * @throws \Doctrine\DBAL\DBALException
574
     */
575
    protected function convertToPHPValue($value, $type)
576
    {
577
        return Type::getType($type)->convertToPHPValue($value, $this->connection->getDatabasePlatform());
578
    }
579
580
    /**
581
     * Resets adapter
582
     *
583
     * @return $this
584
     */
585
    public function reset()
586
    {
587
        $this->builder->resetQueryParts();
588
        $this->relations = [];
589
        $this->queryString = null;
590
        $this->queryParams = [];
591
        $this->casts = [];
592
        $this->resetBinds();
593
594
        $this->setQuery();
595
        $this->fields();
596
597
        return $this;
598
    }
599
}
600