Базовый класс — waController
.
Этот контроллер позволяет реализовать потенциально длительные операции, когда нет возможности избежать срабатывания серверного ограничения на длительность исполнения PHP-скриптов (max_execution_time
). Контроллер работает в связке с браузерным скриптом, который должен время от времени отправлять запросы к серверу для получения информации о статусе работы контроллера.
Схематический пример JavaScript-скрипта, отправляющего запросы к контроллеру:
var url = '...'; //URL, обрабатываемый контроллером waLongActionController var processId = undefined; var step = function (delay) { //интервал запросов к серверному контроллеру delay = delay || 2000; var timer_id = setTimeout(function () { $.post( url, {processId: processId}, function (r) { if (!r) { step(3000); } else if (r && r.ready) { //работа контроллера завершена //устанавливаем индикатор выполнения процесса на значение 100% $('.progressbar .progressbar-inner').css({ width: '100%' }); $('.progressbar-description').text('100%'); //получаем отчет о выполненной работе $.post(url, {processId: processId, cleanup: 1}, function (r) { if (r.report) { setTimeout(function () { //показываем отчет пользователю $('.progressbar').hide(); $('.report').show(); $('.report').html(r.report); }, 1000); } }, 'json'); } else if (r && r.error) { //если произошла ошибка, показываем ее текст //и прекращаем работу $('.errormsg').text(r.error); } else { //если все нормально, обновляем значение индикатора if (r && r.progress) { var progress = parseFloat(r.progress.replace(/,/, '.')); $('.progressbar .progressbar-inner').animate({ 'width': progress + '%' }); $('.progressbar-description').text(r.progress); $('.progressbar-hint').text(r.hint); } //если контроллер вернул предупреждение, //показываем его пользователю и продолжаем работу if (r && r.warning) { $('.progressbar-description').append('<i class="icon16 exclamation"></i><p>' + r.warning + '</p>'); } //переходим к следующему запросу к серверу step(); } }, 'json' ).error(function () { //если при выполнении POST-запроса возникла ошибка //повторим попытку через несколько секунд step(3000); }); }, delay); }; //первый запуск скрипта $.post(url, {}, function (r) { if (r && r.processId) { processId = r.processId; step(); } else if (r && r.error) { $('.errormsg').text(r.error); } else { $('.errormsg').text('Server error'); } }, 'json').error(function () { $('.errormsg').text('Server error'); });
Каждый запрос браузерного скрипта приводит к запуску отдельного процесса на сервере. Из всех процессов, порожденных контроллером, один (self::TYPE_RUNNER
) выполняет полезную работу — либо до ее полного завершения, либо до его преждевременного прекращения из-за срабатывания ограничения max_execution_time
, а остальные (self::TYPE_MESSENGER
) отправляют в браузер информацию о текущем статусе выполнения операции.
Основная логика контроллера должна быть разбита на отдельные фрагменты, каждый из которых гарантированно может быть выполнен до достижения ограничения на время исполнения PHP-скриптов. Логика каждого такого фрагмента должна быть описана в методе step
.
Ниже перечислены методы, которые должны быть реализованы разработчиком в собственном контроллере.
Методы
-
finish
Определяет, можно ли очистить связанные с выполнением операции временные данные (файлы) после ее завершения.
-
info
Возвращает в браузер информацию о статусе выполнения операции.
-
init
Инициализация значений, которые могут использоваться в работе контроллера.
-
isDone
Определяет факт завершения операции.
-
step
Логика выполнения отдельного фрагмента операции.
protected function finish ($filename)
Определяет, можно ли очистить связанные с выполнением операции временные данные (файлы) после ее завершения.
Параметры
-
$filename
Имя файла, гарантированно содержащего корректную информацию о последнем успешно выполненном фрагменте операции.
Пример
protected function finish($filename) { //Для надежности в некоторых ситуациях вы можете здесь проанализировать //содержимое файла $filename $this->info(); if ($this->getRequest()::post('cleanup')) { return true; } return false; }
protected function info ()
Возвращает в браузер информацию о статусе выполнения операции.
Пример
protected function info() { $interval = 0; if (!empty($this->data['timestamp'])) { $interval = time() - $this->data['timestamp']; } $response = [ 'time' => sprintf('%d:%02d:%02d', floor($interval / 3600), floor($interval / 60) % 60, $interval % 60), 'processId' => $this->processId, 'progress' => 0.0, 'ready' => $this->isDone(), 'offset' => $this->data['offset'], 'hint' => $this->data['hint'], ]; $response['progress'] = empty($this->data['products_total_count']) ? 100 : ($this->data['offset'] / $this->data['products_total_count']) * 100; $response['progress'] = sprintf('%0.3f%%', $response['progress']); if ($this->getRequest()->post('cleanup')) { $response['report'] = $this->report(); } echo json_encode($response); }
protected function init ()
Инициализация значений, которые могут использоваться в работе контроллера.
Пример
protected function init() { $product_model = new shopProductModel(); $this->data['products_total_count'] = $product_model->countAll(); $this->data['offset'] = 0; $this->data['product_id'] = null; $this->data['product_count'] = 0; $this->data['timestamp'] = time(); $this->data['hint'] = _wp('Starting data update...'); }
protected function isDone ()
Определяет факт завершения операции. Как только метод вернет true
, прекращается выполнение основной логики контроллера в методе step
.
Пример
protected function isDone() { return $this->data['offset'] >= $this->data['products_total_count']; }
protected function step ()
Логика выполнения отдельного фрагмента операции.
Пример
protected function step() { static $products; if (empty($products)) { $products = ...; // получаем новые данные для обновления записей в БД $this->data['hint'] = _wp('Applying new values...'); } static $product_model; if (empty($product_model)) { $product_model = new shopProductModel(); } // ограничиваем количество обновляемых записей база данных небольшим // числом, чтобы при их обновлении гарантированно не выйти за размер // ограничения max_execution_time $chunk_size = 10; $chunk = array_slice($products, $this->data['offset'], $chunk_size); foreach ($chunk as $product) { $product_model->updateById($product['id'], [ 'field_name' => $product['field_name'], ]); if ($this->data['product_id'] != $product['id']) { sleep(0.2); $this->data['product_id'] = $product['id']; $this->data['product_count'] += 1; } $this->data['offset'] += 1; } return true; }