Working on adding better file IO and a CURL transporter.

This commit is contained in:
Matt Butcher
2012-01-13 13:55:14 -06:00
parent d76c773a1a
commit 40762ea422
3 changed files with 215 additions and 0 deletions

View File

@@ -8,11 +8,137 @@ namespace HPCloud\Transport;
/**
* Provide HTTP transport with CURL.
*
* You should choose the Curl backend if...
*
* - You KNOW Curl support is compiled into your PHP version
* - You do not like the built-in PHP HTTP handler
* - Performance is a big deal to you
* - You will be sending large objects (>2M)
* - Or PHP stream wrappers for URLs are not supported on your system.
*
* CURL is demonstrably faster than the built-in PHP HTTP handling, so
* ths library gives a performance boost. Error reporting is slightly
* better too.
*
* But the real strong point to Curl is that it can take file objects
* and send them over HTTP without having to buffer them into strings
* first. This saves memory and processing.
*
* The only downside to Curl is that it is not available on all hosts.
* Some installations of PHP do not compile support.
*/
class CURLTransport implements Transporter {
public function doRequest($uri, $method = 'GET', $headers = array(), $body = '') {
$in = NULL;
if (!empty($body)) {
// First we turn our body into a temp-backed buffer.
$in = fopen('php://temp', 'wr', FALSE);
fwrite($in, $body, strlen($body));
rewind($in);
}
return $this->handleDoRequest($uri, $method, $headers, $in);
}
public function doRequestWithResource($uri, $method, $headers, $resource) {
if (is_string($resource)) {
$in = open($resource, 'rb', FALSE);
}
else {
$in = $resource;
}
return $this->handleDoRequest($uri, $method, $headers, $resource);
}
/**
* Internal workhorse.
*/
protected function handleDoRequest($uri, $method, $headers, $in = NULL) {
// Write to in-mem handle backed by a temp file.
$out = fopen('php://temp', 'w');
$curl = curl_init($uri);
// Set method
$this->determineMethod($curl, $method);
// Set headers
$this->setHeaders($curl, $headers);
// Set the upload
if (!empty($in)) {
curl_setopt($curl, CURLOPT_INFILE, $in);
}
// Get the output.
curl_setopt($curl, CURLOPT_FILE, $out);
curl_exec($curl);
// Now we need to build a response.
// Option 1: Subclass response.
// Option 2: Build an adapter.
curl_close($curl);
fclose($in);
}
/**
* Set the appropriate constant on the CURL object.
*
* Curl handles method name setting in a slightly counter-intuitive
* way, so we have a special function for setting the method
* correctly. Note that since we do not POST as www-form-*, we
* use a custom post.
*
* @param resource $curl
* A curl object.
* @param string $method
* An HTTP method name.
*/
protected function determineMethod($curl, $method) {
$method = strtoupper($method);
switch ($method) {
case 'GET':
curl_setopt($curl, CURLOPT_HTTPGET, TRUE);
break;
case 'HEAD':
curl_setopt($curl, CURLOPT_NOBODY, TRUE);
break;
// Put is problematic: Some PUT requests might not have
// a body.
case 'PUT':
curl_setopt($curl, CURLOPT_PUT, TRUE);
break;
// We use customrequest for post because we are
// not submitting form data.
case 'POST':
case 'DELETE':
case 'COPY':
default:
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, TRUE);
}
}
public function setHeaders($curl, $headers) {
$buffer = array();
$format = '%s: %s';
foreach ($headers as $name => $value) {
$buffer[] = sprintf($format, $name, $value);
}
curl_setopt($curl, CURLOPT_HTTPHEADER, $buffer);
}
}

View File

@@ -73,6 +73,60 @@ class PHPStreamTransport implements Transporter {
return $response;
}
/**
* Implements Transporter::doRequestWithResource().
*
* Unfortunately, PHP Stream Wrappers do not allow HTTP data to be read
* out of a file resource, so using this method will allow some
* performance improvement (because grabage collection can collect faster),
* but not a lot.
*
* While PHP's underlying architecture should still adequately buffer large
* strings, the effects of this buffering on really large data (5G or so)
* is unknown.
*/
public function doRequestWithResource($uri, $method, $headers, $resource) {
// In a PHP stream there is no way to buffer content for sending.
// XXX: Could we create a class with a __tostring that read data in piecemeal?
// That wouldn't solve the problem, but it might minimize damage.
if (is_string($resource)) {
$in = fopen($resource, 'rb', FALSE);
}
else {
$in = $resource;
}
$body = '';
while (!feof($in)) {
$body .= fread($in, 8192);
}
$cxt = $this->buildStreamContext($method, $headers, $body);
$res = @fopen($uri, 'rb', FALSE, $cxt);
// If there is an error, we try to react
// intelligently.
if ($res === FALSE) {
$err = error_get_last();
if (empty($err['message'])) {
throw new \HPCloud\Exception("An unknown exception occurred while sending a request.");
}
$this->guessError($err['message'], $uri, $method);
// Should not get here.
return;
}
$metadata = stream_get_meta_data($res);
$response = new Response($res, $metadata);
return $response;
}
/**
* Given an error, this tries to guess the cause and throw an exception.
*

View File

@@ -1,5 +1,6 @@
<?php
/**
* @file
* This file contains the interface for transporters.
*/
@@ -38,4 +39,38 @@ interface Transporter {
* The string containing the request body.
*/
public function doRequest($uri, $method = 'GET', $headers = array(), $body = '');
/**
* Perform a request, but use a resource to read the body.
*
* This is a special version of the doRequest() function.
* It handles a very spefic case where...
*
* - The HTTP verb requires a body (viz. PUT, POST)
* - The body is in a resource, not a string
*
* Examples of appropriate cases for this variant:
*
* - Uploading large files.
* - Streaming data out of a stream and into an HTTP request.
* - Minimizing memory usage ($content strings are big).
*
* Note that all parameters are required.
*
* @todo Declare this in Transporter.
*
* @param string $uri
* The target URI.
* @param string $method
* The method to be sent.
* @param array $headers
* An array of name/value header pairs.
* @param mixed $resource
* The string with a file path or a stream URL; or a file object resource.
* If it is a string, then it will be opened with the default context.
* So if you need a special context, you should open the file elsewhere
* and pass the resource in here.
*/
public function doRequestWithResource($uri, $method, $headers, $resource);
}