1<?php 2/* 3 * Copyright 2012 Google Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18namespace Google\Http; 19 20use Google\Client; 21use Google\Http\REST; 22use Google\Service\Exception as GoogleServiceException; 23use GuzzleHttp\Psr7; 24use GuzzleHttp\Psr7\Request; 25use GuzzleHttp\Psr7\Response; 26use Psr\Http\Message\RequestInterface; 27use Psr\Http\Message\ResponseInterface; 28 29/** 30 * Class to handle batched requests to the Google API service. 31 * 32 * Note that calls to `Google\Http\Batch::execute()` do not clear the queued 33 * requests. To start a new batch, be sure to create a new instance of this 34 * class. 35 */ 36class Batch 37{ 38 const BATCH_PATH = 'batch'; 39 40 private static $CONNECTION_ESTABLISHED_HEADERS = array( 41 "HTTP/1.0 200 Connection established\r\n\r\n", 42 "HTTP/1.1 200 Connection established\r\n\r\n", 43 ); 44 45 /** @var string Multipart Boundary. */ 46 private $boundary; 47 48 /** @var array service requests to be executed. */ 49 private $requests = array(); 50 51 /** @var Client */ 52 private $client; 53 54 private $rootUrl; 55 56 private $batchPath; 57 58 public function __construct( 59 Client $client, 60 $boundary = false, 61 $rootUrl = null, 62 $batchPath = null 63 ) { 64 $this->client = $client; 65 $this->boundary = $boundary ?: mt_rand(); 66 $this->rootUrl = rtrim($rootUrl ?: $this->client->getConfig('base_path'), '/'); 67 $this->batchPath = $batchPath ?: self::BATCH_PATH; 68 } 69 70 public function add(RequestInterface $request, $key = false) 71 { 72 if (false == $key) { 73 $key = mt_rand(); 74 } 75 76 $this->requests[$key] = $request; 77 } 78 79 public function execute() 80 { 81 $body = ''; 82 $classes = array(); 83 $batchHttpTemplate = <<<EOF 84--%s 85Content-Type: application/http 86Content-Transfer-Encoding: binary 87MIME-Version: 1.0 88Content-ID: %s 89 90%s 91%s%s 92 93 94EOF; 95 96 /** @var RequestInterface $req */ 97 foreach ($this->requests as $key => $request) { 98 $firstLine = sprintf( 99 '%s %s HTTP/%s', 100 $request->getMethod(), 101 $request->getRequestTarget(), 102 $request->getProtocolVersion() 103 ); 104 105 $content = (string) $request->getBody(); 106 107 $headers = ''; 108 foreach ($request->getHeaders() as $name => $values) { 109 $headers .= sprintf("%s:%s\r\n", $name, implode(', ', $values)); 110 } 111 112 $body .= sprintf( 113 $batchHttpTemplate, 114 $this->boundary, 115 $key, 116 $firstLine, 117 $headers, 118 $content ? "\n".$content : '' 119 ); 120 121 $classes['response-' . $key] = $request->getHeaderLine('X-Php-Expected-Class'); 122 } 123 124 $body .= "--{$this->boundary}--"; 125 $body = trim($body); 126 $url = $this->rootUrl . '/' . $this->batchPath; 127 $headers = array( 128 'Content-Type' => sprintf('multipart/mixed; boundary=%s', $this->boundary), 129 'Content-Length' => strlen($body), 130 ); 131 132 $request = new Request( 133 'POST', 134 $url, 135 $headers, 136 $body 137 ); 138 139 $response = $this->client->execute($request); 140 141 return $this->parseResponse($response, $classes); 142 } 143 144 public function parseResponse(ResponseInterface $response, $classes = array()) 145 { 146 $contentType = $response->getHeaderLine('content-type'); 147 $contentType = explode(';', $contentType); 148 $boundary = false; 149 foreach ($contentType as $part) { 150 $part = explode('=', $part, 2); 151 if (isset($part[0]) && 'boundary' == trim($part[0])) { 152 $boundary = $part[1]; 153 } 154 } 155 156 $body = (string) $response->getBody(); 157 if (!empty($body)) { 158 $body = str_replace("--$boundary--", "--$boundary", $body); 159 $parts = explode("--$boundary", $body); 160 $responses = array(); 161 $requests = array_values($this->requests); 162 163 foreach ($parts as $i => $part) { 164 $part = trim($part); 165 if (!empty($part)) { 166 list($rawHeaders, $part) = explode("\r\n\r\n", $part, 2); 167 $headers = $this->parseRawHeaders($rawHeaders); 168 169 $status = substr($part, 0, strpos($part, "\n")); 170 $status = explode(" ", $status); 171 $status = $status[1]; 172 173 list($partHeaders, $partBody) = $this->parseHttpResponse($part, false); 174 $response = new Response( 175 $status, 176 $partHeaders, 177 Psr7\Utils::streamFor($partBody) 178 ); 179 180 // Need content id. 181 $key = $headers['content-id']; 182 183 try { 184 $response = REST::decodeHttpResponse($response, $requests[$i-1]); 185 } catch (GoogleServiceException $e) { 186 // Store the exception as the response, so successful responses 187 // can be processed. 188 $response = $e; 189 } 190 191 $responses[$key] = $response; 192 } 193 } 194 195 return $responses; 196 } 197 198 return null; 199 } 200 201 private function parseRawHeaders($rawHeaders) 202 { 203 $headers = array(); 204 $responseHeaderLines = explode("\r\n", $rawHeaders); 205 foreach ($responseHeaderLines as $headerLine) { 206 if ($headerLine && strpos($headerLine, ':') !== false) { 207 list($header, $value) = explode(': ', $headerLine, 2); 208 $header = strtolower($header); 209 if (isset($headers[$header])) { 210 $headers[$header] = array_merge((array)$headers[$header], (array)$value); 211 } else { 212 $headers[$header] = $value; 213 } 214 } 215 } 216 return $headers; 217 } 218 219 /** 220 * Used by the IO lib and also the batch processing. 221 * 222 * @param $respData 223 * @param $headerSize 224 * @return array 225 */ 226 private function parseHttpResponse($respData, $headerSize) 227 { 228 // check proxy header 229 foreach (self::$CONNECTION_ESTABLISHED_HEADERS as $established_header) { 230 if (stripos($respData, $established_header) !== false) { 231 // existed, remove it 232 $respData = str_ireplace($established_header, '', $respData); 233 // Subtract the proxy header size unless the cURL bug prior to 7.30.0 234 // is present which prevented the proxy header size from being taken into 235 // account. 236 // @TODO look into this 237 // if (!$this->needsQuirk()) { 238 // $headerSize -= strlen($established_header); 239 // } 240 break; 241 } 242 } 243 244 if ($headerSize) { 245 $responseBody = substr($respData, $headerSize); 246 $responseHeaders = substr($respData, 0, $headerSize); 247 } else { 248 $responseSegments = explode("\r\n\r\n", $respData, 2); 249 $responseHeaders = $responseSegments[0]; 250 $responseBody = isset($responseSegments[1]) ? $responseSegments[1] : 251 null; 252 } 253 254 $responseHeaders = $this->parseRawHeaders($responseHeaders); 255 256 return array($responseHeaders, $responseBody); 257 } 258} 259