Browser.php 31 KB

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