config('listings.form.publication.period.mode', static::UNAVAILABLE, TYPE_UINT); } /** * Количество дней для UNAVAILABLE * @return int */ public function period() { return $this->config('listings.item.publication.period', 7, TYPE_UINT); } /** * Список периодов для AVAILABLE * @return array */ public function periods() { return $this->config('listings.item.publication.periods', [], TYPE_ARRAY); } /** * Значение по умолчанию для AVAILABLE * @return int */ public function default() { return (int)$this->config('listings.item.publication.period.default', 30, TYPE_UINT); } /** * Значение refresh period * @return int|mixed */ public function refresh() { $days = $this->config('listings.items.refresh.period', 7, TYPE_UINT); if ($days <= 0) { $days = 7; } return $days; } /** * Compare publication mode * @param $mode * @return bool */ public function is($mode) { return $this->mode() === $mode; } /** * Period can be specified during publication * @return bool */ public function isAvailable() { return $this->is(static::AVAILABLE); } /** * Period can not be specified during publication * @return bool */ public function isUnavailable() { return $this->is(static::UNAVAILABLE); } /** * Publication period is unlimited * @return bool */ public function isIndefinitely() { return $this->is(static::INDEFINITELY); } /** * Набор значений для возможности выбора периода публикации static::AVAILABLE * @return array */ public function variants() { $days = _t('', 'day;days;days'); $weeks = _t('', 'week;weeks;weeks'); $months = _t('', 'month;months;months'); $years = _t('', 'year;years;years'); $days = $this->app->filter('listings.items.publication.period.variants', [ 3 => ['t' => tpl::declension(3, $days), 'a' => 0, 'def' => 0], 7 => ['t' => tpl::declension(1, $weeks), 'a' => 1, 'def' => 0], 14 => ['t' => tpl::declension(2, $weeks), 'a' => 1, 'def' => 0], 30 => ['t' => tpl::declension(1, $months),'a' => 1, 'def' => 1], 60 => ['t' => tpl::declension(2, $months),'a' => 1, 'def' => 0], 90 => ['t' => tpl::declension(3, $months),'a' => 1, 'def' => 0], 180 => ['t' => tpl::declension(6, $months),'a' => 0, 'def' => 0], 365 => ['t' => tpl::declension(1, $years), 'a' => 0, 'def' => 0], 730 => ['t' => tpl::declension(2, $years), 'a' => 0, 'def' => 0], ], ['days' => $days, 'weeks' => $weeks, 'months' => $months, 'years' => $years]); foreach ($days as $k => &$v) { $v['days'] = $k; } unset($v); return $days; } /** * Значения периода публикации для HTML::selectOptions * @param array $opts * @return array */ public function options($opts = []) { if (isset($opts['default'])) { $default = & $opts['default']; } $save = $this->periods(); $default = $this->default(); if (empty($save[$default])) { $default = 0; } $result = $this->variants(); foreach ($result as $k => &$v) { if (empty($save[ $k ])) { unset($result[$k]); continue; } } unset($v); if (empty($result)) { $result = $this->variants(); } if (! $default) { foreach ($result as $v) { if ($v['def']) { $default = $v['days']; break; } } } foreach ($result as &$v) { $v['a'] = ($default == $v['days']); unset($v['def']); } unset($v); return $result; } /** * Получаем срок публикации объявления в днях * @param array $opts параметры * @return int|string */ public function publishTo($opts = []) { $opts = $this->defaults($opts, [ 'days' => 0, 'from' => '', 'format' => 'Y-m-d H:i:s', 'cat' => 0, 'period' => false, ]); $days = $opts['days']; $from = $opts['from']; $format = $opts['format']; if ($opts['period'] !== false) { if ($opts['period'] > 0) { $days = $opts['period']; if ($this->isAvailable()) { $default = 0; $options = $this->options(['default' => & $default]); if (! isset($options[$days])) { $days = $default; } } else { if ($this->isUnavailable()) { $days = $this->period(); } } if ($days <= 0) { $days = 7; } } else { $days = 0; } } if ($this->isIndefinitely()) { $days = 0; } if (empty($from)) { $from = $this->db->now(); } if (is_string($from)) { $from = strtotime($from); if ($from === false) { $from = strtotime($this->db->now()); } } $period = strtotime('+' . $days . ' days', $from); if (! empty($format)) { return date($format, $period); } else { return $period; } } /** * Получаем срок продления объявления в днях (столбец publicated_to) * @param mixed $from дата от которой выполняется подсчет срока публикации * @param string $format тип требуемого результата, строка = формат даты, false - unixtime * @return int */ public function refreshTo($from = false, string $format = 'Y-m-d H:i:s') { $days = $this->refresh(); if ($this->isIndefinitely()) { $maxDays = 0; } else { if ($this->isAvailable()) { $options = $this->options(); $options = end($options); $maxDays = $options['days'] ?? 0; $maxDays = max($days, $maxDays); } else { # isUnavailable $publicationPeriod = $this->period(); $maxDays = max($days, $publicationPeriod); } } $maxDate = strtotime('+' . $maxDays . ' days'); if (empty($from)) { $from = $this->db->now(); } if (is_string($from)) { $from = strtotime($from); if ($from === false) { $from = strtotime($this->db->now()); } } $period = strtotime('+' . $days . ' days', $from); if ($period > $maxDate) { $period = $maxDate; } if (! empty($format)) { return date($format, $period); } else { return $period; } } /** * Получаем значение периода публикации объявления (столбец publicated_period) * @param int $period текущее значение периода * @param int|array $cat категория объявления или настройки категории (если есть) * @return int */ public function publishPeriod($period, $cat = 0) { if (! empty($cat)) { if (is_numeric($cat)) { $cat = Listings::model()->catData($cat, ['settings']); $cat = $cat['settings'] ?? []; } if (! empty($cat['publication']['infinite'])) { return 0; } } switch ($this->mode()) { case static::UNAVAILABLE: return 30; case static::AVAILABLE: return $period ?: $this->default(); case static::INDEFINITELY: return 0; } return $this->default(); } /** * @param mixed $save * @param array $opts * @return mixed */ public function cronRecalculateStatus($save = null, $opts = []) { $key = 'listings.publication.period.recalculate.status'; $opts = $this->defaults($opts, [ 'replace' => false, 'merge' => true, 'unset' => [], 'key' => false, 'id' => false, ]); if (is_null($save)) { $data = $this->app->cronManager()->lockGet($key, []); if (! is_array($data)) { $data = []; } if (! empty($opts['key'])) { $k = $opts['key']; if (empty($opts['id'])) { return $data[$k] ?? false; } else { $id = $opts['id']; return $data[$k][$id] ?? false; } } return $data; } else { $this->app->cronManager()->lockUpdate($key, static function ($data) use (&$save, &$opts) { if (! is_array($data)) { $data = []; } $apply = static function ($data, $save, $opts) { if ($opts['replace']) { $data = $save; } elseif ($opts['merge'] && is_array($save)) { $data = array_merge($data, $save); } if (! empty($opts['unset'])) { $unset = $opts['unset']; if (! is_array($unset)) { $unset = [$unset]; } foreach ($unset as $v) { unset($data[$v]); } } return $data; }; if (! empty($opts['key'])) { $k = $opts['key']; $o = $opts; unset($o['key']); if (empty($o['id'])) { $data[$k] = $apply($data[$k] ?? [], $save, $o); } else { $id = $o['id']; unset($o['id']); $data[$k][$id] = $apply($data[$k][$id] ?? [], $save, $o); } } else { $data = $apply($data, $save, $opts); } return $data; }); } return null; } /** * Событие сохранение настроек категории * @param int $categoryId * @param array $new новые данные категории * @param array $before данные категории до сохранения */ public function onCategorySave($categoryId, $new, $before, $opts = []) { if (empty($categoryId)) { return; } $catData = Listings::model()->catData($categoryId, ['id', 'numleft', 'numright']); if (empty($catData)) { return; } if (! empty($new) && ! empty($before)) { if ($catData['numright'] - $catData['numleft'] == 1 || empty($opts['copyToSubs'])) { if (empty($new['settings']) || empty($before['settings'])) { return; } $infiniteNew = (int)($new['settings']['publication']['infinite'] ?? 0); $infiniteBefore = (int)($before['settings']['publication']['infinite'] ?? 0); if ($infiniteNew === $infiniteBefore) { return; } } if (! empty($opts['copyToSubs']) && ! empty($opts['copyToSubsParams'])) { if (! in_array('publication', $opts['copyToSubsParams'])) { return; } } } $isCatsDepends = static function ($a, $b) { if ($a['numleft'] < $b['numleft']) { $min = $a; $max = $b; } else { $min = $b; $max = $a; } if ( $min['numleft'] < $max['numleft'] && $min['numright'] < $max['numleft'] && $max['numleft'] > $min['numright'] && $max['numright'] > $min['numright'] ) { return false; } return true; }; $status = $this->cronRecalculateStatus(); $err = false; if (! empty($status['cat'])) { if (isset($status['cat'][$categoryId])) { $err = true; } else { foreach ($status['cat'] as $v) { if ($isCatsDepends($v, $catData)) { $err = true; break; } } } } if ($err) { $this->errors->set(_t('listings', 'Change Listing Publishing Period temporarily prohibited. Try again later.'), 'settings[publication][infinite]'); return; } $process = [ 'categoryId' => $categoryId, 'numleft' => $catData['numleft'], 'numright' => $catData['numright'], 'status' => 'scheduled', 'time' => time(), ]; $this->cronRecalculateStatus($process, ['key' => 'cat', 'id' => $categoryId]); $this->app->cronManager()->executeOnce('listings', 'onceRecalculatePublicationPeriod', ['categoryId' => $categoryId], $categoryId); } /** * Проверка возможности переключения глобальной настройки 'listings.form.publication.period.mode' * При необходимости будет запланирована задача для пересчета всех категорий * @param integer $newValue новое значение * @return bool */ public function recalculateAllCheck($newValue) { $mode = $this->mode(); if ($mode == $newValue) { return true; } $recalculate = false; if ($newValue == static::INDEFINITELY && in_array($mode, [static::AVAILABLE, static::UNAVAILABLE])) { $recalculate = true; } if ($mode == static::INDEFINITELY && in_array($newValue, [static::AVAILABLE, static::UNAVAILABLE])) { $recalculate = true; } if (! $recalculate) { return true; } $this->onCategorySave(Listings::CATS_ROOTID, [], []); return $this->errors->no(); } /** * Крон задача для пересчета publicated_period у объявлений в категориях * @param array $params */ public function cronOnceRecalculate($params = []) { if (empty($params)) { $this->log([__METHOD__, 'Empty params']); return; } if (empty($params['categoryId'])) { $this->log([__METHOD__, 'Empty categoryId']); return; } $categoryId = $params['categoryId']; $status = $this->cronRecalculateStatus(); if (! isset($status['cat'][$categoryId])) { $this->log([__METHOD__, 'Empty task for category', 'categoryId' => $categoryId]); return; } $process = $status['cat'][$categoryId]; if (empty($process['status']) || $process['status'] != 'scheduled') { $this->log([__METHOD__, 'Incorrect task status', 'categoryId' => $categoryId, 'process' => $process]); return; } $process['status'] = 'executing'; $process['pid'] = getmypid(); $process['time'] = time(); $this->cronRecalculateStatus($process, ['key' => 'cat', 'id' => $categoryId]); $this->cronRecalculateCategory($categoryId); $this->cronRecalculateStatus(false, ['key' => 'cat', 'unset' => $categoryId]); } /** * Пересчет publicated_period у объявлений в категориях. Chunk перебор подкатегорий * @param int $categoryId */ protected function cronRecalculateCategory($categoryId) { $catData = Listings::model()->catData($categoryId, ['id', 'numleft', 'numright', 'settings']); if (empty($catData)) { $this->log([__METHOD__, 'Category not found', 'categoryId' => $categoryId]); return; } $last = 0; do { $filter = [ 'id' => ['>', $last], 'numleft' => ['>=', $catData['numleft']], 'numright' => ['<=', $catData['numright']], ]; $cats = Listings::model()->catsDataByFilter($filter, ['id', 'numleft', 'numright', 'settings'], ['limit' => 100, 'order' => 'C.id']); foreach ($cats as $v) { $last = $v['id']; $this->cronRecalculateItemsInCategory($v['id'], $v['settings']['publication']['infinite'] ?? false); } } while (! empty($cats)); } /** * Пересчет publicated_period у объявлений в категориях. Chunk перебор объявлений в категории * @param int $categoryId * @param int $infinite */ protected function cronRecalculateItemsInCategory($categoryId, $infinite) { $period = $this->publishPeriod($this->default()); if ($period == 0) { $infinite = true; } $last = 0; do { $filter = [ 'id' => ['>', $last], 'cat_id' => $categoryId, ]; $items = Listings::model()->itemsDataByFilter($filter, ['id', 'publicated_period', 'publicated_to'], [ 'orderBy' => 'id', 'limit' => 100, 'groupKey' => false, ]); foreach ($items as $v) { $last = $v['id']; if ($infinite) { if ($v['publicated_period'] > 0) { Listings::model()->itemSave($v['id'], ['publicated_period' => 0]); } } else { if ($v['publicated_period'] == 0) { $update = [ 'publicated_period' => $period, ]; if (strtotime($v['publicated_to']) < time()) { $update['publicated_to'] = $this->publishTo(['period' => $period]); $update['publicated'] = $this->db->now(); $update['publicated_order'] = $this->db->now(); } Listings::model()->itemSave($v['id'], $update); } } } } while (! empty($items)); } }