1<?php 2namespace GuzzleHttp\Ring; 3 4use GuzzleHttp\Stream\StreamInterface; 5use GuzzleHttp\Ring\Future\FutureArrayInterface; 6use GuzzleHttp\Ring\Future\FutureArray; 7 8/** 9 * Provides core functionality of Ring handlers and middleware. 10 */ 11class Core 12{ 13 /** 14 * Returns a function that calls all of the provided functions, in order, 15 * passing the arguments provided to the composed function to each function. 16 * 17 * @param callable[] $functions Array of functions to proxy to. 18 * 19 * @return callable 20 */ 21 public static function callArray(array $functions) 22 { 23 return function () use ($functions) { 24 $args = func_get_args(); 25 foreach ($functions as $fn) { 26 call_user_func_array($fn, $args); 27 } 28 }; 29 } 30 31 /** 32 * Gets an array of header line values from a message for a specific header 33 * 34 * This method searches through the "headers" key of a message for a header 35 * using a case-insensitive search. 36 * 37 * @param array $message Request or response hash. 38 * @param string $header Header to retrieve 39 * 40 * @return array 41 */ 42 public static function headerLines($message, $header) 43 { 44 $result = []; 45 46 if (!empty($message['headers'])) { 47 foreach ($message['headers'] as $name => $value) { 48 if (!strcasecmp($name, $header)) { 49 $result = array_merge($result, $value); 50 } 51 } 52 } 53 54 return $result; 55 } 56 57 /** 58 * Gets a header value from a message as a string or null 59 * 60 * This method searches through the "headers" key of a message for a header 61 * using a case-insensitive search. The lines of the header are imploded 62 * using commas into a single string return value. 63 * 64 * @param array $message Request or response hash. 65 * @param string $header Header to retrieve 66 * 67 * @return string|null Returns the header string if found, or null if not. 68 */ 69 public static function header($message, $header) 70 { 71 $match = self::headerLines($message, $header); 72 return $match ? implode(', ', $match) : null; 73 } 74 75 /** 76 * Returns the first header value from a message as a string or null. If 77 * a header line contains multiple values separated by a comma, then this 78 * function will return the first value in the list. 79 * 80 * @param array $message Request or response hash. 81 * @param string $header Header to retrieve 82 * 83 * @return string|null Returns the value as a string if found. 84 */ 85 public static function firstHeader($message, $header) 86 { 87 if (!empty($message['headers'])) { 88 foreach ($message['headers'] as $name => $value) { 89 if (!strcasecmp($name, $header)) { 90 // Return the match itself if it is a single value. 91 $pos = strpos($value[0], ','); 92 return $pos ? substr($value[0], 0, $pos) : $value[0]; 93 } 94 } 95 } 96 97 return null; 98 } 99 100 /** 101 * Returns true if a message has the provided case-insensitive header. 102 * 103 * @param array $message Request or response hash. 104 * @param string $header Header to check 105 * 106 * @return bool 107 */ 108 public static function hasHeader($message, $header) 109 { 110 if (!empty($message['headers'])) { 111 foreach ($message['headers'] as $name => $value) { 112 if (!strcasecmp($name, $header)) { 113 return true; 114 } 115 } 116 } 117 118 return false; 119 } 120 121 /** 122 * Parses an array of header lines into an associative array of headers. 123 * 124 * @param array $lines Header lines array of strings in the following 125 * format: "Name: Value" 126 * @return array 127 */ 128 public static function headersFromLines($lines) 129 { 130 $headers = []; 131 132 foreach ($lines as $line) { 133 $parts = explode(':', $line, 2); 134 $headers[trim($parts[0])][] = isset($parts[1]) 135 ? trim($parts[1]) 136 : null; 137 } 138 139 return $headers; 140 } 141 142 /** 143 * Removes a header from a message using a case-insensitive comparison. 144 * 145 * @param array $message Message that contains 'headers' 146 * @param string $header Header to remove 147 * 148 * @return array 149 */ 150 public static function removeHeader(array $message, $header) 151 { 152 if (isset($message['headers'])) { 153 foreach (array_keys($message['headers']) as $key) { 154 if (!strcasecmp($header, $key)) { 155 unset($message['headers'][$key]); 156 } 157 } 158 } 159 160 return $message; 161 } 162 163 /** 164 * Replaces any existing case insensitive headers with the given value. 165 * 166 * @param array $message Message that contains 'headers' 167 * @param string $header Header to set. 168 * @param array $value Value to set. 169 * 170 * @return array 171 */ 172 public static function setHeader(array $message, $header, array $value) 173 { 174 $message = self::removeHeader($message, $header); 175 $message['headers'][$header] = $value; 176 177 return $message; 178 } 179 180 /** 181 * Creates a URL string from a request. 182 * 183 * If the "url" key is present on the request, it is returned, otherwise 184 * the url is built up based on the scheme, host, uri, and query_string 185 * request values. 186 * 187 * @param array $request Request to get the URL from 188 * 189 * @return string Returns the request URL as a string. 190 * @throws \InvalidArgumentException if no Host header is present. 191 */ 192 public static function url(array $request) 193 { 194 if (isset($request['url'])) { 195 return $request['url']; 196 } 197 198 $uri = (isset($request['scheme']) 199 ? $request['scheme'] : 'http') . '://'; 200 201 if ($host = self::header($request, 'host')) { 202 $uri .= $host; 203 } else { 204 throw new \InvalidArgumentException('No Host header was provided'); 205 } 206 207 if (isset($request['uri'])) { 208 $uri .= $request['uri']; 209 } 210 211 if (isset($request['query_string'])) { 212 $uri .= '?' . $request['query_string']; 213 } 214 215 return $uri; 216 } 217 218 /** 219 * Reads the body of a message into a string. 220 * 221 * @param array|FutureArrayInterface $message Array containing a "body" key 222 * 223 * @return null|string Returns the body as a string or null if not set. 224 * @throws \InvalidArgumentException if a request body is invalid. 225 */ 226 public static function body($message) 227 { 228 if (!isset($message['body'])) { 229 return null; 230 } 231 232 if ($message['body'] instanceof StreamInterface) { 233 return (string) $message['body']; 234 } 235 236 switch (gettype($message['body'])) { 237 case 'string': 238 return $message['body']; 239 case 'resource': 240 return stream_get_contents($message['body']); 241 case 'object': 242 if ($message['body'] instanceof \Iterator) { 243 return implode('', iterator_to_array($message['body'])); 244 } elseif (method_exists($message['body'], '__toString')) { 245 return (string) $message['body']; 246 } 247 default: 248 throw new \InvalidArgumentException('Invalid request body: ' 249 . self::describeType($message['body'])); 250 } 251 } 252 253 /** 254 * Rewind the body of the provided message if possible. 255 * 256 * @param array $message Message that contains a 'body' field. 257 * 258 * @return bool Returns true on success, false on failure 259 */ 260 public static function rewindBody($message) 261 { 262 if ($message['body'] instanceof StreamInterface) { 263 return $message['body']->seek(0); 264 } 265 266 if ($message['body'] instanceof \Generator) { 267 return false; 268 } 269 270 if ($message['body'] instanceof \Iterator) { 271 $message['body']->rewind(); 272 return true; 273 } 274 275 if (is_resource($message['body'])) { 276 return rewind($message['body']); 277 } 278 279 return is_string($message['body']) 280 || (is_object($message['body']) 281 && method_exists($message['body'], '__toString')); 282 } 283 284 /** 285 * Debug function used to describe the provided value type and class. 286 * 287 * @param mixed $input 288 * 289 * @return string Returns a string containing the type of the variable and 290 * if a class is provided, the class name. 291 */ 292 public static function describeType($input) 293 { 294 switch (gettype($input)) { 295 case 'object': 296 return 'object(' . get_class($input) . ')'; 297 case 'array': 298 return 'array(' . count($input) . ')'; 299 default: 300 ob_start(); 301 var_dump($input); 302 // normalize float vs double 303 return str_replace('double(', 'float(', rtrim(ob_get_clean())); 304 } 305 } 306 307 /** 308 * Sleep for the specified amount of time specified in the request's 309 * ['client']['delay'] option if present. 310 * 311 * This function should only be used when a non-blocking sleep is not 312 * possible. 313 * 314 * @param array $request Request to sleep 315 */ 316 public static function doSleep(array $request) 317 { 318 if (isset($request['client']['delay'])) { 319 usleep($request['client']['delay'] * 1000); 320 } 321 } 322 323 /** 324 * Returns a proxied future that modifies the dereferenced value of another 325 * future using a promise. 326 * 327 * @param FutureArrayInterface $future Future to wrap with a new future 328 * @param callable $onFulfilled Invoked when the future fulfilled 329 * @param callable $onRejected Invoked when the future rejected 330 * @param callable $onProgress Invoked when the future progresses 331 * 332 * @return FutureArray 333 */ 334 public static function proxy( 335 FutureArrayInterface $future, 336 callable $onFulfilled = null, 337 callable $onRejected = null, 338 callable $onProgress = null 339 ) { 340 return new FutureArray( 341 $future->then($onFulfilled, $onRejected, $onProgress), 342 [$future, 'wait'], 343 [$future, 'cancel'] 344 ); 345 } 346 347 /** 348 * Returns a debug stream based on the provided variable. 349 * 350 * @param mixed $value Optional value 351 * 352 * @return resource 353 */ 354 public static function getDebugResource($value = null) 355 { 356 if (is_resource($value)) { 357 return $value; 358 } elseif (defined('STDOUT')) { 359 return STDOUT; 360 } else { 361 return fopen('php://output', 'w'); 362 } 363 } 364} 365