Asynchronous Requests in PHP

Mon, Nov 12, 2012 - 4:04am -- Isaac Sukin

I recently wrote an API callback script that performed some heavy calculations and took a long time to return. To keep the user from having to wait, I wanted to have the script immediately return cached results and asynchronously process the calculations. There are a few partial solutions on the web but none of them properly deal with sites using HTTPS, so here's my solution:

/**
 * Posts to a page in the background.
 *
 * Specifically what this does is open a connection, write the data, and then
 * immediately close the connection.
 *
 * Example usage: background_post(current_full_url(), array('background' => 1));
 *
 * Pages that expect to be called in the background should usually call
 * ignore_user_abort(true) as soon as possible to avoid being terminated early
 * when the connection is closed.
 *
 * @param $url
 *   The URL to call in the background.
 * @param $data
 *   (Optional) An associative array of data to POST.
 * @param $debug
 *   (Optional) If TRUE, the request is executed synchronously and an array of
 *   relevant information is returned on success (instead of returning TRUE).
 *
 * @return
 *   TRUE if the request succeeded or an error string if it failed.
 */
function background_post($url, $data = array(), $debug = FALSE) {
  $parts = parse_url($url);
  $host = $parts['host'];
  if (isset($parts['port'])) {
    $port = $parts['port'];
  }
  else {
    $port = $parts['scheme'] == 'https' ? 443 : 80;
  }
  if ($port == 443) {
    $host = 'ssl://' . $host;
  }
 
  $data_params = array();
  foreach ($data as $k => $v) {
    $data_params[] = urlencode($k) . '=' . urlencode($v);
  }
  $data_str = implode('&', $data_params);
 
  $fp = fsockopen(
    $host,
    $port,
    $errno,
    $errstr,
    3
  );
 
  if (!$fp) {
    return "Error $errno: $errstr";
  }
  else {
    $out = "POST " . $parts['path'] . " HTTP/1.1\r\n";
    $out .= "Host: " . $parts['host'] . "\r\n";
    $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
    if (!empty($data_str)) {
      $out .= "Content-Length: " . strlen($data_str) . "\r\n";
    }
    $out .= "Connection: Close\r\n\r\n";
    if (!empty($data_str)) {
      $out .= $data_str;
    }
    fwrite($fp, $out);
    if ($debug) {
      $contents = '';
      while (!feof($fp)) {
        $contents .= fgets($fp, 128);
      }
    }
    fclose($fp);
    if (!$debug) {
      return TRUE;
    }
    else {
      return array(
        'url' => $parts,
        'hostname' => $host,
        'port' => $port,
        'request' => $out,
        'result' => $contents,
      );
    }
  }
}

Example usage:

ignore_user_abort(TRUE);
 
/**
 * Your time-consuming operations go here.
 */
function perform_heavy_calculations() {}
 
/**
 * Returns the current full URL.
 *
 * Example response: https://example.com/index.php
 */
function current_full_url() {
  return (
    isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on'
    ? 'https' : 'http'
  ) . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
 
if (empty($_POST['background'])) {
  echo file_get_contents('cache.html');
  background_post(current_full_url(), array('background' => TRUE));
  die();
}
 
$results = perform_heavy_calculations();
 
file_put_contents('cache.html', $results);