friendship ended with social-app. php is my new best friend
at main 32 kB view raw
1<?php 2 3namespace React\Http; 4 5use Psr\Http\Message\ResponseInterface; 6use React\EventLoop\Loop; 7use React\EventLoop\LoopInterface; 8use React\Http\Io\Sender; 9use React\Http\Io\Transaction; 10use React\Http\Message\Request; 11use React\Http\Message\Uri; 12use React\Promise\PromiseInterface; 13use React\Socket\Connector; 14use React\Socket\ConnectorInterface; 15use React\Stream\ReadableStreamInterface; 16use InvalidArgumentException; 17 18/** 19 * @final This class is final and shouldn't be extended as it is likely to be marked final in a future release. 20 */ 21class Browser 22{ 23 private $transaction; 24 private $baseUrl; 25 private $protocolVersion = '1.1'; 26 private $defaultHeaders = array( 27 'User-Agent' => 'ReactPHP/1' 28 ); 29 30 /** 31 * The `Browser` is responsible for sending HTTP requests to your HTTP server 32 * and keeps track of pending incoming HTTP responses. 33 * 34 * ```php 35 * $browser = new React\Http\Browser(); 36 * ``` 37 * 38 * This class takes two optional arguments for more advanced usage: 39 * 40 * ```php 41 * // constructor signature as of v1.5.0 42 * $browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); 43 * 44 * // legacy constructor signature before v1.5.0 45 * $browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); 46 * ``` 47 * 48 * If you need custom connector settings (DNS resolution, TLS parameters, timeouts, 49 * proxy servers etc.), you can explicitly pass a custom instance of the 50 * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): 51 * 52 * ```php 53 * $connector = new React\Socket\Connector(array( 54 * 'dns' => '127.0.0.1', 55 * 'tcp' => array( 56 * 'bindto' => '192.168.10.1:0' 57 * ), 58 * 'tls' => array( 59 * 'verify_peer' => false, 60 * 'verify_peer_name' => false 61 * ) 62 * )); 63 * 64 * $browser = new React\Http\Browser($connector); 65 * ``` 66 * 67 * This class takes an optional `LoopInterface|null $loop` parameter that can be used to 68 * pass the event loop instance to use for this object. You can use a `null` value 69 * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). 70 * This value SHOULD NOT be given unless you're sure you want to explicitly use a 71 * given event loop instance. 72 * 73 * @param null|ConnectorInterface|LoopInterface $connector 74 * @param null|LoopInterface|ConnectorInterface $loop 75 * @throws \InvalidArgumentException for invalid arguments 76 */ 77 public function __construct($connector = null, $loop = null) 78 { 79 // swap arguments for legacy constructor signature 80 if (($connector instanceof LoopInterface || $connector === null) && ($loop instanceof ConnectorInterface || $loop === null)) { 81 $swap = $loop; 82 $loop = $connector; 83 $connector = $swap; 84 } 85 86 if (($connector !== null && !$connector instanceof ConnectorInterface) || ($loop !== null && !$loop instanceof LoopInterface)) { 87 throw new \InvalidArgumentException('Expected "?ConnectorInterface $connector" and "?LoopInterface $loop" arguments'); 88 } 89 90 $loop = $loop ?: Loop::get(); 91 $this->transaction = new Transaction( 92 Sender::createFromLoop($loop, $connector ?: new Connector(array(), $loop)), 93 $loop 94 ); 95 } 96 97 /** 98 * Sends an HTTP GET request 99 * 100 * ```php 101 * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 102 * var_dump((string)$response->getBody()); 103 * }, function (Exception $e) { 104 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 105 * }); 106 * ``` 107 * 108 * See also [GET request client example](../examples/01-client-get-request.php). 109 * 110 * @param string $url URL for the request. 111 * @param array $headers 112 * @return PromiseInterface<ResponseInterface> 113 */ 114 public function get($url, array $headers = array()) 115 { 116 return $this->requestMayBeStreaming('GET', $url, $headers); 117 } 118 119 /** 120 * Sends an HTTP POST request 121 * 122 * ```php 123 * $browser->post( 124 * $url, 125 * [ 126 * 'Content-Type' => 'application/json' 127 * ], 128 * json_encode($data) 129 * )->then(function (Psr\Http\Message\ResponseInterface $response) { 130 * var_dump(json_decode((string)$response->getBody())); 131 * }, function (Exception $e) { 132 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 133 * }); 134 * ``` 135 * 136 * See also [POST JSON client example](../examples/04-client-post-json.php). 137 * 138 * This method is also commonly used to submit HTML form data: 139 * 140 * ```php 141 * $data = [ 142 * 'user' => 'Alice', 143 * 'password' => 'secret' 144 * ]; 145 * 146 * $browser->post( 147 * $url, 148 * [ 149 * 'Content-Type' => 'application/x-www-form-urlencoded' 150 * ], 151 * http_build_query($data) 152 * ); 153 * ``` 154 * 155 * This method will automatically add a matching `Content-Length` request 156 * header if the outgoing request body is a `string`. If you're using a 157 * streaming request body (`ReadableStreamInterface`), it will default to 158 * using `Transfer-Encoding: chunked` or you have to explicitly pass in a 159 * matching `Content-Length` request header like so: 160 * 161 * ```php 162 * $body = new React\Stream\ThroughStream(); 163 * Loop::addTimer(1.0, function () use ($body) { 164 * $body->end("hello world"); 165 * }); 166 * 167 * $browser->post($url, array('Content-Length' => '11'), $body); 168 * ``` 169 * 170 * @param string $url URL for the request. 171 * @param array $headers 172 * @param string|ReadableStreamInterface $body 173 * @return PromiseInterface<ResponseInterface> 174 */ 175 public function post($url, array $headers = array(), $body = '') 176 { 177 return $this->requestMayBeStreaming('POST', $url, $headers, $body); 178 } 179 180 /** 181 * Sends an HTTP HEAD request 182 * 183 * ```php 184 * $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 185 * var_dump($response->getHeaders()); 186 * }, function (Exception $e) { 187 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 188 * }); 189 * ``` 190 * 191 * @param string $url URL for the request. 192 * @param array $headers 193 * @return PromiseInterface<ResponseInterface> 194 */ 195 public function head($url, array $headers = array()) 196 { 197 return $this->requestMayBeStreaming('HEAD', $url, $headers); 198 } 199 200 /** 201 * Sends an HTTP PATCH request 202 * 203 * ```php 204 * $browser->patch( 205 * $url, 206 * [ 207 * 'Content-Type' => 'application/json' 208 * ], 209 * json_encode($data) 210 * )->then(function (Psr\Http\Message\ResponseInterface $response) { 211 * var_dump(json_decode((string)$response->getBody())); 212 * }, function (Exception $e) { 213 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 214 * }); 215 * ``` 216 * 217 * This method will automatically add a matching `Content-Length` request 218 * header if the outgoing request body is a `string`. If you're using a 219 * streaming request body (`ReadableStreamInterface`), it will default to 220 * using `Transfer-Encoding: chunked` or you have to explicitly pass in a 221 * matching `Content-Length` request header like so: 222 * 223 * ```php 224 * $body = new React\Stream\ThroughStream(); 225 * Loop::addTimer(1.0, function () use ($body) { 226 * $body->end("hello world"); 227 * }); 228 * 229 * $browser->patch($url, array('Content-Length' => '11'), $body); 230 * ``` 231 * 232 * @param string $url URL for the request. 233 * @param array $headers 234 * @param string|ReadableStreamInterface $body 235 * @return PromiseInterface<ResponseInterface> 236 */ 237 public function patch($url, array $headers = array(), $body = '') 238 { 239 return $this->requestMayBeStreaming('PATCH', $url , $headers, $body); 240 } 241 242 /** 243 * Sends an HTTP PUT request 244 * 245 * ```php 246 * $browser->put( 247 * $url, 248 * [ 249 * 'Content-Type' => 'text/xml' 250 * ], 251 * $xml->asXML() 252 * )->then(function (Psr\Http\Message\ResponseInterface $response) { 253 * var_dump((string)$response->getBody()); 254 * }, function (Exception $e) { 255 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 256 * }); 257 * ``` 258 * 259 * See also [PUT XML client example](../examples/05-client-put-xml.php). 260 * 261 * This method will automatically add a matching `Content-Length` request 262 * header if the outgoing request body is a `string`. If you're using a 263 * streaming request body (`ReadableStreamInterface`), it will default to 264 * using `Transfer-Encoding: chunked` or you have to explicitly pass in a 265 * matching `Content-Length` request header like so: 266 * 267 * ```php 268 * $body = new React\Stream\ThroughStream(); 269 * Loop::addTimer(1.0, function () use ($body) { 270 * $body->end("hello world"); 271 * }); 272 * 273 * $browser->put($url, array('Content-Length' => '11'), $body); 274 * ``` 275 * 276 * @param string $url URL for the request. 277 * @param array $headers 278 * @param string|ReadableStreamInterface $body 279 * @return PromiseInterface<ResponseInterface> 280 */ 281 public function put($url, array $headers = array(), $body = '') 282 { 283 return $this->requestMayBeStreaming('PUT', $url, $headers, $body); 284 } 285 286 /** 287 * Sends an HTTP DELETE request 288 * 289 * ```php 290 * $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 291 * var_dump((string)$response->getBody()); 292 * }, function (Exception $e) { 293 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 294 * }); 295 * ``` 296 * 297 * @param string $url URL for the request. 298 * @param array $headers 299 * @param string|ReadableStreamInterface $body 300 * @return PromiseInterface<ResponseInterface> 301 */ 302 public function delete($url, array $headers = array(), $body = '') 303 { 304 return $this->requestMayBeStreaming('DELETE', $url, $headers, $body); 305 } 306 307 /** 308 * Sends an arbitrary HTTP request. 309 * 310 * The preferred way to send an HTTP request is by using the above 311 * [request methods](#request-methods), for example the [`get()`](#get) 312 * method to send an HTTP `GET` request. 313 * 314 * As an alternative, if you want to use a custom HTTP request method, you 315 * can use this method: 316 * 317 * ```php 318 * $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { 319 * var_dump((string)$response->getBody()); 320 * }, function (Exception $e) { 321 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 322 * }); 323 * ``` 324 * 325 * This method will automatically add a matching `Content-Length` request 326 * header if the size of the outgoing request body is known and non-empty. 327 * For an empty request body, if will only include a `Content-Length: 0` 328 * request header if the request method usually expects a request body (only 329 * applies to `POST`, `PUT` and `PATCH`). 330 * 331 * If you're using a streaming request body (`ReadableStreamInterface`), it 332 * will default to using `Transfer-Encoding: chunked` or you have to 333 * explicitly pass in a matching `Content-Length` request header like so: 334 * 335 * ```php 336 * $body = new React\Stream\ThroughStream(); 337 * Loop::addTimer(1.0, function () use ($body) { 338 * $body->end("hello world"); 339 * }); 340 * 341 * $browser->request('POST', $url, array('Content-Length' => '11'), $body); 342 * ``` 343 * 344 * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. 345 * @param string $url URL for the request 346 * @param array $headers Additional request headers 347 * @param string|ReadableStreamInterface $body HTTP request body contents 348 * @return PromiseInterface<ResponseInterface> 349 */ 350 public function request($method, $url, array $headers = array(), $body = '') 351 { 352 return $this->withOptions(array('streaming' => false))->requestMayBeStreaming($method, $url, $headers, $body); 353 } 354 355 /** 356 * Sends an arbitrary HTTP request and receives a streaming response without buffering the response body. 357 * 358 * The preferred way to send an HTTP request is by using the above 359 * [request methods](#request-methods), for example the [`get()`](#get) 360 * method to send an HTTP `GET` request. Each of these methods will buffer 361 * the whole response body in memory by default. This is easy to get started 362 * and works reasonably well for smaller responses. 363 * 364 * In some situations, it's a better idea to use a streaming approach, where 365 * only small chunks have to be kept in memory. You can use this method to 366 * send an arbitrary HTTP request and receive a streaming response. It uses 367 * the same HTTP message API, but does not buffer the response body in 368 * memory. It only processes the response body in small chunks as data is 369 * received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). 370 * This works for (any number of) responses of arbitrary sizes. 371 * 372 * ```php 373 * $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { 374 * $body = $response->getBody(); 375 * assert($body instanceof Psr\Http\Message\StreamInterface); 376 * assert($body instanceof React\Stream\ReadableStreamInterface); 377 * 378 * $body->on('data', function ($chunk) { 379 * echo $chunk; 380 * }); 381 * 382 * $body->on('error', function (Exception $e) { 383 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 384 * }); 385 * 386 * $body->on('close', function () { 387 * echo '[DONE]' . PHP_EOL; 388 * }); 389 * }, function (Exception $e) { 390 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 391 * }); 392 * ``` 393 * 394 * See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) 395 * and the [streaming response](#streaming-response) for more details, 396 * examples and possible use-cases. 397 * 398 * This method will automatically add a matching `Content-Length` request 399 * header if the size of the outgoing request body is known and non-empty. 400 * For an empty request body, if will only include a `Content-Length: 0` 401 * request header if the request method usually expects a request body (only 402 * applies to `POST`, `PUT` and `PATCH`). 403 * 404 * If you're using a streaming request body (`ReadableStreamInterface`), it 405 * will default to using `Transfer-Encoding: chunked` or you have to 406 * explicitly pass in a matching `Content-Length` request header like so: 407 * 408 * ```php 409 * $body = new React\Stream\ThroughStream(); 410 * Loop::addTimer(1.0, function () use ($body) { 411 * $body->end("hello world"); 412 * }); 413 * 414 * $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); 415 * ``` 416 * 417 * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. 418 * @param string $url URL for the request 419 * @param array $headers Additional request headers 420 * @param string|ReadableStreamInterface $body HTTP request body contents 421 * @return PromiseInterface<ResponseInterface> 422 */ 423 public function requestStreaming($method, $url, $headers = array(), $body = '') 424 { 425 return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $body); 426 } 427 428 /** 429 * Changes the maximum timeout used for waiting for pending requests. 430 * 431 * You can pass in the number of seconds to use as a new timeout value: 432 * 433 * ```php 434 * $browser = $browser->withTimeout(10.0); 435 * ``` 436 * 437 * You can pass in a bool `false` to disable any timeouts. In this case, 438 * requests can stay pending forever: 439 * 440 * ```php 441 * $browser = $browser->withTimeout(false); 442 * ``` 443 * 444 * You can pass in a bool `true` to re-enable default timeout handling. This 445 * will respects PHP's `default_socket_timeout` setting (default 60s): 446 * 447 * ```php 448 * $browser = $browser->withTimeout(true); 449 * ``` 450 * 451 * See also [timeouts](#timeouts) for more details about timeout handling. 452 * 453 * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 454 * method actually returns a *new* [`Browser`](#browser) instance with the 455 * given timeout value applied. 456 * 457 * @param bool|number $timeout 458 * @return self 459 */ 460 public function withTimeout($timeout) 461 { 462 if ($timeout === true) { 463 $timeout = null; 464 } elseif ($timeout === false) { 465 $timeout = -1; 466 } elseif ($timeout < 0) { 467 $timeout = 0; 468 } 469 470 return $this->withOptions(array( 471 'timeout' => $timeout, 472 )); 473 } 474 475 /** 476 * Changes how HTTP redirects will be followed. 477 * 478 * You can pass in the maximum number of redirects to follow: 479 * 480 * ```php 481 * $browser = $browser->withFollowRedirects(5); 482 * ``` 483 * 484 * The request will automatically be rejected when the number of redirects 485 * is exceeded. You can pass in a `0` to reject the request for any 486 * redirects encountered: 487 * 488 * ```php 489 * $browser = $browser->withFollowRedirects(0); 490 * 491 * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 492 * // only non-redirected responses will now end up here 493 * var_dump($response->getHeaders()); 494 * }, function (Exception $e) { 495 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 496 * }); 497 * ``` 498 * 499 * You can pass in a bool `false` to disable following any redirects. In 500 * this case, requests will resolve with the redirection response instead 501 * of following the `Location` response header: 502 * 503 * ```php 504 * $browser = $browser->withFollowRedirects(false); 505 * 506 * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 507 * // any redirects will now end up here 508 * var_dump($response->getHeaderLine('Location')); 509 * }, function (Exception $e) { 510 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 511 * }); 512 * ``` 513 * 514 * You can pass in a bool `true` to re-enable default redirect handling. 515 * This defaults to following a maximum of 10 redirects: 516 * 517 * ```php 518 * $browser = $browser->withFollowRedirects(true); 519 * ``` 520 * 521 * See also [redirects](#redirects) for more details about redirect handling. 522 * 523 * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 524 * method actually returns a *new* [`Browser`](#browser) instance with the 525 * given redirect setting applied. 526 * 527 * @param bool|int $followRedirects 528 * @return self 529 */ 530 public function withFollowRedirects($followRedirects) 531 { 532 return $this->withOptions(array( 533 'followRedirects' => $followRedirects !== false, 534 'maxRedirects' => \is_bool($followRedirects) ? null : $followRedirects 535 )); 536 } 537 538 /** 539 * Changes whether non-successful HTTP response status codes (4xx and 5xx) will be rejected. 540 * 541 * You can pass in a bool `false` to disable rejecting incoming responses 542 * that use a 4xx or 5xx response status code. In this case, requests will 543 * resolve with the response message indicating an error condition: 544 * 545 * ```php 546 * $browser = $browser->withRejectErrorResponse(false); 547 * 548 * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 549 * // any HTTP response will now end up here 550 * var_dump($response->getStatusCode(), $response->getReasonPhrase()); 551 * }, function (Exception $e) { 552 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 553 * }); 554 * ``` 555 * 556 * You can pass in a bool `true` to re-enable default status code handling. 557 * This defaults to rejecting any response status codes in the 4xx or 5xx 558 * range: 559 * 560 * ```php 561 * $browser = $browser->withRejectErrorResponse(true); 562 * 563 * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 564 * // any successful HTTP response will now end up here 565 * var_dump($response->getStatusCode(), $response->getReasonPhrase()); 566 * }, function (Exception $e) { 567 * if ($e instanceof React\Http\Message\ResponseException) { 568 * // any HTTP response error message will now end up here 569 * $response = $e->getResponse(); 570 * var_dump($response->getStatusCode(), $response->getReasonPhrase()); 571 * } else { 572 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 573 * } 574 * }); 575 * ``` 576 * 577 * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 578 * method actually returns a *new* [`Browser`](#browser) instance with the 579 * given setting applied. 580 * 581 * @param bool $obeySuccessCode 582 * @return self 583 */ 584 public function withRejectErrorResponse($obeySuccessCode) 585 { 586 return $this->withOptions(array( 587 'obeySuccessCode' => $obeySuccessCode, 588 )); 589 } 590 591 /** 592 * Changes the base URL used to resolve relative URLs to. 593 * 594 * If you configure a base URL, any requests to relative URLs will be 595 * processed by first resolving this relative to the given absolute base 596 * URL. This supports resolving relative path references (like `../` etc.). 597 * This is particularly useful for (RESTful) API calls where all endpoints 598 * (URLs) are located under a common base URL. 599 * 600 * ```php 601 * $browser = $browser->withBase('http://api.example.com/v3/'); 602 * 603 * // will request http://api.example.com/v3/users 604 * $browser->get('users')->then(…); 605 * ``` 606 * 607 * You can pass in a `null` base URL to return a new instance that does not 608 * use a base URL: 609 * 610 * ```php 611 * $browser = $browser->withBase(null); 612 * ``` 613 * 614 * Accordingly, any requests using relative URLs to a browser that does not 615 * use a base URL can not be completed and will be rejected without sending 616 * a request. 617 * 618 * This method will throw an `InvalidArgumentException` if the given 619 * `$baseUrl` argument is not a valid URL. 620 * 621 * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method 622 * actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. 623 * 624 * @param string|null $baseUrl absolute base URL 625 * @return self 626 * @throws InvalidArgumentException if the given $baseUrl is not a valid absolute URL 627 * @see self::withoutBase() 628 */ 629 public function withBase($baseUrl) 630 { 631 $browser = clone $this; 632 if ($baseUrl === null) { 633 $browser->baseUrl = null; 634 return $browser; 635 } 636 637 $browser->baseUrl = new Uri($baseUrl); 638 if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') { 639 throw new \InvalidArgumentException('Base URL must be absolute'); 640 } 641 642 return $browser; 643 } 644 645 /** 646 * Changes the HTTP protocol version that will be used for all subsequent requests. 647 * 648 * All the above [request methods](#request-methods) default to sending 649 * requests as HTTP/1.1. This is the preferred HTTP protocol version which 650 * also provides decent backwards-compatibility with legacy HTTP/1.0 651 * servers. As such, there should rarely be a need to explicitly change this 652 * protocol version. 653 * 654 * If you want to explicitly use the legacy HTTP/1.0 protocol version, you 655 * can use this method: 656 * 657 * ```php 658 * $browser = $browser->withProtocolVersion('1.0'); 659 * 660 * $browser->get($url)->then(…); 661 * ``` 662 * 663 * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 664 * method actually returns a *new* [`Browser`](#browser) instance with the 665 * new protocol version applied. 666 * 667 * @param string $protocolVersion HTTP protocol version to use, must be one of "1.1" or "1.0" 668 * @return self 669 * @throws InvalidArgumentException 670 */ 671 public function withProtocolVersion($protocolVersion) 672 { 673 if (!\in_array($protocolVersion, array('1.0', '1.1'), true)) { 674 throw new InvalidArgumentException('Invalid HTTP protocol version, must be one of "1.1" or "1.0"'); 675 } 676 677 $browser = clone $this; 678 $browser->protocolVersion = (string) $protocolVersion; 679 680 return $browser; 681 } 682 683 /** 684 * Changes the maximum size for buffering a response body. 685 * 686 * The preferred way to send an HTTP request is by using the above 687 * [request methods](#request-methods), for example the [`get()`](#get) 688 * method to send an HTTP `GET` request. Each of these methods will buffer 689 * the whole response body in memory by default. This is easy to get started 690 * and works reasonably well for smaller responses. 691 * 692 * By default, the response body buffer will be limited to 16 MiB. If the 693 * response body exceeds this maximum size, the request will be rejected. 694 * 695 * You can pass in the maximum number of bytes to buffer: 696 * 697 * ```php 698 * $browser = $browser->withResponseBuffer(1024 * 1024); 699 * 700 * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 701 * // response body will not exceed 1 MiB 702 * var_dump($response->getHeaders(), (string) $response->getBody()); 703 * }, function (Exception $e) { 704 * echo 'Error: ' . $e->getMessage() . PHP_EOL; 705 * }); 706 * ``` 707 * 708 * Note that the response body buffer has to be kept in memory for each 709 * pending request until its transfer is completed and it will only be freed 710 * after a pending request is fulfilled. As such, increasing this maximum 711 * buffer size to allow larger response bodies is usually not recommended. 712 * Instead, you can use the [`requestStreaming()` method](#requeststreaming) 713 * to receive responses with arbitrary sizes without buffering. Accordingly, 714 * this maximum buffer size setting has no effect on streaming responses. 715 * 716 * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 717 * method actually returns a *new* [`Browser`](#browser) instance with the 718 * given setting applied. 719 * 720 * @param int $maximumSize 721 * @return self 722 * @see self::requestStreaming() 723 */ 724 public function withResponseBuffer($maximumSize) 725 { 726 return $this->withOptions(array( 727 'maximumSize' => $maximumSize 728 )); 729 } 730 731 /** 732 * Add a request header for all following requests. 733 * 734 * ```php 735 * $browser = $browser->withHeader('User-Agent', 'ACME'); 736 * 737 * $browser->get($url)->then(…); 738 * ``` 739 * 740 * Note that the new header will overwrite any headers previously set with 741 * the same name (case-insensitive). Following requests will use these headers 742 * by default unless they are explicitly set for any requests. 743 * 744 * @param string $header 745 * @param string $value 746 * @return Browser 747 */ 748 public function withHeader($header, $value) 749 { 750 $browser = $this->withoutHeader($header); 751 $browser->defaultHeaders[$header] = $value; 752 753 return $browser; 754 } 755 756 /** 757 * Remove any default request headers previously set via 758 * the [`withHeader()` method](#withheader). 759 * 760 * ```php 761 * $browser = $browser->withoutHeader('User-Agent'); 762 * 763 * $browser->get($url)->then(…); 764 * ``` 765 * 766 * Note that this method only affects the headers which were set with the 767 * method `withHeader(string $header, string $value): Browser` 768 * 769 * @param string $header 770 * @return Browser 771 */ 772 public function withoutHeader($header) 773 { 774 $browser = clone $this; 775 776 /** @var string|int $key */ 777 foreach (\array_keys($browser->defaultHeaders) as $key) { 778 if (\strcasecmp($key, $header) === 0) { 779 unset($browser->defaultHeaders[$key]); 780 break; 781 } 782 } 783 784 return $browser; 785 } 786 787 /** 788 * Changes the [options](#options) to use: 789 * 790 * The [`Browser`](#browser) class exposes several options for the handling of 791 * HTTP transactions. These options resemble some of PHP's 792 * [HTTP context options](http://php.net/manual/en/context.http.php) and 793 * can be controlled via the following API (and their defaults): 794 * 795 * ```php 796 * // deprecated 797 * $newBrowser = $browser->withOptions(array( 798 * 'timeout' => null, // see withTimeout() instead 799 * 'followRedirects' => true, // see withFollowRedirects() instead 800 * 'maxRedirects' => 10, // see withFollowRedirects() instead 801 * 'obeySuccessCode' => true, // see withRejectErrorResponse() instead 802 * 'streaming' => false, // deprecated, see requestStreaming() instead 803 * )); 804 * ``` 805 * 806 * See also [timeouts](#timeouts), [redirects](#redirects) and 807 * [streaming](#streaming) for more details. 808 * 809 * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 810 * method actually returns a *new* [`Browser`](#browser) instance with the 811 * options applied. 812 * 813 * @param array $options 814 * @return self 815 * @see self::withTimeout() 816 * @see self::withFollowRedirects() 817 * @see self::withRejectErrorResponse() 818 */ 819 private function withOptions(array $options) 820 { 821 $browser = clone $this; 822 $browser->transaction = $this->transaction->withOptions($options); 823 824 return $browser; 825 } 826 827 /** 828 * @param string $method 829 * @param string $url 830 * @param array $headers 831 * @param string|ReadableStreamInterface $body 832 * @return PromiseInterface<ResponseInterface> 833 */ 834 private function requestMayBeStreaming($method, $url, array $headers = array(), $body = '') 835 { 836 if ($this->baseUrl !== null) { 837 // ensure we're actually below the base URL 838 $url = Uri::resolve($this->baseUrl, new Uri($url)); 839 } 840 841 foreach ($this->defaultHeaders as $key => $value) { 842 $explicitHeaderExists = false; 843 foreach (\array_keys($headers) as $headerKey) { 844 if (\strcasecmp($headerKey, $key) === 0) { 845 $explicitHeaderExists = true; 846 break; 847 } 848 } 849 if (!$explicitHeaderExists) { 850 $headers[$key] = $value; 851 } 852 } 853 854 return $this->transaction->send( 855 new Request($method, $url, $headers, $body, $this->protocolVersion) 856 ); 857 } 858}