Completed
Pull Request — master (#26)
by
unknown
06:22
created

BaseQuery::exec()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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