Completed
Pull Request — master (#17)
by
unknown
01:35
created

BaseQuery   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 558
Duplicated Lines 3.76 %

Coupling/Cohesion

Components 4
Dependencies 5

Importance

Changes 0
Metric Value
wmc 56
lcom 4
cbo 5
dl 21
loc 558
rs 5.5555
c 0
b 0
f 0

30 Methods

Rating   Name   Duplication   Size   Complexity  
count() 0 1 ?
B getList() 0 23 5
loadModels() 0 1 ?
A __construct() 0 6 1
A first() 0 4 1
A getById() 0 11 3
A sort() 0 6 2
A order() 0 4 1
A filter() 0 6 1
A resetFilter() 0 6 1
A addFilter() 0 8 2
A navigation() 0 6 1
A select() 0 6 2
A cache() 0 6 1
A keyBy() 0 6 1
A limit() 0 6 1
A page() 0 6 1
A take() 0 4 1
A forPage() 0 4 1
A paginate() 11 11 1
A simplePaginate() 10 10 1
A stopQuery() 0 6 1
D addItemToResultsUsingKeyBy() 0 42 10
A fieldsMustBeSelected() 0 4 1
A propsMustBeSelected() 0 6 3
A substituteField() 0 8 3
A clearSelectArray() 0 6 1
A rememberInCache() 0 19 3
A handleCacheIfNeeded() 0 6 2
A __call() 0 18 4

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 BaseQuery 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 BaseQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Arrilot\BitrixModels\Queries;
4
5
use Arrilot\BitrixModels\Models\BaseBitrixModel;
6
use BadMethodCallException;
7
use Bitrix\Main\Data\Cache;
8
use Closure;
9
use CPHPCache;
10
use Illuminate\Pagination\Paginator;
11
use Illuminate\Pagination\LengthAwarePaginator;
12
use Illuminate\Support\Collection;
13
use LogicException;
14
15
abstract class BaseQuery
16
{
17
    use BaseRelationQuery;
18
19
    /**
20
     * Query select.
21
     *
22
     * @var array
23
     */
24
    public $select = [];
25
    /**
26
     * Bitrix object to be queried.
27
     *
28
     * @var object|string
29
     */
30
    protected $bxObject;
31
32
    /**
33
     * Name of the model that calls the query.
34
     *
35
     * @var string
36
     */
37
    protected $modelName;
38
39
    /**
40
     * Model that calls the query.
41
     *
42
     * @var object
43
     */
44
    protected $model;
45
46
    /**
47
     * Query sort.
48
     *
49
     * @var array
50
     */
51
    public $sort = [];
52
53
    /**
54
     * Query filter.
55
     *
56
     * @var array
57
     */
58
    public $filter = [];
59
60
    /**
61
     * Query navigation.
62
     *
63
     * @var array|bool
64
     */
65
    public $navigation = false;
66
67
    /**
68
     * The key to list items in array of results.
69
     * Set to false to have auto incrementing integer.
70
     *
71
     * @var string|bool
72
     */
73
    public $keyBy = 'ID';
74
75
    /**
76
     * Number of minutes to cache a query
77
     *
78
     * @var double|int
79
     */
80
    public $cacheTtl = 0;
81
82
    /**
83
     * Indicates that the query should be stopped instead of touching the DB.
84
     * Can be set in query scopes or manually.
85
     *
86
     * @var bool
87
     */
88
    protected $queryShouldBeStopped = false;
89
90
    /**
91
     * Get count of users that match $filter.
92
     *
93
     * @return int
94
     */
95
    abstract public function count();
96
97
    /**
98
     * Подготавливает запрос и вызывает loadModels()
99
     *
100
     * @return Collection
101
     */
102
    public function getList()
103
    {
104
        if ($this->queryShouldBeStopped) {
105
            return new Collection();
106
        }
107
108
        if (!is_null($this->primaryModel)) {
109
            // Запрос - подгрузка релейшена. Надо добавить filter
110
            $this->filterByModels([$this->primaryModel]);
111
        }
112
113
        $models = $this->loadModels();
114
115
        if (!empty($this->with)) {
116
            $this->findWith($this->with, $models);
117
        }
118
119
        if ($this->inverseOf !== null) {
120
            $this->addInverseRelations($models);
121
        }
122
123
        return $models;
0 ignored issues
show
Bug Compatibility introduced by
The expression return $models; of type Illuminate\Support\Colle...odels\BaseBitrixModel[] is incompatible with the return type documented by Arrilot\BitrixModels\Queries\BaseQuery::getList of type Illuminate\Support\Collection as it can also be of type Arrilot\BitrixModels\Models\BaseBitrixModel[] which is not included in this return type.
Loading history...
124
    }
125
126
    /**
127
     * Get list of items.
128
     *
129
     * @return Collection
130
     */
131
    abstract protected function loadModels();
132
133
    /**
134
     * Constructor.
135
     *
136
     * @param object|string $bxObject
137
     * @param string $modelName
138
     */
139
    public function __construct($bxObject, $modelName)
140
    {
141
        $this->bxObject = $bxObject;
142
        $this->modelName = $modelName;
143
        $this->model = new $modelName();
144
    }
145
146
    /**
147
     * Get the first item that matches query params.
148
     *
149
     * @return mixed
150
     */
151
    public function first()
152
    {
153
        return $this->limit(1)->getList()->first(null, false);
154
    }
155
156
    /**
157
     * Get item by its id.
158
     *
159
     * @param int $id
160
     *
161
     * @return mixed
162
     */
163
    public function getById($id)
164
    {
165
        if (!$id || $this->queryShouldBeStopped) {
166
            return false;
167
        }
168
169
        $this->sort = [];
170
        $this->filter['ID'] = $id;
171
172
        return $this->getList()->first(null, false);
173
    }
174
175
    /**
176
     * Setter for sort.
177
     *
178
     * @param mixed  $by
179
     * @param string $order
180
     *
181
     * @return $this
182
     */
183
    public function sort($by, $order = 'ASC')
184
    {
185
        $this->sort = is_array($by) ? $by : [$by => $order];
186
187
        return $this;
188
    }
189
190
    /**
191
     * Another setter for sort.
192
     *
193
     * @param mixed  $by
194
     * @param string $order
195
     *
196
     * @return $this
197
     */
198
    public function order($by, $order = 'ASC')
199
    {
200
        return $this->sort($by, $order);
201
    }
202
203
    /**
204
     * Setter for filter.
205
     *
206
     * @param array $filter
207
     *
208
     * @return $this
209
     */
210
    public function filter($filter)
211
    {
212
        $this->filter = array_merge($this->filter, $filter);
213
214
        return $this;
215
    }
216
217
    /**
218
     * Reset filter.
219
     *
220
     * @return $this
221
     */
222
    public function resetFilter()
223
    {
224
        $this->filter = [];
225
226
        return $this;
227
    }
228
229
    /**
230
     * Add another filter to filters array.
231
     *
232
     * @param array $filters
233
     *
234
     * @return $this
235
     */
236
    public function addFilter($filters)
237
    {
238
        foreach ($filters as $field => $value) {
239
            $this->filter[$field] = $value;
240
        }
241
242
        return $this;
243
    }
244
245
    /**
246
     * Setter for navigation.
247
     *
248
     * @param $value
249
     *
250
     * @return $this
251
     */
252
    public function navigation($value)
253
    {
254
        $this->navigation = $value;
255
256
        return $this;
257
    }
258
259
    /**
260
     * Setter for select.
261
     *
262
     * @param $value
263
     *
264
     * @return $this
265
     */
266
    public function select($value)
267
    {
268
        $this->select = is_array($value) ? $value : func_get_args();
269
270
        return $this;
271
    }
272
273
    /**
274
     * Setter for cache ttl.
275
     *
276
     * @param float|int $minutes
277
     *
278
     * @return $this
279
     */
280
    public function cache($minutes)
281
    {
282
        $this->cacheTtl = $minutes;
283
284
        return $this;
285
    }
286
287
    /**
288
     * Setter for keyBy.
289
     *
290
     * @param string $value
291
     *
292
     * @return $this
293
     */
294
    public function keyBy($value)
295
    {
296
        $this->keyBy = $value;
297
298
        return $this;
299
    }
300
301
    /**
302
     * Set the "limit" value of the query.
303
     *
304
     * @param int $value
305
     *
306
     * @return $this
307
     */
308
    public function limit($value)
309
    {
310
        $this->navigation['nPageSize'] = $value;
311
312
        return $this;
313
    }
314
315
    /**
316
     * Set the "page number" value of the query.
317
     *
318
     * @param int $num
319
     *
320
     * @return $this
321
     */
322
    public function page($num)
323
    {
324
        $this->navigation['iNumPage'] = $num;
325
326
        return $this;
327
    }
328
329
    /**
330
     * Alias for "limit".
331
     *
332
     * @param int $value
333
     *
334
     * @return $this
335
     */
336
    public function take($value)
337
    {
338
        return $this->limit($value);
339
    }
340
341
    /**
342
     * Set the limit and offset for a given page.
343
     *
344
     * @param  int  $page
345
     * @param  int  $perPage
346
     * @return $this
347
     */
348
    public function forPage($page, $perPage = 15)
349
    {
350
        return $this->page($page)->take($perPage);
351
    }
352
353
    /**
354
     * Paginate the given query into a paginator.
355
     *
356
     * @param  int  $perPage
357
     * @param  string  $pageName
358
     *
359
     * @return \Illuminate\Pagination\LengthAwarePaginator
360
     */
361 View Code Duplication
    public function paginate($perPage = 15, $pageName = 'page')
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...
362
    {
363
        $page = Paginator::resolveCurrentPage($pageName);
364
        $total = $this->count();
365
        $results = $this->forPage($page, $perPage)->getList();
366
367
        return new LengthAwarePaginator($results, $total, $perPage, $page, [
368
            'path' => Paginator::resolveCurrentPath(),
369
            'pageName' => $pageName,
370
        ]);
371
    }
372
373
    /**
374
     * Get a paginator only supporting simple next and previous links.
375
     *
376
     * This is more efficient on larger data-sets, etc.
377
     *
378
     * @param  int  $perPage
379
     * @param  string  $pageName
380
     *
381
     * @return \Illuminate\Pagination\Paginator
382
     */
383 View Code Duplication
    public function simplePaginate($perPage = 15, $pageName = 'page')
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...
384
    {
385
        $page = Paginator::resolveCurrentPage($pageName);
386
        $results = $this->forPage($page, $perPage + 1)->getList();
387
388
        return new Paginator($results, $perPage, $page, [
389
            'path' => Paginator::resolveCurrentPath(),
390
            'pageName' => $pageName,
391
        ]);
392
    }
393
394
    /**
395
     * Stop the query from touching DB.
396
     *
397
     * @return $this
398
     */
399
    public function stopQuery()
400
    {
401
        $this->queryShouldBeStopped = true;
402
403
        return $this;
404
    }
405
406
    /**
407
     * Adds $item to $results using keyBy value.
408
     *
409
     * @param $results
410
     * @param BaseBitrixModel $object
411
     *
412
     * @return void
413
     */
414
    protected function addItemToResultsUsingKeyBy(&$results, BaseBitrixModel $object)
415
    {
416
        $item = $object->fields;
417
        if (!isset($item[$this->keyBy])) {
418
            throw new LogicException("Field {$this->keyBy} is not found in object");
419
        }
420
421
        $keyByValue = $item[$this->keyBy];
422
423
        if (!isset($results[$keyByValue])) {
424
            $results[$keyByValue] = $object;
425
        } else {
426
            $oldFields = $results[$keyByValue]->fields;
427
            foreach ($oldFields as $field => $oldValue) {
428
                // пропускаем служебные поля.
429
                if (in_array($field, ['_were_multiplied', 'PROPERTIES'])) {
430
                    continue;
431
                }
432
433
                $alreadyMultiplied = !empty($oldFields['_were_multiplied'][$field]);
434
435
                // мультиплицируем только несовпадающие значения полей
436
                $newValue = $item[$field];
437
                if ($oldValue !== $newValue) {
438
                    // если еще не мультиплицировали поле, то его надо превратить в массив.
439
                    if (!$alreadyMultiplied) {
440
                        $oldFields[$field] = [
441
                            $oldFields[$field]
442
                        ];
443
                        $oldFields['_were_multiplied'][$field] = true;
444
                    }
445
446
                    // добавляем новое значению поле если такого еще нет.
447
                    if (empty($oldFields[$field]) || (is_array($oldFields[$field]) && !in_array($newValue, $oldFields[$field]))) {
448
                        $oldFields[$field][] = $newValue;
449
                    }
450
                }
451
            }
452
453
            $results[$keyByValue]->fields = $oldFields;
454
        }
455
    }
456
457
    /**
458
     * Determine if all fields must be selected.
459
     *
460
     * @return bool
461
     */
462
    protected function fieldsMustBeSelected()
463
    {
464
        return in_array('FIELDS', $this->select);
465
    }
466
467
    /**
468
     * Determine if all fields must be selected.
469
     *
470
     * @return bool
471
     */
472
    protected function propsMustBeSelected()
473
    {
474
        return in_array('PROPS', $this->select)
475
            || in_array('PROPERTIES', $this->select)
476
            || in_array('PROPERTY_VALUES', $this->select);
477
    }
478
479
    /**
480
     * Set $array[$new] as $array[$old] and delete $array[$old].
481
     *
482
     * @param array $array
483
     * @param $old
484
     * @param $new
485
     *
486
     * return null
487
     */
488
    protected function substituteField(&$array, $old, $new)
489
    {
490
        if (isset($array[$old]) && !isset($array[$new])) {
491
            $array[$new] = $array[$old];
492
        }
493
494
        unset($array[$old]);
495
    }
496
497
    /**
498
     * Clear select array from duplication and additional fields.
499
     *
500
     * @return array
501
     */
502
    protected function clearSelectArray()
503
    {
504
        $strip = ['FIELDS', 'PROPS', 'PROPERTIES', 'PROPERTY_VALUES', 'GROUPS', 'GROUP_ID', 'GROUPS_ID'];
505
506
        return array_values(array_diff(array_unique($this->select), $strip));
507
    }
508
509
    /**
510
     * Store closure's result in the cache for a given number of minutes.
511
     *
512
     * @param string $key
513
     * @param double $minutes
514
     * @param Closure $callback
515
     * @return mixed
516
     */
517
    protected function rememberInCache($key, $minutes, Closure $callback)
518
    {
519
        $minutes = (double) $minutes;
520
        if ($minutes <= 0) {
521
            return $callback();
522
        }
523
524
        $cache = Cache::createInstance();
525
        if ($cache->initCache($minutes * 60, $key, 'bitrix-models')) {
526
            $vars = $cache->getVars();
527
            return $vars['cache'];
528
        }
529
530
        $cache->startDataCache();
531
        $result = $callback();
532
        $cache->endDataCache(['cache' => $result]);
533
534
        return $result;
535
    }
536
537
    protected function handleCacheIfNeeded($cacheKeyParams, Closure $callback)
538
    {
539
        return $this->cacheTtl
540
            ? $this->rememberInCache(md5(json_encode($cacheKeyParams)), $this->cacheTtl, $callback)
541
            : $callback();
542
    }
543
544
    /**
545
     * Handle dynamic method calls into the method.
546
     *
547
     * @param string $method
548
     * @param array  $parameters
549
     *
550
     * @throws BadMethodCallException
551
     *
552
     * @return $this
553
     */
554
    public function __call($method, $parameters)
555
    {
556
        if (method_exists($this->model, 'scope'.$method)) {
557
            array_unshift($parameters, $this);
558
559
            $query = call_user_func_array([$this->model, 'scope'.$method], $parameters);
560
561
            if ($query === false) {
562
                $this->stopQuery();
563
            }
564
565
            return $query instanceof static ? $query : $this;
566
        }
567
568
        $className = get_class($this);
569
570
        throw new BadMethodCallException("Call to undefined method {$className}::{$method}()");
571
    }
572
}
573