HPCloud-PHP  1.2.0
PHP bindings for HPCloud and OpenStack services.
 All Classes Namespaces Files Functions Variables Pages
PHPStreamTransport.php
Go to the documentation of this file.
1 <?php
2 /* ============================================================================
3 (c) Copyright 2012 Hewlett-Packard Development Company, L.P.
4 Permission is hereby granted, free of charge, to any person obtaining a copy
5 of this software and associated documentation files (the "Software"), to deal
6 in the Software without restriction, including without limitation the rights to
7 use, copy, modify, merge,publish, distribute, sublicense, and/or sell copies of
8 the Software, and to permit persons to whom the Software is furnished to do so,
9 subject to the following conditions:
10 
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13 
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
21 ============================================================================ */
22 /**
23  * @file
24  * Implements a transporter with the PHP HTTP Stream Wrapper.
25  */
26 
27 namespace HPCloud\Transport;
28 
29 /**
30  * Provide HTTP transport with the PHP HTTP stream wrapper.
31  *
32  * PHP comes with a stream wrapper for HTTP. Actually, it comes with two such
33  * stream wrappers, and the compile-time options determine which is used.
34  * This transporter uses the stream wrapper library to send requests to the
35  * remote host.
36  *
37  * Several properties are declared public, and can be changed to suite your
38  * needs.
39  *
40  * You can use a single PHPStreamTransport object to execute multiple requests.
41  *
42  * @attention This class should not be constructed directly.
43  * Use HPCloud::Transport::instance() to get an instance.
44  *
45  * See HPCloud::Transport and HPCloud::Bootstrap.
46  */
47 class PHPStreamTransport implements Transporter {
48 
49  const HTTP_USER_AGENT_SUFFIX = ' (b2d770) PHP/1.0';
50 
51  /**
52  * The HTTP version this should use.
53  *
54  * By default, this is set to 1.1, which is not PHP's default. We do
55  * this to take advantage of chunked encoding. While this requires PHP
56  * 5.3.0 or greater, this is not viewed as a problem, given that the
57  * entire library requires PHP 5.3.
58  */
59  public $httpVersion = '1.1';
60 
61  /**
62  * The event watcher callback.
63  *
64  */
65  protected $notificationCallback = NULL;
66 
67  public function doRequest($uri, $method = 'GET', $headers = array(), $body = '') {
68  $cxt = $this->buildStreamContext($method, $headers, $body);
69 
70  $res = @fopen($uri, 'rb', FALSE, $cxt);
71 
72  // If there is an error, we try to react
73  // intelligently.
74  if ($res === FALSE) {
75  $err = error_get_last();
76 
77  if (empty($err['message'])) {
78  // FIXME: Under certain circumstances, all this really means is that
79  // there is a 404. So we pretend that it's always a 404.
80  // throw new \HPCloud\Exception("An unknown exception occurred while sending a request.");
81  $msg = "File not found, perhaps due to a network failure.";
82  throw new \HPCloud\Transport\FileNotFoundException($msg);
83  }
84  $this->guessError($err['message'], $uri, $method);
85 
86  // Should not get here.
87  return;
88  }
89 
90  $metadata = stream_get_meta_data($res);
91  if (\HPCloud\Bootstrap::hasConfig('transport.debug')) {
92  $msg = implode(PHP_EOL, $metadata['wrapper_data']);
93  $msg .= sprintf("\nWaiting to read %d bytes.\n", $metadata['unread_bytes']);
94 
95  if (defined('STDOUT')) {
96  fwrite(STDOUT, $msg);
97  }
98  else {
99  print $msg;
100  }
101  }
102 
103  $response = new Response($res, $metadata);
104 
105  return $response;
106  }
107 
108  /**
109  * Implements Transporter::doRequestWithResource().
110  *
111  * Unfortunately, PHP Stream Wrappers do not allow HTTP data to be read
112  * out of a file resource, so using this method will allow some
113  * performance improvement (because grabage collection can collect faster),
114  * but not a lot.
115  *
116  * While PHP's underlying architecture should still adequately buffer large
117  * strings, the effects of this buffering on really large data (5G or so)
118  * is unknown.
119  */
120  public function doRequestWithResource($uri, $method, $headers, $resource) {
121 
122 
123  // In a PHP stream there is no way to buffer content for sending.
124  // XXX: Could we create a class with a __tostring that read data in piecemeal?
125  // That wouldn't solve the problem, but it might minimize damage.
126  if (is_string($resource)) {
127  $in = fopen($resource, 'rb', FALSE);
128  }
129  else {
130  $in = $resource;
131  }
132  $body = '';
133  while (!feof($in)) {
134  $body .= fread($in, 8192);
135  }
136 
137  $cxt = $this->buildStreamContext($method, $headers, $body);
138  $res = @fopen($uri, 'rb', FALSE, $cxt);
139 
140  // If there is an error, we try to react
141  // intelligently.
142  if ($res === FALSE) {
143  $err = error_get_last();
144 
145  if (empty($err['message'])) {
146  throw new \HPCloud\Exception("An unknown exception occurred while sending a request.");
147  }
148  $this->guessError($err['message'], $uri, $method);
149 
150  // Should not get here.
151  return;
152  }
153 
154  $metadata = stream_get_meta_data($res);
155 
156  $response = new Response($res, $metadata);
157 
158  return $response;
159 
160  }
161 
162  /**
163  * Given an error, this tries to guess the cause and throw an exception.
164  *
165  * Stream wrappers do not deal with error conditions gracefully. (For starters,
166  * during an error one cannot access the HTTP headers). The only useful piece
167  * of data given is the contents of the last error buffer.
168  *
169  * This uses the contents of that buffer to attempt to learn what happened
170  * during the request. It then throws an exception that seems appropriate for the
171  * given context.
172  */
173  protected function guessError($err, $uri, $method) {
174 
175  $regex = '/HTTP\/1\.[01]? ([0-9]+) ([ a-zA-Z]+)/';
176  $matches = array();
177  preg_match($regex, $err, $matches);
178 
179  if (count($matches) < 3) {
180  throw new \HPCloud\Exception($err);
181  }
182 
183  Response::failure($matches[1], $matches[0], $uri, $method);
184  }
185 
186  /**
187  * Register an event handler for notifications.
188  * During the course of a transaction, the stream wrapper emits a variety
189  * of notifications. This function can be used to register an event
190  * handler to listen for notifications.
191  *
192  * @param callable $callable
193  * Any callable, including an anonymous function or closure.
194  *
195  * @see http://us3.php.net/manual/en/function.stream-notification-callback.php
196  */
197  public function onNotification(callable $callable) {
198  $this->notificationCallback = $callable;
199  }
200 
201  /**
202  * Given an array of headers, build a header string.
203  *
204  * This builds an HTTP header string in the form required by the HTTP stream
205  * wrapper for PHP.
206  *
207  * @param array $headers
208  * An associative array of header names to header values.
209  * @retval string
210  * @return string
211  * A string containing formatted headers.
212  */
213  protected function smashHeaders($headers) {
214 
215  if (empty($headers)) {
216  return;
217  }
218 
219  $buffer = array();
220  foreach ($headers as $name => $value) {
221  // $buffer[] = sprintf("%s: %s", $name, urlencode($value));
222  $buffer[] = sprintf("%s: %s", $name, $value);
223  }
224  $headerStr = implode("\r\n", $buffer);
225 
226  return $headerStr . "\r\n";
227  }
228 
229  /**
230  * Build the stream context for a request.
231  *
232  * All of the HTTP transport data is passed into PHP's stream wrapper via a
233  * stream context. This builds the context.
234  */
235  protected function buildStreamContext($method, $headers, $body) {
236  // Construct the stream options.
237  $config = array(
238  'http' => array(
239  'protocol_version' => $this->httpVersion,
240  'method' => strtoupper($method),
241  'header' => $this->smashHeaders($headers),
242  'user_agent' => Transporter::HTTP_USER_AGENT . self::HTTP_USER_AGENT_SUFFIX,
243  ),
244  );
245 
246  if (!empty($body)) {
247  $config['http']['content'] = $body;
248  }
249 
250  if (\HPCloud\Bootstrap::hasConfig('transport.timeout')) {
251  $config['http']['timeout'] = (float) \HPCloud\Bootstrap::config('transport.timeout');
252  }
253 
254  // Set the params. (Currently there is only one.)
255  $params = array();
256  if (!empty($this->notificationCallback)) {
257  $params['notification'] = $this->notificationCallback;
258  }
259  // Enable debugging:
260  elseif (\HPCloud\Bootstrap::hasConfig('transport.debug')) {
261  //fwrite(STDOUT, "Sending debug messages to STDOUT\n");
262  $params['notification'] = array($this, 'printNotifications');
263  }
264 
265  // Build the context.
266  $context = stream_context_create($config, $params);
267 
268  return $context;
269  }
270  public function printNotifications($code, $severity, $msg, $msgcode, $bytes, $len) {
271  static $filesize = 'Unknown';
272 
273  switch ($code) {
274  case STREAM_NOTIFY_RESOLVE:
275  $out = sprintf("Resolved. %s\n", $msg);
276  break;
277  case STREAM_NOTIFY_FAILURE:
278  $out = sprintf("socket-level failure: %s\n", $msg);
279  break;
280  case STREAM_NOTIFY_COMPLETED:
281  $out = sprintf("Transaction complete. %s\n", $msg);
282  break;
283  //case STREAM_NOTIFY_REDIRECT:
284  // $out = sprintf("Redirect... %s\n", $msg);
285  // break;
286  case STREAM_NOTIFY_CONNECT:
287  $out = sprintf("Connect... %s\n", $msg);
288  break;
289  case STREAM_NOTIFY_FILE_SIZE_IS:
290  $out = sprintf("Content-length: %d\n", $len);
291  $filesize = $len;
292  break;
293  case STREAM_NOTIFY_MIME_TYPE_IS:
294  $out = sprintf("Content-Type: %s\n", $msg);
295  break;
296  case STREAM_NOTIFY_PROGRESS:
297  $out = sprintf($msg . PHP_EOL);
298  $out .= sprintf("%d bytes of %s\n", $bytes, $filesize);
299  break;
300  default:
301  $out = sprintf("Code: %d, Message: %s\n", $code, $msg);
302  break;
303  }
304 
305  // Circumvent output buffering for PHPUnit.
306  if (defined('STDOUT')) {
307  fwrite(STDOUT, $out);
308  }
309  else {
310  print $out;
311  }
312 
313  }
314 }