1<?php 2 3namespace GuzzleHttp\Handler; 4 5use GuzzleHttp\Exception\RequestException; 6use GuzzleHttp\HandlerStack; 7use GuzzleHttp\Promise as P; 8use GuzzleHttp\Promise\PromiseInterface; 9use GuzzleHttp\TransferStats; 10use GuzzleHttp\Utils; 11use Psr\Http\Message\RequestInterface; 12use Psr\Http\Message\ResponseInterface; 13use Psr\Http\Message\StreamInterface; 14 15/** 16 * Handler that returns responses or throw exceptions from a queue. 17 * 18 * @final 19 */ 20class MockHandler implements \Countable 21{ 22 /** 23 * @var array 24 */ 25 private $queue = []; 26 27 /** 28 * @var RequestInterface|null 29 */ 30 private $lastRequest; 31 32 /** 33 * @var array 34 */ 35 private $lastOptions = []; 36 37 /** 38 * @var callable|null 39 */ 40 private $onFulfilled; 41 42 /** 43 * @var callable|null 44 */ 45 private $onRejected; 46 47 /** 48 * Creates a new MockHandler that uses the default handler stack list of 49 * middlewares. 50 * 51 * @param array|null $queue Array of responses, callables, or exceptions. 52 * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. 53 * @param callable|null $onRejected Callback to invoke when the return value is rejected. 54 */ 55 public static function createWithMiddleware(array $queue = null, callable $onFulfilled = null, callable $onRejected = null): HandlerStack 56 { 57 return HandlerStack::create(new self($queue, $onFulfilled, $onRejected)); 58 } 59 60 /** 61 * The passed in value must be an array of 62 * {@see \Psr\Http\Message\ResponseInterface} objects, Exceptions, 63 * callables, or Promises. 64 * 65 * @param array<int, mixed>|null $queue The parameters to be passed to the append function, as an indexed array. 66 * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. 67 * @param callable|null $onRejected Callback to invoke when the return value is rejected. 68 */ 69 public function __construct(array $queue = null, callable $onFulfilled = null, callable $onRejected = null) 70 { 71 $this->onFulfilled = $onFulfilled; 72 $this->onRejected = $onRejected; 73 74 if ($queue) { 75 // array_values included for BC 76 $this->append(...array_values($queue)); 77 } 78 } 79 80 public function __invoke(RequestInterface $request, array $options): PromiseInterface 81 { 82 if (!$this->queue) { 83 throw new \OutOfBoundsException('Mock queue is empty'); 84 } 85 86 if (isset($options['delay']) && \is_numeric($options['delay'])) { 87 \usleep((int) $options['delay'] * 1000); 88 } 89 90 $this->lastRequest = $request; 91 $this->lastOptions = $options; 92 $response = \array_shift($this->queue); 93 94 if (isset($options['on_headers'])) { 95 if (!\is_callable($options['on_headers'])) { 96 throw new \InvalidArgumentException('on_headers must be callable'); 97 } 98 try { 99 $options['on_headers']($response); 100 } catch (\Exception $e) { 101 $msg = 'An error was encountered during the on_headers event'; 102 $response = new RequestException($msg, $request, $response, $e); 103 } 104 } 105 106 if (\is_callable($response)) { 107 $response = $response($request, $options); 108 } 109 110 $response = $response instanceof \Throwable 111 ? P\Create::rejectionFor($response) 112 : P\Create::promiseFor($response); 113 114 return $response->then( 115 function (?ResponseInterface $value) use ($request, $options) { 116 $this->invokeStats($request, $options, $value); 117 if ($this->onFulfilled) { 118 ($this->onFulfilled)($value); 119 } 120 121 if ($value !== null && isset($options['sink'])) { 122 $contents = (string) $value->getBody(); 123 $sink = $options['sink']; 124 125 if (\is_resource($sink)) { 126 \fwrite($sink, $contents); 127 } elseif (\is_string($sink)) { 128 \file_put_contents($sink, $contents); 129 } elseif ($sink instanceof StreamInterface) { 130 $sink->write($contents); 131 } 132 } 133 134 return $value; 135 }, 136 function ($reason) use ($request, $options) { 137 $this->invokeStats($request, $options, null, $reason); 138 if ($this->onRejected) { 139 ($this->onRejected)($reason); 140 } 141 142 return P\Create::rejectionFor($reason); 143 } 144 ); 145 } 146 147 /** 148 * Adds one or more variadic requests, exceptions, callables, or promises 149 * to the queue. 150 * 151 * @param mixed ...$values 152 */ 153 public function append(...$values): void 154 { 155 foreach ($values as $value) { 156 if ($value instanceof ResponseInterface 157 || $value instanceof \Throwable 158 || $value instanceof PromiseInterface 159 || \is_callable($value) 160 ) { 161 $this->queue[] = $value; 162 } else { 163 throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found '.Utils::describeType($value)); 164 } 165 } 166 } 167 168 /** 169 * Get the last received request. 170 */ 171 public function getLastRequest(): ?RequestInterface 172 { 173 return $this->lastRequest; 174 } 175 176 /** 177 * Get the last received request options. 178 */ 179 public function getLastOptions(): array 180 { 181 return $this->lastOptions; 182 } 183 184 /** 185 * Returns the number of remaining items in the queue. 186 */ 187 public function count(): int 188 { 189 return \count($this->queue); 190 } 191 192 public function reset(): void 193 { 194 $this->queue = []; 195 } 196 197 /** 198 * @param mixed $reason Promise or reason. 199 */ 200 private function invokeStats( 201 RequestInterface $request, 202 array $options, 203 ResponseInterface $response = null, 204 $reason = null 205 ): void { 206 if (isset($options['on_stats'])) { 207 $transferTime = $options['transfer_time'] ?? 0; 208 $stats = new TransferStats($request, $response, $transferTime, $reason); 209 ($options['on_stats'])($stats); 210 } 211 } 212} 213