- Saved searches
- Use saved searches to filter your results more quickly
- License
- nickbeen/php-cli-progress-bar
- Name already in use
- Sign In Required
- Launching GitHub Desktop
- Launching GitHub Desktop
- Launching Xcode
- Launching Visual Studio Code
- Latest commit
- Git stats
- Files
- README.md
- Прогресс выполнения тяжелой задачи в PHP
Saved searches
Use saved searches to filter your results more quickly
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session.
For creating minimal progress bars in PHP CLI scripts
License
nickbeen/php-cli-progress-bar
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Sign In Required
Please sign in to use Codespaces.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching Xcode
If nothing happens, download Xcode and try again.
Launching Visual Studio Code
Your codespace will open once ready.
There was a problem preparing your codespace, please try again.
Latest commit
Git stats
Files
Failed to load latest commit information.
README.md
For creating minimal progress bars in PHP CLI scripts. It has no dependencies, but requires PHP 8.0 or higher. This library is mainly built for iterating to countable variables, but also works easily with ticking through less structured step-by-step scripts.
Many PHP CLI progress bars have been written in the past, but most haven’t been updated in years. This library uses the latest PHP features such as return types, named arguments and constructor property promotions. For creating richer, more customizable progress bars, check alternatives such as the progress bar helper included in the symfony/console package.
Install the library into your project with Composer.
composer require nickbeen/php-cli-progress-bar
With this library you can display a progress bar in a PHP CLI script to indicate the script is doing its work and how far it has progressed. All you need to do is start displaying the progress bar, tick through the steps the script goes through and finish the display of the progress bar.
It is possible to tick through the steps of your scripts manually when the steps in your script cannot be looped. Each tick adds one progression, but you can override the progression made by including an integer in tick() .
You do need to set maxProgress for the progress bar to display the correct numbers by including it in the constructor. If you don’t know the maxProgress during initialization, you can set it later with the setMaxProgress() method.
$progressBar = new \NickBeen\ProgressBar\ProgressBar(maxProgress: 62); $progressBar->start(); doSomething(); $progressBar->tick(); doSomethingElse(); $progressBar->tick(); doSixtyTasks(); $progressBar->tick(60); $progressBar->finish();
If you have a little more structure in your step-by-step code, you can easily place tick() in a for loop. There is however a more convenient method when dealing with e.g. arrays.
Iterating through arrays or traversable instances
This class method works with anything of the pseudo-type iterable which includes any array or any instance of Traversable. The iterate() method automatically handles starting the progress bar, managing ticking through the iteration and finally finish displaying the progress bar.
$array = [ 1 => 'A', 2 => 'B', 3 => 'C', ]; $progressBar = new \NickBeen\ProgressBar\ProgressBar(); foreach ($progressBar->iterate($array as $key => $value);) < echo "$key: $value" . PHP_EOL; >
Interact with the progress bar
It is possible to interact with the progress bar during its run. You can retrieve the estimated time to finish, the progress it has made, the maximum progress that has been set and the amount of completion in percentage. You can use this information e.g. for notifications or other tasks in the background.
foreach ($progressBar->iterate($array);) < // Some custom notification sendToDiscord($progressBar->getEstimatedTime()); // Some custom task application syncWithCloud($progressBar->getPercentage()) // Some other custom application sendToRaspberryPiDisplay($progressBar->getProgress(), $progressBar->getMaxProgress()) >
This library is licensed under the MIT License (MIT). See the LICENSE for more details.
Прогресс выполнения тяжелой задачи в PHP
Случилось мне как-то иметь дело с тяжелым PHP-скриптом. Нужно было каким-то образом в браузере отображать прогресс выполнения задачи в то время, пока в достаточно длительном цикле на стороне PHP проводились расчёты. В таких случаях обычно прибегают к периодичному выводу строки вроде этой:
Этот вариант меня не устраивал по нескольким причинам, к тому же мне в принципе не нравится такой подход.
Итераций у меня было порядка 3000—5000. Я прикинул, что великоват трафик для такой несложной затеи. Кроме того, мне такой вариант казался очень некрасивым с технической точки зрения, а внешний вид страницы и вовсе получался уродлив: футер дойдет еще не скоро — после последнего уведомления о 100% выполнении задачи.
Увернуться от проблемы некрасивой страницы большого труда не составляло, но остальные минусы заставили меня обрадоваться и приступить к поискам более изящного решения.
Несколько наводящих вопросов. Асинхронные HTTP-запросы возможны? — Да. Можно ли с помощью одного-единственного байта сообщить, что часть большой задачи выполнена? — Да. Можем ли мы постепенно (последовательно) получать и обрабатывать данные с помощью XMLHttpRequest.onreadystatechange ? — Да. Мы даже можем воспользоваться заголовками HTTP для передачи предварительного уведомления об общей продолжительности выполняемой задачи (если это возможно в принципе).
Решение простое. Основанная страница — это пульт управления. С пульта можно запустить и остановить задачу. Эта страница инициирует XMLHttpRequest — стартует выполнение основной задачи. В процессе выполнения этой задачи (внутри основного цикла) скрипт отправляет клиенту один байт — символ пробела. На пульте в обработчике onreadystatechange мы, получая байт за байтом, сможем делать вывод о прогрессе выполнения задачи.
Схема такая. Скрипт операции:
xhr.onreadystatechange = function() < if (this.readyState == 3) < var progress = this.responseText.length; document.getElementById('progress').style.width = progress + '%'; >>;
Однако, итераций всего 50. Об этом мы знаем, потому что сами определили их количество в файле скрипта. А если не знаем или количество может меняться? При readyState == 2 мы можем получить информацию из заголовков. Давайте этим и воспользуемся для определения количества итераций:
А на пульте получим и запомним это значение:
var progressMax = 100; xhr.onreadystatechange = function() < if (this.readyState == 2) < progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax; >else if (this.readyState == 3) < var progress = 100 * this.responseText.length / progressMax; document.getElementById('progress').style.width = progress + '%'; >>;
Общая схема должна быть ясна. Поговорим теперь о подводных камнях.
Во-первых, если в PHP включена опция output_buffering , нужно это учесть. Здесь все просто: если она включена, то при запуске скрипта ob_get_level() будет больше 0. Нужно обойти буферизацию. Еще, если вы используете связку Nginx FastCGI PHP, нужно учесть, что и FastCGI и сам Nginx будут буферизовать вывод. Последний это будет делать в том случае, если собирается сжимать данные для отправки. Устраняется проблема просто (из PHP-скрипта):
header('X-Accel-Buffering: no', true);
Кроме того, то ли Nginx, то ли FastCGI, то ли сам Chrome считают, что инициировать прием-передачу тела ответа, которое содержит всего-навсего один байт — слишком расточительно. Поэтому нужно предварить всю операцию дополнительными байтами. Нужно договориться, скажем, что первые 20 пробелов вообще ничего не должны означать. На стороне PHP их нужно просто «выплюнуть» в вывод, а в обработчике onreadystatechange их нужно проигнорировать. На мой взгляд — раз уж вся конфигурационная составляющая передается в заголовках — то и это число игнорируемых пробелов тоже лучше передать в заголовке. Назовем это padding-ом.
На стороне клиента это тоже нужно учесть:
var progressMax = 100, progressPadding = 0; xhr.onreadystatechange = function() < if (this.readyState == 2) < progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax; progressPadding = +this.getResponseHeader('X-Progress-Padding') || progressPadding; >else if (this.readyState == 3) < var progress = 100 * (this.responseText.length - progressPadding) / progressMax; document.getElementById('progress').style.width = progress + '%'; >>;
Откуда число 20? Если подскажете — буду весьма признателен. Я его установил экспериментальным путем.
Кстати, насчет настройки PHP output_buffering . Если у вас сложная буферизация и вы не хотите ее нарушать, можно воспользоваться такой функцией:
function ob_ignore($data, $flush = false) < $ob = array(); while (ob_get_level()) < array_unshift($ob, ob_get_contents()); ob_end_clean(); >echo $data; if ($flush) flush(); foreach ($ob as $ob_data) < ob_start(); echo $ob_data; >return count($ob); >
С ее помощью можно обойти все уровни буферизации, вывести данные напрямую, после чего все буферы восстанавливаются.
Кстати, а почему именно пробел используется для уведомления о выполненной части задачи? Просто потому что почти любой формат представления данных в вебе такими пробелами не испортишь. Можно применить такой метод передачи уведомления о прогрессе операции, а после всего этого вывести отчет о результатах в JSON.
Если все привести в порядок, немного оптимизировать и дополнить код всеми возможностями, которые могут пригодиться, получится вот что:
function ProgressLoader(url, callbacks) < var _this = this; for (var k in callbacks) < if (typeof callbacks[k] != 'function') < callbacks[k] = false; >> delete k; function getXHR() < var xhr; try < xhr = new ActiveXObject("Msxml2.XMLHTTP"); >catch (e) < try < xhr = new ActiveXObject("Microsoft.XMLHTTP"); >catch (E) < xhr = false; >> if (!xhr && typeof XMLHttpRequest != 'undefined') < xhr = new XMLHttpRequest(); >return xhr; > this.xhr = getXHR(); this.xhr.open('GET', url, true); var contentLoading = false, progressPadding = 0, progressMax = -1, progress = 0, progressPerc = 0; this.xhr.onreadystatechange = function() < if (this.readyState == 2) < contentLoading = false; progressPadding = +this.getResponseHeader('X-Progress-Padding') || progressPadding; progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax; if (callbacks.start) < callbacks.start.call(_this, this.status); >> else if (this.readyState == 3) < if (!contentLoading) < contentLoading = !!this.responseText .replace(/^\s+/, ''); // .trimLeft() — медленнее О_о >if (!contentLoading) < progress = this.responseText.length - progressPadding; progressPerc = progressMax >0 ? progress / progressMax : -1; if (callbacks.progress) < callbacks.progress.call(_this, this.status, progress, progressPerc, progressMax ); >> else if (callbacks.loading) < callbacks.loading.call(_this, this.status, this.responseText); >> else if (this.readyState == 4) < if (callbacks.end) < callbacks.end.call(_this, this.status, this.responseText); >> >; if (callbacks.abort) < this.xhr.onabort = callbacks.abort; >this.xhr.send(null); this.abort = function() < return this.xhr.abort(); >; this.getProgress = function() < return progress; >; this.getProgressMax = function() < return progressMax; >; this.getProgressPerc = function() < return progressPerc; >; return this; >
echo $data; if ($flush) flush(); foreach ($ob as $ob_data) < ob_start(); echo $ob_data; >return count($ob); > if (($work = @$_GET['work']) > 0) < header("X-Progress-Max: $work", true, 200); header("X-Progress-Padding: 20"); ob_ignore(str_repeat(' ', 20), true); for ($i = 0; $i < $work; $i++) < usleep(rand(100000, 500000)); ob_ignore(' ', true); >echo $work.' done!'; die(); >
progress, button
Вместо вступления перед катом: Прошу не бить ногами. Гугление перед проработкой схемы не дало вменяемых результатов, поэтому ее и понадобилось выдумать, ввиду чего я и решил ее изложить в этой своей первой публикации.