7

I am trying to reproduce the behaviour of the facebook batch requests function on their graph api.

So I think that the easiest solution is to make several requests on a controller to my application like:

public function batchAction (Request $request)
{
    $requests = $request->all();
    $responses = [];

    foreach ($requests as $req) {
        $response = $this->get('some_http_client')
            ->request($req['method'],$req['relative_url'],$req['options']);

        $responses[] = [
            'method' => $req['method'],
            'url' => $req['url'],
            'code' => $response->getCode(),
            'headers' => $response->getHeaders(),
            'body' => $response->getContent()
        ]
    }

    return new JsonResponse($responses)
}

So with this solution, I think that my functional tests would be green.

However, I fill like initializing the service container X times might make the application much slower. Because for each request, every bundle is built, the service container is rebuilt each time etc...

Do you see any other solution for my problem?

In other words, do I need to make complete new HTTP requests to my server to get responses from other controllers in my application?

Thank you in advance for your advices!

Hammerbot
  • 15,696
  • 9
  • 61
  • 103
  • I don't think it's a good practice to use Controller to do batches actions. – Weenesta - Mathieu Dormeval Nov 23 '16 at 15:25
  • You should perfer "Symfony Command" : https://symfony.com/doc/current/console.html / "Symfony Application" : http://symfony.com/doc/current/components/console/single_command_tool.html and GuzzleClient : http://docs.guzzlephp.org/en/latest/ – Weenesta - Mathieu Dormeval Nov 23 '16 at 15:42
  • Thank you for the advices. I looked at the console solution, but I don't think that it would be appropriate for my use case as my endpoints are already defined in controllers, from a routing.yml file. So in order to use commands, I think that I would need to rewrite all my controllers as commands, and linking somehow routing with commands, and I would still need to respond with the command output. With this solution, I am also concerned with strict request information like headers, cookies etc... – Hammerbot Nov 23 '16 at 16:03
  • However, this is very close to what I want to do: https://symfony.com/doc/current/console/calling_commands.html except this is for console and not controllers. This line is very interesting: `$command = $this->getApplication()->find('demo:greet');`, I would in fact need something like that: `$response = $this->getApplication()->getResponseFrom('/api/some-endpoint');`, that would be like **awesome** – Hammerbot Nov 23 '16 at 16:06
  • This is the stuff of Guzzle Client !! You can externalize your code in services, in this case, you call them from both command and controllers... – Weenesta - Mathieu Dormeval Nov 23 '16 at 16:06
  • Take a look at this : http://stackoverflow.com/questions/40729316/how-to-design-an-app-that-does-heavy-tasks-and-show-the-result-in-the-frontend/40746980#40746980, in your case, use guzzle client as specific service in your own service – Weenesta - Mathieu Dormeval Nov 23 '16 at 16:10
  • Thank you for sharing your answer! I actually already use services, and my controllers are pretty good at using them (like 4 or 5 lines each controller). However, I'm still convinced that the use of Guzzle will make HTTP requests to my own server, which means that my server will instanciate a new service container at each request. I need to access the batch endpoint from an http request. So with this solution: 1 request to handle user batch request + X requests to handle every request part of the batch request. Of course, only one request will really travel from client to server. – Hammerbot Nov 23 '16 at 16:19
  • 2
    Have a look to : http://guzzle3.readthedocs.io/batching/batching.html – Weenesta - Mathieu Dormeval Nov 23 '16 at 16:24

2 Answers2

6

Internally Symfony handle a Request with the http_kernel component. So you can simulate a Request for every batch action you want to execute and then pass it to the http_kernel component and then elaborate the result.

Consider this Example controller:

/**
 * @Route("/batchAction", name="batchAction")
 */
public function batchAction()
{
    // Simulate a batch request of existing route
    $requests = [
        [
            'method' => 'GET',
            'relative_url' => '/b',
            'options' => 'a=b&cd',
        ],
        [
            'method' => 'GET',
            'relative_url' => '/c',
            'options' => 'a=b&cd',
        ],
    ];

    $kernel = $this->get('http_kernel');

    $responses = [];
    foreach($requests as $aRequest){

        // Construct a query params. Is only an example i don't know your input
        $options=[];
        parse_str($aRequest['options'], $options);

        // Construct a new request object for each batch request
        $req = Request::create(
            $aRequest['relative_url'],
            $aRequest['method'],
            $options
        );
        // process the request
        // TODO handle exception
        $response = $kernel->handle($req);

        $responses[] = [
            'method' => $aRequest['method'],
            'url' => $aRequest['relative_url'],
            'code' => $response->getStatusCode(),
            'headers' => $response->headers,
            'body' => $response->getContent()
        ];
    }
    return new JsonResponse($responses);
}

With the following controller method:

/**
 * @Route("/a", name="route_a_")
 */
public function aAction(Request $request)
{
    return new Response('A');
}

/**
 * @Route("/b", name="route_b_")
 */
public function bAction(Request $request)
{
    return new Response('B');
}

/**
 * @Route("/c", name="route_c_")
 */
public function cAction(Request $request)
{
    return new Response('C');
}

The output of the request will be:

[
{"method":"GET","url":"\/b","code":200,"headers":{},"body":"B"},
{"method":"GET","url":"\/c","code":200,"headers":{},"body":"C"}
]

PS: I hope that I have correctly understand what you need.

Matteo
  • 37,680
  • 11
  • 100
  • 115
  • I think that you got it! I need to test that tomorrow, I keep you informed, thank you! – Hammerbot Nov 23 '16 at 20:20
  • Well, as expected this was exactly what I needed, I also looked at the `app.php` file that made me understand much better the request travel into the application. – Hammerbot Nov 24 '16 at 12:42
  • Hi @El_Matella, exactly! Have you find some performance benefit with this approach? – Matteo Nov 24 '16 at 12:45
  • 1
    I did not make very huge tests yet on the real application, I'll post it here in comments when I will. But I am convinced that performances will be much better with this. – Hammerbot Nov 24 '16 at 12:49
  • Yes, of course! Depends about how many request will be passed to a single batch to the main controllers consider to increase the time limit of the operation – Matteo Nov 24 '16 at 12:53
  • 1
    For your information, on an example like yours. A single request to `/some-endpoint` takes `170ms`, and a request to the batch endpoint making several fake requests to the kernel takes `190ms`. So, for me, it is much, much better. – Hammerbot Nov 24 '16 at 13:06
0

There are ways to optimise test-speed, both with PHPunit configuration (for example, xdebug config, or running the tests with the phpdbg SAPI instead of including the Xdebug module into the usual PHP instance).

Because the code will always be running the AppKernel class, you can also put some optimisations in there for specific environments - including initiali[zs]ing the container less often during a test.

I'm using one such example by Kris Wallsmith. Here is his sample code.

class AppKernel extends Kernel
{
// ... registerBundles() etc
// In dev & test, you can also set the cache/log directories 
// with getCacheDir() & getLogDir() to a ramdrive (/tmpfs).
// particularly useful when running in VirtualBox

protected function initializeContainer()
{
    static $first = true;

    if ('test' !== $this->getEnvironment()) {
        parent::initializeContainer();
        return;
    }

    $debug = $this->debug;

    if (!$first) {
        // disable debug mode on all but the first initialization
        $this->debug = false;
    }

    // will not work with --process-isolation
    $first = false;

    try {
        parent::initializeContainer();
    } catch (\Exception $e) {
        $this->debug = $debug;
        throw $e;
    }

    $this->debug = $debug;
}
Alister Bulman
  • 34,482
  • 9
  • 71
  • 110
  • Hi, thank you for taking the time to respond to my question. I think that I understand your optimization for my tests. However, my question is not targeting any test environment. When I said *the service container will be rebuilt each time*, I meant after each request made by my controller with `$response = $this->get('some_http_client')->request()`. My question is more about "Do I really need to make http requests to respond to my problem?" – Hammerbot Nov 23 '16 at 15:06