Chris@0: 'http://www.foo.com/1.0/', Chris@0: * 'timeout' => 0, Chris@0: * 'allow_redirects' => false, Chris@0: * 'proxy' => '192.168.16.1:10' Chris@0: * ]); Chris@0: * Chris@0: * Client configuration settings include the following options: Chris@0: * Chris@0: * - handler: (callable) Function that transfers HTTP requests over the Chris@0: * wire. The function is called with a Psr7\Http\Message\RequestInterface Chris@0: * and array of transfer options, and must return a Chris@0: * GuzzleHttp\Promise\PromiseInterface that is fulfilled with a Chris@0: * Psr7\Http\Message\ResponseInterface on success. "handler" is a Chris@0: * constructor only option that cannot be overridden in per/request Chris@0: * options. If no handler is provided, a default handler will be created Chris@0: * that enables all of the request options below by attaching all of the Chris@0: * default middleware to the handler. Chris@0: * - base_uri: (string|UriInterface) Base URI of the client that is merged Chris@0: * into relative URIs. Can be a string or instance of UriInterface. Chris@0: * - **: any request option Chris@0: * Chris@0: * @param array $config Client configuration settings. Chris@0: * Chris@0: * @see \GuzzleHttp\RequestOptions for a list of available request options. Chris@0: */ Chris@0: public function __construct(array $config = []) Chris@0: { Chris@0: if (!isset($config['handler'])) { Chris@0: $config['handler'] = HandlerStack::create(); Chris@0: } elseif (!is_callable($config['handler'])) { Chris@0: throw new \InvalidArgumentException('handler must be a callable'); Chris@0: } Chris@0: Chris@0: // Convert the base_uri to a UriInterface Chris@0: if (isset($config['base_uri'])) { Chris@0: $config['base_uri'] = Psr7\uri_for($config['base_uri']); Chris@0: } Chris@0: Chris@0: $this->configureDefaults($config); Chris@0: } Chris@0: Chris@0: public function __call($method, $args) Chris@0: { Chris@0: if (count($args) < 1) { Chris@0: throw new \InvalidArgumentException('Magic request methods require a URI and optional options array'); Chris@0: } Chris@0: Chris@0: $uri = $args[0]; Chris@0: $opts = isset($args[1]) ? $args[1] : []; Chris@0: Chris@0: return substr($method, -5) === 'Async' Chris@0: ? $this->requestAsync(substr($method, 0, -5), $uri, $opts) Chris@0: : $this->request($method, $uri, $opts); Chris@0: } Chris@0: Chris@0: public function sendAsync(RequestInterface $request, array $options = []) Chris@0: { Chris@0: // Merge the base URI into the request URI if needed. Chris@0: $options = $this->prepareDefaults($options); Chris@0: Chris@0: return $this->transfer( Chris@0: $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), Chris@0: $options Chris@0: ); Chris@0: } Chris@0: Chris@0: public function send(RequestInterface $request, array $options = []) Chris@0: { Chris@0: $options[RequestOptions::SYNCHRONOUS] = true; Chris@0: return $this->sendAsync($request, $options)->wait(); Chris@0: } Chris@0: Chris@0: public function requestAsync($method, $uri = '', array $options = []) Chris@0: { Chris@0: $options = $this->prepareDefaults($options); Chris@0: // Remove request modifying parameter because it can be done up-front. Chris@0: $headers = isset($options['headers']) ? $options['headers'] : []; Chris@0: $body = isset($options['body']) ? $options['body'] : null; Chris@0: $version = isset($options['version']) ? $options['version'] : '1.1'; Chris@0: // Merge the URI into the base URI. Chris@0: $uri = $this->buildUri($uri, $options); Chris@0: if (is_array($body)) { Chris@0: $this->invalidBody(); Chris@0: } Chris@0: $request = new Psr7\Request($method, $uri, $headers, $body, $version); Chris@0: // Remove the option so that they are not doubly-applied. Chris@0: unset($options['headers'], $options['body'], $options['version']); Chris@0: Chris@0: return $this->transfer($request, $options); Chris@0: } Chris@0: Chris@0: public function request($method, $uri = '', array $options = []) Chris@0: { Chris@0: $options[RequestOptions::SYNCHRONOUS] = true; Chris@0: return $this->requestAsync($method, $uri, $options)->wait(); Chris@0: } Chris@0: Chris@0: public function getConfig($option = null) Chris@0: { Chris@0: return $option === null Chris@0: ? $this->config Chris@0: : (isset($this->config[$option]) ? $this->config[$option] : null); Chris@0: } Chris@0: Chris@0: private function buildUri($uri, array $config) Chris@0: { Chris@0: // for BC we accept null which would otherwise fail in uri_for Chris@0: $uri = Psr7\uri_for($uri === null ? '' : $uri); Chris@0: Chris@0: if (isset($config['base_uri'])) { Chris@0: $uri = Psr7\UriResolver::resolve(Psr7\uri_for($config['base_uri']), $uri); Chris@0: } Chris@0: Chris@0: return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Configures the default options for a client. Chris@0: * Chris@0: * @param array $config Chris@0: */ Chris@0: private function configureDefaults(array $config) Chris@0: { Chris@0: $defaults = [ Chris@0: 'allow_redirects' => RedirectMiddleware::$defaultSettings, Chris@0: 'http_errors' => true, Chris@0: 'decode_content' => true, Chris@0: 'verify' => true, Chris@0: 'cookies' => false Chris@0: ]; Chris@0: Chris@0: // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. Chris@0: Chris@0: // We can only trust the HTTP_PROXY environment variable in a CLI Chris@0: // process due to the fact that PHP has no reliable mechanism to Chris@0: // get environment variables that start with "HTTP_". Chris@0: if (php_sapi_name() == 'cli' && getenv('HTTP_PROXY')) { Chris@0: $defaults['proxy']['http'] = getenv('HTTP_PROXY'); Chris@0: } Chris@0: Chris@0: if ($proxy = getenv('HTTPS_PROXY')) { Chris@0: $defaults['proxy']['https'] = $proxy; Chris@0: } Chris@0: Chris@0: if ($noProxy = getenv('NO_PROXY')) { Chris@0: $cleanedNoProxy = str_replace(' ', '', $noProxy); Chris@0: $defaults['proxy']['no'] = explode(',', $cleanedNoProxy); Chris@0: } Chris@0: Chris@0: $this->config = $config + $defaults; Chris@0: Chris@0: if (!empty($config['cookies']) && $config['cookies'] === true) { Chris@0: $this->config['cookies'] = new CookieJar(); Chris@0: } Chris@0: Chris@0: // Add the default user-agent header. Chris@0: if (!isset($this->config['headers'])) { Chris@0: $this->config['headers'] = ['User-Agent' => default_user_agent()]; Chris@0: } else { Chris@0: // Add the User-Agent header if one was not already set. Chris@0: foreach (array_keys($this->config['headers']) as $name) { Chris@0: if (strtolower($name) === 'user-agent') { Chris@0: return; Chris@0: } Chris@0: } Chris@0: $this->config['headers']['User-Agent'] = default_user_agent(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Merges default options into the array. Chris@0: * Chris@0: * @param array $options Options to modify by reference Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function prepareDefaults($options) Chris@0: { Chris@0: $defaults = $this->config; Chris@0: Chris@0: if (!empty($defaults['headers'])) { Chris@0: // Default headers are only added if they are not present. Chris@0: $defaults['_conditional'] = $defaults['headers']; Chris@0: unset($defaults['headers']); Chris@0: } Chris@0: Chris@0: // Special handling for headers is required as they are added as Chris@0: // conditional headers and as headers passed to a request ctor. Chris@0: if (array_key_exists('headers', $options)) { Chris@0: // Allows default headers to be unset. Chris@0: if ($options['headers'] === null) { Chris@0: $defaults['_conditional'] = null; Chris@0: unset($options['headers']); Chris@0: } elseif (!is_array($options['headers'])) { Chris@0: throw new \InvalidArgumentException('headers must be an array'); Chris@0: } Chris@0: } Chris@0: Chris@0: // Shallow merge defaults underneath options. Chris@0: $result = $options + $defaults; Chris@0: Chris@0: // Remove null values. Chris@0: foreach ($result as $k => $v) { Chris@0: if ($v === null) { Chris@0: unset($result[$k]); Chris@0: } Chris@0: } Chris@0: Chris@0: return $result; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Transfers the given request and applies request options. Chris@0: * Chris@0: * The URI of the request is not modified and the request options are used Chris@0: * as-is without merging in default options. Chris@0: * Chris@0: * @param RequestInterface $request Chris@0: * @param array $options Chris@0: * Chris@0: * @return Promise\PromiseInterface Chris@0: */ Chris@0: private function transfer(RequestInterface $request, array $options) Chris@0: { Chris@0: // save_to -> sink Chris@0: if (isset($options['save_to'])) { Chris@0: $options['sink'] = $options['save_to']; Chris@0: unset($options['save_to']); Chris@0: } Chris@0: Chris@0: // exceptions -> http_errors Chris@0: if (isset($options['exceptions'])) { Chris@0: $options['http_errors'] = $options['exceptions']; Chris@0: unset($options['exceptions']); Chris@0: } Chris@0: Chris@0: $request = $this->applyOptions($request, $options); Chris@0: $handler = $options['handler']; Chris@0: Chris@0: try { Chris@0: return Promise\promise_for($handler($request, $options)); Chris@0: } catch (\Exception $e) { Chris@0: return Promise\rejection_for($e); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Applies the array of request options to a request. Chris@0: * Chris@0: * @param RequestInterface $request Chris@0: * @param array $options Chris@0: * Chris@0: * @return RequestInterface Chris@0: */ Chris@0: private function applyOptions(RequestInterface $request, array &$options) Chris@0: { Chris@13: $modify = [ Chris@13: 'set_headers' => [], Chris@13: ]; Chris@13: Chris@13: if (isset($options['headers'])) { Chris@13: $modify['set_headers'] = $options['headers']; Chris@13: unset($options['headers']); Chris@13: } Chris@0: Chris@0: if (isset($options['form_params'])) { Chris@0: if (isset($options['multipart'])) { Chris@0: throw new \InvalidArgumentException('You cannot use ' Chris@0: . 'form_params and multipart at the same time. Use the ' Chris@0: . 'form_params option if you want to send application/' Chris@0: . 'x-www-form-urlencoded requests, and the multipart ' Chris@0: . 'option to send multipart/form-data requests.'); Chris@0: } Chris@0: $options['body'] = http_build_query($options['form_params'], '', '&'); Chris@0: unset($options['form_params']); Chris@13: // Ensure that we don't have the header in different case and set the new value. Chris@13: $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); Chris@0: $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; Chris@0: } Chris@0: Chris@0: if (isset($options['multipart'])) { Chris@0: $options['body'] = new Psr7\MultipartStream($options['multipart']); Chris@0: unset($options['multipart']); Chris@0: } Chris@0: Chris@0: if (isset($options['json'])) { Chris@0: $options['body'] = \GuzzleHttp\json_encode($options['json']); Chris@0: unset($options['json']); Chris@13: // Ensure that we don't have the header in different case and set the new value. Chris@13: $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); Chris@0: $options['_conditional']['Content-Type'] = 'application/json'; Chris@0: } Chris@0: Chris@0: if (!empty($options['decode_content']) Chris@0: && $options['decode_content'] !== true Chris@0: ) { Chris@13: // Ensure that we don't have the header in different case and set the new value. Chris@13: $options['_conditional'] = Psr7\_caseless_remove(['Accept-Encoding'], $options['_conditional']); Chris@0: $modify['set_headers']['Accept-Encoding'] = $options['decode_content']; Chris@0: } Chris@0: Chris@0: if (isset($options['body'])) { Chris@0: if (is_array($options['body'])) { Chris@0: $this->invalidBody(); Chris@0: } Chris@0: $modify['body'] = Psr7\stream_for($options['body']); Chris@0: unset($options['body']); Chris@0: } Chris@0: Chris@0: if (!empty($options['auth']) && is_array($options['auth'])) { Chris@0: $value = $options['auth']; Chris@0: $type = isset($value[2]) ? strtolower($value[2]) : 'basic'; Chris@0: switch ($type) { Chris@0: case 'basic': Chris@13: // Ensure that we don't have the header in different case and set the new value. Chris@13: $modify['set_headers'] = Psr7\_caseless_remove(['Authorization'], $modify['set_headers']); Chris@0: $modify['set_headers']['Authorization'] = 'Basic ' Chris@0: . base64_encode("$value[0]:$value[1]"); Chris@0: break; Chris@0: case 'digest': Chris@0: // @todo: Do not rely on curl Chris@0: $options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST; Chris@0: $options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]"; Chris@0: break; Chris@0: case 'ntlm': Chris@0: $options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_NTLM; Chris@0: $options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]"; Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: if (isset($options['query'])) { Chris@0: $value = $options['query']; Chris@0: if (is_array($value)) { Chris@0: $value = http_build_query($value, null, '&', PHP_QUERY_RFC3986); Chris@0: } Chris@0: if (!is_string($value)) { Chris@0: throw new \InvalidArgumentException('query must be a string or array'); Chris@0: } Chris@0: $modify['query'] = $value; Chris@0: unset($options['query']); Chris@0: } Chris@0: Chris@0: // Ensure that sink is not an invalid value. Chris@0: if (isset($options['sink'])) { Chris@0: // TODO: Add more sink validation? Chris@0: if (is_bool($options['sink'])) { Chris@0: throw new \InvalidArgumentException('sink must not be a boolean'); Chris@0: } Chris@0: } Chris@0: Chris@0: $request = Psr7\modify_request($request, $modify); Chris@0: if ($request->getBody() instanceof Psr7\MultipartStream) { Chris@0: // Use a multipart/form-data POST if a Content-Type is not set. Chris@13: // Ensure that we don't have the header in different case and set the new value. Chris@13: $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); Chris@0: $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' Chris@0: . $request->getBody()->getBoundary(); Chris@0: } Chris@0: Chris@0: // Merge in conditional headers if they are not present. Chris@0: if (isset($options['_conditional'])) { Chris@0: // Build up the changes so it's in a single clone of the message. Chris@0: $modify = []; Chris@0: foreach ($options['_conditional'] as $k => $v) { Chris@0: if (!$request->hasHeader($k)) { Chris@0: $modify['set_headers'][$k] = $v; Chris@0: } Chris@0: } Chris@0: $request = Psr7\modify_request($request, $modify); Chris@0: // Don't pass this internal value along to middleware/handlers. Chris@0: unset($options['_conditional']); Chris@0: } Chris@0: Chris@0: return $request; Chris@0: } Chris@0: Chris@0: private function invalidBody() Chris@0: { Chris@0: throw new \InvalidArgumentException('Passing in the "body" request ' Chris@0: . 'option as an array to send a POST request has been deprecated. ' Chris@0: . 'Please use the "form_params" request option to send a ' Chris@0: . 'application/x-www-form-urlencoded request, or the "multipart" ' Chris@0: . 'request option to send a multipart/form-data request.'); Chris@0: } Chris@0: }