Skip to content

Instantly share code, notes, and snippets.

@dac514
Last active November 4, 2022 19:52
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dac514/a2fa7712efe135d5854f4d32d67ca09f to your computer and use it in GitHub Desktop.
Save dac514/a2fa7712efe135d5854f4d32d67ca09f to your computer and use it in GitHub Desktop.
Refactor Your Slow Form Using PHP Generators and Event Streams
<?php
/**
* @license GPLv3 (or any later version)
* @see http://kizu514.com/blog/refactor-your-slow-form-using-php-generators-and-event-streams/
*/
namespace KIZU514;
class EventEmitter
{
const JQUERY_VERSION = '3.3.1';
const JQUERY_UI_VERSION = '1.12.1';
/**
* @var array
*/
public $msgStack = [];
/**
*/
public function __construct()
{
}
/**
* This method accepts a generator that yields a key/value pair
* The key is an integer between 1-100 that represents percentage completed
* The value is a string of information for the user
* Emits event-stream responses (SSE)
*
* @param \Generator $generator
*
* @return bool
*/
public function emit(\Generator $generator)
{
$this->sendEventStreamHeaders();
$complete = [
'action' => 'complete',
'error' => false
];
try {
foreach ($generator as $percentage => $info) {
$data = [
'action' => 'updateStatusBar',
'percentage' => $percentage,
'info' => $info,
];
$this->emitMessage($data);
}
} catch (\Exception $e) {
$complete['error'] = $e->getMessage();
}
flush();
$this->emitMessage($complete);
if ($complete['error'] !== false) {
// Something went wrong
return false;
}
return true;
}
/**
* Emit a Server-Sent Events message.
*
* @param mixed $data Data to be JSON-encoded and sent in the message.
*/
public function emitMessage($data)
{
$msg = "event: message\n";
$msg .= 'data: ' . json_encode($data) . "\n\n";
$msg .= ':' . str_repeat(' ', 2048) . "\n\n";
// Buffers are nested. While one buffer is active, flushing from child buffers are not really sent to the browser,
// but rather to the parent buffer. Only when there is no parent buffer are contents sent to the browser.
if (ob_get_level()) {
// Keep for later
$this->msgStack[] = $msg;
} else {
// Flush to browser
foreach ($this->msgStack as $stack) {
echo $stack;
}
$this->msgStack = []; // Reset
echo $msg;
flush();
}
}
/**
*
*/
public function sendEventStreamHeaders()
{
// Turn off PHP output compression
ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', false);
if (strpos($_SERVER['SERVER_SOFTWARE'], 'nginx') !== false) {
header('X-Accel-Buffering: no');
header('Content-Encoding: none');
}
// Start the event stream
header('Content-Type: text/event-stream');
// 2KB padding for IE
echo ':' . str_repeat(' ', 2048) . "\n\n";
// In it for the long run
ignore_user_abort(true);
set_time_limit(0);
// Ensure we're not buffered
$levels = ob_get_level();
for ($i = 0; $i < $levels; $i++) {
ob_end_flush();
}
flush();
$this->msgStack = []; // Reset
}
/**
* @see https://developers.google.com/speed/libraries/#jquery
* @see https://developers.google.com/speed/libraries/#jquery-ui
*
* @return string
*/
public function jsHeaders()
{
ob_start();
?>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/<?php echo self::JQUERY_VERSION; ?>/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/<?php echo self::JQUERY_UI_VERSION; ?>/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/<?php echo self::JQUERY_UI_VERSION; ?>/jquery-ui.min.js"></script>
<?php
$buffer = ob_get_clean();
return $buffer;
}
/**
* @param string $form_id
*
* @return string
*/
public function jsBody($form_id)
{
if (substr($form_id, 0, 1) !== '#') {
$form_id = "#{$form_id}";
}
ob_start();
?>
<script>
jQuery(function($) {
$('<?php echo $form_id; ?>').on('submit', function(e) {
e.preventDefault();
let formSubmitButton = $('<?php echo $form_id; ?> :submit');
formSubmitButton.attr('disabled', true);
let form = $('<?php echo $form_id; ?>');
let actionUrl = form.prop('action');
let eventSourceUrl = actionUrl + (actionUrl.includes('?') ? '&' : '?') + $.param(form.find(':input'));
let evtSource = new EventSource(eventSourceUrl);
evtSource.onopen = function() {
formSubmitButton.hide();
};
evtSource.onmessage = function(message) {
let bar = $('#sse-progressbar');
let info = $('#sse-info');
let data = JSON.parse(message.data);
switch (data.action) {
case 'updateStatusBar':
bar.progressbar({value: parseInt(data.percentage, 10)});
info.html(data.info);
break;
case 'complete':
evtSource.close();
if (data.error) {
bar.progressbar({value: false});
info.html(data.error);
} else {
window.location = actionUrl;
}
break;
}
};
evtSource.onerror = function() {
evtSource.close();
$('#sse-progressbar').progressbar({value: false});
$('#sse-info').html('EventStream Connection Error');
};
});
});
</script>
<?php
$buffer = ob_get_clean();
return $buffer;
}
}
<?php
/**
* @license GPLv3 (or any later version)
* @see http://kizu514.com/blog/refactor-your-slow-form-using-php-generators-and-event-streams/
*/
/**
* @param string $firstame
* @param string $lastname
* @param int $hawaiianshirtday
* @return \Generator
*/
function loooooooooooooooooooooooongGenerator($firstname, $lastname, $hawaiianshirtday)
{
yield 10 => "Hey {$firstname} what's happening. I'm going to need those TPS reports... ASAP...";
sleep(2);
yield 30 => "Ah, ah, I almost forgot... I'm also going to need you to go ahead and come in on Sunday, too. We, uhhh, lost some people this week and we sorta need to play catch-up. Mmmmmkay? Thaaaaaanks.";
sleep(2);
yield 50 => '...So, if you could do that, that would be great...';
sleep(2);
yield 60 => 'Excuse me, I believe you have my stapler.';
sleep(2);
yield 90 => 'PC LOAD LETTER';
sleep(2);
yield 100 => 'Success!';
}
require('EventEmitter.php');
$emitter = new \KIZU514\EventEmitter();
$messages = [];
if (isset($_GET['firstname'], $_GET['lastname'], $_GET['hawaiianshirtday'])) {
$firstname = !empty($_GET['firstname']) ? strip_tags($_GET['firstname']) : 'Peter';
$lastname = !empty($_GET['lastname']) ? strip_tags($_GET['lastname']) : 'Gibbons';
$hawaiianshirtday = !empty($_GET['hawaiianshirtday']) ? strtotime($_GET['hawaiianshirtday']) : strtotime('1999-02-19');
$emitter->emit(loooooooooooooooooooooooongGenerator($firstname, $lastname, $hawaiianshirtday));
}
?>
<!DOCTYPE html>
<html>
<head>
<title>TPS REPORT</title>
<style>
@import url(//fonts.googleapis.com/css?family=Josefin+Sans:700|Amatic+SC:700);
body { font-family: 'Josefin Sans', sans-serif; margin: 1em; background: #ffffff; color: #000000; }
h1 { font-family: 'Amatic SC', cursive; text-transform: uppercase; }
</style>
<?php echo $emitter->jsHeaders(); ?>
</head>
<body>
<form id='tpsreport' action="<?php echo basename(__FILE__); ?>" method="POST">
<h1>TPS REPORT</h1>
<label>
First name: <input type="text" name="firstname" value="Peter"><br>
</label>
<label>
Last name: <input type="text" name="lastname" value="Gibbons"><br>
</label>
<label>
Hawaiian Shirt Day: <input type="date" name="hawaiianshirtday" value="1999-02-19"><br>
</label>
<p><input type="submit"></p>
<div id="sse-progressbar"></div>
<p id="sse-info" style='color:orangered;'></p>
</form>
<?php echo $emitter->jsBody('tpsreport'); ?>
</body>
</html>
<?php
/**
* @license GPLv3 (or any later version)
* @see http://kizu514.com/blog/refactor-your-slow-form-using-php-generators-and-event-streams/
*/
/**
* @param string $firstame
* @param string $lastname
* @param int $hawaiianshirtday
*
* @return array
*/
function loooooooooooooooooooooooong($firstname, $lastname, $hawaiianshirtday)
{
$messages[] = "Hey {$firstname} what's happening. I'm going to need those TPS reports... ASAP...";
sleep(2);
$messages[] = "Ah, ah, I almost forgot... I'm also going to need you to go ahead and come in on Sunday, too. We, uhhh, lost some people this week and we sorta need to play catch-up. Mmmmmkay? Thaaaaaanks.";
sleep(2);
$messages [] = '...So, if you could do that, that would be great...';
sleep(2);
$messages[] = 'Excuse me, I believe you have my stapler.';
sleep(2);
$messages[] = 'PC LOAD LETTER';
sleep(2);
$messages[] = 'Success!';
return $messages;
}
$messages = [];
if (isset($_POST['firstname'], $_POST['lastname'], $_POST['hawaiianshirtday'])) {
$firstname = !empty($_POST['firstname']) ? strip_tags($_POST['firstname']) : 'Peter';
$lastname = !empty($_POST['lastname']) ? strip_tags($_POST['lastname']) : 'Gibbons';
$hawaiianshirtday = !empty($_POST['hawaiianshirtday']) ? strtotime($_POST['hawaiianshirtday']) : strtotime('1999-02-19');
$messages = loooooooooooooooooooooooong($firstname, $lastname, $hawaiianshirtday);
}
?>
<!DOCTYPE html>
<html>
<head>
<title>TPS REPORT</title>
<style>
@import url(//fonts.googleapis.com/css?family=Josefin+Sans:700|Amatic+SC:700);
body { font-family: 'Josefin Sans', sans-serif; margin: 1em; background: #ffffff; color: #000000; }
h1 { font-family: 'Amatic SC', cursive; text-transform: uppercase; }
</style>
</head>
<body>
<form id='tpsreport' action="<?php echo basename(__FILE__); ?>" method="POST">
<h1>TPS REPORT</h1>
<?php
if (!empty($messages)) {
foreach ($messages as $message) {
echo "<p style='color:orangered;'>$message</p>\n";
}
}
?>
<label>
First name: <input type="text" name="firstname" value="Peter"><br>
</label>
<label>
Last name: <input type="text" name="lastname" value="Gibbons"><br>
</label>
<label>
Hawaiian Shirt Day: <input type="date" name="hawaiianshirtday" value="1999-02-19"><br>
</label>
<p><input type="submit"></p>
</form>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment