Opencart - Отслеживание Прогресса Длительных Операций В Php

Тема в разделе "SEO-вопросы (оптимизация и продвижение магазина)", создана пользователем admin, 22 июл 2016.

  1. TopicStarter Overlay
    Offline

    admin Команда форума Администратор

    Сообщения:
    2.327
    Симпатии:
    77.102
    Репутация:
    170
    Несмотря на то, что PHP чаще всего используется для создания сайтов и Web-сервисов, ни для кого не секрет, что его можно применять и для других задач, в том числе тех, которые могут выполняться достаточно длительное время. Например, когда вы рассылаете спам вконтактесоздаете дамп базы данных или, скажем, архивируете файлы на сайте, чтобы сделать бэкап, неплохо бы отобразить прогресс текущей операции. Но как это получше сделать? Именно об этом я вам и собираюсь рассказать.

    Лично мне пришлось столкнуться с такой задачей, когда я разрабатывал свой PHP Obfuscator версии 2. Процесс обфускации толстых PHP-скриптов (или их большого количества) может оказаться достаточно долгим, и пользователю приятно будет видеть прогресс выполнения работы.


    Как известно, PHP предлагает встроенный механизм для работы с сессиями. Это поможет нам в решении задачи. Если кратко, то все будет происходить примерно так:

    1. Сервер (PHP) начинает сессию, передает клиенту соответствующий cookie при первом обращении клиента на веб-страницу. Это происходит, когда пользователь открыл веб-страничку, на которой можно стартовать длительную операцию, в браузере.
    2. Клиент посылает запрос серверу, сервер выдает клиенту уникальный идентификатор задачи. По этому идентификатору в дальнейшем можно будет получать прогресс выполнения длительной операции. Этот идентификатор уникален только в пределах сессии. Таким образом, сессия + идентификатор (вместе) являются глобально уникальными в пределах сервера. Идентификатор необходим для того, чтобы можно было стартовать параллельно (из разных вкладок в одном браузере) несколько длительных операций и параллельно запрашивать их прогресс.
    3. Клиент посылает серверу запрос, который может выполняться очень долгое время, и ждет ответа. В этот запрос клиент включает полученный из пункта 1 идентификатор. Этот запрос можно делать через AJAX либо через скрытый IFRAME, это неважно. Сервер выполняет запрос (например, архивирует большое количество файлов), и дает знать о текущем прогрессе клиенту (см. следующий пункт).
    4. Клиент регулярно с некоторой частотой опрашивает сервер, передавая в запросе идентификатор задачи, и получает прогресс выполнения операции. Клиент перестает опрашивать сервер, когда запрос из пункта 3 закончил выполняться (это значит, задача выполнена).
    Пришло время перейти к практической реализации описанного взаимодействия. Начнем с серверной части на PHP. Нам понадобится работать с сессиями, поэтому сделаем для этой задачи небольшой вспомогательный статический класс:
    Код:
    final class SessionHelper
    {
        //Открыта ли сессия
        private static $started = false;
       
        private function __construct()
        {
        }
       
        //Безопасное открытие сессии
        static private function safeSessionStart()
        {
            $name = session_name();
            $cookie_session = false;
            if(ini_get('session.use_cookies') && isset($_COOKIE[$name]))
            {
                $cookie_session = true;
                $sessid = $_COOKIE[$name];
            }
            else if(!ini_get('session.use_only_cookies') && isset($_GET[$name]))
            {
                $sessid = $_GET[$name];
            }
            else
            {
                return @session_start();
            }
           
            if(is_array($sessid) || !preg_match('/^[a-zA-Z0-9,-]+$/', $sessid))
            {
                if($cookie_session) //Try to reset incorrect session cookie
                {
                    setcookie($name, '', 1);
                    unset($_COOKIE[$name]);
                    if(!ini_get('session.use_only_cookies') && isset($_GET[$name]))
                        unset($_GET[$name]);
                   
                    return @session_start();
                }
               
                return false;
            }
           
            return @session_start();
        }
    
        //Открыть сессию
        static public function init()
        {
            if(!self::$started)
            {
                if(self::safeSessionStart())
                    self::$started = true;
            }
        }
       
        //Открыта ли сессия
        static public function isStarted()
        {
            return self::$started;
        }
       
        //Завершить сессию
        static public function close()
        {
            if(self::$started)
            {
                session_write_close();
                self::$started = false;
            }
        }
       
        //Получить значение ключа с именем $name из сессии
        //Если ключ отсутствует, будет возвращено значение $default_value
        static public function get($name, $default_value = null)
        {
            return isset($_SESSION[$name]) && !is_array($_SESSION[$name])
                ? $_SESSION[$name] : $default_value;
        }
       
        //Установить значение ключа с именем $name в $value
        static public function set($name, $value)
        {
            $_SESSION[$name] = $value;
        }
       
        //Удалить ключ с именем $name из сессии
        static public function remove($name)
        {
            unset($_SESSION[$name]);
        }
    }
    В этом классе определенный интерес представляет функция safeSessionStart, которая позволяет безопасно открыть сессию. В PHP нет удобных механизмов определения, передан ли скрипту корректный идентификатор сессии, поэтому приходится делать такой механизм вручную, проверяя этот идентификатор. Если сессия передана через cookie, то у нас есть возможность также сбросить некорректный идентификатор и перегенерировать его, чтобы всё же корректно начать сессию. Иногда это делается автоматически в PHP при вызове функции session_start с некорректным идентификатором, но когда-то и нет (видимо, зависит от настроек php.ini и версии PHP). Если идентификатор сессии не проверять, просто вызывая session_start(), мы рискуем получить пачку предупреждений, если пользователь подсунет кривой PHPSESSID.

    Для того, чтобы открывать сессию и автоматически закрывать ее, когда она больше не требуется, я написал еще один класс (имплементирует этакое RAII для сессии):
    Код:
    class SessionInitializer
    {
        //Была ли инициализирована сессия при создании класса
        private $session_initialized;
       
        public function __construct()
        {
            $this->session_initialized = SessionHelper::isStarted();
            SessionHelper::init();
        }
       
        public function __destruct()
        {
            if(!$this->session_initialized)
                SessionHelper::close();
        }
    }
    Этот класс в конструкторе будет стартовать сессию (если она еще не была открыта), а в деструкторе - закрывать ее в том случае, если сессия не была открыта в момент создания класса.

    Далее, напишем совсем небольшой класс, помогающий получать параметры запросов к веб-страничке и отвечать клиенту в формате JSON:
    Код:
    final class WebHelpers
    {
        private function __construct()
        {
        }
       
        //Получить значение из массива $_REQUEST
        //Если значение отсутствует, вернуть $default_value
        static public function request($name, $default_value = null)
        {
            return isset($_REQUEST[$name]) && !is_array($_REQUEST[$name]) ? $_REQUEST[$name] : $default_value;
        }
       
        //Выдать ответ в формате JSON
        static public function echoJson(Array $value)
        {
            header('Content-Type: application/json; charset=UTF-8');
            $ret = json_encode($value);
            if($ret !== false)
            {
                echo $ret;
                return true;
            }
           
            return false;
        }
    }
    Кроме того, нам придется генерировать идентификаторы задач, которые должны быть уникальными в пределах сессии. Напишем класс и для этого (он также будет позволять получать текущий идентификатор задачи, переданный клиентом):
    Код:
    final class TaskHelper
    {
        private function __construct()
        {
        }
       
        //Создать уникальный в пределах сессии идентификатор задачи
        static public function generateTaskId()
        {
            $session_initializer = new SessionInitializer;
            $id = SessionHelper::get('max_task', 0) + 1;
            SessionHelper::set('max_task', $id);
            return $id;
        }
       
        //Получить идентификатор задачи, переданный клиентом
        static public function getTaskId()
        {
            $task_id = WebHelpers::request('task');
           
            if(!preg_match('/^\d{1,9}$/', $task_id))
                return null;
           
            return (int)$_REQUEST['task'];
        }
    }
    десь в функции generateTaskId мы получаем из сессии значение для ключа max_task (если оно отсутствует, то по умолчанию берется значение 0), прибавляем к нему единицу и возвращаем это значение, не забыв сохранить его в сессии перед этим. Пока сессия открыта в том или ином скрипте PHP, файл сессии, который создает для нас PHP, блокируется на чтение и запись, поэтому другие обращения к тому же самому скрипту с тем же самым идентификатором сессии (PHPSESSID) не приведут к порче идентификатора задачи, т.е. никакая синхронизация нам здесь не нужна - PHP уже сделал это за нас.

    Функция getTaskId необходима для получения и проверки идентификатора задачи, который клиент нам будет передавать (см. пункты 3 и 4). Пусть параметр идентификатора задачи будет называтьсяtask.

    Наконец, нам потребуется управлять прогрессом длительных операций (увеличивать его в процессе выполнения операции и сообщать о нем клиенту, если тот спросит). Для этого мы также сделаем класс:
    Код:
    class ProgressManager
    {
        //Идентификатор задачи
        private $task_id = 0;
        //Количество шагов в задаче
        private $step_count = 1;
        //Текущий шаг
        private $current_step = 0;
        //Инициализатор сессии на время работы менеджера
        private $session_initializer;
       
        //Создание менеджера прогресса для задачи с идентификатором $task_id
        public function __construct($task_id)
        {
            $this->session_initializer = new SessionInitializer;
            $this->task_id = $task_id;
            SessionHelper::set('progress' . $this->task_id, 0);
            SessionHelper::close();
        }
       
        //Установка количества шагов прогресса
        public function setStepCount($step_count)
        {
            $this->step_count = $step_count;
            $this->current_step = 0;
        }
       
        //Увеличение прогресса на 1 (переход к следующему шагу)
        public function incrementProgress()
        {
            if(++$this->current_step >= $this->step_count)
                $this->current_step = $this->step_count;
           
            SessionHelper::init();
            SessionHelper::set('progress' . $this->task_id,
                (int)(($this->current_step * 100.0) / $this->step_count));
            SessionHelper::close();
           
            header_remove('Set-Cookie');
        }
       
        //Завершение подсчета прогресса
        public function __destruct()
        {
            SessionHelper::init();
            SessionHelper::remove('progress' . $this->task_id);
        }
       
        //Получение значения прогресса для идентификатора задачи, переданного клиентом
        public static function getProgress()
        {
            $task_id = TaskHelper::getTaskId();
            if($task_id === null)
                return null;
           
            $session_initializer = new SessionInitializer;
            $progress = SessionHelper::get('progress' . $task_id, null);
           
            if($progress === null)
                return null;
           
            return (int)$progress;
        }
    }
    При создании нового менеджера прогресса инициализируется сессия (если еще не была инициализирована), и текущий прогресс выставляется в 0, после чего сессия закрывается. Это очень важный момент: так сделано для того, чтобы клиент мог параллельно с тем же идентификатором сессии запрашивать прогресс с помощью функции getProgress. Если бы сессия после модификации не закрывалась, PHP бы блокировал файл сессии до тех пор, пока длительная операция не завершится (я уже писал об этом выше), и прогресс запросить было бы невозможно.

    Функции setStepCount и incrementProgress используются для непосредственной настройки прогресса. Первая выставляет количество шагов, а вторая инкрементирует текущий номер шага. Например, можно задать 10 шагов прогресса, и после этого он будет равномерно увеличиваться при десяти последовательных вызовых функции incrementProgress. В функции incrementProgressинтересна строка, удаляющая все заголовки с cookies, которые были установлены. Дело в том, что функция session_start, которая выполняется каждый раз при вызове incrementProgress, устанавливает cookie с идентификатором сессии, который нам в данном случае не нужен. Если бы мы не удаляли этот заголовок, в браузер бы выдалось множество их дубликатов, так как session_startвызывается в процессе изменения прогресса многократно. Следует отметить, что удалятся вообще все заголовки, касающиеся cookies, так что если вы будете во время выполнения длительной операции с отслеживанием прогресса устанавливать какие-то посторонние cookies, они до браузера не дойдут (просто придется несколько переделать код функции incrementProgress). Мне этого не требовалось, поэтому для демонстрации я сделал всё просто и топорно.

    Теперь у нас есть всё необходимое, поэтому перейдем к написанию кода, который всеми этими вспомогательными классами управляет. Сперва снимаем ограничение времени выполнения скрипта, так как будем выполнять длительные операции.
    Код:
    error_reporting(E_ALL);
    set_time_limit(0);
    Далее, если клиент запросил генерирование нового идентификатора задачи, выполняем соответствующую функцию и выходим:
    Код:
    if(WebHelpers::request('new_task') === '1') //Генерируем новый ID задачи
    {
        WebHelpers::echoJson(['task' => TaskHelper::generateTaskId()]);
        return;
    }
    Если клиент хочет получить текущее значение прогресса для той или иной задачи:
    Код:
    if(WebHelpers::request('get_progress') === '1') //Получаем прогресс
    {
        $progress = ProgressManager::getProgress();
        if($progress !== null)
            WebHelpers::echoJson(['progress' => $progress]);
        else
            WebHelpers::echoJson([]);
       
        return;
    }
    И, наконец, если клиент желает запустить длительную операцию:
    Код:
    //Запускаем длительный процесс (на 60 шагов по 200 миллисекунд) с контролем прогресса
    const STEP_COUNT = 60;
    const STEP_DELAY = 200000;
    if(WebHelpers::request('long_process') === '1')
    {
        $task_id = TaskHelper::getTaskId();
        if($task_id === null)
            return;
       
        $manager = new ProgressManager($task_id);
        $manager->setStepCount(STEP_COUNT);
       
        for($i = 0; $i !== STEP_COUNT; ++$i)
        {
            $manager->incrementProgress();
            usleep(STEP_DELAY);
        }
       
        WebHelpers::echoJson([]);
        return;
    }
    Этот код создает экземпляр класса ProgressManager, описанного выше, указывая в вызове конструктора идентификатор задачи, переданный клиентом. Далее в цикле выполняем длительную операцию, инкрементируя на каждой итерации прогресс.

    С серверной частью мы закончили, переходим к клиентской. Для начала стартуем сессию и готовимся выводить HTML:
    Код:
    //Вывод странички
    SessionHelper::init();
    ?>
    Код:
    <!doctype html>
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
            <title>Progress test</title>
    Я использую в этом примере библиотеку JQuery для упрощения и ускорения разработки:
    Код:
            <script type="text/javascript" src="//code.jquery.com/jquery-latest.min.js"></script>
            <script type="text/javascript">
                //Тут будут наши скрипты
            </script>
            <style>
                h3
                {
                    text-align: center;
                }
               
                .progress-div
                {
                    padding: 5px;
                    border: 1px solid gray;
                    margin: 3px;
                }
            </style>
        </head>
        <body>
            <h3>Progress test</h3>
            <div id="progressContainer">
            </div>
            <div>
                <button onclick="startProgress();">Start progress</button>
            </div>
        </body>
    </html>
    На демонстрационной страничке будет расположена кнопка "Start progress", стартующая длительную операцию с отслеживанием прогресса. Сделаем так, чтобы можно было начать несколько таких длительных процессов одновременно. Для этого нам необходим div с идентификаторомprogressContainer: в него мы будем динамически добавлять информацию обо всех запущенных процессах.

    Теперь непосредственно перейдем к JavaScript'у:
    Код:
                //Идентификаторы завершенных задач
                var finishedTasks = [];
               
                //Стартовать длительную задачу
                var startLongTask = function(task_id)
                {
                    $.get("?", {long_process: 1, task: task_id}, function(data)
                    {
                        finishedTasks.push(task_id);
                        $("#task-" + task_id).text("Finished");
                    }, "json");
                }
               
                //Отслеживать прогресс длительной задачи
                var monitorProgress = function(task_id)
                {
                    $.get("?", {get_progress: 1, task: task_id}, function(data)
                    {
                        if($.inArray(task_id, finishedTasks) != -1)
                            return;
                       
                        if(data.progress !== undefined)
                            $("#task-" + task_id).text("Progress: " + data.progress + "%");
                       
                        setTimeout(function() { monitorProgress(task_id); }, 100);
                    }, "json");
                }
               
                //Запустить длительную задачу с отслеживанием прогресса
                var runTask = function(task_id)
                {
                    var progressDiv = $("<div/>").addClass("progress-div");
                    $("<div/>").text("Task ID: " + task_id).appendTo(progressDiv);
                    $("<div/>").attr("id", "task-" + task_id).text("Starting...")
                        .appendTo(progressDiv);
                    $("#progressContainer").append(progressDiv);
                   
                    startLongTask(task_id);
                    monitorProgress(task_id);
                }
               
                //Получить новый уникальный идентификатор задачи, после чего
                //запустить длительную задачу с отслеживанием прогресса
                var startProgress = function()
                {
                    $.get("?", {new_task: 1}, function(data)
                    {
                        runTask(data.task);
                    }, "json");
                }
    Здесь функция startProgress вызывается при нажатии на кнопку "Start progress". Она запрашивает у сервера новый уникальный в пределах сессии идентификатор задачи. Когда ответ от сервера получен, можно начинать выполнение длительной задачи и отслеживать ее прогресс. Это делается в функцииrunTask, которая в свою очередь добавляет в div с идентификатором progressContainer (о котором я писал выше) информацию о новой задаче (ее идентификатор и прогресс). Далее в функцииstartLongTask делается запрос, который будет выполняться очень долго, при этом будет возможность отследить его прогресс. Когда запрос будет выполнен, в массив finishedTasks будет добавлен соответствующий идентификатор задачи. Это нужно для того, чтобы в функции, выполняющей отслеживание прогресса (monitorProgress) можно было определить, что задача действительно выполнена. Функция отслеживания прогресса вызывается с интервалом 1 раз в 100 миллисекунд, отправляя запрос прогресса выполнения задачи серверу и выводя значение прогресса (в процентах) в соответствующий элемент div. Когда задача выполнена (ее идентификатор добавлен в массивfinishedTasks), отслеживание прогресса завершается.

    В работе наша страничка будет выглядеть как-то так:
    progress.png
    Вот, собственно, и всё. Я достаточно подробно расписал идею такого механизма и привел полностью рабочий пример, чтобы можно было без проблем использовать описанную методику в собственных проектах. Напоследок прикладываю полный вариант демо-скрипта
     

    Вложения:

    • progress.zip
      Размер файла:
      3,1 КБ
      Просмотров:
      3
    generalman нравится это.
  2. Offline

    generalman Пользователь

    Сообщения:
    20
    Симпатии:
    2
    Репутация:
    0
    Спасибо