Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
statistics.php
1<?php
8namespace Bitrix\Sender\Stat;
9
12use Bitrix\Main\Entity\ExpressionField;
23
24Loc::loadMessages(__FILE__);
25
27{
28 CONST AVERAGE_EFFICIENCY = 0.15;
29 CONST USER_OPTION_FILTER_NAME = 'statistics_filter';
30
32 protected $filter;
33
35 protected $cacheTtl = 3600;
36
38 protected $counters = null;
39
41 protected $userId = null;
42
49 public static function create(Filter $filter = null)
50 {
51 return new static($filter);
52 }
53
59 public function __construct(Filter $filter = null)
60 {
61 if ($filter)
62 {
63 $this->filter = $filter;
64 }
65 else
66 {
67 $this->filter = new Filter();
68 }
69
70 }
71
78 public function setCacheTtl($cacheTtl)
79 {
80 $this->cacheTtl = $cacheTtl;
81 return $this;
82 }
83
90 public function setUserId($userId)
91 {
92 $this->userId = $userId;
93 return $this;
94 }
95
101 public function getCacheTtl()
102 {
103 return $this->cacheTtl;
104 }
105
111 public function getFilter()
112 {
113 return $this->filter;
114 }
115
123 public function filter($name, $value = null)
124 {
125 $this->filter->set($name, $value);
126 return $this;
127 }
128
134 public function initFilterFromRequest()
135 {
136 $request = Context::getCurrent()->getRequest();
137 if ($request->isPost())
138 {
139 $list = $this->filter->getNames();
140 foreach ($list as $name)
141 {
142 $this->filter->set($name, (string) $request->get($name));
143 }
144
145 $this->saveFilterToUserOption();
146 }
147 else
148 {
150 }
151
152 return $this;
153 }
154
160 protected function initFilterFromUserOption()
161 {
162 $isFilterSet = false;
163 if ($this->userId)
164 {
165 $userOptionFilters = \CUserOptions::getOption(
166 'sender',
167 self::USER_OPTION_FILTER_NAME,
168 array(),
169 false,
170 $this->userId
171 );
172 $list = $this->filter->getNames();
173 foreach ($list as $name)
174 {
175 if (
176 !isset($userOptionFilters[$name])
177 || !$userOptionFilters[$name]
178 || !$this->checkFilterValue($userOptionFilters[$name])
179 )
180 {
181 continue;
182 }
183 try
184 {
185 $this->filter->set($name, (string) $userOptionFilters[$name]);
186 $isFilterSet = true;
187 }
188 catch (\Exception $e)
189 {
190 }
191 }
192 }
193
194 if (!$isFilterSet && !$this->filter->get('period'))
195 {
196 $this->filter->set('period', Filter::PERIOD_MONTH);
197 }
198
199 return $this;
200 }
201
202 private function checkFilterValue($filter)
203 {
204 return $filter === null
205 || is_scalar($filter);
206 }
207
208 protected function saveFilterToUserOption()
209 {
210 if (!$this->userId)
211 {
212 return;
213 }
214
215 $filter = array();
216 $list = $this->filter->getNames();
217 foreach ($list as $name)
218 {
219 $value = $this->filter->get($name);
220 if (!$value)
221 {
222 continue;
223 }
224
225 $filter[$name] = $value;
226 }
227
228 \CUserOptions::setOption('sender', self::USER_OPTION_FILTER_NAME, $filter, false, $this->userId);
229 }
230
231 protected static function calculateEfficiency($counters, $maxEfficiency = null)
232 {
233 $efficiency = self::div('CLICK', 'READ', $counters);
234 if (!$maxEfficiency)
235 {
236 $maxEfficiency = self::AVERAGE_EFFICIENCY * 2;
237 }
238 $efficiency = $efficiency > $maxEfficiency ? $maxEfficiency : $efficiency;
239 return self::getCounterCalculation('EFFICIENCY', $efficiency, $maxEfficiency);
240 }
241
242 protected static function div($dividendCode, $dividerCode, $items)
243 {
244 $divider = 0;
245 foreach ($items as $item)
246 {
247 if ($item['CODE'] == $dividerCode)
248 {
249 $divider = (float) $item['VALUE'];
250 break;
251 }
252 }
253
254 if ($divider == 0)
255 {
256 return 0;
257 }
258
259 $dividend = 0;
260 foreach ($items as $item)
261 {
262 if ($item['CODE'] == $dividendCode)
263 {
264 $dividend = (float) $item['VALUE'];
265 $dividend = $dividend > $divider ? $divider : $dividend;
266 break;
267 }
268 }
269
270 return $dividend / $divider;
271 }
272
273 protected static function formatNumber($number, $num = 1)
274 {
275 $formatted = number_format($number, $num, '.', ' ');
276 $formatted = mb_substr($formatted, -($num + 1)) == '.'.str_repeat('0', $num)? mb_substr($formatted, 0, -2) : $formatted;
277 return $formatted;
278 }
279
280 protected static function getCounterCalculation($code, $value, $percentBase = 0)
281 {
282 $value = (float) $value;
283 $percentValue = $percentBase > 0 ? $value / $percentBase : 0;
284
285 return array(
286 'CODE' => $code,
287 'VALUE' => round($value, 3),
288 'VALUE_DISPLAY' => self::formatNumber($value),
289 'PERCENT_VALUE' => round($percentValue, 3),
290 'PERCENT_VALUE_DISPLAY' => self::formatNumber($percentValue * 100),
291 );
292 }
293
294 protected function getMappedFilter()
295 {
296 $filter = array(
297 '!=STATUS' => PostingTable::STATUS_NEW,
298 '=MAILING.IS_TRIGGER' => 'N',
299 '=MAILING_CHAIN.MESSAGE_CODE' => Message\iBase::CODE_MAIL
300 );
301
302 $fieldsMap = array(
303 'chainId' => '=MAILING_CHAIN_ID',
304 'periodFrom' => '>DATE_SENT',
305 'periodTo' => '<DATE_SENT',
306 'mailingId' => '=MAILING_ID',
307 'postingId' => '=ID',
308 'authorId' => '=MAILING_CHAIN.CREATED_BY',
309 );
310 return $this->filter->getMappedArray($fieldsMap, $filter);
311 }
312
318 public function getEfficiency()
319 {
320 return self::calculateEfficiency($this->getCounters());
321 }
322
328 public function getCountersDynamic()
329 {
330 $list = array();
331 $filter = $this->getMappedFilter();
332 $select = array(
333 'SEND_ALL' => 'COUNT_SEND_ALL',
334 'SEND_ERROR' => 'COUNT_SEND_ERROR',
335 'SEND_SUCCESS' => 'COUNT_SEND_SUCCESS',
336 'READ' => 'COUNT_READ',
337 'CLICK' => 'COUNT_CLICK',
338 'UNSUB' => 'COUNT_UNSUB'
339 );
340 $sqlHelper = Application::getConnection()->getSqlHelper();
341 $runtime = [
342 new ExpressionField('CNT', 'COUNT(%s)', 'ID'),
343 new ExpressionField('DATE', $sqlHelper->getDatetimeToDateFunction('%s'), 'DATE_SENT'),
344 ];
345 foreach ($select as $alias => $fieldName)
346 {
347 $runtime[] = new ExpressionField($alias, 'SUM(%s)', $fieldName);
348 }
349 $select = array_keys($select);
350 $select[] = 'DATE';
351 $select[] = 'CNT';
352 $listDb = PostingTable::getList(array(
353 'select' => $select,
354 'filter' => $filter,
355 'runtime' => $runtime,
356 'order' => array('DATE' => 'ASC'),
357 'cache' => array('ttl' => $this->getCacheTtl(), 'cache_joins' => true)
358 ));
359 while($item = $listDb->fetch())
360 {
361 $date = null;
362 foreach ($item as $name => $value)
363 {
364 if (!in_array($name, array('DATE')))
365 {
366 continue;
367 }
368
369 if ($item['DATE'])
370 {
372 $date = $item['DATE']->getTimestamp();
373 }
374 }
375
376 $counters = array();
377 foreach ($item as $name => $value)
378 {
379 if (!in_array($name, array('READ', 'CLICK', 'UNSUB')))
380 {
381 continue;
382 }
383 else
384 {
385 $base = $item['SEND_SUCCESS'];
386 }
387
388 $counter = self::getCounterCalculation($name, $value, $base);
389 $counter['DATE'] = $date;
390 $counters[] = $counter;
391 $list[$name][] = $counter;
392 }
393
394 $effCounter = self::calculateEfficiency($counters, 1);
395 $effCounter['DATE'] = $date;
396 $list['EFFICIENCY'][] = $effCounter;
397 }
398
399 return $list;
400
401 }
402
408 public function getCounters()
409 {
410 if ($this->counters !== null)
411 {
412 return $this->counters;
413 }
414
415 $list = array();
416 $filter = $this->getMappedFilter();
417 $select = array(
418 'SEND_ALL' => 'COUNT_SEND_ALL',
419 'SEND_ERROR' => 'COUNT_SEND_ERROR',
420 'SEND_SUCCESS' => 'COUNT_SEND_SUCCESS',
421 'READ' => 'COUNT_READ',
422 'CLICK' => 'COUNT_CLICK',
423 'UNSUB' => 'COUNT_UNSUB'
424 );
425 $runtime = array();
426 foreach ($select as $alias => $fieldName)
427 {
428 $runtime[] = new ExpressionField($alias, 'SUM(%s)', $fieldName);
429 }
430 $listDb = PostingTable::getList(array(
431 'select' => array_keys($select),
432 'filter' => $filter,
433 'runtime' => $runtime,
434 'cache' => array('ttl' => $this->getCacheTtl(), 'cache_joins' => true)
435 ));
436 while ($item = $listDb->fetch())
437 {
438 $list = array_merge($list, $this->createListFromItem($item));
439 }
440
441 $this->counters = $list;
442 return $list;
443 }
444
445 public function initFromArray($postingData)
446 {
447 $item = [
448 'SEND_ALL' => (int)$postingData['COUNT_SEND_ALL'],
449 'SEND_ERROR' => (int)$postingData['COUNT_SEND_ERROR'],
450 'SEND_SUCCESS' => (int)$postingData['COUNT_SEND_SUCCESS'],
451 'READ' => (int)$postingData['COUNT_READ'],
452 'CLICK' => (int)$postingData['COUNT_CLICK'],
453 'UNSUB' => (int)$postingData['COUNT_UNSUB']
454 ];
455 $this->counters = $this->createListFromItem($item);
456
457 return $this;
458 }
459
460 protected function createListFromItem($item)
461 {
462 $list = [];
463 foreach ($item as $name => $value)
464 {
465 if (mb_substr($name, 0, 4) == 'SEND')
466 {
467 $base = $item['SEND_ALL'];
468 }
469 else
470 {
471 $base = $item['SEND_SUCCESS'];
472 }
473 $list[] = self::getCounterCalculation($name, $value, $base);
474 }
475 return $list;
476 }
477
483 public function getCounterPostings()
484 {
485 $query = PostingTable::query();
486 $query->addSelect(new ExpressionField('CNT', 'COUNT(1)'));
487 $query->setFilter($this->getMappedFilter());
488 $query->setCacheTtl($this->getCacheTtl());
489 $query->cacheJoins(true);
490 $result = $query->exec()->fetch();
491
492 return self::getCounterCalculation('POSTINGS', $result['CNT']);
493 }
494
500 public function getCounterSubscribers()
501 {
502 $filter = array('=IS_UNSUB' => 'N');
503 $map = array(
504 'mailingId' => '=MAILING_ID',
505 'periodFrom' => '>DATE_INSERT',
506 'periodTo' => '<DATE_INSERT',
507 );
508 $filter = $this->filter->getMappedArray($map, $filter);
509
510 $query = MailingSubscriptionTable::query();
511 $query->addSelect(new ExpressionField('CNT', 'COUNT(1)'));
512 $query->setFilter($filter);
513 $query->setCacheTtl($this->getCacheTtl());
514 $query->cacheJoins(true);
515 $result = $query->exec()->fetch();
516
517 return self::getCounterCalculation('SUBS', $result['CNT']);
518 }
519
526 public function getClickLinks($limit = 15)
527 {
528 $list = array();
529 $clickDb = PostingClickTable::getList(array(
530 'select' => array('URL', 'CNT'),
531 'filter' => array(
532 '=POSTING_ID' => $this->filter->get('postingId'),
533 ),
534 'runtime' => array(
535 new ExpressionField('CNT', 'COUNT(%s)', 'ID'),
536 ),
537 'group' => array('URL'),
538 'order' => array('CNT' => 'DESC'),
539 'limit' => $limit
540 ));
541 while($click = $clickDb->fetch())
542 {
543 $list[] = $click;
544 }
545
546 // TODO: temporary block! Remove
547 if (!empty($list))
548 {
549 $letter = Entity\Letter::createInstanceByPostingId($this->filter->get('postingId'));
550 $linkParams = $letter->getMessage()->getConfiguration()->get('LINK_PARAMS');
551 if (!$linkParams)
552 {
553 return $list;
554 }
555
556 $parametersTmp = [];
557 parse_str($linkParams, $parametersTmp);
558 if (!is_array($parametersTmp) || empty($parametersTmp))
559 {
560 return $list;
561 }
562 $linkParams = array_keys($parametersTmp);
563
564 $groupedList = [];
565 foreach ($list as $index => $item)
566 {
567 $item['URL'] = (new Uri($item['URL']))
568 ->deleteParams($linkParams, true)
569 ->getUri();
570 $item['URL'] = urldecode($item['URL']);
571 if (!isset($groupedList[$item['URL']]))
572 {
573 $groupedList[$item['URL']] = 0;
574 }
575 $groupedList[$item['URL']] += $item['CNT'];
576 }
577 $list = [];
578 foreach ($groupedList as $url => $cnt)
579 {
580 $list[] = ['URL' => $url, 'CNT' => $cnt];
581 }
582 }
583
584 return $list;
585 }
586
593 public function getReadingByDayTime($step = 2)
594 {
595 $list = array();
596 for ($i = 0; $i < 24; $i++)
597 {
598 $list[$i] = array(
599 'CNT' => 0,
600 'CNT_DISPLAY' => 0,
601 'DAY_HOUR' => $i,
602 'DAY_HOUR_DISPLAY' => (mb_strlen($i) == 1 ? '0' : '') . $i . ':00',
603 );
604 }
605
606 $filter = $this->getMappedFilter();
607 $readDb = PostingTable::getList(array(
608 'select' => array('DAY_HOUR', 'CNT'),
609 'filter' => $filter,
610 'runtime' => array(
611 new ExpressionField('CNT', 'COUNT(%s)', 'POSTING_READ.ID'),
612 new ExpressionField('DAY_HOUR', 'HOUR(%s)', 'POSTING_READ.DATE_INSERT'),
613 ),
614 'order' => array('DAY_HOUR' => 'ASC'),
615 ));
616 while($read = $readDb->fetch())
617 {
618 $read['DAY_HOUR'] = intval($read['DAY_HOUR']);
619 if (array_key_exists($read['DAY_HOUR'], $list))
620 {
621 $list[$read['DAY_HOUR']]['CNT'] = $read['CNT'];
622 $list[$read['DAY_HOUR']]['CNT_DISPLAY'] = self::formatNumber($read['CNT'], 0);
623 }
624 }
625
626 if ($step > 1)
627 {
628 for ($i = 0; $i < 24; $i+=$step)
629 {
630 for ($j = 1; $j < $step; $j++)
631 {
632 $list[$i]['CNT'] += $list[$i + $j]['CNT'];
633 unset($list[$i + $j]);
634 }
635 $list[$i]['CNT_DISPLAY'] = self::formatNumber($list[$i]['CNT'], 0);
636 }
637 }
638
639 $list = array_values($list);
640
641 return $list;
642 }
643
650 public function getRecommendedSendTime($chainId = null)
651 {
652 $timeList = $this->getReadingByDayTime(1);
653 $len = count($timeList);
654 $weightList = array();
655 for ($i = 0; $i <= $len; $i++)
656 {
657 $j = $i + 1;
658 if ($j > $len)
659 {
660 $j = 0;
661 }
662 else if ($j < 0)
663 {
664 $j = 23;
665 }
666 $weight = $timeList[$i]['CNT'] + $timeList[$j]['CNT'];
667 $weightList[$i] = $weight;
668 }
669
670 $deliveryTime = 0;
671 if ($chainId)
672 {
673 $listDb = PostingTable::getList(array(
674 'select' => array('COUNT_SEND_ALL'),
675 'filter' => array(
676 '=MAILING_CHAIN_ID' => $chainId
677 ),
678 'order' => array('DATE_CREATE' => 'DESC'),
679 ));
680 if ($item = $listDb->fetch())
681 {
682 $deliveryTime = intval($item['COUNT_SEND_ALL'] * 1/10 * 1/3600);
683 }
684 }
685 if ($deliveryTime <= 0)
686 {
687 $deliveryTime = 1;
688 }
689
690 arsort($weightList);
691 foreach ($weightList as $i => $weight)
692 {
693 $i -= $deliveryTime;
694 if ($i >= $len)
695 {
696 $i = $i - $len;
697 }
698 else if ($i < 0)
699 {
700 $i = $len + $i;
701 }
702 $timeList[$i]['DELIVERY_TIME'] = $deliveryTime;
703 return $timeList[$i];
704 }
705
706 return null;
707 }
708
715 public function getChainList($limit = 20)
716 {
717 $filter = $this->getMappedFilter();
718 $listDb = PostingTable::getList(array(
719 'select' => array(
720 'MAX_DATE_SENT',
721 'CHAIN_ID' => 'MAILING_CHAIN_ID',
722 'TITLE' => 'MAILING_CHAIN.TITLE',
723 'MAILING_ID',
724 'MAILING_NAME' => 'MAILING.NAME',
725 ),
726 'filter' => $filter,
727 'runtime' => array(
728 new ExpressionField('MAX_DATE_SENT', 'MAX(%s)', 'DATE_SENT'),
729 ),
730 //'group' => array('CHAIN_ID', 'TITLE', 'SUBJECT', 'MAILING_ID', 'MAILING_NAME'),
731 'order' => array('MAX_DATE_SENT' => 'DESC'),
732 'limit' => $limit,
733 'cache' => array('ttl' => $this->getCacheTtl(), 'cache_joins' => true)
734 ));
735 $list = array();
736 while ($item = $listDb->fetch())
737 {
738 $dateSentFormatted = '';
739 if ($item['MAX_DATE_SENT'])
740 {
741 $dateSentFormatted = \FormatDate('x', $item['MAX_DATE_SENT']->getTimestamp());
742 }
743
744 $list[] = array(
745 'ID' => $item['CHAIN_ID'],
746 'NAME' => $item['TITLE'] ? $item['TITLE'] : $item['SUBJECT'],
747 'MAILING_ID' => $item['MAILING_ID'],
748 'MAILING_NAME' => $item['MAILING_NAME'],
749 'DATE_SENT' => (string) $item['MAX_DATE_SENT'],
750 'DATE_SENT_FORMATTED' => $dateSentFormatted,
751 );
752 }
753 return $list;
754 }
755
761 public function getGlobalFilterData()
762 {
763 $period = $this->getFilter()->get('period');
764
765 return array(
766 array(
767 'name' => 'authorId',
768 'value' => $this->getFilter()->get('authorId'),
769 'list' => $this->getAuthorList(),
770 ),
771 array(
772 'name' => 'period',
773 'value' => $period ? $period : Filter::PERIOD_MONTH,
774 'list' => $this->getPeriodList(),
775 )
776 );
777 }
778
784 protected function getPeriodList()
785 {
786 $list = array(
787 Filter::PERIOD_WEEK,
788 Filter::PERIOD_MONTH,
789 Filter::PERIOD_MONTH_3,
790 Filter::PERIOD_MONTH_6,
791 Filter::PERIOD_MONTH_12
792 );
793
794 $result = array();
795 foreach ($list as $period)
796 {
797 $result[] = array(
798 'ID' => $period,
799 'NAME' => Loc::getMessage('SENDER_STAT_STATISTICS_FILTER_PERIOD_' . $period)
800 );
801 }
802
803 return $result;
804 }
805
811 protected function getAuthorList()
812 {
813 $listDb = MailingChainTable::getList(array(
814 'select' => ['CREATED_BY', 'MAX_DATE_INSERT'],
815 'group' => ['CREATED_BY'],
816 'runtime' => [new ExpressionField('MAX_DATE_INSERT', 'MAX(%s)', 'DATE_INSERT'),],
817 'limit' => 100,
818 'order' => ['MAX_DATE_INSERT' => 'DESC'],
819 'cache' => ['ttl' => $this->getCacheTtl(), 'cache_joins' => true]
820 ));
821 $userList = array();
822 while ($item = $listDb->fetch())
823 {
824 if (!$item['CREATED_BY'])
825 {
826 continue;
827 }
828
829 $userList[] = $item['CREATED_BY'];
830 }
831
832 $list = array();
833 $list[] = array(
834 'ID' => 'all',
835 'NAME' => Loc::getMessage('SENDER_STAT_STATISTICS_FILTER_AUTHOR_FROM_ALL')
836 );
837
839 global $USER;
840 if (is_object($USER) && $USER->getID())
841 {
842 $list[] = array(
843 'ID' => $USER->getID(),
844 'NAME' => Loc::getMessage('SENDER_STAT_STATISTICS_FILTER_AUTHOR_FROM_ME')
845 );
846 }
847
848 $listDb = UserTable::getList(array(
849 'select' => array(
850 'ID',
851 'TITLE',
852 'NAME',
853 'SECOND_NAME',
854 'LAST_NAME',
855 'LOGIN',
856 ),
857 'filter' => array('=ID' => $userList),
858 'order' => array('NAME' => 'ASC')
859 ));
860 while ($item = $listDb->fetch())
861 {
862 $name = \CUser::formatName(\CSite::getNameFormat(true), $item, true, true);
863 $list[] = array(
864 'ID' => $item['ID'],
865 'NAME' => $name,
866 );
867 }
868
869 return $list;
870 }
871}
static getConnection($name="")
static getCurrent()
Definition context.php:241
static loadMessages($file)
Definition loc.php:64
static getMessage($code, $replace=null, $language=null)
Definition loc.php:29
static getCounterCalculation($code, $value, $percentBase=0)
static calculateEfficiency($counters, $maxEfficiency=null)
__construct(Filter $filter=null)
static create(Filter $filter=null)
static formatNumber($number, $num=1)
filter($name, $value=null)
static div($dividendCode, $dividerCode, $items)
getRecommendedSendTime($chainId=null)