Slim Framework: how to early close client connection - php

I have a long task for a slim controller, I would like to early end the output to client and then continue the backend elaboration.
$app->get("/test",function() use($app){
$app->render("page.html"); //this is the client output
$app->easlyStop(); //a slim hypothetical command to call
$task=new MyTask();
$task->longAsyncTask(); //this take a few, client don't have to wait.
});
Is there a solution with Slim?

The easiest option here is to call a method with a system call and return before it finishes:
exec('/bin/php /path/to/a/script.php > /dev/null &');
Note that this is a simplification as PHP is request oriented, which means that a new process is started for every request, and the webserver sends the response to the user once the request is finished. You could use flush and other techniques, but these are prone to errors and depends on the configurations of the webserver too.

This is a method for Slim controller with Json view:
$app->get( '/test/', function () use($app) {
$app->view = new Json();
try{
//here the output of Json view
$model=["myjsondata"=>[]];
$app->render(200,$model);
}catch (\Slim\Exception\Stop $e) {}
//following code is copied from Slim->run() to early output
$app->response()->headers->replace(["Content-Length"=>$app->response()->length()]);
$app->response()->headers->replace(["Connection"=>"close"]);
list($status, $headers, $body) = $app->response->finalize();
\Slim\Http\Util::serializeCookies($headers, $app->response->cookies, $app->settings);
if (headers_sent() === false) {
if (strpos(PHP_SAPI, 'cgi') === 0) {
header(sprintf('Status: %s', \Slim\Http\Response::getMessageForCode($status)));
} else {
header(sprintf('HTTP/%s %s', $app->config('http.version'), \Slim\Http\Response::getMessageForCode($status)));
}
foreach ($headers as $name => $value) {
$hValues = explode("\n", $value);
foreach ($hValues as $hVal) {
header("$name: $hVal", false);
}
}
}
if (!$app->request->isHead()) {
echo $body;
}
//early output to client
ob_end_flush();
ob_flush();
flush();
if (session_id()) session_write_close();
//my async job
sleep(5);
});
I think this can be easily insert in a Slim plugin. This works only with Json view becase this is my need but it can be used with Twig or other Slim views getting output with ob* php functions instead of catching the Stop() exception.

Related

How do I use StreamedResponse to render template view in Symfony2

I am trying to use streamedResponse to output progress to my index page in Symfony2.
This code below does show my progress on the api calls as it occurs, but I am having trouble rendering the streamed information in an actual view. Right now it is just outputing plain text on the top of the page, then rendering the view when its all complete.
I don't want to return the final array and close the function until everything is loaded, but I can't seem to get a regular twig template to show while I output the progress.
I have tried using render but nothing seems to truly ouput that view file to the screen unless I return.
public function indexAction($countryCode)
{
//anywhere from five to fifteen api calls are going to take place
foreach ($Widgets as $Widget) {
$response = new StreamedResponse();
$curlerUrl = $Widget->getApiUrl()
. '?action=returnWidgets'
. '&data=' . urlencode(serialize(array(
'countryCode' => $countryCode
)));
$requestStartTime = microtime(true);
$curler = $this->get('curler')->curlAUrl($curlerUrl);
$curlResult = json_decode($curler['body'], true);
if(isset($curlResult['data'])){
//do some processing on the data
}
$response->setCallback(function() use ($Widget, $executionTime) {
flush();
sleep(1);
var_dump($Widget->getName());
var_dump($executionTime);
flush();
});
$response->send();
}
//rest of indexAction with a return statement
return array(
//all the vars my template will need
);
}
Also, another important detail is that I am trying to render all to twig and there seems to be some interesting issues with that.
As I understand it, you only get one chance to output something to the browser from the server (PHP/Twig), then it's up to JavaScript to make any further changes (like update a progress bar).
I'd recommend using multi-cURL to perform all 15 requests asynchronously. This effectively makes the total request time equal to the slowest request so you can serve your page much faster and maybe eliminate the need for the progress bar.
// Create the multiple cURL handle
$mh = curl_multi_init();
$handles = array();
$responses = array();
// Create and add the cURL handles to the $mh
foreach($widgets as $widget) {
$ch = $curler->getHandle($widget->getURL()); // Code that returns a cURL handle
$handles[] = $ch;
curl_multi_add_handle($mh, $ch);
}
// Execute the requests
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
// Get the request content
foreach($handles as $handle) {
$responses[] = curl_multi_getcontent($handle);
// Close the handles
curl_close($handle);
}
curl_multi_close();
// Do something with the responses
// ...
Ideally, this would be a method of your Curler service.
public function processHandles(array $widgets)
{
// most of the above
return $responses;
}
You may implements all of the logic in the setCallback method, so consider this code:
public function indexAction($countryCode)
{
$Widgets = [];
$response = new StreamedResponse();
$curlerService = $this->get('curler');
$response->setCallback(function() use ($Widgets, $curlerService, $countryCode) {
foreach ($Widgets as $Widget) {
$curlerUrl = $Widget->getApiUrl()
. '?action=returnWidgets'
. '&data=' . urlencode(serialize(array(
'countryCode' => $countryCode
)));
$requestStartTime = microtime(true);
$curler = $curlerService->curlAUrl($curlerUrl);
$curlResult = json_decode($curler['body'], true);
if(isset($curlResult['data'])){
//do some processing on the data
}
flush();
sleep(1);
var_dump($Widget->getName());
var_dump( (microtime(true) - $requestStartTime) );
flush();
}
});
// Directly return the streamed response object
return $response;
}
Further reading this and this article.
Hope this help

Enable the request log, even when a router script is used for the PHP built-in web server

PHP's built in web server allows "router scripts" to be used, allowing for rewriting URLs internally.
The problem with such a router script is the fact that whenever it actually handles a file instead of letting PHP handle it, this causes the request log output for that request to be suppressed. For example, consider the following script:
<?php
if (preg_match('/^\/(js|css)/', $_SERVER['REQUEST_URI']) === 1) {
return false;
}
else {
echo 'hello world!'
}
This causes requests for /js/* and /css/* to be logged on the console; whereas requests for any other URLs simply skip logging the request.
How can I enable logging of all requests to the console?
router.php :
if (preg_match('/^\/(js|css)/', $_SERVER['REQUEST_URI']) === 1) {
return false;
}
else {
$stdErr = fopen("php://stderr",'w+');
fwrite($stdErr, 'LogRequest:'.$_SERVER['REQUEST_URI']."\n");
echo 'hello world!1';
}
Server start: php -S localhost:8000 router.php 2>&1
To log response headers:
if (preg_match('/^\/(js|css)/', $_SERVER['REQUEST_URI']) === 1) {
return false;
}
else {
ob_start()
$stdErr = fopen("php://stderr",'w+');
fwrite($stdErr, 'Request:'.json_encode($_SERVER)."\n");
try {
echo 'hello world!1';
} catch (\Exception $e) {
http_response_code(500);
}
$response = ob_get_clean();
fwrite($stdErr, 'Response:'.json_encode([ http_response_code(),headers_list() ])."\n");
echo $response;
}
In your log you can see errors from php build-in server, so you can do like #cske, but i think you should stick another approach.You can use own logger and write explicitly info to this log. The best logger right now it is monolog, so you don't have to reveal the wheel. Here my example:
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a log channel.
$log = new Logger('general');
$log->pushHandler(new StreamHandler('/var/log/php/general.log', Logger::INFO));
if (preg_match('/^\/(js|css)/', $_SERVER['REQUEST_URI']) === 1) {
$log->error('Invalid request.');
return false;
} else {
$log->info('Success.');
echo 'hello world!';
}
I hope you agree that it looks much clear and simple.
Also when you will move from php build-in server to for example nginx your code will work correctly. You need just run in another window into terminal tail -f /var/log/php/general.log and you will observe behavior of your script like in window with php build-in server.
PS: I hope you don't use php build-in server in production

Phalcon Micro Application(facing prob with finish middleware event)

I am facing a problem while implementing micro application in which i want a middle ware that is executed after response is send to browser. As mentioned in documentation of phalcon finish middle ware suppose to that work but its not working, browser still waiting to complete whole process then it return response.
my code is like:
$app->before(function() use ($app, $di) { $di->get('log')->log("This is a message before"); $di->get('log')->close(); });
$testCtrl = new testCtrl();
$app->get('/ctrltest', array($testCtrl, "indexAction"));
$app->after(function() use ($app, $di) {
echo "after";
$di->get('log')->log("This is a message after");
$di->get('log')->close();
});
$app->finish(function() use ($app, $di) {
$count = 100000;
while ($count) {`enter code here`
$di->get('log')->log("count " . $count);
$count--;
}
});
the response is coming after the whole loop is executed. Any sample code or some suggestion will be helpful

AJAX call returns status 200 but no content

I have a simple AJAX call that retrieves text from a file, pushes it into a table, and displays it. The call works without issue when testing on a Mac running Apache 2.2.26/PHP 5.3 and on an Ubuntu box running Apache 2.2.1.6/PHP 5.3. It does not work on RedHat running Apache 2.2.4/PHP 5.1. Naturally, the RedHat box is the only place where I need it to be working.
The call returns 200 OK but no content. Even if nothing is found in the file (or it's inaccessible), the table header is echoed so if permissions were a problem I would still expect to see something. But to be sure, I verified the file is readable by all users.
Code has been redacted and simplified.
My ajax function:
function ajax(page,targetElement,ajaxFunction,getValues)
{
xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState===4 && xmlhttp.status===200)
{
document.getElementById(targetElement).innerHTML=xmlhttp.responseText;
}
};
xmlhttp.open('GET','/appdir/dir/filedir/'+page+'_funcs.php?function='+ajaxFunction+'&'+getValues+'&'+new Date().getTime(),false);
xmlhttp.setRequestHeader('cache-control','no-cache');
xmlhttp.send();
}
I call it like this:
ajax('pagename','destelement','load_info');
And return the results:
// Custom file handler
function warn_error($errno, $errstr) {
// Common function for warning-prone functions
throw new Exception($errstr, $errno);
}
function get_file_contents() {
// File operation failure would return a warning
// So handle specially to suppress the default message
set_error_handler('warn_error');
try
{
$fh = fopen(dirname(dirname(__FILE__))."/datafile.txt","r");
}
catch (Exception $e)
{
// Craft a nice-looking error message and get out of here
$info = "<tr><td class=\"center\" colspan=\"9\"><b>Fatal Error: </b>Could not load customer data.</td></tr>";
restore_error_handler();
return $info;
}
restore_error_handler();
// Got the file so get and return its contents
while (!feof($fh))
{
$line = fgets($fh);
// Be sure to avoid empty lines in our array
if (!empty($line))
{
$info[] = explode(",",$line);
}
}
fclose($fh);
return $info;
}
function load_info() {
// Start the table
$content .= "<table>
<th>Head1</th>
<th>Head2</th>
<th>Head3</th>
<th>Head4</th>";
// Get the data
// Returns all contents in an array if successful,
// Returns an error string if it fails
$info = get_file_contents();
if (!is_array($info))
{
// String was returned because of an error
echo $content.$info;
exit();
}
// Got valid data array, so loop through it to build the table
foreach ($info as $detail)
{
list($field1,$field2,$field3,$field4) = $detail;
$content .= "<tr>
<td>$field1</td>
<td>$field2</td>
<td>$field3</td>
<td>$field4</td>
</tr>";
}
$content .= "</table>";
echo $content;
}
Where it works, the response header indicates the connection as keep-alive; where it fails, the connection is closed. I don't know if that matters.
I've looked all over SO and the net for some clues but "no content" issues invariably point to same-origin policy problems. In my case, all content is on the same server.
I'm at a loss as to what to do/where to look next.
file_get_contents() expects a parameter. It does not know what you want, so it returned false. Also, you used get_file_contents() which is the wrong order.
This turned out to be a PHP version issue. In the load_info function I was using filter_input(INPUT_GET,"value"), but that was not available in PHP 5.1. I pulled that from my initial code post because I didn't think it was part of the problem. Lesson learned.

Very slow ajax response on yii framework

i am having strange issue with yii framework. on localhost, ajax response takes 200ms (which is fast and i am satsified) where as on my live server, same function take 4 to 7 seconds.
below is my php ajax function:-
public function actionOpenpopup() {
$this->checkAjaxRequest();
$user_id = $_GET['uid'];
$rows = Yii::app()->db->createCommand()
->select('*')
->from('saved_designs')
->where('uid=:id', array(':id' => $user_id))
->order('date desc')
->queryAll();
$i = 0;
foreach ($rows as $row) {
$rows[$i] = $row;
$i++;
}
if ($rows) {
echo json_encode($rows);
}
else
echo json_encode(null);
}
function checkAjaxRequest() {
if (Yii::app()->request->isAjaxRequest) {
header('Content-Type: application/json; charset="UTF-8"');
return true;
} else {
throw new CHttpException('403', 'Forbidden Access');
exit;
}
}
javascript code is:-
function sendAjaxCall(data){
$.ajax({
type : 'GET',
url : 'index.php/request/openpopup',
datatype : 'json',
data :data,
success: function (data) {
console.log(data);
}
});
}
*Note:- So far database has only 10 to 20 records. Also On live server, all my ajax calls give me slow response.
I would try a few things. First off after you echo your json I would kill your script to make sure nothing else runs:
if ($rows) {
echo json_encode($rows);
die();
}
Also on your index.php make sure you have the site taken out of debug mode, if you have either of the middle two lines that start with defined() enabled each page load Yii is recreating cached files and it can take a while, especially if you have extensions like bootstrap included. I have had this exact issue when doing some work for someone and their site was hosted on GoDaddy. For some reason the file creation was really slow and was really dragging everything.
<?php
$yii=dirname(__FILE__).'/../framework/yii.php';
$config=dirname(__FILE__).'/protected/config/test.php';
//defined('YII_DEBUG') or define('YII_DEBUG',true);
//defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL',3);
require_once($yii);
Yii::createWebApplication($config)->run();
Also are any other functions running slow? Any errors in your error log?
Another option to help debug create another action that doesn't require a AJAX call. It is much easier to debug this way instead of relying on ajax, plus it helps you narrow down the source of the problem. Plus don't know why but you get your array of rows then re-populate your array of rows, this is very redundant.
public function actionCheckpopup() {
$user_id = $_GET['uid'];
$rows = Yii::app()->db->createCommand()
->select('*')
->from('saved_designs')
->where('uid=:id', array(':id' => $user_id))
->order('date desc')
->queryAll();
echo json_encode($rows);
die();
}
Then simply use a browser and go to http://yoursite.com/index.php/request/checkpopup?uid=1

Categories