Implementing asynchronous API calls in PHP

7 Oct 2020
5 min read

A few months ago, we came across a use case where we had to make multiple API calls. These calls had to be triggered in parallel, additionally, we had to achieve this in PHP.
So we went ahead and did the only thing that we do when we get stuck – we Googled the problem!

Out of the many results that Google showed us, we found two results that were ideal for us. One of them was ReactPHP and the other was using Guzzle Client.

What is ReactPHP?

Do not get confused by its name, it has nothing to do with React. This is a low-level library which enhances PHP to make it event-driven and non-blocking I/O. ReactPHP is not a new project, this has been around for quite some time. So we can utilize this to make PHP asynchronous.

At its core ReactPHP has the following components:

EventLoop

This component provides a common `LoopInterface` for libraries to access the same event loop.

       $loop = React\EventLoop\Factory::create();

       $loop->addTimer(3, function () {
            echo 'world!' . PHP_EOL;
       });

       $loop->addTimer(4, function () {
           echo 'hello ';
       });

       $loop->run();

In the code snippet above we make use of Eventloop to run both the functions simultaneously. As a result, the time taken to execute the above code is 4 sec rather than 7 sec.

Stream

This component allows for easy usage of Eventloop. Streams allow us to process large sums of data into smaller chunks without having to store anything in the data.

	$loop = new React\EventLoop\StreamSelectLoop;

	$out = new React\Stream\WritableResourceStream(STDOUT $loop);
	$in = new React\Stream\ReadableResourceStream(STDIN, $loop,1);

	$timer = $loop->addPeriodicTimer(0.2, function () {
	    echo 'tick!' . PHP_EOL;
	});

	$in->on('data',function($data) use ($out, $in, $loop){
		$out->write($data.PHP_EOL);

		$in->pause();

		$loop->addTimer(1 , function() use($in) {
			$in->resume();
		});
	});

        $loop->run();

In the above code snippet, we are setting the `readChunkSize` in the ReadableResourceStream to 1. This results in the reading of data in chunks of 1 character.  We can also pause a stream and resume it, however, it will not block other functions. Here the stream is paused for 1 sec and resumed, but this does not affect the timer. It keeps on echoing `tick` every 0.2 sec irrespective of the pause.

Promise

A Promise is a placeholder for a future response object or a value that you will receive. This component provides us with libraries that help to implement CommonJS Promise/A for PHP

	function get($uri)
	{
	    $deferred = new React\Promise\Deferred();

	    // Execute a Node.js-style function using the callback pattern
	    callexternalApi($uri, function (\Throwable $error, $result) use ($deferred) {
	        if ($error) {
	            $deferred->reject($error);
	        } else {
	            $deferred->resolve($result);
	        }
	    });

	    // Return the promise
	    return $deferred->promise();
	}

	get('https//{externaluri}')->then(
	    function ($value) {
	       // Deferred resolved, do something with $value
	    },
	    function (\Throwable $reason) {
	       // Deferred rejected, do something with $reason
	    }
	);

Here instead of waiting for the response from the API, it returns a promise and once it is fulfilled we continue executing our code or we are going to reject it.
The other solution that we are going to discuss is Guzzle Client

Guzzle:

Guzzle is a PHP HTTP client that helps to abstract and provides easy use of accessing web services. In the latest version of guzzle, we can send  HTTP request using PHP Stream without the using cURL. However, we would require cURL for concurrent request.

Laravel package comes with Guzzle included in it. So we decided to go with this solution.
This client provides a magic method which helps to send asynchronous requests.

$response = $client->requestAsync('GET', '{URI}');

The response returned by this method implements the Promise/A specification. We can chain the then() calls off of the promise. These calls are then either fulfilled by successful Psr\Http\Message\ResponseInterface or rejected with an exception.

$response->then(
    function (ResponseInterface $res) {
        echo $res->getStatusCode() . "\n";
    },
    function (RequestException $e) {
        echo $e->getMessage() . "\n";
        echo $e->getRequest()->getMethod();
    }
);

But our objective is to send multiple asynchronous requests simultaneously. For this Guzzle provides us GuzzleHttp\Pool object. 

$requests = function ($total) use ($client, $d) {
    for ($i = 0; $i < $total; $i++) {
        yield function() use ($client, $d, $i) {
            return $client->requestAsync($d[$i]['method'], $d[$i]['fullurl'], $d[$i]['param']);
        };
    }
};

$responses = Pool::batch($client, $requests(count($d)), [
    'concurrency' => 20,
    'fulfilled' => function (Response $response, $index) {
        // this is delivered each successful response
    },
    'rejected' => function (RequestException $reason, $index) {
        // this is delivered each failed request
    },
]);   

Here we use closure, which is called by the pool. Once they are called they return a promise.

Implementation in Response System:

The Response system has a listing page called `calls-for-today`, where we list the Shaadi members that need to be called by the advisors. In this we have to check if a particular campaign is applicable to these members, however, the API that gives us this information only allows one member per call. As a result, we had to make 20 individual API calls per page. Calling these API by the traditional method, ie. one after another was never an option. So Implementing this concurrent asynchronous requests using guzzle client helped us reduce the time from approx 10 to 15 sec to less than 1 sec. 

Discover more from Tech Shaadi

Subscribe now to keep reading and get access to the full archive.

Continue reading