1<?php 2namespace GuzzleHttp; 3 4use GuzzleHttp\Promise\PromiseInterface; 5use Psr\Http\Message\RequestInterface; 6use Psr\Http\Message\ResponseInterface; 7 8/** 9 * Creates a composed Guzzle handler function by stacking middlewares on top of 10 * an HTTP handler function. 11 */ 12class HandlerStack 13{ 14 /** @var callable|null */ 15 private $handler; 16 17 /** @var array */ 18 private $stack = []; 19 20 /** @var callable|null */ 21 private $cached; 22 23 /** 24 * Creates a default handler stack that can be used by clients. 25 * 26 * The returned handler will wrap the provided handler or use the most 27 * appropriate default handler for your system. The returned HandlerStack has 28 * support for cookies, redirects, HTTP error exceptions, and preparing a body 29 * before sending. 30 * 31 * The returned handler stack can be passed to a client in the "handler" 32 * option. 33 * 34 * @param callable $handler HTTP handler function to use with the stack. If no 35 * handler is provided, the best handler for your 36 * system will be utilized. 37 * 38 * @return HandlerStack 39 */ 40 public static function create(callable $handler = null) 41 { 42 $stack = new self($handler ?: choose_handler()); 43 $stack->push(Middleware::httpErrors(), 'http_errors'); 44 $stack->push(Middleware::redirect(), 'allow_redirects'); 45 $stack->push(Middleware::cookies(), 'cookies'); 46 $stack->push(Middleware::prepareBody(), 'prepare_body'); 47 48 return $stack; 49 } 50 51 /** 52 * @param callable $handler Underlying HTTP handler. 53 */ 54 public function __construct(callable $handler = null) 55 { 56 $this->handler = $handler; 57 } 58 59 /** 60 * Invokes the handler stack as a composed handler 61 * 62 * @param RequestInterface $request 63 * @param array $options 64 * 65 * @return ResponseInterface|PromiseInterface 66 */ 67 public function __invoke(RequestInterface $request, array $options) 68 { 69 $handler = $this->resolve(); 70 71 return $handler($request, $options); 72 } 73 74 /** 75 * Dumps a string representation of the stack. 76 * 77 * @return string 78 */ 79 public function __toString() 80 { 81 $depth = 0; 82 $stack = []; 83 if ($this->handler) { 84 $stack[] = "0) Handler: " . $this->debugCallable($this->handler); 85 } 86 87 $result = ''; 88 foreach (array_reverse($this->stack) as $tuple) { 89 $depth++; 90 $str = "{$depth}) Name: '{$tuple[1]}', "; 91 $str .= "Function: " . $this->debugCallable($tuple[0]); 92 $result = "> {$str}\n{$result}"; 93 $stack[] = $str; 94 } 95 96 foreach (array_keys($stack) as $k) { 97 $result .= "< {$stack[$k]}\n"; 98 } 99 100 return $result; 101 } 102 103 /** 104 * Set the HTTP handler that actually returns a promise. 105 * 106 * @param callable $handler Accepts a request and array of options and 107 * returns a Promise. 108 */ 109 public function setHandler(callable $handler) 110 { 111 $this->handler = $handler; 112 $this->cached = null; 113 } 114 115 /** 116 * Returns true if the builder has a handler. 117 * 118 * @return bool 119 */ 120 public function hasHandler() 121 { 122 return (bool) $this->handler; 123 } 124 125 /** 126 * Unshift a middleware to the bottom of the stack. 127 * 128 * @param callable $middleware Middleware function 129 * @param string $name Name to register for this middleware. 130 */ 131 public function unshift(callable $middleware, $name = null) 132 { 133 array_unshift($this->stack, [$middleware, $name]); 134 $this->cached = null; 135 } 136 137 /** 138 * Push a middleware to the top of the stack. 139 * 140 * @param callable $middleware Middleware function 141 * @param string $name Name to register for this middleware. 142 */ 143 public function push(callable $middleware, $name = '') 144 { 145 $this->stack[] = [$middleware, $name]; 146 $this->cached = null; 147 } 148 149 /** 150 * Add a middleware before another middleware by name. 151 * 152 * @param string $findName Middleware to find 153 * @param callable $middleware Middleware function 154 * @param string $withName Name to register for this middleware. 155 */ 156 public function before($findName, callable $middleware, $withName = '') 157 { 158 $this->splice($findName, $withName, $middleware, true); 159 } 160 161 /** 162 * Add a middleware after another middleware by name. 163 * 164 * @param string $findName Middleware to find 165 * @param callable $middleware Middleware function 166 * @param string $withName Name to register for this middleware. 167 */ 168 public function after($findName, callable $middleware, $withName = '') 169 { 170 $this->splice($findName, $withName, $middleware, false); 171 } 172 173 /** 174 * Remove a middleware by instance or name from the stack. 175 * 176 * @param callable|string $remove Middleware to remove by instance or name. 177 */ 178 public function remove($remove) 179 { 180 $this->cached = null; 181 $idx = is_callable($remove) ? 0 : 1; 182 $this->stack = array_values(array_filter( 183 $this->stack, 184 function ($tuple) use ($idx, $remove) { 185 return $tuple[$idx] !== $remove; 186 } 187 )); 188 } 189 190 /** 191 * Compose the middleware and handler into a single callable function. 192 * 193 * @return callable 194 */ 195 public function resolve() 196 { 197 if (!$this->cached) { 198 if (!($prev = $this->handler)) { 199 throw new \LogicException('No handler has been specified'); 200 } 201 202 foreach (array_reverse($this->stack) as $fn) { 203 $prev = $fn[0]($prev); 204 } 205 206 $this->cached = $prev; 207 } 208 209 return $this->cached; 210 } 211 212 /** 213 * @param string $name 214 * @return int 215 */ 216 private function findByName($name) 217 { 218 foreach ($this->stack as $k => $v) { 219 if ($v[1] === $name) { 220 return $k; 221 } 222 } 223 224 throw new \InvalidArgumentException("Middleware not found: $name"); 225 } 226 227 /** 228 * Splices a function into the middleware list at a specific position. 229 * 230 * @param string $findName 231 * @param string $withName 232 * @param callable $middleware 233 * @param bool $before 234 */ 235 private function splice($findName, $withName, callable $middleware, $before) 236 { 237 $this->cached = null; 238 $idx = $this->findByName($findName); 239 $tuple = [$middleware, $withName]; 240 241 if ($before) { 242 if ($idx === 0) { 243 array_unshift($this->stack, $tuple); 244 } else { 245 $replacement = [$tuple, $this->stack[$idx]]; 246 array_splice($this->stack, $idx, 1, $replacement); 247 } 248 } elseif ($idx === count($this->stack) - 1) { 249 $this->stack[] = $tuple; 250 } else { 251 $replacement = [$this->stack[$idx], $tuple]; 252 array_splice($this->stack, $idx, 1, $replacement); 253 } 254 } 255 256 /** 257 * Provides a debug string for a given callable. 258 * 259 * @param array|callable $fn Function to write as a string. 260 * 261 * @return string 262 */ 263 private function debugCallable($fn) 264 { 265 if (is_string($fn)) { 266 return "callable({$fn})"; 267 } 268 269 if (is_array($fn)) { 270 return is_string($fn[0]) 271 ? "callable({$fn[0]}::{$fn[1]})" 272 : "callable(['" . get_class($fn[0]) . "', '{$fn[1]}'])"; 273 } 274 275 return 'callable(' . spl_object_hash($fn) . ')'; 276 } 277} 278