friendship ended with social-app. php is my new best friend
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}