When working with web services curl quickly becomes your best friend. It gets even better when you dig into PHP's multi_curl functions. The downside to accessing web services at run time is that HTTP connections can be slow. This problem is multiplied when you have to call multiple web services for a given page. PHP's multi_curl_* functions help drastically because they allow you to make unblocking asynchronous/parallel requests. This means you can continue processing the request without waiting for a response.

Most of the tutorials online show examples of parallel curl requests using the same pattern. Fire of a handful of requests and then block later on until all the requests have completed. But what if you don't need all the information right away? Perhaps the response you need is available immediately and another request is still waiting. Wouldn't it be nice to get what you need when you need it? Of course it would.

  1. Execute curl requests as needed
  2. Access responses as needed
  3. Wrap this functionality in an easy to use class
  4. Offer a consistent interface to access the response
  5. Show a working example

Execute curl requests as needed
PHP's multi_curl_init acts as a container for one or more curl handles created by curl_init. It also lets you run them in parallel and continue processing other PHP code. You can call curl_multi_exec at any time to fire off any curl handle in the stack which haven't been yet. The second parameter to this function is passed by reference and returns a reference to a flag to tell if there are operations still running.

mch = curl_multi_init();

$ch1 = curl_init('http://www.yahoo.com');
curl_setopt($ch1, CURLOPT_RETURNTRANSFER);
$ch2 = curl_init('http://www.google.com');
curl_setopt($ch2, CURLOPT_RETURNTRANSFER);

curl_multi_add_handle($mch, $ch1);
curl_multi_exec($mch, $active);

//

curl_multi_add_handle($mch, $ch2);
curl_multi_exec($mch, $active);

//

do{
  curl_multi_exec($mch, $active);
}while($active > 0);

$resp1 = curl_multi_getcontent($ch1);
$resp2 = curl_multi_getcontent($ch2);

Access responses as needed
The above code is a huge improvement from blocking for both curl requests to Yahoo! and Google. But say Google was being slow and you needed the response from Yahoo! first? The above code would force you to wait for the response from Google before you could use the response from Yahoo!. We can use the 2nd parameter to curl_multi_exec to let us know if there are any completed responses. What we can do is to check $active each time the do while loop processes and store any response received. If the response received is the one we're looking for then we can simply exit the loop.

do{
  curl_multi_exec($mch, $active);
  if($active != $previousActive){
    // new response to save
    // if this is the response we were looking for then exit the loop
  }
  $previousActive = $active;
}while($active > 0);

Wrap this functionality in an easy to use class
It turns out that all you need to do is manage your curl handles. In order to do this we're going to make the curl wrapper class a singleton. Additionally, we will create another class to manage the curl handles. When adding a curl handle we are going to return an instance of this manager class after having instantiated it with a unique identifier. The unique identifier we're going to use is the string value of the curl handle (string)curl_init(). We'll get into the code later.

Offer a consistent interface to access the response
In order to offer a consistent interface we are going define a few member variables for the manager class. For simplicity sake we will start with data for the response and code for the HTTP status code. Instead of initializing these with the object we can use PHP's __get magic method. Now the first time we access $manager->data it will call the __get method. In the get method we will do the blocking and wait for the response. Once the response is received we'll store it in case it's accessed again later. I am not going into the details of this code as it should be self explanatory with the notes above.

Source also available at GitHub.

class EpiCurl
{
  const timeout = 3;
  static $inst = null;
  static $singleton = 0;
  private $mc;
  private $msgs;
  private $running;
  private $requests = array();
  private $responses = array();
  private $properties = array();

  function __construct()
  {
    if(self::$singleton == 0)
    {
      throw new Exception('You must instantiate it using: $obj = EpiCurl::getInstance();');
    }

    $this->mc = curl_multi_init();
    $this->properties = array(
      'code'  => CURLINFO_HTTP_CODE,
      'time'  => CURLINFO_TOTAL_TIME,
      'length'=> CURLINFO_CONTENT_LENGTH_DOWNLOAD,
      'type'  => CURLINFO_CONTENT_TYPE
      );
  }

  public function addCurl($ch)
  {
    $key = (string)$ch;
    $this->requests[$key] = $ch;

    $res = curl_multi_add_handle($this->mc, $ch);
    if($res == 0)
    {
      curl_multi_exec($this->mc, $active);
      return new EpiCurlManager($key);
    }
    else
    {
      return $res;
    }
  }

  public function getResult($key = null)
  {
    if($key != null)
    {
      if(isset($this->responses[$key]))
      {
        return $this->responses[$key];
      }

      $running = null;
      do
      {
        $resp = curl_multi_exec($this->mc, $runningCurrent);
        if($running !== null && $runningCurrent != $running)
        {
          $this->storeResponses($key);
          if(isset($this->responses[$key]))
          {
            return $this->responses[$key];
          }
        }
        $running = $runningCurrent;
      }while($runningCurrent > 0);
    }

    return false;
  }

  private function storeResponses()
  {
    while($done = curl_multi_info_read($this->mc))
    {
      $key = (string)$done['handle'];
      $this->responses[$key]['data'] = curl_multi_getcontent($done['handle']);
      foreach($this->properties as $name => $const)
      {
        $this->responses[$key][$name] = curl_getinfo($done['handle'], $const);
        curl_multi_remove_handle($this->mc, $done['handle']);
      }
    }
  }

  static function getInstance()
  {
    if(self::$inst == null)
    {
      self::$singleton = 1;
      self::$inst = new EpiCurl();
    }

    return self::$inst;
  }
}

class EpiCurlManager
{
  private $key;
  private $epiCurl;

  function __construct($key)
  {
    $this->key = $key;
    $this->epiCurl = EpiCurl::getInstance();
  }

  function __get($name)
  {
    $responses = $this->epiCurl->getResult($this->key);
    return $responses[$name];
  }
}

Show a working example
Here is how it looks to implement. It's very clean and consistent…two of the goals we set out for. If you have any questions then let me know in the comments.

  1. include 'EpiCurl.php';  
  2. $mc = EpiCurl::getInstance();  
  3.   
  4. $ch1 = curl_init('http://www.yahoo.com');  
  5. curl_setopt($ch1, CURLOPT_RETURNTRANSFER, 1);  
  6. $curl1 = $mc->addCurl($ch1);  
  7.   
  8. // connect to a database  
  9. // loop over some records  
  10. // authenticate a user  
  11.   
  12. $ch2 = curl_init('http://www.google.com');  
  13. curl_setopt($ch2, CURLOPT_RETURNTRANSFER, 1);  
  14. $curl2 = $mc->addCurl($ch2);  
  15.   
  16. // open a file  
  17. // loop over the lines in the file  
  18. // close the file  
  19.   
  20. $ch3 = curl_init('http://www.slooooooooooooooooow.com');  
  21. curl_setopt($ch3, CURLOPT_RETURNTRANSFER, 1);  
  22. $curl3 = $mc->addCurl($ch3);  
  23.   
  24. echo "Response code from Yahoo! is {$curl1->code}\n";  
  25. echo "Response code from Google is {$curl2->code}\n";  

Resources