Completed
Pull Request — master (#28)
by
unknown
01:38
created

BaseQuery   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 578
Duplicated Lines 3.63 %

Coupling/Cohesion

Components 4
Dependencies 6

Importance

Changes 0
Metric Value
wmc 60
lcom 4
cbo 6
dl 21
loc 578
rs 3.6
c 0
b 0
f 0

32 Methods

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

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
580
    {
581
    
582
    }
583
    
584
    /**
585
     * Проверка включен ли тегированный кеш
586
     * @return bool
587
     */
588
    protected function isManagedCacheOn()
589
    {
590
        $config = BitrixWrapper::configProvider();
591
        return $config::GetOptionString('main', 'component_managed_cache_on', 'N') == 'Y';
592
    }
593
}
594