One month ago I posted an article called Building a simple HTTP client with PHP. A REST client. In this post I tried to create a simple fluid interface to call REST webservices in PHP. Some days ago I read an article and one light switch on on my mind. I can use curl’s “multi” functions to improve my library and perform simultaneous calls very easily.
I’ve got a project that needs to call to different webservices. Those webservices sometimes are slow (2-3 seconds). If I need to call to, for example, three webservices my script will use the add of every single call’s time. With this improve to the library I will use only the time of the slowest webservice. 2 seconds instead of 2+2+2 seconds. Great.
For the example I’ve created a really complex php script that sleeps x seconds depend on an input param:
sleep((integer) $_REQUEST['sleep']); echo $_REQUEST['sleep'];
With synchronous calls:
echo Http::connect('localhost', 8082) ->doGet('/tests/gam_http/sleep.php', array('sleep' => 3)); echo Http::connect('localhost', 8082) ->doPost('/tests/gam_http/sleep.php', array('sleep' => 2)); echo Http::connect('localhost', 8082) ->doGet('/tests/gam_http/sleep.php', array('sleep' => 1));
This script takes more or less 6 seconds (3+2+1)
But If I switch it to:
$out = Http::connect('localhost', 8082) ->get('/tests/gam_http/sleep.php', array('sleep' => 3)) ->post('/tests/gam_http/sleep.php', array('sleep' => 2)) ->get('/tests/gam_http/sleep.php', array('sleep' => 1)) ->run(); print_r($out);
The script only uses 3 seconds (the slowest process)
I’ve got a project that uses it. But I have a problem. I have webservices in different hosts so I’ve done a bit change to the library:
$out = Http::multiConnect() ->add(Http::connect('localhost', 8082)->get('/tests/gam_http/sleep.php', array('sleep' => 3))) ->add(Http::connect('localhost', 8082)->post('/tests/gam_http/sleep.php', array('sleep' => 2))) ->add(Http::connect('localhost', 8082)->get('/tests/gam_http/sleep.php', array('sleep' => 1))) ->run();
With a single connection, the exceptions are easy to implement. If curl_getinfo() returns an error message I throw an exception, but now with a multiple interface how I can do it? I throw an exception if one call fail, or not? I have decided not to use exceptions in multiple interface. I always return an array with all the output of every webservice’s call and if something wrong happens instead of the output I will return an instance of Http_Multiple_Error class. Why I use a class instead of a error message? The answer is easy. If I want to check all the answers I can check if any of them is an instanceof Http_Multiple_Error. Also I don’t want to check anything I put a silentMode() function to switch off all error messages.
$out = Http::multiConnect() ->silentMode() ->add(Http::connect('localhost', 8082)->get('/tests/gam_http/sleep.php', array('sleep' => 3))) ->add(Http::connect('localhost', 8082)->post('/tests/gam_http/sleep.php', array('sleep' => 2))) ->add(Http::connect('localhost', 8082)->get('/tests/gam_http/sleep.php', array('sleep' => 1))) ->run();
The full code is available on google code but the main function is the following one:
... private function _run() { $headers = $this->_headers; $curly = $result = array(); $mh = curl_multi_init(); foreach ($this->_requests as $id => $reg) { $curly[$id] = curl_init(); $type = $reg[0]; $url = $reg[1]; $params = $reg[2]; if(!is_null($this->_user)){ curl_setopt($curly[$id], CURLOPT_USERPWD, $this->_user.':'.$this->_pass); } switch ($type) { case self::DELETE: curl_setopt($curly[$id], CURLOPT_URL, $url . '?' . http_build_query($params)); curl_setopt($curly[$id], CURLOPT_CUSTOMREQUEST, self::DELETE); break; case self::POST: curl_setopt($curly[$id], CURLOPT_URL, $url); curl_setopt($curly[$id], CURLOPT_POST, true); curl_setopt($curly[$id], CURLOPT_POSTFIELDS, $params); break; case self::GET: curl_setopt($curly[$id], CURLOPT_URL, $url . '?' . http_build_query($params)); break; } curl_setopt($curly[$id], CURLOPT_RETURNTRANSFER, true); curl_setopt($curly[$id], CURLOPT_HTTPHEADER, $headers); curl_multi_add_handle($mh, $curly[$id]); } $running = null; do { curl_multi_exec($mh, $running); sleep(0.2); } while($running > 0); foreach($curly as $id => $c) { $status = curl_getinfo($c, CURLINFO_HTTP_CODE); switch ($status) { case self::HTTP_OK: case self::HTTP_CREATED: case self::HTTP_ACEPTED: $result[$id] = curl_multi_getcontent($c); break; default: if (!$this->_silentMode) { $result[$id] = new Http_Multiple_Error($status, $type, $url, $params); } } curl_multi_remove_handle($mh, $c); } curl_multi_close($mh); return $result;