From 99836ddc9b9b755457670b4f0cd019a82863a237 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 14:32:30 +1000 Subject: [PATCH 01/33] Add multiple request support This is a first pass and will probably be altered before merging. Still needs more hooks and more tests. Only implemented for cURL so far. Trivia: I wrote this completely without internet access (and hence, no documentation either). Imagine my surprise when it worked first time. --- library/Requests.php | 92 +++++++++++++++++ library/Requests/Transport/cURL.php | 154 +++++++++++++++++++++++++--- 2 files changed, 232 insertions(+), 14 deletions(-) diff --git a/library/Requests.php b/library/Requests.php index 33ccdb819..fe4076c5e 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -340,6 +340,79 @@ public static function request($url, $headers = array(), $data = array(), $type return self::parse_response($response, $url, $headers, $data, $options); } + /** + * Send multiple HTTP requests simultaneously + * + * @param array $requests Requests data (see description for more information) + * @param array $options Global and default options (see {@see Requests::request}) + * @return array Responses (either Requests_Response or a Requests_Exception object) + */ + public static function request_multiple($requests, $options = array()) { + $defaults = array( + 'timeout' => 10, + 'useragent' => 'php-requests/' . self::VERSION, + 'redirected' => 0, + 'redirects' => 10, + 'follow_redirects' => true, + 'blocking' => true, + 'type' => self::GET, + 'filename' => false, + 'auth' => false, + 'idn' => true, + 'hooks' => null, + 'transport' => null, + ); + $options = array_merge($defaults, $options); + + foreach ($requests as $id => &$request) { + if (!isset($request['headers'])) { + $request['headers'] = array(); + } + if (!isset($request['data'])) { + $request['data'] = array(); + } + if (!isset($request['type'])) { + $request['type'] = self::GET; + } + if (!isset($request['options'])) { + $request['options'] = $options; + $request['options']['type'] = $request['type']; + } + else { + $request['options'] = array_merge($options, $request['options']); + } + if (empty($request['options']['hooks'])) { + $request['options']['hooks'] = new Requests_Hooks(); + } + + $request['options']['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); + } + unset($request); + + if (!empty($options['transport'])) { + $transport = $options['transport']; + + if (is_string($options['transport'])) { + $transport = new $transport(); + } + } + else { + $transport = self::get_transport(); + } + $responses = $transport->request_multiple($requests, $options); + + foreach ($responses as $id => &$response) { + // If our hook got messed with somehow, ensure we end up with the + // correct response + if (is_string($response)) { + $request = $requests[$id]; + $response = self::parse_multiple($response, $request); + } + } + + return $responses; + } + /** * HTTP response parser * @@ -431,6 +504,25 @@ protected static function parse_response($headers, $url, $req_headers, $req_data return $return; } + /** + * Callback for `transport.internal.parse_response` + * + * Internal use only. Converts a raw HTTP response to a Requests_Response + * while still executing a multiple request. + * + * @param string $headers Full response text including headers and body + * @param array $request Request data as passed into {@see Requests::request_multiple()} + * @return null `$response` is either set to a Requests_Response instance, or a Requests_Exception object + */ + public static function parse_multiple(&$response, $request) { + try { + $response = self::parse_response($response, $request['url'], $request['headers'], $request['data'], $request['options']); + } + catch (Requests_Exception $e) { + $response = $e; + } + } + /** * Decoded a chunked body as per RFC 2616 * diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index ede7c0564..0384a695e 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -79,6 +79,144 @@ public function __construct() { public function request($url, $headers = array(), $data = array(), $options = array()) { $options['hooks']->dispatch('curl.before_request', array(&$this->fp)); + $this->setup_handle($url, $headers, $data, $options); + + $options['hooks']->dispatch('curl.before_send', array(&$this->fp)); + + if ($options['filename'] !== false) { + $stream_handle = fopen($options['filename'], 'wb'); + curl_setopt($this->fp, CURLOPT_FILE, $stream_handle); + } + + $response = curl_exec($this->fp); + + $options['hooks']->dispatch('curl.after_send', array(&$fake_headers)); + + if (curl_errno($this->fp) === 23 || curl_errno($this->fp) === 61) { + curl_setopt($this->fp, CURLOPT_ENCODING, 'none'); + $response = curl_exec($this->fp); + } + + $this->process_response($response, $options); + + if ($options['blocking'] === false) { + curl_close($this->fp); + $fake_headers = ''; + $options['hooks']->dispatch('curl.after_request', array(&$fake_headers)); + return false; + } + if ($options['filename'] !== false) { + fclose($stream_handle); + $this->headers = trim($this->headers); + } + else { + $this->headers .= $response; + } + + if (curl_errno($this->fp)) { + throw new Requests_Exception('cURL error ' . curl_errno($this->fp) . ': ' . curl_error($this->fp), 'curlerror', $this->fp); + return; + } + $this->info = curl_getinfo($this->fp); + curl_close($this->fp); + + $options['hooks']->dispatch('curl.after_request', array(&$this->headers)); + return $this->headers; + } + + /** + * Send multiple requests simultaneously + * + * @param array $requests Request data + * @param array $options Global options + * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well) + */ + public function request_multiple($requests, $options) { + $multihandle = curl_multi_init(); + $subrequests = array(); + $subhandles = array(); + + $class = get_class($this); + foreach ($requests as $id => $request) { + $subrequests[$id] = new $class(); + $subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']); + curl_multi_add_handle($multihandle, $subhandles[$id]); + } + + $completed = 0; + $responses = array(); + + do { + // blah + $active = false; + + while (($status = curl_multi_exec($multihandle, $active)) === CURLM_CALL_MULTI_PERFORM) { + if (count($handles) > 0) { + break; + } + } + + $to_process = array(); + + while ($done = curl_multi_info_read($multihandle)) { + if ($done['result'] > 0) { + throw Requests_Exception(); + } + elseif (!isset($to_process[(int) $done['handle']])) { + $key = array_search($done['handle'], $subhandles, true); + $to_process[$key] = $done; + } + } + + foreach ($to_process as $key => $done) { + $options = $requests[$key]['options']; + $responses[$key] = $subrequests[$key]->process_response(curl_multi_getcontent($done['handle']), $options); + + $options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key])); + + curl_multi_remove_handle($multihandle, $done['handle']); + curl_close($done['handle']); + + $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key])); + $completed++; + } + } + while ($active || $completed < count($subrequests)); + + curl_multi_close($multihandle); + + return $responses; + } + + /** + * Get the cURL handle for use in a multi-request + * + * @param string $url URL to request + * @param array $headers Associative array of request headers + * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD + * @param array $options Request options, see {@see Requests::response()} for documentation + * @return resource Subrequest's cURL handle + */ + public function &get_subrequest_handle($url, $headers, $data, $options) { + $this->setup_handle($url, $headers, $data, $options); + + if ($options['filename'] !== false) { + $stream_handle = fopen($options['filename'], 'wb'); + curl_setopt($this->fp, CURLOPT_FILE, $stream_handle); + } + + return $this->fp; + } + + /** + * Setup the cURL handle for the given data + * + * @param string $url URL to request + * @param array $headers Associative array of request headers + * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD + * @param array $options Request options, see {@see Requests::response()} for documentation + */ + protected function setup_handle($url, $headers, $data, $options) { $headers = Requests::flattern($headers); if (in_array($options['type'], array(Requests::HEAD, Requests::GET, Requests::DELETE)) & !empty($data)) { $url = self::format_get($url, $data); @@ -112,16 +250,9 @@ public function request($url, $headers = array(), $data = array(), $options = ar if (true === $options['blocking']) { curl_setopt($this->fp, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers')); } + } - $options['hooks']->dispatch('curl.before_send', array(&$this->fp)); - - if ($options['filename'] !== false) { - $stream_handle = fopen($options['filename'], 'wb'); - curl_setopt($this->fp, CURLOPT_FILE, $stream_handle); - } - - $response = curl_exec($this->fp); - + public function &process_response($response, $options) { $options['hooks']->dispatch('curl.after_send', array(&$fake_headers)); if ($options['blocking'] === false) { @@ -138,16 +269,11 @@ public function request($url, $headers = array(), $data = array(), $options = ar $this->headers .= $response; } - if (curl_errno($this->fp) === 23 || curl_errno($this->fp) === 61) { - curl_setopt($this->fp, CURLOPT_ENCODING, 'none'); - $this->headers = curl_exec($this->fp); - } if (curl_errno($this->fp)) { throw new Requests_Exception('cURL error ' . curl_errno($this->fp) . ': ' . curl_error($this->fp), 'curlerror', $this->fp); return; } $this->info = curl_getinfo($this->fp); - curl_close($this->fp); $options['hooks']->dispatch('curl.after_request', array(&$this->headers)); return $this->headers; From fb5e1c22c2f6329c09140106fcc6ccf4b2edc6db Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 14:35:31 +1000 Subject: [PATCH 02/33] Only fire multiple.request.complete once parsed If we end up with a situation where our hook doesn't fire for some reason, we shouldn't pass this on to the hooks. Instead, wait until the end when we ensure they're instances. --- library/Requests.php | 1 + library/Requests/Transport/cURL.php | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/library/Requests.php b/library/Requests.php index fe4076c5e..5561326d2 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -407,6 +407,7 @@ public static function request_multiple($requests, $options = array()) { if (is_string($response)) { $request = $requests[$id]; $response = self::parse_multiple($response, $request); + $request['options']['hooks']->dispatch('multiple.request.complete', array(&$response)); } } diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index 0384a695e..3cb67e29a 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -177,7 +177,9 @@ public function request_multiple($requests, $options) { curl_multi_remove_handle($multihandle, $done['handle']); curl_close($done['handle']); - $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key])); + if (!is_string($responses[$key])) { + $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key])); + } $completed++; } } From 66235e583575584d237a525ae7c770cf3192dcf5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 14:39:17 +1000 Subject: [PATCH 03/33] Add request_multiple to the Transport interface --- library/Requests/Transport.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/library/Requests/Transport.php b/library/Requests/Transport.php index 9948cb809..7e4a26293 100755 --- a/library/Requests/Transport.php +++ b/library/Requests/Transport.php @@ -24,6 +24,15 @@ interface Requests_Transport { */ public function request($url, $headers = array(), $data = array(), $options = array()); + /** + * Send multiple requests simultaneously + * + * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request} + * @param array $options Global options, see {@see Requests::response()} for documentation + * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well) + */ + public function request_multiple($requests, $options); + /** * Self-test whether the transport can be used * @return bool From bd4816f269ae5455631d7ce84fc68f3db0d69c77 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 14:43:57 +1000 Subject: [PATCH 04/33] Add multiple request support to Transport_fsockopen fsockopen doesn't support multiple simultaneous requests, so we just fake it here instead. What it actually does is to send every request sequentially, so the result back from Requests::request_multiple() will be the same. --- library/Requests/Transport/fsockopen.php | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/library/Requests/Transport/fsockopen.php b/library/Requests/Transport/fsockopen.php index 8a2cd252b..649ecb438 100755 --- a/library/Requests/Transport/fsockopen.php +++ b/library/Requests/Transport/fsockopen.php @@ -165,6 +165,35 @@ public function request($url, $headers = array(), $data = array(), $options = ar return $this->headers; } + /** + * Send multiple requests simultaneously + * + * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request} + * @param array $options Global options, see {@see Requests::response()} for documentation + * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well) + */ + public function request_multiple($requests, $options) { + $responses = array(); + $class = get_class($this); + foreach ($requests as $id => $request) { + try { + $handler = new $class(); + $responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']); + + $request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request)); + } + catch (Requests_Exception $e) { + $responses[$id] = $e; + } + + if (!is_string($responses[$id])) { + $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key])); + } + } + + return $responses; + } + /** * Retrieve the encodings we can accept * From 4b0199e64eb1f17e59263ff5288815c1dea77b50 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 14:49:52 +1000 Subject: [PATCH 05/33] Add more hooks for cURL multiple requests --- library/Requests/Transport/cURL.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index 3cb67e29a..a30b3d160 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -140,14 +140,16 @@ public function request_multiple($requests, $options) { foreach ($requests as $id => $request) { $subrequests[$id] = new $class(); $subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']); + $request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id])); curl_multi_add_handle($multihandle, $subhandles[$id]); } $completed = 0; $responses = array(); + $request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle)); + do { - // blah $active = false; while (($status = curl_multi_exec($multihandle, $active)) === CURLM_CALL_MULTI_PERFORM) { @@ -158,6 +160,7 @@ public function request_multiple($requests, $options) { $to_process = array(); + // Read the information as needed while ($done = curl_multi_info_read($multihandle)) { if ($done['result'] > 0) { throw Requests_Exception(); @@ -168,6 +171,7 @@ public function request_multiple($requests, $options) { } } + // Parse the finished requests before we start getting the new ones foreach ($to_process as $key => $done) { $options = $requests[$key]['options']; $responses[$key] = $subrequests[$key]->process_response(curl_multi_getcontent($done['handle']), $options); @@ -185,6 +189,8 @@ public function request_multiple($requests, $options) { } while ($active || $completed < count($subrequests)); + $request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle)); + curl_multi_close($multihandle); return $responses; From fe5e7eda8c506ea52c1f28b31a235a36b6eea4c1 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 14:52:29 +1000 Subject: [PATCH 06/33] Remove redundant parsing for single cURL requests --- library/Requests/Transport/cURL.php | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index a30b3d160..6c8b9dd6d 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -98,29 +98,6 @@ public function request($url, $headers = array(), $data = array(), $options = ar } $this->process_response($response, $options); - - if ($options['blocking'] === false) { - curl_close($this->fp); - $fake_headers = ''; - $options['hooks']->dispatch('curl.after_request', array(&$fake_headers)); - return false; - } - if ($options['filename'] !== false) { - fclose($stream_handle); - $this->headers = trim($this->headers); - } - else { - $this->headers .= $response; - } - - if (curl_errno($this->fp)) { - throw new Requests_Exception('cURL error ' . curl_errno($this->fp) . ': ' . curl_error($this->fp), 'curlerror', $this->fp); - return; - } - $this->info = curl_getinfo($this->fp); - curl_close($this->fp); - - $options['hooks']->dispatch('curl.after_request', array(&$this->headers)); return $this->headers; } From ddcff9acdbd927179fe3541e711843b95a550413 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 14:53:49 +1000 Subject: [PATCH 07/33] Change cURL hooks around a bit curl.before_request and curl.after_request are now fired on every cURL request. curl.before_send and curl.after_send are for single requests, while curl.before_multi_add, curl.before_multi_exec and curl.after_multi_exec are for multiple requests. --- library/Requests/Transport/cURL.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index 6c8b9dd6d..d543a6059 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -77,8 +77,6 @@ public function __construct() { * @return string Raw HTTP result */ public function request($url, $headers = array(), $data = array(), $options = array()) { - $options['hooks']->dispatch('curl.before_request', array(&$this->fp)); - $this->setup_handle($url, $headers, $data, $options); $options['hooks']->dispatch('curl.before_send', array(&$this->fp)); @@ -202,6 +200,8 @@ public function &get_subrequest_handle($url, $headers, $data, $options) { * @param array $options Request options, see {@see Requests::response()} for documentation */ protected function setup_handle($url, $headers, $data, $options) { + $options['hooks']->dispatch('curl.before_request', array(&$this->fp)); + $headers = Requests::flattern($headers); if (in_array($options['type'], array(Requests::HEAD, Requests::GET, Requests::DELETE)) & !empty($data)) { $url = self::format_get($url, $data); @@ -238,8 +238,6 @@ protected function setup_handle($url, $headers, $data, $options) { } public function &process_response($response, $options) { - $options['hooks']->dispatch('curl.after_send', array(&$fake_headers)); - if ($options['blocking'] === false) { curl_close($this->fp); $fake_headers = ''; From a2037d6b6e22ffb11dc1c6851eef1c96d17b9867 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 15:03:45 +1000 Subject: [PATCH 08/33] Avoid double-checking for cURL errors curl_multi_info_read()'s 'result' value is the same as curl_errno(), which we handle per-request in a nicer way. --- library/Requests/Transport/cURL.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index d543a6059..e27fcc467 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -137,10 +137,7 @@ public function request_multiple($requests, $options) { // Read the information as needed while ($done = curl_multi_info_read($multihandle)) { - if ($done['result'] > 0) { - throw Requests_Exception(); - } - elseif (!isset($to_process[(int) $done['handle']])) { + if (!isset($to_process[(int) $done['handle']])) { $key = array_search($done['handle'], $subhandles, true); $to_process[$key] = $done; } From 80efd61d2fbe52f4526cddd86a1f8011c756c385 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 15:05:25 +1000 Subject: [PATCH 09/33] Stick to a single shared key for all cURL data Rather than relying on typecasting the cURL resource to an integer to form a key, use the proper key instead. --- library/Requests/Transport/cURL.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index e27fcc467..2f95b32d1 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -137,8 +137,8 @@ public function request_multiple($requests, $options) { // Read the information as needed while ($done = curl_multi_info_read($multihandle)) { - if (!isset($to_process[(int) $done['handle']])) { - $key = array_search($done['handle'], $subhandles, true); + $key = array_search($done['handle'], $subhandles, true); + if (!isset($to_process[$key])) { $to_process[$key] = $done; } } From 277ee6606714aaebdec979fbfe3b6467f413c962 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 15:22:46 +1000 Subject: [PATCH 10/33] Fix spelling error in fsockopen --- library/Requests/Transport/fsockopen.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Requests/Transport/fsockopen.php b/library/Requests/Transport/fsockopen.php index 649ecb438..8952819e7 100755 --- a/library/Requests/Transport/fsockopen.php +++ b/library/Requests/Transport/fsockopen.php @@ -187,7 +187,7 @@ public function request_multiple($requests, $options) { } if (!is_string($responses[$id])) { - $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key])); + $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$id])); } } From f39a48087d8e8f0712f85368e3743173f86c49db Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 15:23:28 +1000 Subject: [PATCH 11/33] Only hook the internal parser in once per handler --- library/Requests.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/Requests.php b/library/Requests.php index 5561326d2..468e1cddb 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -364,6 +364,10 @@ public static function request_multiple($requests, $options = array()) { ); $options = array_merge($defaults, $options); + if (!empty($options['hooks'])) { + $options['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); + } + foreach ($requests as $id => &$request) { if (!isset($request['headers'])) { $request['headers'] = array(); @@ -385,7 +389,10 @@ public static function request_multiple($requests, $options = array()) { $request['options']['hooks'] = new Requests_Hooks(); } - $request['options']['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); + // Ensure we only hook in once + if ($request['options']['hooks'] !== $options['hooks']) { + $request['options']['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); + } } unset($request); From f9250138ab8fe0eb3a0323941a9fae83b805f83b Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 15:24:36 +1000 Subject: [PATCH 12/33] Pass the ID into the multiple request callback To make it easier to track a request after it's complete, we pass in the original identifier along with the response. --- library/Requests.php | 2 +- library/Requests/Transport/cURL.php | 2 +- library/Requests/Transport/fsockopen.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/Requests.php b/library/Requests.php index 468e1cddb..2ab6c663f 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -414,7 +414,7 @@ public static function request_multiple($requests, $options = array()) { if (is_string($response)) { $request = $requests[$id]; $response = self::parse_multiple($response, $request); - $request['options']['hooks']->dispatch('multiple.request.complete', array(&$response)); + $request['options']['hooks']->dispatch('multiple.request.complete', array(&$response, $id)); } } diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index 2f95b32d1..9677d85cd 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -154,7 +154,7 @@ public function request_multiple($requests, $options) { curl_close($done['handle']); if (!is_string($responses[$key])) { - $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key])); + $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key)); } $completed++; } diff --git a/library/Requests/Transport/fsockopen.php b/library/Requests/Transport/fsockopen.php index 8952819e7..69acc6d9b 100755 --- a/library/Requests/Transport/fsockopen.php +++ b/library/Requests/Transport/fsockopen.php @@ -187,7 +187,7 @@ public function request_multiple($requests, $options) { } if (!is_string($responses[$id])) { - $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$id])); + $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id)); } } From d4825a09e75284a43baa709a771650cdde35bfbd Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 15:44:36 +1000 Subject: [PATCH 13/33] Ensure multi defaults match normal defaults This should all be abstracted away. --- library/Requests.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/library/Requests.php b/library/Requests.php index 2ab6c663f..1f1aa32d3 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -393,6 +393,18 @@ public static function request_multiple($requests, $options = array()) { if ($request['options']['hooks'] !== $options['hooks']) { $request['options']['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); } + + if (is_array($request['options']['auth'])) { + $request['options']['auth'] = new Requests_Auth_Basic($request['options']['auth']); + } + if ($request['options']['auth'] !== false) { + $request['options']['auth']->register($request['options']['hooks']); + } + if ($request['options']['idn'] !== false) { + $iri = new Requests_IRI($request['url']); + $iri->host = Requests_IDNAEncoder::encode($iri->ihost); + $request['url'] = $iri->uri; + } } unset($request); From aca20ac9389c6312e60349117f555ef8de914530 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 15:45:23 +1000 Subject: [PATCH 14/33] Document the parameters to `Requests::request_multiple()` --- library/Requests.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/library/Requests.php b/library/Requests.php index 1f1aa32d3..74c1ad79c 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -343,6 +343,33 @@ public static function request($url, $headers = array(), $data = array(), $type /** * Send multiple HTTP requests simultaneously * + * The `$requests` parameter takes an associative or indexed array of + * request fields. The key of each request can be used to match up the + * request with the returned data, or with the request passed into your + * `multiple.request.complete` callback. + * + * The request fields value is an associative array with the following keys: + * + * - `url`: Request URL Same as the `$url` parameter to + * {@see Requests::request} + * (string, required) + * - `headers`: Associative array of header fields. Same as the `$headers` + * parameter to {@see Requests::request} + * (array, default: `array()`) + * - `data`: Associative array of data fields or a string. Same as the + * `$data` parameter to {@see Requests::request} + * (array|string, default: `array()`) + * - `type`: HTTP request type (use Requests constants). Same as the `$type` + * parameter to {@see Requests::request} + * (string, default: `Requests::GET`) + * - `data`: Associative array of options. Same as the `$options` parameter + * to {@see Requests::request} + * (array, default: see {@see Requests::request}) + * + * If the `$options` parameter is specified, individual requests will + * inherit options from it. This can be used to use a single hooking system, + * or set all the types to `Requests::POST`, for example. + * * @param array $requests Requests data (see description for more information) * @param array $options Global and default options (see {@see Requests::request}) * @return array Responses (either Requests_Response or a Requests_Exception object) From d7e68b3e28077d152dac755bfb9c7be8803a2b67 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 16:01:06 +1000 Subject: [PATCH 15/33] Allow easy hooking into multiple.request.complete This is a bit of duplication (given that you can just make your own Requests_Hooks and add it), but it saves a few extra lines on the user's end. --- library/Requests.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/library/Requests.php b/library/Requests.php index 74c1ad79c..6807a3002 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -370,6 +370,14 @@ public static function request($url, $headers = array(), $data = array(), $type * inherit options from it. This can be used to use a single hooking system, * or set all the types to `Requests::POST`, for example. * + * In addition, the `$options` parameter takes the following global options: + * + * - `complete`: A callback for when a request is complete. Takes two + * parameters, a Requests_Response/Requests_Exception reference, and the + * ID from the request array (Note: this can also be overriden on a + * per-request basis, although that's a little silly) + * (callback) + * * @param array $requests Requests data (see description for more information) * @param array $options Global and default options (see {@see Requests::request}) * @return array Responses (either Requests_Response or a Requests_Exception object) @@ -388,11 +396,15 @@ public static function request_multiple($requests, $options = array()) { 'idn' => true, 'hooks' => null, 'transport' => null, + 'complete' => null, ); $options = array_merge($defaults, $options); if (!empty($options['hooks'])) { $options['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); + if (!empty($options['complete'])) { + $options['hooks']->register('multiple.request.complete', $options['complete']); + } } foreach ($requests as $id => &$request) { @@ -419,6 +431,9 @@ public static function request_multiple($requests, $options = array()) { // Ensure we only hook in once if ($request['options']['hooks'] !== $options['hooks']) { $request['options']['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); + if (!empty($request['options']['complete'])) { + $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']); + } } if (is_array($request['options']['auth'])) { From f8b795d39239b16f6eaac121f10cc69ab2628463 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 16:04:14 +1000 Subject: [PATCH 16/33] Add example for multiple requesting --- examples/multiple.php | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100755 examples/multiple.php diff --git a/examples/multiple.php b/examples/multiple.php new file mode 100755 index 000000000..69757c3b1 --- /dev/null +++ b/examples/multiple.php @@ -0,0 +1,45 @@ + 'http://httpbin.org/get', + 'headers' => array('Accept' => 'application/javascript'), + ), + 'post' => array( + 'url' => 'http://httpbin.org/post', + 'data' => array('mydata' => 'something'), + ), + 'delayed' => array( + 'url' => 'http://httpbin.org/delay/10', + 'options' => array( + 'timeout' => 20, + ), + ), +); + +// Setup a callback +function my_callback(&$request, $id) { + var_dump($id, $request); +} + +// Tell Requests to use the callback +$options = array( + 'complete' => 'my_callback', +); + +// Send the request! +$responses = Requests::request_multiple($requests, $options); + +// Note: the response from the above call will be an associative array matching +// $requests with the response data, however we've already handled it in +// my_callback() anyway! +// +// If you don't believe me, uncomment this: +# var_dump($responses); \ No newline at end of file From cbdd44247d1030694f76a142c6aeca8d433daf2a Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 16:41:37 +1000 Subject: [PATCH 17/33] Add request_multiple to MockTransport --- tests/bootstrap.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 7ba36047c..1945f8991 100755 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -80,6 +80,25 @@ public function request($url, $headers = array(), $data = array(), $options = ar return $response; } + public function request_multiple($requests, $options) { + $responses = array(); + foreach ($requests as $id => $request) { + $handler = new MockTransport(); + $handler->code = $request['options']['mock.code']; + $handler->chunked = $request['options']['mock.chunked']; + $handler->body = $request['options']['mock.body']; + $handler->raw_headers = $request['options']['mock.raw_headers']; + $responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']); + + if (!empty($options['mock.parse'])) { + $request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request)); + $request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id)); + } + } + + return $responses; + } + public static function test() { return true; } From bacf847b98d6ddafae7f0c95f12bbb32e36b2d1a Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 16:45:22 +1000 Subject: [PATCH 18/33] Add fake request_multiple for RawTransport too --- tests/bootstrap.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 1945f8991..e89cc136b 100755 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -109,6 +109,15 @@ class RawTransport implements Requests_Transport { public function request($url, $headers = array(), $data = array(), $options = array()) { return $this->data; } + public function request_multiple($requests, $options) { + foreach ($requests as $id => &$request) { + $handler = new RawTransport(); + $handler->data = $request['options']['raw.data']; + $request = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']); + } + + return $requests; + } public static function test() { return true; } From f81b9f1803b7cf22d70c2a38cbbeaf5c219bcdb3 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 16:59:12 +1000 Subject: [PATCH 19/33] Ensure streaming files works properly This should fix the currently broken unit tests. Yay! --- library/Requests/Transport/cURL.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index 9677d85cd..516036f5e 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -48,6 +48,13 @@ class Requests_Transport_cURL implements Requests_Transport { */ protected $done_headers = false; + /** + * If streaming to a file, keep the file pointer + * + * @var resource + */ + protected $stream_handle; + /** * Constructor */ @@ -82,8 +89,8 @@ public function request($url, $headers = array(), $data = array(), $options = ar $options['hooks']->dispatch('curl.before_send', array(&$this->fp)); if ($options['filename'] !== false) { - $stream_handle = fopen($options['filename'], 'wb'); - curl_setopt($this->fp, CURLOPT_FILE, $stream_handle); + $this->stream_handle = fopen($options['filename'], 'wb'); + curl_setopt($this->fp, CURLOPT_FILE, $this->stream_handle); } $response = curl_exec($this->fp); @@ -181,8 +188,8 @@ public function &get_subrequest_handle($url, $headers, $data, $options) { $this->setup_handle($url, $headers, $data, $options); if ($options['filename'] !== false) { - $stream_handle = fopen($options['filename'], 'wb'); - curl_setopt($this->fp, CURLOPT_FILE, $stream_handle); + $this->stream_handle = fopen($options['filename'], 'wb'); + curl_setopt($this->fp, CURLOPT_FILE, $this->stream_handle); } return $this->fp; @@ -242,7 +249,7 @@ public function &process_response($response, $options) { return false; } if ($options['filename'] !== false) { - fclose($stream_handle); + fclose($this->stream_handle); $this->headers = trim($this->headers); } else { From df5c6485d29c0f7452a8f4b711e50481b5b026b0 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 17:04:05 +1000 Subject: [PATCH 20/33] Don't return cURL result by reference If we're in non-blocking mode, there's no reference to return. We're not really saving anything with this anyway. --- library/Requests/Transport/cURL.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index 516036f5e..deca91ea3 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -241,7 +241,7 @@ protected function setup_handle($url, $headers, $data, $options) { } } - public function &process_response($response, $options) { + public function process_response($response, $options) { if ($options['blocking'] === false) { curl_close($this->fp); $fake_headers = ''; From 9a4e351d7380387bc5787d32e094a9faea0f7348 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 17:23:54 +1000 Subject: [PATCH 21/33] Move defaulting code to a common method This is currently lacking documentation. Coming soon. --- library/Requests.php | 111 +++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 63 deletions(-) diff --git a/library/Requests.php b/library/Requests.php index 37d374388..3709ec5e3 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -287,42 +287,12 @@ public static function request($url, $headers = array(), $data = array(), $type if (!preg_match('/^http(s)?:\/\//i', $url)) { throw new Requests_Exception('Only HTTP requests are handled.', 'nonhttp', $url); } - $defaults = array( - 'timeout' => 10, - 'useragent' => 'php-requests/' . self::VERSION, - 'redirected' => 0, - 'redirects' => 10, - 'follow_redirects' => true, - 'blocking' => true, - 'type' => $type, - 'filename' => false, - 'auth' => false, - 'idn' => true, - 'hooks' => null, - 'transport' => null, - ); - $options = array_merge($defaults, $options); - - if (empty($options['hooks'])) { - $options['hooks'] = new Requests_Hooks(); - } + $options = array_merge(self::get_default_options(), $options); - // Special case for simple basic auth - if (is_array($options['auth'])) { - $options['auth'] = new Requests_Auth_Basic($options['auth']); - } - if ($options['auth'] !== false) { - $options['auth']->register($options['hooks']); - } + self::set_defaults($url, $headers, $data, $type, $options); $options['hooks']->dispatch('requests.before_request', array(&$url, &$headers, &$data, &$type, &$options)); - if ($options['idn'] !== false) { - $iri = new Requests_IRI($url); - $iri->host = Requests_IDNAEncoder::encode($iri->ihost); - $url = $iri->uri; - } - if (!empty($options['transport'])) { $transport = $options['transport']; @@ -383,22 +353,7 @@ public static function request($url, $headers = array(), $data = array(), $type * @return array Responses (either Requests_Response or a Requests_Exception object) */ public static function request_multiple($requests, $options = array()) { - $defaults = array( - 'timeout' => 10, - 'useragent' => 'php-requests/' . self::VERSION, - 'redirected' => 0, - 'redirects' => 10, - 'follow_redirects' => true, - 'blocking' => true, - 'type' => self::GET, - 'filename' => false, - 'auth' => false, - 'idn' => true, - 'hooks' => null, - 'transport' => null, - 'complete' => null, - ); - $options = array_merge($defaults, $options); + $options = array_merge(self::get_default_options(true), $options); if (!empty($options['hooks'])) { $options['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); @@ -424,9 +379,8 @@ public static function request_multiple($requests, $options = array()) { else { $request['options'] = array_merge($options, $request['options']); } - if (empty($request['options']['hooks'])) { - $request['options']['hooks'] = new Requests_Hooks(); - } + + self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']); // Ensure we only hook in once if ($request['options']['hooks'] !== $options['hooks']) { @@ -435,18 +389,6 @@ public static function request_multiple($requests, $options = array()) { $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']); } } - - if (is_array($request['options']['auth'])) { - $request['options']['auth'] = new Requests_Auth_Basic($request['options']['auth']); - } - if ($request['options']['auth'] !== false) { - $request['options']['auth']->register($request['options']['hooks']); - } - if ($request['options']['idn'] !== false) { - $iri = new Requests_IRI($request['url']); - $iri->host = Requests_IDNAEncoder::encode($iri->ihost); - $request['url'] = $iri->uri; - } } unset($request); @@ -475,6 +417,49 @@ public static function request_multiple($requests, $options = array()) { return $responses; } + protected static function get_default_options($multirequest = false) { + $defaults = array( + 'timeout' => 10, + 'useragent' => 'php-requests/' . self::VERSION, + 'redirected' => 0, + 'redirects' => 10, + 'follow_redirects' => true, + 'blocking' => true, + 'type' => self::GET, + 'filename' => false, + 'auth' => false, + 'idn' => true, + 'hooks' => null, + 'transport' => null, + 'complete' => null, + ); + if ($multirequest !== false) { + $defaults['complete'] = null; + } + return $defaults; + } + + protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { + if (empty($options['hooks'])) { + $options['hooks'] = new Requests_Hooks(); + } + + if (is_array($options['auth'])) { + $options['auth'] = new Requests_Auth_Basic($options['auth']); + } + if ($options['auth'] !== false) { + $options['auth']->register($options['hooks']); + } + + if ($options['idn'] !== false) { + $iri = new Requests_IRI($url); + $iri->host = Requests_IDNAEncoder::encode($iri->ihost); + $url = $iri->uri; + } + + return $options; + } + /** * HTTP response parser * From b04ac1b8c2263713090e0c2aec5449823c3aa4eb Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 17:27:14 +1000 Subject: [PATCH 22/33] Add documentation for new methods --- library/Requests.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/library/Requests.php b/library/Requests.php index 3709ec5e3..c1bd7a7aa 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -417,6 +417,13 @@ public static function request_multiple($requests, $options = array()) { return $responses; } + /** + * Get the default options + * + * @see Requests::request() for values returned by this method + * @param boolean $multirequest Is this a multirequest? + * @return array Default option values + */ protected static function get_default_options($multirequest = false) { $defaults = array( 'timeout' => 10, @@ -439,6 +446,16 @@ protected static function get_default_options($multirequest = false) { return $defaults; } + /** + * Set the default values + * + * @param string $url URL to request + * @param array $headers Extra headers to send with the request + * @param array $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests + * @param string $type HTTP request type + * @param array $options Options for the request + * @return array $options + */ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { if (empty($options['hooks'])) { $options['hooks'] = new Requests_Hooks(); From ab535ff58137e108dfa9fd2e3ad596037d6078b7 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Tue, 3 Jul 2012 17:27:45 +1000 Subject: [PATCH 23/33] Remove redundant code --- library/Requests.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/Requests.php b/library/Requests.php index c1bd7a7aa..764931188 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -438,7 +438,6 @@ protected static function get_default_options($multirequest = false) { 'idn' => true, 'hooks' => null, 'transport' => null, - 'complete' => null, ); if ($multirequest !== false) { $defaults['complete'] = null; @@ -473,8 +472,6 @@ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$optio $iri->host = Requests_IDNAEncoder::encode($iri->ihost); $url = $iri->uri; } - - return $options; } /** From 6d142203def26b2095062d27b5dd2d9a5747ac18 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 4 Jul 2012 11:47:50 +1000 Subject: [PATCH 24/33] Avoid using double-quoted strings for concatenation --- library/Requests/Transport/fsockopen.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Requests/Transport/fsockopen.php b/library/Requests/Transport/fsockopen.php index 69acc6d9b..0844f9310 100755 --- a/library/Requests/Transport/fsockopen.php +++ b/library/Requests/Transport/fsockopen.php @@ -231,7 +231,7 @@ protected static function format_get($url_parts, $data) { } if (isset($url_parts['path'])) { if (isset($url_parts['query'])) { - $get = "$url_parts[path]?$url_parts[query]"; + $get = $url_parts['path'] . '?' . $url_parts['query']; } else { $get = $url_parts['path']; From b4b6b675ce3059174ae364e712aa7ef3b722b1e4 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 4 Jul 2012 13:00:12 +1000 Subject: [PATCH 25/33] Ensure the type is always set correctly --- library/Requests.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/Requests.php b/library/Requests.php index 764931188..439ec184f 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -287,6 +287,9 @@ public static function request($url, $headers = array(), $data = array(), $type if (!preg_match('/^http(s)?:\/\//i', $url)) { throw new Requests_Exception('Only HTTP requests are handled.', 'nonhttp', $url); } + if (empty($options['type'])) { + $options['type'] = $type; + } $options = array_merge(self::get_default_options(), $options); self::set_defaults($url, $headers, $data, $type, $options); From a112b3f664c58aa8ecb7c17d184fc70b02350bae Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Wed, 4 Jul 2012 23:46:26 +1000 Subject: [PATCH 26/33] Add multirequest tests --- tests/Transport/Base.php | 71 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/Transport/Base.php b/tests/Transport/Base.php index 59b63a847..2376e498c 100755 --- a/tests/Transport/Base.php +++ b/tests/Transport/Base.php @@ -341,4 +341,75 @@ public function testTimeout() { $request = Requests::get('http://httpbin.org/delay/10', array(), $this->getOptions($options)); var_dump($request); } + + public function testMultiple() { + $requests = array( + 'test1' => array( + 'url' => 'http://httpbin.org/get' + ), + 'test2' => array( + 'url' => 'http://httpbin.org/get' + ), + ); + $responses = Requests::request_multiple($requests, $this->getOptions()); + + // test1 + $this->assertNotEmpty($responses['test1']); + $this->assertInstanceOf('Requests_Response', $responses['test1']); + $this->assertEquals(200, $responses['test1']->status_code); + + $result = json_decode($responses['test1']->body, true); + $this->assertEquals('http://httpbin.org/get', $result['url']); + $this->assertEmpty($result['args']); + + // test2 + $this->assertNotEmpty($responses['test1']); + $this->assertInstanceOf('Requests_Response', $responses['test1']); + $this->assertEquals(200, $responses['test1']->status_code); + + $result = json_decode($responses['test1']->body, true); + $this->assertEquals('http://httpbin.org/get', $result['url']); + $this->assertEmpty($result['args']); + } + + public function testMultipleWithDifferingMethods() { + $requests = array( + 'get' => array( + 'url' => 'http://httpbin.org/get', + ), + 'post' => array( + 'url' => 'http://httpbin.org/post', + 'data' => 'test', + ), + ); + $responses = Requests::request_multiple($requests, $this->getOptions()); + + // get + $this->assertEquals(200, $responses['get']->status_code); + + // post + $this->assertEquals(200, $responses['post']->status_code); + $result = json_decode($responses['post']->body, true); + $this->assertEquals('test', $result['data']); + } + + /** + * @depends testTimeout + */ + public function testMultipleWithFailure() { + $requests = array( + 'success' => array( + 'url' => 'http://httpbin.org/get', + ), + 'timeout' => array( + 'url' => 'http://httpbin.org/delay/10', + 'options' => array( + 'timeout' => 1, + ), + ), + ); + $responses = Requests::request_multiple($requests, $this->getOptions()); + $this->assertEquals(200, $responses['success']->status_code); + $this->assertInstanceOf('Requests_Exception', $responses['timeout']); + } } \ No newline at end of file From 3886af08861f10f13fb414f0eedc7290695ee331 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 5 Jul 2012 14:17:38 +1000 Subject: [PATCH 27/33] Ensure fsockopen uses the correct hooking system multiple.request.complete is called on the request's hooking system, not the global one. --- library/Requests/Transport/fsockopen.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Requests/Transport/fsockopen.php b/library/Requests/Transport/fsockopen.php index 0844f9310..e41e776f7 100755 --- a/library/Requests/Transport/fsockopen.php +++ b/library/Requests/Transport/fsockopen.php @@ -187,7 +187,7 @@ public function request_multiple($requests, $options) { } if (!is_string($responses[$id])) { - $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id)); + $request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id)); } } From bbcded4686088faa25ffc2e3cb0c9b063c604638 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 6 Jul 2012 20:17:14 +1000 Subject: [PATCH 28/33] Ensure we set the correct HTTP type in tests --- tests/Transport/Base.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Transport/Base.php b/tests/Transport/Base.php index 2376e498c..886855b9d 100755 --- a/tests/Transport/Base.php +++ b/tests/Transport/Base.php @@ -379,6 +379,7 @@ public function testMultipleWithDifferingMethods() { ), 'post' => array( 'url' => 'http://httpbin.org/post', + 'type' => Requests::POST, 'data' => 'test', ), ); From 570d675f0b21f0f2944c7069ecadab92fd2582eb Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 25 Oct 2012 17:36:38 +1000 Subject: [PATCH 29/33] Test the per-request complete callback --- tests/Transport/Base.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Transport/Base.php b/tests/Transport/Base.php index 886855b9d..b8d5201f7 100755 --- a/tests/Transport/Base.php +++ b/tests/Transport/Base.php @@ -413,4 +413,26 @@ public function testMultipleWithFailure() { $this->assertEquals(200, $responses['success']->status_code); $this->assertInstanceOf('Requests_Exception', $responses['timeout']); } + + public function testMultipleUsingCallback() { + $requests = array( + 'get' => array( + 'url' => 'http://httpbin.org/get', + ), + 'post' => array( + 'url' => 'http://httpbin.org/post', + 'type' => Requests::POST, + 'data' => 'test', + ), + ); + $results = array(); + $options = array( + 'complete' => function ($response, $key) use (&$results) { + $results[$key] = $response; + } + ); + $responses = Requests::request_multiple($requests, $this->getOptions($options)); + + $this->assertEquals($results, $responses); + } } \ No newline at end of file From 2b4ce1db87aa24fee2ad6ff32bcc4f4cc273be28 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 25 Oct 2012 20:25:15 +1000 Subject: [PATCH 30/33] Test failures in multiple requests too Also, move the callback to a proper method. Fixes 5.2 compatibility as well as avoiding duplication. --- tests/Transport/Base.php | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/Transport/Base.php b/tests/Transport/Base.php index b8d5201f7..0b2ba7ffb 100755 --- a/tests/Transport/Base.php +++ b/tests/Transport/Base.php @@ -425,14 +425,39 @@ public function testMultipleUsingCallback() { 'data' => 'test', ), ); - $results = array(); + $this->completed = array(); $options = array( - 'complete' => function ($response, $key) use (&$results) { - $results[$key] = $response; - } + 'complete' => array($this, 'completeCallback'), + ); + $responses = Requests::request_multiple($requests, $this->getOptions($options)); + + $this->assertEquals($this->completed, $responses); + $this->completed = array(); + } + + public function testMultipleUsingCallbackAndFailure() { + $requests = array( + 'success' => array( + 'url' => 'http://httpbin.org/get', + ), + 'timeout' => array( + 'url' => 'http://httpbin.org/delay/10', + 'options' => array( + 'timeout' => 1, + ), + ), + ); + $this->completed = array(); + $options = array( + 'complete' => array($this, 'completeCallback'), ); $responses = Requests::request_multiple($requests, $this->getOptions($options)); - $this->assertEquals($results, $responses); + $this->assertEquals($this->completed, $responses); + $this->completed = array(); + } + + public function completeCallback($response, $key) { + $this->completed[$key] = $response; } } \ No newline at end of file From f8d3aa64719c44d51883ac290a1e4110069c6d3e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 25 Oct 2012 22:10:16 +1000 Subject: [PATCH 31/33] Rewrite the multirequest loop Previously, this checked a nonexistent variable, and I'm not sure why. If it ever needs to be fixed, blame me. --- library/Requests/Transport/cURL.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index deca91ea3..21b031e08 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -134,11 +134,10 @@ public function request_multiple($requests, $options) { do { $active = false; - while (($status = curl_multi_exec($multihandle, $active)) === CURLM_CALL_MULTI_PERFORM) { - if (count($handles) > 0) { - break; - } + do { + $status = curl_multi_exec($multihandle, $active); } + while ($status === CURLM_CALL_MULTI_PERFORM); $to_process = array(); From 937c748d8a1acef4f50c7e87883f004e44d2dcd5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 25 Oct 2012 22:28:03 +1000 Subject: [PATCH 32/33] Ensure we default the HTTP type correctly --- library/Requests.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/Requests.php b/library/Requests.php index 439ec184f..da178f412 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -380,6 +380,9 @@ public static function request_multiple($requests, $options = array()) { $request['options']['type'] = $request['type']; } else { + if (empty($request['options']['type'])) { + $request['options']['type'] = $request['type']; + } $request['options'] = array_merge($options, $request['options']); } From 23c2087104966a630351e30d7c8cfa719fbad4f1 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Thu, 25 Oct 2012 23:21:44 +1000 Subject: [PATCH 33/33] Test multiple requests with output to a file --- tests/Transport/Base.php | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Transport/Base.php b/tests/Transport/Base.php index 0b2ba7ffb..a96e0134d 100755 --- a/tests/Transport/Base.php +++ b/tests/Transport/Base.php @@ -460,4 +460,38 @@ public function testMultipleUsingCallbackAndFailure() { public function completeCallback($response, $key) { $this->completed[$key] = $response; } + + public function testMultipleToFile() { + $requests = array( + 'get' => array( + 'url' => 'http://httpbin.org/get', + 'options' => array( + 'filename' => tempnam(sys_get_temp_dir(), 'RLT') // RequestsLibraryTest + ), + ), + 'post' => array( + 'url' => 'http://httpbin.org/post', + 'type' => Requests::POST, + 'data' => 'test', + 'options' => array( + 'filename' => tempnam(sys_get_temp_dir(), 'RLT') // RequestsLibraryTest + ), + ), + ); + $responses = Requests::request_multiple($requests, $this->getOptions()); + + // GET request + $contents = file_get_contents($requests['get']['options']['filename']); + $result = json_decode($contents, true); + $this->assertEquals('http://httpbin.org/get', $result['url']); + $this->assertEmpty($result['args']); + unlink($requests['get']['options']['filename']); + + // POST request + $contents = file_get_contents($requests['post']['options']['filename']); + $result = json_decode($contents, true); + $this->assertEquals('http://httpbin.org/post', $result['url']); + $this->assertEquals('test', $result['data']); + unlink($requests['post']['options']['filename']); + } } \ No newline at end of file