In order to fix users waiting pain due to some (already optimized) DB calculations : about 3 to 10 seconds.
We need to make a waiting page during the long calculation process as every flights comparators do for example.
Our architecture is based on Silex 1.3.
What we want to achieve is:
I tested it with $app->before
attribute bound to the route and returning/rendering stops the calculation...
So how to do that double rendering based on calculation?
NOT SUITABLE SOLUTION FOR OUR PURPOSE:
The first workaround I implemented is a JavaScript show/hide spinner element. But this is not a suitable solution because we really need to get a temporary rendering from server.
EDIT
The first render is working but I'm not enable to do the second rendering by calling my controller action (defined as controller as service) which is doing the rendering and I get this EXCEPTION:
RuntimeException: Accessed request service outside of request scope. Try moving that call to a before handler or controller.
Here is my controller definition:
$app['index.controller'] = $app->share(function() use ($app) {
return new IndexController($app);
});
Here is my route definition:
$app->get('/vue-ensemble/{city}', function (Request $request, Application $app)
{
$content = function() use ($app) {
$wait = $app->render('index_test.twig', array());
$wait->send();
flush();
// Long process
$process = $app['index.controller']->overviewAction();
$process->send();
flush();
};
return $app->stream($content);
});
Here is my controller action:
protected $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public function overviewAction(){
/* DO LONG PROCESS */
return $this->app->render('overview.twig', array('some elements'=>'some values'));
}
EDIT '
Unfortunately I still have the same issue, here is the stack trace:
fatal error: Uncaught exception 'RuntimeException' with message 'Accessed request service outside of request scope. Try moving that call to a before handler or controller.' in C:\inforisq\application\vendor\silex\silex\src\Silex\Application.php on line 150
( ! ) RuntimeException: Accessed request service outside of request scope. Try moving that call to a before handler or controller. in C:\inforisq\application\vendor\silex\silex\src\Silex\Application.php on line 150
Call Stack
# Time Memory Function Location
1 0.0010 240848 {main}( ) ...\index.php:0
2 0.5250 4506656 Silex\Application->run( ) ...\index.php:14
3 0.7040 13098664 Symfony\Component\HttpFoundation\Response->send( ) ...\Application.php:564
4 0.7040 13101248 Symfony\Component\HttpFoundation\StreamedResponse->sendContent( ) ...\Response.php:372
5 0.7040 13101296 call_user_func:{C:\inforisq\application\vendor\symfony\http-foundation\StreamedResponse.php:90} ( ) ...\StreamedResponse.php:90
6 0.7040 13101384 {closure:C:\inforisq\application\app\config\routing.php:20-27}( ) ...\StreamedResponse.php:90
7 0.7040 13118168 Inforisq\Controller\IndexController->overviewAction( ) ...\routing.php:25
8 0.7040 13118328 Lib\InforisqApplication->place_analyzeURL( ) ...\IndexController.php:64
9 0.7060 13306184 Indicator\Repository\PlaceRepository->analyzeURLPlace( ) ...\PlaceTrait.php:23
10 0.7060 13306304 Lib\InforisqApplication->request( ) ...\PlaceRepository.php:423
11 0.7060 13306384 Pimple->offsetGet( ) ...\PlaceRepository.php:26
12 0.7060 13306464 Silex\Application->Silex\{closure}( ) ...\Pimple.php:83
You need to use a Symfony\Component\HttpFoundation\StreamedResponse
see https://symfony.com/doc/current/components/http_foundation/introduction.html#streaming-a-response
When you return a response from a controller, the kernel calls $response->send()
, but internally Response::send()
calls Response::sendHeaders()
then Response::sendContent()
.
So sendHeaders()
in this case will be send once by the kernel on the streamed response, then if you need other Response
objects in your callback for convenience, you must call only sendContent()
.
If you need to custom the http response code or the headers you can pass them as arguments in the method Application::stream($callback, $statusCode, array $headers)
.
Before editing my answer I used flush()
like the example in the symfony docs, but you may need some cache to be able to handle a second controller in the callback so first use ob_start()
and ob_flush()
for the "waiting" response.
use \Silex\Application;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Response;
$app->get('/vue-ensemble/{city}', function (Request $request, Application $app, $city)
{
/** @var \Acme\Controller\IndexController $indexController */
$indexController = $app['index.controller'];
/** @var Response $wait */
$wait = $app->render('wait.html.twig', array('city' => $city);
// Callback
$content = function () use ($wait, $indexController) {
ob_start();
$wait->sendContent();
ob_flush();
$indexController->overviewAction($city)->sendContent();
flush();
}
return $app->stream($content);
// Will send a \Symfony\Component\HttpFoundation\StreamedResponse
// equivalent to :
// $streamedResponse = new StreamedResponse();
// $streamedResponse->setCallback($content);
//
// return streamedResponse;
});
You should only pass the services you need in a constructor of service instead of pass the whole container each time :
$app['index.controller'] = $app->share(function() use ($app) {
return new IndexController($app['twig'], $app['some_helper']);
});
$app['some_helper'] = $app->protect(function($arg1, arg2) use ($app) {
$helper = new \Acme\Helper($app['some_dependency'];
return $helper->help(arg1, arg2);
});
Then :
class IndexController
{
protected $twig;
protected $helper;
public function __construct(\Twig_Engine $twig, \Acme\Helper $helper)
{
$this->twig = $twig;
$this->helper = $helper;
}
public function overview($city)
{
$some_arg = ...
$viewArg = $this->helper($some_arg, $city);
return $this->twig->renderResponse('overview.twig', array('some elements' => $viewArg));
}
}
But then it would be better if the indexController
just returned $this->twig->render(...)
but then it should be echo $indexController->overview($city)