Last active
November 4, 2022 19:52
-
-
Save dac514/a2fa7712efe135d5854f4d32d67ca09f to your computer and use it in GitHub Desktop.
Refactor Your Slow Form Using PHP Generators and Event Streams
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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