Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
compatible.php
1<?php
2
4
8 Bitrix\Main\Entity\Query,
9 Bitrix\Main\Entity\Field,
10 Bitrix\Main\Entity\ReferenceField,
11 Bitrix\Main\Entity\ExpressionField,
13
15
16class AliasedQuery extends Query
17{
18 private $aliases = array();
19
20 public function __construct($source)
21 {
22 parent::__construct($source);
23
24 $aliases = &$this->aliases;
25
27 foreach ($this->getEntity()->getFields() as $field)
28 {
29 if (! $field instanceof ReferenceField)
30 {
31 $name = $field->getName();
32 $aliases[$name] = $name;
33 }
34 }
35 }
36
37 public function getAliases()
38 {
39 return $this->aliases;
40 }
41
42 public function addAliases(array $aliases)
43 {
44 foreach ($aliases as $alias => $field)
45 {
46 $this->addAlias($alias, $field);
47 }
48
49 return $this;
50 }
51
52 public function addAlias($alias, $field = null)
53 {
54 if (($this->aliases[$alias] ?? false))
55 {
56 throw new SystemException("`$alias` already added", 0, __FILE__, __LINE__);
57 }
58 elseif (! $field)
59 {
60 $this->aliases[$alias] = $alias;
61 }
62 elseif (is_string($field) || (is_array($field) && $field['expression'])) // TODO Field support
63 {
64 $this->aliases[$alias] = $field;
65 }
66 else
67 {
68 throw new SystemException("invalid `$alias` type", 0, __FILE__, __LINE__);
69 }
70
71 return $this;
72 }
73
74 public function getAliasName($alias)
75 {
76 if (!isset($this->aliases[$alias]))
77 {
78 return null;
79 }
80
81 $field = $this->aliases[$alias];
82
83 if (is_string($field))
84 {
85 return $field; // name
86 }
87 elseif (is_array($field)) // TODO Field support
88 {
89 $name = '__'.$alias.'_ALIAS__';
90 $field['registered'] ??= false;
91 if (!$field['registered'])
92 {
93 $field['registered'] = true;
94 $this->registerRuntimeField($name, $field);
95 }
96
97 return $name;
98 }
99
100 throw new SystemException("invalid alias '$alias' type", 0, __FILE__, __LINE__);
101 }
102
103 public function addAliasSelect($alias)
104 {
105 return ($name = $this->getAliasName($alias))
106 ? $this->addSelect($name, $alias)
107 : $this;
108 }
109
110 public function addAliasGroup($alias)
111 {
112 return ($name = $this->getAliasName($alias))
113 ? $this->addGroup($name)
114 : $this;
115 }
116
117 public function addAliasOrder($alias, $order)
118 {
119 return ($name = $this->getAliasName($alias))
120 ? $this->addOrder($name, $order)
121 : $this;
122 }
123
124 public function addAliasFilter($key, $value)
125 {
126 preg_match('/^([!%@<=>]{0,3})(.*)$/', $key, $matches);
127
128 return ($name = $this->getAliasName($matches[2]))
129 ? $this->addFilter($matches[1].$name, $value)
130 : $this;
131
132 // TODO recursive filters maybe?
133// if (is_null($key) && is_array($value))
134// {
135// return ($filter = self::getAliasFilterRecursive($value))
136// ? $this->addFilter(null, $filter)
137// : $this;
138// }
139// else
140// {
141// preg_match('/^([!%@<=>]{0,3})(.*)$/', $key, $matches);
142//
143// $alias = $matches[2];
144//
145// if (! ($name = $this->getAliasName($alias)))
146// {
147// if ($this->getEntity()->hasField($alias))
148// $name = $alias;
149// else
150// return $this;
151// }
152//
153// $key = $matches[1].$name;
154// return parent::addFilter($key, $value);
155// }
156 }
157
158// private function getAliasFilterRecursive(array $filter)
159// {
160// $resolved = array();
161//
162// foreach ($filter as $key => $value)
163// {
164// if ($key === 'LOGIC')
165// {
166// $resolved['LOGIC'] = $value;
167// }
168// elseif (is_array($value))
169// {
170// $resolved []= self::getAliasFilterRecursive($value);
171// }
172// else
173// {
174// preg_match('/^([!%@<=>]{0,3})(.*)$/', $key, $matches);
175//
176// $alias = $matches[2];
177//
178// if (! ($name = $this->getAliasName($alias)))
179// {
180// if ($this->getEntity()->hasField($alias))
181// $name = $alias;
182// else
183// continue;
184// }
185//
186// $key = $matches[1].$name;
187// $resolved[$key] = $value;
188// }
189// }
190//
191// return $resolved;
192// }
193}
194
195final class CDBResult extends \CDBResult
196{
197 function compatibleNavQuery(Query $query, array $arNavStartParams) //, $bIgnoreErrors = false)
198 {
199 $cnt = $query->exec()->getSelectedRowsCount(); // TODO check groups
200
201 global $DB;
202
203 if(isset($arNavStartParams["SubstitutionFunction"]))
204 {
205 $arNavStartParams["SubstitutionFunction"]($this, $query->getLastQuery(), $cnt, $arNavStartParams);
206 return null;
207 }
208
209 if(isset($arNavStartParams["bDescPageNumbering"]))
210 $bDescPageNumbering = $arNavStartParams["bDescPageNumbering"];
211 else
212 $bDescPageNumbering = false;
213
214 $this->InitNavStartVars($arNavStartParams);
215 $this->NavRecordCount = $cnt;
216
217 if($this->NavShowAll)
218 $this->NavPageSize = $this->NavRecordCount;
219
220 //calculate total pages depend on rows count. start with 1
221 $this->NavPageCount = ($this->NavPageSize>0 ? floor($this->NavRecordCount/$this->NavPageSize) : 0);
222 if($bDescPageNumbering)
223 {
224 $makeweight = 0;
225 if($this->NavPageSize > 0)
226 $makeweight = ($this->NavRecordCount % $this->NavPageSize);
227 if($this->NavPageCount == 0 && $makeweight > 0)
228 $this->NavPageCount = 1;
229
230 //page number to display
231 $this->calculatePageNumber($this->NavPageCount);
232
233 //rows to skip
234 $NavFirstRecordShow = 0;
235 if($this->NavPageNomer != $this->NavPageCount)
236 $NavFirstRecordShow += $makeweight;
237
238 $NavFirstRecordShow += ($this->NavPageCount - $this->NavPageNomer) * $this->NavPageSize;
239 $NavLastRecordShow = $makeweight + ($this->NavPageCount - $this->NavPageNomer + 1) * $this->NavPageSize;
240 }
241 else
242 {
243 if($this->NavPageSize > 0 && ($this->NavRecordCount % $this->NavPageSize > 0))
244 $this->NavPageCount++;
245
246 //calculate total pages depend on rows count. start with 1
247 $this->calculatePageNumber(1, true, (bool)($arNavStartParams["checkOutOfRange"] ?? false));
248 if ($this->NavPageNomer === null)
249 {
250 return null;
251 }
252
253 //rows to skip
254 $NavFirstRecordShow = $this->NavPageSize*($this->NavPageNomer-1);
255 $NavLastRecordShow = $this->NavPageSize*$this->NavPageNomer;
256 }
257
258 $NavAdditionalRecords = 0;
259 if(is_set($arNavStartParams, "iNavAddRecords"))
260 $NavAdditionalRecords = $arNavStartParams["iNavAddRecords"];
261
262 if(!$this->NavShowAll)
263 {
264 $query->setOffset($NavFirstRecordShow);
265 $query->setLimit($NavLastRecordShow - $NavFirstRecordShow + $NavAdditionalRecords);
266 }
267
268 $res_tmp = $query->exec(); //, $bIgnoreErrors);
269
270// // Return false on sql errors (if $bIgnoreErrors == true)
271// if ($bIgnoreErrors && ($res_tmp === false))
272// return false;
273
274// $this->result = $res_tmp->result;
275 $this->DB = $DB;
276
277 if($this->SqlTraceIndex)
278 $start_time = microtime(true);
279
280 $temp_arrray = array();
281 $temp_arrray_add = array();
282 $tmp_cnt = 0;
283
284 while($ar = $res_tmp->fetch())
285 {
286 $tmp_cnt++;
287 if (intval($NavLastRecordShow - $NavFirstRecordShow) > 0 && $tmp_cnt > ($NavLastRecordShow - $NavFirstRecordShow))
288 $temp_arrray_add[] = $ar;
289 else
290 $temp_arrray[] = $ar;
291 }
292
293 if($this->SqlTraceIndex)
294 {
296 $exec_time = round(microtime(true) - $start_time, 10);
297 $DB->addDebugTime($this->SqlTraceIndex, $exec_time);
298 $DB->timeQuery += $exec_time;
299 }
300
301 $this->arResult = (!empty($temp_arrray)? $temp_arrray : false);
302 $this->arResultAdd = (!empty($temp_arrray_add)? $temp_arrray_add : false);
303 $this->nSelectedCount = $cnt;
304 $this->bDescPageNumbering = $bDescPageNumbering;
305 $this->bFromLimited = true;
306
307 return null;
308 }
309
310 // FetchAdapter
311
312 private $fetchAdapters = array();
313
314 public function addFetchAdapter(FetchAdapter $adapter)
315 {
316 $this->fetchAdapters[] = $adapter;
317 }
318
319 function Fetch()
320 {
321 if ($row = parent::Fetch())
322 foreach ($this->fetchAdapters as $adapter)
323 $row = $adapter->adapt($row);
324
325 return $row;
326 }
327}
328
330{
331 public function adapt(array $row);
332}
333
335{
336 private $aggregated = array();
337
338 function __construct(array $aggregated)
339 {
340 $this->aggregated = $aggregated;
341 }
342
343 public function adapt(array $row)
344 {
345 foreach ($this->aggregated as $alias => $name)
346 {
347 $row[$name] = $row[$alias];
348 unset ($row[$alias]);
349 }
350
351 return $row;
352 }
353}
354
356{
357 private $counted, $grouped, $allSelected, $aggregated = array();
358
359 public function counted()
360 {
361 return $this->counted;
362 }
363
364 public function grouped()
365 {
366 return $this->grouped;
367 }
368
369 public function allSelected()
370 {
371 return $this->allSelected;
372 }
373
374 public function aggregated()
375 {
376 return $this->aggregated ? true : false;
377 }
378
379 private function addAggregatedSelect($alias, $aggregate, $name = null)
380 {
381 $aggregateAlias = '__'.$aggregate.'_'.$alias.'_ALIAS__';
382 $this->aggregated[$aggregateAlias] = $alias;
383
384 return $this->addSelect(
385 $name
386 ? new ExpressionField($aggregateAlias, $aggregate.'(%s)', $name)
387 : new ExpressionField($aggregateAlias, $aggregate)
388 );
389 }
390
391 public static function explodeFilterKey($key)
392 {
393 preg_match('/^([!+*]{0,1})([<=>@%~?]{0,2})(.*)$/', $key, $matches);
394
395 return array(
396 'modifier' => $matches[1], // can be ""
397 'operator' => $matches[2], // can be ""
398 'alias' => $matches[3], // can be ""
399 );
400 }
401
402 public function compatibleAddFilter($key, $value)
403 {
404 $keyMatch = static::explodeFilterKey($key);
405 $modifier = $keyMatch['modifier'];
406 $operator = $keyMatch['operator'];
407 $alias = $keyMatch['alias' ];
408
409 if (! $name = $this->getAliasName($alias))
410 return $this;
411
412 switch ($operator)
413 {
414 case '':
415 case '@': $operator = ($modifier === '*' ? '' : '=');
416 break;
417
418 case '~': $operator = '';
419 break;
420 // default: with no changes
421 }
422
423 switch ($modifier)
424 {
425 case '*':
426 case '' : return $this->addFilter($modifier.$operator.$name, $value);
427
428 case '!': return $operator == '=' && $value
429 ? $this->addFilter(null, array('LOGIC' => 'OR', array('!='.$name => $value), array('='.$name => '')))
430 : $this->addFilter($modifier.$operator.$name, $value);
431
432 case '+': return $this->addFilter(null, array('LOGIC' => 'OR', array($operator.$name => $value), array('='.$name => '')));
433
434 default : throw new SystemException("invalid modifier '$modifier'", 0, __FILE__, __LINE__);
435 }
436 }
437
438 protected function mapLocationRuntimeField($field, $asFilter = false)
439 {
440 return $field;
441 }
442
443 public function prepare(array $order, array $filter, $group, array $select)
444 {
445 // Do not remove!!!
446// file_put_contents('/var/www/log'
447// , spl_object_hash($this)."\n"
448// . 'Order: '.print_r($order,true)
449// . 'Filter: '.print_r($filter,true)
450// . 'Group: '.print_r($group,true)
451// . 'Select: '.print_r($select,true)
452// ."\n\n\n"
453// , FILE_APPEND);
454
455 static $aggregates = array('COUNT'=>1, 'AVG'=>1, 'MIN'=>1, 'MAX'=>1, 'SUM'=>1);
456
457 foreach ($filter as $key => $value)
458 {
459 $key = $this->mapLocationRuntimeField($key, true);
460
461 $this->compatibleAddFilter($key, $value);
462 }
463
464 if (is_array($group))
465 {
466 if (empty($group))
467 {
468 $this->counted = true;
469 return;
470 }
471 else
472 {
473 foreach ($group as $key => $alias)
474 {
475 if ($name = $this->getAliasName($alias))
476 {
477 if (is_string($key) && ($aggregate = ToUpper($key)) && $aggregates[$aggregate])
478 {
479 $this->addAggregatedSelect($alias, $aggregate, $name);
480 }
481 else
482 {
483 $this->grouped = true;
484 $this->addGroup($name);
485 $this->addSelect($name, $alias);
486 }
487 }
488 }
489
490 if ($this->grouped)
491 {
492 $this->addAggregatedSelect('CNT', 'COUNT(*)');
493 // TODO Maybe? "COUNT(%%_DISTINCT_%% ".$arFields[$arFieldsKeys[0]]["FIELD"].") as CNT ";
494 }
495 }
496 }
497 else
498 {
499 if (empty($select) || $select == array('*'))
500 {
501 $this->allSelected = true;
502
503 foreach ($this->getAliases() as $alias => $field)
504 {
505 $field = $this->mapLocationRuntimeField($field);
506
507 $this->addAliasSelect($alias);
508 }
509 }
510 else
511 {
512 foreach ($select as $key => $alias)
513 {
514 $alias = $this->mapLocationRuntimeField($alias);
515
516 if ($name = $this->getAliasName($alias))
517 {
518 if (is_string($key) && ($aggregate = ToUpper($key)) && $aggregates[$aggregate])
519 {
520 $this->addAggregatedSelect($alias, $aggregate, $name);
521 }
522 else
523 {
524 $this->addSelect($name, $alias);
525 }
526 }
527 }
528 }
529 }
530
531 foreach ($order as $alias => $value)
532 {
533 $alias = $this->mapLocationRuntimeField($alias);
534
535 $this->addAliasOrder($alias, $value);
536 }
537 }
538
539 public function getSelectNamesAssoc()
540 {
541 $names = array();
542
543 foreach ($this->getSelect() as $k => $v)
544 {
545 if (is_numeric($k))
546 {
547 if ($v instanceof Field)
548 $names[$v->getName()] = true;
549 else
550 throw new SystemException("invalid", 0, __FILE__, __LINE__);
551 }
552 else
553 {
554 $names[$k] = true;
555 }
556 }
557
558 return $names;
559 }
560
561 protected function prepareCompatibleRows(array $rows)
562 {
563 return $rows;
564 }
565
566 public function compatibleExec(CDBResult $result, $navStart)
567 {
568 if ($this->aggregated)
569 {
570 $result->addFetchAdapter(new AggregateAdapter($this->aggregated));
571 }
572
573 if (!empty($navStart) && is_array($navStart))
574 {
575 if (!empty($navStart['nTopCount']))
576 {
577 $this->setLimit($navStart['nTopCount']);
578 }
579 else
580 {
581 $result->compatibleNavQuery($this, $navStart);
582 return $result;
583 }
584 }
585
586 $rows = $this->exec()->fetchAll();
587 $rows = $this->prepareCompatibleRows($rows);
588 $result->InitFromArray($rows);
589
590 return $result;
591 }
592}
593
595{
600 protected function prepareCompatibleRows(array $rows)
601 {
602 $locationIds = [];
603 foreach ($rows as $key => $row)
604 {
605 if ($row['TYPE'] === 'LOCATION' && !empty($row['VALUE']))
606 {
607 if (is_array($row['VALUE']))
608 {
609 $locationIds = array_merge($locationIds, $row['VALUE']);
610 }
611 else
612 {
613 $locationIds[] = $row['VALUE'];
614 }
615 }
616 }
617
618 if (!empty($locationIds))
619 {
620 $locationMap = [];
621 $locationRaw = \Bitrix\Sale\Location\LocationTable::getList([
622 'filter' => ['=CODE' => $locationIds],
623 'select' => ['ID', 'CODE']
624 ]);
625 while ($location = $locationRaw->fetch())
626 {
627 $locationMap[$location['CODE']] = $location['ID'];
628 }
629 }
630
631 foreach ($rows as &$row)
632 {
633 if (isset($row['VALUE']))
634 {
635 if ($row['TYPE'] === 'LOCATION' && !empty($row['VALUE']))
636 {
637 if (is_array($row['VALUE']))
638 {
639 foreach ($row['VALUE'] as &$valueItem)
640 {
641 $valueItem = $locationMap[$valueItem];
642 }
643 }
644 else
645 {
646 $row['VALUE'] = $locationMap[$row['VALUE']];
647 }
648 }
649
650 $row['PROXY_VALUE'] = $row['VALUE'];
651 if (is_array($row['VALUE']))
652 {
653 $row['PROXY_VALUE'] = serialize($row['VALUE']);
654 }
655 unset($row['VALUE']);
656 }
657 }
658
659 return $rows;
660 }
661}
662
664{
665 protected $locationFieldMap = array();
666
667 public function addLocationRuntimeField($fieldName, $ref = false)
668 {
669 if((string) $fieldName == '')
670 return false;
671
672
674 'LOCATION',
675 array(
676 'data_type' => '\Bitrix\Sale\Location\LocationTable',
677 'reference' => array(
678 '=this.'.$fieldName => 'ref.CODE'
679 ),
680 'join_type' => 'left'
681 )
682 );
683
684 $fieldType = "CHAR";
686 $connection = Application::getConnection();
687 if ($connection instanceof DB\MssqlConnection)
688 {
689 $fieldType = "VARCHAR";
690 }
691 elseif($connection instanceof DB\OracleConnection)
692 {
693 $fieldType = "VARCHAR2";
694 }
695
697 'PROXY_'.$fieldName.'_LINK',
698 array(
699 'data_type' => 'string',
700 'expression' => array(
701 "CASE WHEN %s = 'LOCATION' THEN CAST(%s AS ".$fieldType."(500)) ELSE %s END",
702 ($ref !== false ? $ref.'.' : '').'TYPE',
703 'LOCATION.ID',
704 $fieldName
705 )
706 )
707 );
708
709 $this->addAliases(array(
710 $fieldName.'_ORIG' => $fieldName,
711 'PROXY_'.$fieldName => 'PROXY_'.$fieldName.'_LINK'
712 ));
713
714 $this->locationFieldMap[$fieldName] = true;
715 }
716
717 protected function mapLocationRuntimeField($field, $asFilter = false)
718 {
719 if($asFilter)
720 {
721 $parsed = static::explodeFilterKey($field);
722
723 if (isset($this->locationFieldMap[$parsed['alias']]))
724 {
725 return $parsed['modifier'].$parsed['operator'].'PROXY_'.$parsed['alias'];
726 }
727 else
728 {
729 return $field;
730 }
731 }
732 else
733 {
734 if (isset($this->locationFieldMap[$field]))
735 {
736 return 'PROXY_'.$field;
737 }
738 else
739 {
740 return $field;
741 }
742 }
743 }
744}
745
746final class Test
747{
748 static function assertLastQuery($name, $query)
749 {
750 $lastQuery = Query::getLastQuery();
751 return $query == $lastQuery ? "ok\n" : "\n$name - Assert Last Query Failed!\nExpected:\n($query)\nGiven:\n($lastQuery)\n\n";
752 }
753
754 static function run()
755 {
756 foreach (glob($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/sale/lib/compatible/tests/*.test.php') as $filename)
757 {
758 include $filename;
759 }
760 }
761}
static getConnection($name="")
addOrder($definition, $order='ASC')
Definition query.php:508
registerRuntimeField($name, $fieldInfo=null)
Definition query.php:831
addFilter($key, $value)
Definition query.php:394
addSelect($definition, $alias='')
Definition query.php:351
compatibleNavQuery(Query $query, array $arNavStartParams)
addFetchAdapter(FetchAdapter $adapter)
prepare(array $order, array $filter, $group, array $select)
mapLocationRuntimeField($field, $asFilter=false)
compatibleExec(CDBResult $result, $navStart)
mapLocationRuntimeField($field, $asFilter=false)
static assertLastQuery($name, $query)