Current Path : /var/www/www-root/data/www.catalog.monolith-realty.ru/bitrix/modules/landing/lib/ |
Current File : /var/www/www-root/data/www.catalog.monolith-realty.ru/bitrix/modules/landing/lib/history.php |
<?php namespace Bitrix\Landing; use Bitrix\Landing\History\ActionFactory; use Bitrix\Landing\History\Action\BaseAction; use Bitrix\Landing\Internals\BlockTable; use Bitrix\Landing\Internals\HistoryTable; use Bitrix\Landing\Internals\HistoryStepTable; use Bitrix\Landing\Internals\LandingTable; use Bitrix\Main\Type\DateTime; /** * Work with History */ class History { /** * Entity type landing. */ public const ENTITY_TYPE_LANDING = 'L'; /** * Entity type designer block. */ public const ENTITY_TYPE_DESIGNER_BLOCK = 'D'; public const AVAILABLE_TYPES = [ self::ENTITY_TYPE_LANDING, self::ENTITY_TYPE_DESIGNER_BLOCK, ]; /** * Activity flag * @var bool */ protected static bool $isActive = false; /** * If set multiply mode some actions will connected and changed as one step * @var bool */ protected static bool $multiplyMode = false; /** * ID of multiply actions group, by default - ID of first action in group * @var int|null */ protected static ?int $multiplyId = null; // todo: $multiplyId and $multiplyStep - is no static. But need getInstance method and like a singletone style /** * Because multiply step is one step, need increase step just once and save value * @var int|null */ protected static ?int $multiplyStep = null; protected int $entityId; protected string $entityType = self::ENTITY_TYPE_LANDING; /** * ID of stepTable row - save for optimisation. If null - row not exists (new item) * @var int|null */ protected ?int $stepRowId = null; /** * List of steps, grouped by multiply * @var array */ protected array $stack = []; protected int $step = 0; protected array $actions = []; /** * Enable history for all * @return void */ public static function activate(): void { self::$isActive = true; } /** * Disable history for all * @return void */ public static function deactivate(): void { self::$isActive = false; } public static function setMultiplyMode(): void { self::$multiplyMode = true; } public static function unsetMultiplyMode(): void { self::$multiplyMode = false; } /** * Check enable or disable global history * @return bool */ public static function isActive(): bool { return self::$isActive; } /** * @param int $entityId * @param string $entityType - one of constants AVAILABLE_TYPES */ public function __construct(int $entityId, string $entityType) { if (!in_array($entityType, self::AVAILABLE_TYPES, true)) { // todo :err or null return; } $this->entityId = $entityId; $this->entityType = $entityType; $this->loadStack(); $this->loadStep(); if ($this->step > $this->getStackCount()) { $this->saveStep($this->getStackCount()); } } protected function loadStack(): void { // todo: maybe cache $this->stack = []; $res = HistoryTable::query() ->addSelect('*') ->where('ENTITY_TYPE', '=', $this->entityType) ->where('ENTITY_ID', '=', $this->entityId) ->setOrder(['ID' => 'ASC']) ->exec() ; $step = 1; $multyId = null; while ($row = $res->fetch()) { $row['ID'] = (int)$row['ID']; if (!is_array($row['ACTION_PARAMS'])) { $this->fixBrokenStep($step, $row['ID']); continue; } $row['STEP'] = $step; $row['ENTITY_ID'] = (int)$row['ENTITY_ID']; $row['MULTIPLY_ID'] = (int)$row['MULTIPLY_ID']; if ($row['MULTIPLY_ID']) { if ($multyId && $multyId !== $row['MULTIPLY_ID']) { $multyId = null; } if (!$multyId) { // first multiply step $row['ACTION_PARAMS'] = [ [ 'ACTION' => $row['ACTION'], 'ACTION_PARAMS' => $row['ACTION_PARAMS'], ], ]; $row['ACTION'] = ActionFactory::MULTIPLY_ACTION_NAME; $multyId = $row['MULTIPLY_ID']; $row['MULTIPLY'] = [$row['MULTIPLY_ID']]; unset($row['MULTIPLY_ID']); $this->stack[$step] = $row; } else { $this->stack[$step - 1]['ACTION_PARAMS'][] = [ 'ACTION' => $row['ACTION'], 'ACTION_PARAMS' => $row['ACTION_PARAMS'], ]; $this->stack[$step - 1]['MULTIPLY'][] = $row['ID']; } } else { $multyId = null; $this->stack[$step] = $row; } $step++; } } /** * For some reasons history row can be broken. * For consistency need remove row and decrease step. * @param int $step number of broken step * @param int $id ID of broken History row * @return bool */ protected function fixBrokenStep(int $step, int $id): bool { $resDelete = HistoryTable::delete($id); if ($resDelete->isSuccess()) { $currentStep = $this->loadStep(); if ($step > $currentStep) { return true; } return $this->saveStep(max(--$currentStep, 0)); } return false; } /** * Delete all steps before chosen. * @param int $step number of step in stack * @return bool */ protected function clearBefore(int $step): bool { if (!isset($this->stack[$step])) { return false; } // if first step - can't delete nothing if ($this->step <= 1) { return true; } // delete only before current step if ($step >= $this->step) { $step = $this->step - 1; } for ($i = 1; $i <= $step; $i++) { if (!$this->deleteStep(1)) { return false; } } return true; } /** * Delete chosen step and all after them. * @param int $step number of step in stack * @return bool */ protected function clearAfter(int $step): bool { if ($step >= $this->getStackCount()) { return true; } // if last step - can't delete nothing $stackCount = $this->getStackCount(); if ($this->step >= $stackCount) { return true; } // delete only after current step if ($step <= $this->step) { $step = $this->step + 1; } $count = $this->getStackCount(); for ($i = $step; $i <= $count; $i++) { if (!$this->deleteStep($step)) { return false; } } return true; } /** * Clear steps after current. Can't redo now * @return bool */ public function clearFuture(): bool { return $this->clearAfter($this->step + 1); } /** * Clear all history * @return bool */ public function clear(): bool { $count = $this->getStackCount(); for ($i = 0; $i < $count; $i++) { if (!$this->deleteStep(1)) { return false; } } $this->stack = []; return true; } /** * Remove one step by step number, with run action delete processing and save new step * @param int $step * @return bool */ protected function deleteStep(int $step): bool { if (!isset($this->stack[$step])) { return false; } $item = $this->stack[$step]; $action = $this->getActionForStep($item['STEP'], false); if (!$action || !$action->delete()) { return false; } if (isset($item['MULTIPLY']) && is_array($item['MULTIPLY']) && !empty($item['MULTIPLY'])) { foreach ($item['MULTIPLY'] as $multyId) { $resDelete = HistoryTable::delete($multyId); if (!$resDelete->isSuccess()) { return false; } } } else { $resDelete = HistoryTable::delete($item['ID']); if (!$resDelete->isSuccess()) { return false; } } // update stack and step unset($this->stack[$step]); $this->resetStackSteps(); if ($step <= $this->step) { return $this->saveStep($this->step - 1); } return true; } /** * Re calculate steps after change stack * @return void */ protected function resetStackSteps(): void { $newStack = []; $step = 1; foreach ($this->stack as $item) { $item['STEP'] = $step; $newStack[$step] = $item; $step++; } // todo: what about multiply step? $this->stack = $newStack; } /** * Remove history records older X days. And save new step. * @param int $days * @return bool */ public function clearOld(int $days): bool { if ($days > 0) { $dateEnd = new DateTime(); $dateEnd->add('-' . $days . ' days'); $deleteBeforeStep = 0; foreach ($this->stack as $stackItem) { $dateCurrent = DateTime::createFromUserTime($stackItem['DATE_CREATE']); if ($dateEnd < $dateCurrent) { break; } $deleteBeforeStep = $stackItem['STEP']; } return $this->clearBefore($deleteBeforeStep); } return false; } public function getStackCount(): int { return count($this->stack); } /** * Get step from table * @return int */ protected function loadStep(): int { $this->step = 0; $step = HistoryStepTable::query() ->addSelect('ID') ->addSelect('STEP') ->where('ENTITY_ID', '=', $this->entityId) ->where('ENTITY_TYPE', '=', $this->entityType) ->exec() ->fetch() ; // todo: del other entities row if exists if ($step) { $this->stepRowId = $step['ID']; $this->step = $step['STEP']; } else { $this->migrateStep(); } return $this->step; } /** * Add exists or add new step row * @param int $step * @return bool */ protected function saveStep(int $step): bool { $this->step = $step; if ($this->stepRowId) { $res = HistoryStepTable::update($this->stepRowId, ['STEP' => $step]); } else { $res = HistoryStepTable::add([ 'ENTITY_ID' => $this->entityId, 'ENTITY_TYPE' => $this->entityType, 'STEP' => $step, ]); } if ($res->isSuccess()) { $this->stepRowId = $res->getId(); $this->step = $step; return true; } return false; } /** * Move steps from old tables to new entity * When will be updated all clients - can delete this method * @return void */ private function migrateStep(): void { $oldStep = null; if ($this->entityType === self::ENTITY_TYPE_LANDING) { if (!array_key_exists('HISTORY_STEP', LandingTable::getMap())) { return; } $landing = LandingTable::query() ->addSelect('HISTORY_STEP') ->where('ID', '=', $this->entityId) ->exec() ->fetch() ; $oldStep = $landing ? $landing['HISTORY_STEP'] : null; } if ($this->entityType === self::ENTITY_TYPE_DESIGNER_BLOCK) { if (!array_key_exists('HISTORY_STEP_DESIGNER', BlockTable::getMap())) { return; } $block = BlockTable::query() ->addSelect('HISTORY_STEP_DESIGNER') ->where('ID', '=', $this->entityId) ->exec() ->fetch() ; $oldStep = $block ? $block['HISTORY_STEP_DESIGNER'] : null; } $isNewStepExists = HistoryStepTable::query() ->addSelect('ID') ->addSelect('STEP') ->where('ENTITY_ID', '=', $this->entityId) ->where('ENTITY_TYPE', '=', $this->entityType) ->exec() ->fetch() ; if ($oldStep && !$isNewStepExists) { $this->saveStep((int)$oldStep); } } /** * Return stack of js commands for actions * @return array */ public function getJsStack(): array { $result = []; foreach ($this->stack as $step => $stackItem) { $actionClass = ActionFactory::getActionClass($stackItem['ACTION']); $result[] = [ 'id' => $stackItem['ID'], 'current' => $step === $this->step, 'command' => (is_callable([$actionClass, 'getJsCommandName'])) ? call_user_func([$actionClass, 'getJsCommandName']) : '' , 'entityId' => $this->entityId, 'entityType' => $this->entityType, ]; } return $result; } /** * Get current step * @return int */ public function getStep(): int { return $this->step; } public function push(string $actionName, array $params): bool { $actionName = strtoupper($actionName); $action = ActionFactory::getAction($actionName); if (!$action) { return false; // todo: or err } $action->setParams($params); $fields = [ 'ENTITY_TYPE' => $this->entityType, 'ENTITY_ID' => $this->entityId, 'ACTION' => $actionName, 'ACTION_PARAMS' => $action->getParams(), 'CREATED_BY_ID' => Manager::getUserId() ?: 1, 'DATE_CREATE' => new DateTime, ]; // check duplicates if ( !empty($this->stack[$this->step]) && ActionFactory::compareSteps($this->stack[$this->step], $fields) ) { return false; } if (!$action->isNeedPush()) { return true; } $stackCount = $this->getStackCount(); if ($this->step < $stackCount) { if (!$this->clearFuture()) { return false; } } $nextStep = (self::$multiplyMode && self::$multiplyStep !== null) ? self::$multiplyStep : $this->step + 1 ; if (!$this->saveStep($nextStep)) { return false; } self::$multiplyStep = $nextStep; // todo: drop $multiplyStep after last element (when set multiply mode off) if (self::$multiplyMode && self::$multiplyId !== null) { $fields['MULTIPLY_ID'] = self::$multiplyId; } $resAdd = HistoryTable::add($fields); // save MULTIPLY_ID for first element in group if (self::$multiplyMode && self::$multiplyId === null) { self::$multiplyId = $resAdd->getId(); HistoryTable::update(self::$multiplyId, [ 'MULTIPLY_ID' => self::$multiplyId, ]); } return $resAdd->isSuccess(); } public function undo(): bool { if ($this->canUndo()) { self::deactivate(); $action = $this->getActionForStep($this->step, true); if ($action && $action->execute()) { return $this->saveStep($this->step - 1); } } return false; } protected function canUndo(): bool { return $this->step > 0 && $this->getStackCount() > 0 && $this->step <= $this->getStackCount() ; } public function redo(): bool { if ($this->canRedo()) { self::deactivate(); $action = $this->getActionForStep($this->step + 1, false); if ($action && $action->execute(false)) { return $this->saveStep($this->step + 1); } } return false; } protected function canRedo(): bool { return $this->step >= 0 && $this->getStackCount() > 0 && $this->step < $this->getStackCount() ; } /** * Get params for JS command for frontend changes * @param bool $undo * @return array */ public function getJsCommand(bool $undo = true): array { $action = $this->getActionForStep( $undo ? $this->step : ($this->step + 1), $undo ); return $action ? $action->getJsCommand($undo) : []; } /** * Create and save in stack action object by step number * @param int $step * @param bool $undo * @return BaseAction|null */ protected function getActionForStep(int $step, bool $undo): ?BaseAction { if (!isset($this->stack[$step])) { return null; } $stepItem = $this->stack[$step]; $stepId = $stepItem['ID']; $direction = ActionFactory::getDirectionName($undo); if (isset($this->actions[$stepId][$direction])) { return $this->actions[$stepId][$direction]; } $params = $stepItem['ACTION_PARAMS']; if ($this->entityType === self::ENTITY_TYPE_LANDING) { $params['lid'] = $this->entityId; } if ($this->entityType === self::ENTITY_TYPE_DESIGNER_BLOCK) { $params['blockId'] = $this->entityId; } $action = ActionFactory::getAction($stepItem['ACTION'], $undo); if (!$action) { return null; } $action->setParams($params, true); $this->actions[$stepId][$direction] = $action; return $action; } }