├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Client.php ├── CurlFormatter.php ├── CurlTransport.php ├── Exception.php ├── FormatterInterface.php ├── JsonFormatter.php ├── JsonParser.php ├── Message.php ├── MockTransport.php ├── ParserInterface.php ├── Request.php ├── RequestEvent.php ├── Response.php ├── StreamTransport.php ├── Transport.php ├── UrlEncodedFormatter.php ├── UrlEncodedParser.php ├── XmlFormatter.php ├── XmlParser.php └── debug ├── HttpClientPanel.php ├── RequestExecuteAction.php ├── SearchModel.php └── views ├── detail.php └── summary.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Yii Framework 2 HTTP client extension Change Log 2 | ================================================ 3 | 4 | 2.0.17 under development 5 | ------------------------ 6 | 7 | - no changes in this release. 8 | 9 | 10 | 2.0.16 February 13, 2025 11 | ------------------------ 12 | 13 | - Bug #240: Fixed `\yii\httpclient\Response::getIsOk()` to include entire 2xx response code range (rhertogh) 14 | - Enh #239: Support for PHP 8.1 (rhertogh) 15 | 16 | 17 | 2.0.15 May 22, 2023 18 | ------------------- 19 | 20 | - Bug #224: Parse content when it is not an empty string (pawmaster) 21 | - Bug #226: Fix error in debug panel due to PHP 8.1 deprecation of implicit float to int conversion (lacek) 22 | 23 | 24 | 2.0.14 August 09, 2021 25 | ---------------------- 26 | 27 | - Enh #215: Added possibility to skip charset in header on `UrlEncodedFormatter::format()` (egorrishe) 28 | - Enh #216: Use `random_int()` when generating boundary (samdark) 29 | 30 | 31 | 2.0.13 December 23, 2020 32 | ------------------------ 33 | 34 | - Bug #209: Fixed error code, thrown by Exception in `CurlTransport` (kwazaro) 35 | 36 | 37 | 2.0.12 October 08, 2019 38 | ----------------------- 39 | 40 | - Enh #192: Implement `Request::responseTime()` which returns the seconds (microtime precession) elapsed from request to response (HenryVolkmer) 41 | 42 | 43 | 2.0.11 May 14, 2019 44 | ------------------- 45 | 46 | - Bug #189: Fixed Content-Length header when using `CURLOPT_INFILE` option (alexkart) 47 | 48 | 49 | 2.0.10 April 30, 2019 50 | --------------------- 51 | 52 | - Enh #167: Added support of multiple parameters with the same name for multipart requests (alexkart) 53 | 54 | 55 | 2.0.9 April 23, 2019 56 | -------------------- 57 | 58 | - Bug #149: Fixed type error in `StreamTransport` when `$http_response_header = null` (alexkart) 59 | - Bug #171: Added "Content-Length: 0" header when sending request with empty body (alexkart) 60 | - Enh #66: Added `CURLOPT_FILE` option support to `CurlTransport` (alexkart) 61 | - Enh #85: Added `CurlFormatter` in order to support `CURLFile` for uploading files (alexkart) 62 | 63 | 64 | 2.0.8 April 16, 2019 65 | -------------------- 66 | 67 | - Bug #168: `Response::detectFormatByContent` falsely detected HTML as XML (CeBe) 68 | - Bug #173: Added extra check to `Message::addData()` to prevent error on trying to merge non-array (samdark) 69 | - Enh #153: Allow configuring `JsonParser` to parse JSON as objects instead of arrays (CeBe) 70 | - Enh #174: Add `MockTransport` for test environments (Slamdunk) 71 | 72 | 73 | 2.0.7 September 24, 2018 74 | ------------------------ 75 | 76 | - Bug #165: `Response::detectFormatByContent` now detects JSON Array (germanow) 77 | - Enh #156: Added `Request::setFullUrl()` return reference (vuongxuongminh) 78 | 79 | 80 | 2.0.6 February 13, 2018 81 | ----------------------- 82 | 83 | - Bug #129: Fixed `Message::getHeaders()` unable to parse HTTP status code in case reason phrase contains `:` character (lan143) 84 | - Enh #142: `Request::createFullUrl()` now prevents appearance of multiple slashes while combining `Client::$baseUrl` and `Request::$url` (zhangdi) 85 | 86 | 87 | 2.0.5 November 03, 2017 88 | ----------------------- 89 | 90 | - Bug #128: Fixed `Response` with redirection takes wrong 'Content-Type' header value for content parsing (klimov-paul) 91 | - Bug: Usage of deprecated `yii\base\Object` changed to `yii\base\BaseObject` allowing compatibility with PHP 7.2 (klimov-paul) 92 | - Enh #119: Options for client SSL certificate specification added to `Request::$options` (bscheshirwork) 93 | - Chg #118: Enforced cookie validation removed from `Request` (klimov-paul) 94 | 95 | 96 | 2.0.4 June 23, 2017 97 | ------------------- 98 | 99 | - Bug #94: Fixed `XmlParser` does not respects character encoding from response headers (klimov-paul) 100 | - Bug #98: Fixed `Request::composeCookieHeader()` no longer performs url-encoding over cookie value (klimov-paul) 101 | - Bug #99: Fixed `Request::$content` is set to empty string by `UrlEncodedFormatter` and `JsonFormatter` from empty data (klimov-paul) 102 | - Bug #102: Fixed `XmlParser` does not converts `\SimpleXMLElement` into array for the grouped tags (kids-return) 103 | 104 | 105 | 2.0.3 February 15, 2017 106 | ----------------------- 107 | 108 | - Bug #74: Fixed unable to reuse `Request` instance for sending several requests with different data (klimov-paul) 109 | - Bug #76: Fixed `HttpClientPanel` triggers `E_WARNING` on attempt to view history debug entry, generated without panel being attached (klimov-paul) 110 | - Bug #79: Fixed inability to use URL with query parameters as `Client::$baseUrl` (klimov-paul) 111 | - Bug #81: Fixed invalid Content-Disposition header in multipart request (cebe, PowerGamer1) 112 | - Bug #87: Fixed `Request::addOptions()` unable to override already set CURL options (klimov-paul) 113 | - Bug #88: Fixed `UrlEncodedFormatter` duplicates GET parameters during multiple request preparations (klimov-paul) 114 | 115 | 116 | 2.0.2 October 31, 2016 117 | ---------------------- 118 | 119 | - Bug #61: Response headers extraction at `StreamTransport` changed to use `$http_response_header` to be more reliable (klimov-paul) 120 | - Bug #70: Fixed `Request::toString()` triggers `E_NOTICE` for not prepared request (klimov-paul) 121 | - Bug #73: Fixed `Response::detectFormatByContent()` unable to detect URL-encoded format, if source content contains `|` symbol (klimov-paul) 122 | 123 | 124 | 2.0.1 August 04, 2016 125 | --------------------- 126 | 127 | - Bug #44: Fixed exception name collision at `Response` and `Transport` (cebe) 128 | - Bug #45: Fixed `XmlFormatter` unable to handle array with numeric keys (klimov-paul) 129 | - Bug #53: Fixed `XmlParser` unable to parse value wrapped with 'CDATA' (DrDeath72) 130 | - Bug #55: Fixed invalid display of Debug Toolbar summary block (rahimov) 131 | - Enh #43: Events `EVENT_BEFORE_SEND` and `EVENT_AFTER_SEND` added to `Request` and `Client` (klimov-paul) 132 | - Enh #46: Added `Request::getFullUrl()` allowing getting the full actual request URL (klimov-paul) 133 | - Enh #47: Added `Message::addData()` allowing addition of the content data to already existing one (klimov-paul) 134 | - Enh #50: Option 'protocolVersion' added to `Request::$options` allowing specification of the HTTP protocol version (klimov-paul) 135 | - Enh #58: Added `UrlEncodedFormatter::$charset` allowing specification of content charset (klimov-paul) 136 | - Enh: Added `XmlFormatter::useTraversableAsArray` allowing processing `\Traversable` as array (klimov-paul) 137 | 138 | 139 | 2.0.0.1 July 01, 2016 140 | --------------------- 141 | 142 | - Enh: Fixed PHPdoc annotations (cebe) 143 | 144 | 145 | 2.0.0 July 1, 2016 146 | ------------------ 147 | 148 | - Initial release. 149 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software LLC nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

HTTP Client Extension for Yii 2

6 |
7 |

8 | 9 | This extension provides the HTTP client for the [Yii framework 2.0](https://www.yiiframework.com). 10 | 11 | For license information check the [LICENSE](LICENSE.md)-file. 12 | 13 | Documentation is at [docs/guide/README.md](docs/guide/README.md). 14 | 15 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/yii2-httpclient/v)](https://packagist.org/packages/yiisoft/yii2-httpclient) 16 | [![Total Downloads](https://poser.pugx.org/yiisoft/yii2-httpclient/downloads)](https://packagist.org/packages/yiisoft/yii2-httpclient) 17 | [![Build Status](https://github.com/yiisoft/yii2-httpclient/workflows/build/badge.svg)](https://github.com/yiisoft/yii2-httpclient/actions) 18 | 19 | 20 | Installation 21 | ------------ 22 | 23 | The preferred way to install this extension is through [composer](https://getcomposer.org/download/). 24 | 25 | Either run 26 | 27 | ``` 28 | php composer.phar require --prefer-dist yiisoft/yii2-httpclient 29 | ``` 30 | 31 | or add 32 | 33 | ``` 34 | "yiisoft/yii2-httpclient": "~2.0.0" 35 | ``` 36 | 37 | to the require section of your composer.json. 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/yii2-httpclient", 3 | "description": "HTTP client extension for the Yii framework", 4 | "keywords": ["yii2", "http", "httpclient", "curl"], 5 | "type": "yii2-extension", 6 | "license": "BSD-3-Clause", 7 | "support": { 8 | "issues": "https://github.com/yiisoft/yii2-httpclient/issues", 9 | "forum": "https://www.yiiframework.com/forum/", 10 | "wiki": "https://www.yiiframework.com/wiki/", 11 | "irc": "ircs://irc.libera.chat:6697/yii", 12 | "source": "https://github.com/yiisoft/yii2-httpclient" 13 | }, 14 | "authors": [ 15 | { 16 | "name": "Paul Klimov", 17 | "email": "klimov.paul@gmail.com" 18 | } 19 | ], 20 | "require": { 21 | "yiisoft/yii2": "~2.0.13", 22 | "paragonie/random_compat": ">=1" 23 | }, 24 | "require-dev": { 25 | "cweagans/composer-patches": "^1.7", 26 | "phpunit/phpunit": "4.8.34" 27 | }, 28 | "repositories": [ 29 | { 30 | "type": "composer", 31 | "url": "https://asset-packagist.org" 32 | } 33 | ], 34 | "config": { 35 | "allow-plugins": { 36 | "cweagans/composer-patches": true, 37 | "yiisoft/yii2-composer": true 38 | } 39 | }, 40 | "autoload": { 41 | "psr-4": { "yii\\httpclient\\": "src" } 42 | }, 43 | "extra": { 44 | "branch-alias": { 45 | "dev-master": "2.0.x-dev" 46 | }, 47 | "composer-exit-on-patch-failure": true, 48 | "patches": { 49 | "phpunit/phpunit-mock-objects": { 50 | "Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch" 51 | }, 52 | "phpunit/phpunit": { 53 | "Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch", 54 | "Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch" 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 22 | * @since 2.0 23 | */ 24 | class Client extends Component 25 | { 26 | /** 27 | * @event RequestEvent an event raised right before sending request. 28 | */ 29 | const EVENT_BEFORE_SEND = 'beforeSend'; 30 | /** 31 | * @event RequestEvent an event raised right after request has been sent. 32 | */ 33 | const EVENT_AFTER_SEND = 'afterSend'; 34 | /** 35 | * JSON format 36 | */ 37 | const FORMAT_JSON = 'json'; 38 | /** 39 | * urlencoded by RFC1738 query string, like name1=value1&name2=value2 40 | * @see https://php.net/manual/en/function.urlencode.php 41 | */ 42 | const FORMAT_URLENCODED = 'urlencoded'; 43 | /** 44 | * urlencoded by PHP_QUERY_RFC3986 query string, like name1=value1&name2=value2 45 | * @see https://php.net/manual/en/function.rawurlencode.php 46 | */ 47 | const FORMAT_RAW_URLENCODED = 'raw-urlencoded'; 48 | /** 49 | * XML format 50 | */ 51 | const FORMAT_XML = 'xml'; 52 | /** 53 | * CURL format 54 | * @since 2.0.9 55 | */ 56 | const FORMAT_CURL = 'curl'; 57 | 58 | /** 59 | * @var string base request URL. 60 | */ 61 | public $baseUrl; 62 | /** 63 | * @var array the formatters for converting data into the content of the specified [[format]]. 64 | * The array keys are the format names, and the array values are the corresponding configurations 65 | * for creating the formatter objects. 66 | */ 67 | public $formatters = []; 68 | /** 69 | * @var array the parsers for converting content of the specified [[format]] into the data. 70 | * The array keys are the format names, and the array values are the corresponding configurations 71 | * for creating the parser objects. 72 | */ 73 | public $parsers = []; 74 | /** 75 | * @var array request object configuration. 76 | */ 77 | public $requestConfig = []; 78 | /** 79 | * @var array response config configuration. 80 | */ 81 | public $responseConfig = []; 82 | /** 83 | * @var int maximum symbols count of the request content, which should be taken to compose a 84 | * log and profile messages. Exceeding content will be truncated. 85 | * @see createRequestLogToken() 86 | */ 87 | public $contentLoggingMaxSize = 2000; 88 | 89 | /** 90 | * @var Transport|array|string|callable HTTP message transport. 91 | */ 92 | private $_transport = 'yii\httpclient\StreamTransport'; 93 | 94 | 95 | /** 96 | * Sets the HTTP message transport. It can be specified in one of the following forms: 97 | * 98 | * - an instance of `Transport`: actual transport object to be used 99 | * - a string: representing the class name of the object to be created 100 | * - a configuration array: the array must contain a `class` element which is treated as the object class, 101 | * and the rest of the name-value pairs will be used to initialize the corresponding object properties 102 | * - a PHP callable: either an anonymous function or an array representing a class method (`[$class or $object, $method]`). 103 | * The callable should return a new instance of the object being created. 104 | * @param Transport|array|string $transport HTTP message transport 105 | */ 106 | public function setTransport($transport) 107 | { 108 | $this->_transport = $transport; 109 | } 110 | 111 | /** 112 | * @return Transport HTTP message transport instance. 113 | * @throws \yii\base\InvalidConfigException 114 | */ 115 | public function getTransport() 116 | { 117 | if (!is_object($this->_transport)) { 118 | $this->_transport = Yii::createObject($this->_transport); 119 | } 120 | return $this->_transport; 121 | } 122 | 123 | /** 124 | * Returns HTTP message formatter instance for the specified format. 125 | * @param string $format format name. 126 | * @return FormatterInterface formatter instance. 127 | * @throws InvalidParamException on invalid format name. 128 | * @throws \yii\base\InvalidConfigException 129 | */ 130 | public function getFormatter($format) 131 | { 132 | static $defaultFormatters = [ 133 | self::FORMAT_JSON => 'yii\httpclient\JsonFormatter', 134 | self::FORMAT_URLENCODED => [ 135 | 'class' => 'yii\httpclient\UrlEncodedFormatter', 136 | 'encodingType' => PHP_QUERY_RFC1738 137 | ], 138 | self::FORMAT_RAW_URLENCODED => [ 139 | 'class' => 'yii\httpclient\UrlEncodedFormatter', 140 | 'encodingType' => PHP_QUERY_RFC3986 141 | ], 142 | self::FORMAT_XML => 'yii\httpclient\XmlFormatter', 143 | self::FORMAT_CURL => 'yii\httpclient\CurlFormatter', 144 | ]; 145 | 146 | if (!isset($this->formatters[$format])) { 147 | if (!isset($defaultFormatters[$format])) { 148 | throw new InvalidParamException("Unrecognized format '{$format}'"); 149 | } 150 | $this->formatters[$format] = $defaultFormatters[$format]; 151 | } 152 | 153 | if (!is_object($this->formatters[$format])) { 154 | $this->formatters[$format] = Yii::createObject($this->formatters[$format]); 155 | } 156 | 157 | return $this->formatters[$format]; 158 | } 159 | 160 | /** 161 | * Returns HTTP message parser instance for the specified format. 162 | * @param string $format format name 163 | * @return ParserInterface parser instance. 164 | * @throws InvalidParamException on invalid format name. 165 | * @throws \yii\base\InvalidConfigException 166 | */ 167 | public function getParser($format) 168 | { 169 | static $defaultParsers = [ 170 | self::FORMAT_JSON => 'yii\httpclient\JsonParser', 171 | self::FORMAT_URLENCODED => 'yii\httpclient\UrlEncodedParser', 172 | self::FORMAT_RAW_URLENCODED => 'yii\httpclient\UrlEncodedParser', 173 | self::FORMAT_XML => 'yii\httpclient\XmlParser', 174 | ]; 175 | 176 | if (!isset($this->parsers[$format])) { 177 | if (!isset($defaultParsers[$format])) { 178 | throw new InvalidParamException("Unrecognized format '{$format}'"); 179 | } 180 | $this->parsers[$format] = $defaultParsers[$format]; 181 | } 182 | 183 | if (!is_object($this->parsers[$format])) { 184 | $this->parsers[$format] = Yii::createObject($this->parsers[$format]); 185 | } 186 | 187 | return $this->parsers[$format]; 188 | } 189 | 190 | /** 191 | * @return Request request instance. 192 | * @throws \yii\base\InvalidConfigException 193 | */ 194 | public function createRequest() 195 | { 196 | $config = $this->requestConfig; 197 | if (!isset($config['class'])) { 198 | $config['class'] = Request::className(); 199 | } 200 | $config['client'] = $this; 201 | return Yii::createObject($config); 202 | } 203 | 204 | /** 205 | * Creates a response instance. 206 | * @param string $content raw content 207 | * @param array $headers headers list. 208 | * @return Response request instance. 209 | * @throws \yii\base\InvalidConfigException 210 | */ 211 | public function createResponse($content = null, array $headers = []) 212 | { 213 | $config = $this->responseConfig; 214 | if (!isset($config['class'])) { 215 | $config['class'] = Response::className(); 216 | } 217 | $config['client'] = $this; 218 | $response = Yii::createObject($config); 219 | $response->setContent($content); 220 | $response->setHeaders($headers); 221 | return $response; 222 | } 223 | 224 | /** 225 | * Performs given request. 226 | * @param Request $request request to be sent. 227 | * @return Response response instance. 228 | * @throws Exception on failure. 229 | * @throws \yii\base\InvalidConfigException 230 | */ 231 | public function send($request) 232 | { 233 | return $this->getTransport()->send($request); 234 | } 235 | 236 | /** 237 | * Performs multiple HTTP requests in parallel. 238 | * This method accepts an array of the [[Request]] objects and returns an array of the [[Response]] objects. 239 | * Keys of the response array correspond the ones from request array. 240 | * 241 | * ```php 242 | * $client = new Client(); 243 | * $requests = [ 244 | * 'news' => $client->get('http://domain.com/news'), 245 | * 'friends' => $client->get('http://domain.com/user/friends', ['userId' => 12]), 246 | * ]; 247 | * $responses = $client->batchSend($requests); 248 | * var_dump($responses['news']->isOk); 249 | * var_dump($responses['friends']->isOk); 250 | * ``` 251 | * 252 | * @param Request[] $requests requests to perform. 253 | * @return Response[] responses list. 254 | * @throws Exception 255 | * @throws \yii\base\InvalidConfigException 256 | */ 257 | public function batchSend(array $requests) 258 | { 259 | return $this->getTransport()->batchSend($requests); 260 | } 261 | 262 | /** 263 | * Composes the log/profiling message token for the given HTTP request parameters. 264 | * This method should be used by transports during request sending logging. 265 | * @param string $method request method name. 266 | * @param string $url request URL. 267 | * @param array $headers request headers. 268 | * @param string $content request content. 269 | * @return string log token. 270 | */ 271 | public function createRequestLogToken($method, $url, $headers, $content) 272 | { 273 | $token = strtoupper($method) . ' ' . $url; 274 | if (!empty($headers)) { 275 | $token .= "\n" . implode("\n", $headers); 276 | } 277 | if ($content !== null) { 278 | $token .= "\n\n" . StringHelper::truncate($content, $this->contentLoggingMaxSize); 279 | } 280 | return $token; 281 | } 282 | 283 | // Create request shortcut methods : 284 | 285 | /** 286 | * Creates 'GET' request. 287 | * @param array|string $url target URL. 288 | * @param array|string $data if array - request data, otherwise - request content. 289 | * @param array $headers request headers. 290 | * @param array $options request options. 291 | * @return Request request instance. 292 | */ 293 | public function get($url, $data = null, $headers = [], $options = []) 294 | { 295 | return $this->createRequestShortcut('GET', $url, $data, $headers, $options); 296 | } 297 | 298 | /** 299 | * Creates 'POST' request. 300 | * @param array|string $url target URL. 301 | * @param array|string $data if array - request data, otherwise - request content. 302 | * @param array $headers request headers. 303 | * @param array $options request options. 304 | * @return Request request instance. 305 | */ 306 | public function post($url, $data = null, $headers = [], $options = []) 307 | { 308 | return $this->createRequestShortcut('POST', $url, $data, $headers, $options); 309 | } 310 | 311 | /** 312 | * Creates 'PUT' request. 313 | * @param array|string $url target URL. 314 | * @param array|string $data if array - request data, otherwise - request content. 315 | * @param array $headers request headers. 316 | * @param array $options request options. 317 | * @return Request request instance. 318 | */ 319 | public function put($url, $data = null, $headers = [], $options = []) 320 | { 321 | return $this->createRequestShortcut('PUT', $url, $data, $headers, $options); 322 | } 323 | 324 | /** 325 | * Creates 'PATCH' request. 326 | * @param array|string $url target URL. 327 | * @param array|string $data if array - request data, otherwise - request content. 328 | * @param array $headers request headers. 329 | * @param array $options request options. 330 | * @return Request request instance. 331 | */ 332 | public function patch($url, $data = null, $headers = [], $options = []) 333 | { 334 | return $this->createRequestShortcut('PATCH', $url, $data, $headers, $options); 335 | } 336 | 337 | /** 338 | * Creates 'DELETE' request. 339 | * @param array|string $url target URL. 340 | * @param array|string $data if array - request data, otherwise - request content. 341 | * @param array $headers request headers. 342 | * @param array $options request options. 343 | * @return Request request instance. 344 | */ 345 | public function delete($url, $data = null, $headers = [], $options = []) 346 | { 347 | return $this->createRequestShortcut('DELETE', $url, $data, $headers, $options); 348 | } 349 | 350 | /** 351 | * Creates 'HEAD' request. 352 | * @param array|string $url target URL. 353 | * @param array $headers request headers. 354 | * @param array $options request options. 355 | * @return Request request instance. 356 | */ 357 | public function head($url, $headers = [], $options = []) 358 | { 359 | return $this->createRequestShortcut('HEAD', $url, null, $headers, $options); 360 | } 361 | 362 | /** 363 | * Creates 'OPTIONS' request. 364 | * @param array|string $url target URL. 365 | * @param array $options request options. 366 | * @return Request request instance. 367 | */ 368 | public function options($url, $options = []) 369 | { 370 | return $this->createRequestShortcut('OPTIONS', $url, null, [], $options); 371 | } 372 | 373 | /** 374 | * This method is invoked right before request is sent. 375 | * The method will trigger the [[EVENT_BEFORE_SEND]] event. 376 | * @param Request $request request instance. 377 | * @since 2.0.1 378 | */ 379 | public function beforeSend($request) 380 | { 381 | $event = new RequestEvent(); 382 | $event->request = $request; 383 | $this->trigger(self::EVENT_BEFORE_SEND, $event); 384 | } 385 | 386 | /** 387 | * This method is invoked right after request is sent. 388 | * The method will trigger the [[EVENT_AFTER_SEND]] event. 389 | * @param Request $request request instance. 390 | * @param Response $response received response instance. 391 | * @since 2.0.1 392 | */ 393 | public function afterSend($request, $response) 394 | { 395 | $event = new RequestEvent(); 396 | $event->request = $request; 397 | $event->response = $response; 398 | $this->trigger(self::EVENT_AFTER_SEND, $event); 399 | } 400 | 401 | /** 402 | * @param string $method 403 | * @param array|string $url 404 | * @param array|string $data 405 | * @param array $headers 406 | * @param array $options 407 | * @return Request request instance. 408 | * @throws \yii\base\InvalidConfigException 409 | */ 410 | protected function createRequestShortcut($method, $url, $data, $headers, $options) 411 | { 412 | $request = $this->createRequest() 413 | ->setMethod($method) 414 | ->setUrl($url) 415 | ->addHeaders($headers) 416 | ->addOptions($options); 417 | if (is_array($data)) { 418 | $request->setData($data); 419 | } else { 420 | $request->setContent($data); 421 | } 422 | return $request; 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /src/CurlFormatter.php: -------------------------------------------------------------------------------- 1 | getData(); 39 | 40 | if (strcasecmp('GET', $request->getMethod()) === 0) { 41 | if ($data !== null) { 42 | $content = http_build_query((array)$data, '', '&', $this->encodingType); 43 | $request->setFullUrl(null); 44 | $url = $request->getFullUrl(); 45 | $url .= (strpos($url, '?') === false) ? '?' : '&'; 46 | $url .= $content; 47 | $request->setFullUrl($url); 48 | } 49 | return $request; 50 | } 51 | 52 | if ($data !== null) { 53 | $request->setContent($data); 54 | } else { 55 | $request->getHeaders()->set('Content-Length', '0'); 56 | } 57 | 58 | return $request; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/CurlTransport.php: -------------------------------------------------------------------------------- 1 | 20 | * @since 2.0 21 | */ 22 | class CurlTransport extends Transport 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function send($request) 28 | { 29 | $request->beforeSend(); 30 | 31 | $curlOptions = $this->prepare($request); 32 | $curlResource = $this->initCurl($curlOptions); 33 | 34 | $responseHeaders = []; 35 | $this->setHeaderOutput($curlResource, $responseHeaders); 36 | 37 | $token = $request->client->createRequestLogToken($request->getMethod(), $curlOptions[CURLOPT_URL], $curlOptions[CURLOPT_HTTPHEADER], print_r($request->getContent(), true)); 38 | Yii::info($token, __METHOD__); 39 | Yii::beginProfile($token, __METHOD__); 40 | $responseContent = curl_exec($curlResource); 41 | Yii::endProfile($token, __METHOD__); 42 | 43 | // check cURL error 44 | $errorNumber = curl_errno($curlResource); 45 | $errorMessage = curl_error($curlResource); 46 | 47 | curl_close($curlResource); 48 | 49 | if ($errorNumber > 0) { 50 | throw new Exception('Curl error: #' . $errorNumber . ' - ' . $errorMessage, $errorNumber); 51 | } 52 | 53 | $response = $request->client->createResponse($responseContent, $responseHeaders); 54 | 55 | $request->afterSend($response); 56 | 57 | return $response; 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function batchSend(array $requests) 64 | { 65 | $curlBatchResource = curl_multi_init(); 66 | 67 | $token = ''; 68 | $curlResources = []; 69 | $responseHeaders = []; 70 | foreach ($requests as $key => $request) { 71 | /* @var $request Request */ 72 | $request->beforeSend(); 73 | 74 | $curlOptions = $this->prepare($request); 75 | $curlResource = $this->initCurl($curlOptions); 76 | 77 | $token .= $request->client->createRequestLogToken($request->getMethod(), $curlOptions[CURLOPT_URL], $curlOptions[CURLOPT_HTTPHEADER], $request->getContent()) . "\n\n"; 78 | 79 | $responseHeaders[$key] = []; 80 | $this->setHeaderOutput($curlResource, $responseHeaders[$key]); 81 | $curlResources[$key] = $curlResource; 82 | curl_multi_add_handle($curlBatchResource, $curlResource); 83 | } 84 | 85 | Yii::info($token, __METHOD__); 86 | Yii::beginProfile($token, __METHOD__); 87 | 88 | try { 89 | $isRunning = null; 90 | do { 91 | // See https://bugs.php.net/bug.php?id=61141 92 | if (curl_multi_select($curlBatchResource) === -1) { 93 | usleep(100); 94 | } 95 | do { 96 | $curlExecCode = curl_multi_exec($curlBatchResource, $isRunning); 97 | } while ($curlExecCode === CURLM_CALL_MULTI_PERFORM); 98 | } while ($isRunning > 0 && $curlExecCode === CURLM_OK); 99 | } catch (\Exception $e) { 100 | Yii::endProfile($token, __METHOD__); 101 | throw new Exception($e->getMessage(), $e->getCode(), $e); 102 | } 103 | 104 | Yii::endProfile($token, __METHOD__); 105 | 106 | $responseContents = []; 107 | foreach ($curlResources as $key => $curlResource) { 108 | $responseContents[$key] = curl_multi_getcontent($curlResource); 109 | curl_multi_remove_handle($curlBatchResource, $curlResource); 110 | } 111 | 112 | curl_multi_close($curlBatchResource); 113 | 114 | $responses = []; 115 | foreach ($requests as $key => $request) { 116 | $response = $request->client->createResponse($responseContents[$key], $responseHeaders[$key]); 117 | $request->afterSend($response); 118 | $responses[$key] = $response; 119 | } 120 | return $responses; 121 | } 122 | 123 | /** 124 | * Prepare request for execution, creating cURL resource for it. 125 | * @param Request $request request instance. 126 | * @return array cURL options. 127 | */ 128 | private function prepare($request) 129 | { 130 | $request->prepare(); 131 | 132 | $curlOptions = $this->composeCurlOptions($request->getOptions()); 133 | 134 | $method = strtoupper($request->getMethod()); 135 | switch ($method) { 136 | case 'POST': 137 | $curlOptions[CURLOPT_POST] = true; 138 | break; 139 | default: 140 | $curlOptions[CURLOPT_CUSTOMREQUEST] = $method; 141 | } 142 | 143 | $content = $request->getContent(); 144 | 145 | if ($method === 'HEAD') { 146 | $curlOptions[CURLOPT_NOBODY] = true; 147 | } 148 | 149 | if ($content !== null) { 150 | $curlOptions[CURLOPT_POSTFIELDS] = $content; 151 | } 152 | 153 | $curlOptions[CURLOPT_RETURNTRANSFER] = true; 154 | $curlOptions[CURLOPT_URL] = $request->getFullUrl(); 155 | $curlOptions[CURLOPT_HTTPHEADER] = $request->composeHeaderLines(); 156 | 157 | if ($request->getOutputFile()) { 158 | $curlOptions[CURLOPT_FILE] = $request->getOutputFile(); 159 | } 160 | 161 | return $curlOptions; 162 | } 163 | 164 | /** 165 | * Initializes cURL resource. 166 | * @param array $curlOptions cURL options. 167 | * @return resource prepared cURL resource. 168 | */ 169 | private function initCurl(array $curlOptions) 170 | { 171 | $curlResource = curl_init(); 172 | foreach ($curlOptions as $option => $value) { 173 | curl_setopt($curlResource, $option, $value); 174 | } 175 | 176 | return $curlResource; 177 | } 178 | 179 | /** 180 | * Composes cURL options from raw request options. 181 | * @param array $options raw request options. 182 | * @return array cURL options, in format: [curl_constant => value]. 183 | */ 184 | private function composeCurlOptions(array $options) 185 | { 186 | static $optionMap = [ 187 | 'protocolVersion' => CURLOPT_HTTP_VERSION, 188 | 'maxRedirects' => CURLOPT_MAXREDIRS, 189 | 'sslCapath' => CURLOPT_CAPATH, 190 | 'sslCafile' => CURLOPT_CAINFO, 191 | 'sslLocalCert' => CURLOPT_SSLCERT, 192 | 'sslLocalPk' => CURLOPT_SSLKEY, 193 | 'sslPassphrase' => CURLOPT_SSLCERTPASSWD, 194 | ]; 195 | 196 | $curlOptions = []; 197 | foreach ($options as $key => $value) { 198 | if (is_int($key)) { 199 | $curlOptions[$key] = $value; 200 | } else { 201 | if (isset($optionMap[$key])) { 202 | $curlOptions[$optionMap[$key]] = $value; 203 | } else { 204 | $key = strtoupper($key); 205 | if (strpos($key, 'SSL') === 0) { 206 | $key = substr($key, 3); 207 | $constantName = 'CURLOPT_SSL_' . $key; 208 | if (!defined($constantName)) { 209 | $constantName = 'CURLOPT_SSL' . $key; 210 | } 211 | } else { 212 | $constantName = 'CURLOPT_' . strtoupper($key); 213 | } 214 | $curlOptions[constant($constantName)] = $value; 215 | } 216 | } 217 | } 218 | return $curlOptions; 219 | } 220 | 221 | /** 222 | * Setup a variable, which should collect the cURL response headers. 223 | * @param resource $curlResource cURL resource. 224 | * @param array $output variable, which should collection headers. 225 | */ 226 | private function setHeaderOutput($curlResource, array &$output) 227 | { 228 | curl_setopt($curlResource, CURLOPT_HEADERFUNCTION, function($resource, $headerString) use (&$output) { 229 | $header = trim($headerString, "\n\r"); 230 | if (strlen($header) > 0) { 231 | $output[] = $header; 232 | } 233 | return mb_strlen($headerString, '8bit'); 234 | }); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 2.0 15 | */ 16 | class Exception extends \yii\base\Exception 17 | { 18 | /** 19 | * @return string the user-friendly name of this exception 20 | */ 21 | public function getName() 22 | { 23 | return 'HTTP Client Exception'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/FormatterInterface.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 2.0 15 | */ 16 | interface FormatterInterface 17 | { 18 | /** 19 | * Formats given HTTP request message. 20 | * @param Request $request HTTP request instance. 21 | * @return Request formatted request. 22 | */ 23 | public function format(Request $request); 24 | } -------------------------------------------------------------------------------- /src/JsonFormatter.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 2.0 18 | */ 19 | class JsonFormatter extends BaseObject implements FormatterInterface 20 | { 21 | /** 22 | * @var int the encoding options. For more details please refer to 23 | * . 24 | */ 25 | public $encodeOptions = 0; 26 | 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function format(Request $request) 32 | { 33 | $request->getHeaders()->set('Content-Type', 'application/json; charset=UTF-8'); 34 | if (($data = $request->getData()) !== null) { 35 | $request->setContent(Json::encode($request->getData(), $this->encodeOptions)); 36 | } 37 | return $request; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/JsonParser.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 2.0 18 | */ 19 | class JsonParser extends BaseObject implements ParserInterface 20 | { 21 | /** 22 | * @var bool whether to return objects in terms of associative arrays. 23 | * @since 2.0.8 24 | */ 25 | public $asArray = true; 26 | 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function parse(Response $response) 32 | { 33 | return Json::decode($response->getContent(), $this->asArray); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Message.php: -------------------------------------------------------------------------------- 1 | 29 | * @since 2.0 30 | */ 31 | class Message extends Component 32 | { 33 | /** 34 | * @var Client owner client instance. 35 | */ 36 | public $client; 37 | 38 | /** 39 | * @var HeaderCollection headers. 40 | */ 41 | private $_headers; 42 | /** 43 | * @var CookieCollection cookies. 44 | */ 45 | private $_cookies; 46 | /** 47 | * @var string|null raw content 48 | */ 49 | private $_content; 50 | /** 51 | * @var mixed content data 52 | */ 53 | private $_data; 54 | /** 55 | * @var string content format name 56 | */ 57 | private $_format; 58 | 59 | 60 | /** 61 | * Sets the HTTP headers associated with HTTP message. 62 | * @param array|HeaderCollection $headers headers collection or headers list in format: [headerName => headerValue] 63 | * @return $this self reference. 64 | */ 65 | public function setHeaders($headers) 66 | { 67 | $this->_headers = $headers; 68 | return $this; 69 | } 70 | 71 | /** 72 | * Returns the header collection. 73 | * The header collection contains the HTTP headers associated with HTTP message. 74 | * @return HeaderCollection the header collection 75 | */ 76 | public function getHeaders() 77 | { 78 | if (!is_object($this->_headers)) { 79 | $headerCollection = new HeaderCollection(); 80 | if (is_array($this->_headers)) { 81 | foreach ($this->_headers as $name => $value) { 82 | if (is_int($name)) { 83 | // parse raw header : 84 | $rawHeader = $value; 85 | if (strpos($rawHeader, 'HTTP/') === 0) { 86 | $parts = explode(' ', $rawHeader, 3); 87 | $headerCollection->add('http-code', $parts[1]); 88 | } elseif (($separatorPos = strpos($rawHeader, ':')) !== false) { 89 | $name = strtolower(trim(substr($rawHeader, 0, $separatorPos))); 90 | $value = trim(substr($rawHeader, $separatorPos + 1)); 91 | $headerCollection->add($name, $value); 92 | } else { 93 | $headerCollection->add('raw', $rawHeader); 94 | } 95 | } else { 96 | $headerCollection->set($name, $value); 97 | } 98 | } 99 | } 100 | $this->_headers = $headerCollection; 101 | } 102 | return $this->_headers; 103 | } 104 | 105 | /** 106 | * Adds more headers to the already defined ones. 107 | * @param array $headers additional headers in format: [headerName => headerValue] 108 | * @return $this self reference. 109 | */ 110 | public function addHeaders(array $headers) 111 | { 112 | $headerCollection = $this->getHeaders(); 113 | foreach ($headers as $name => $value) { 114 | $headerCollection->add($name, $value); 115 | } 116 | return $this; 117 | } 118 | 119 | /** 120 | * Checks of HTTP message contains any header. 121 | * Using this method you are able to check cookie presence without instantiating [[HeaderCollection]]. 122 | * @return bool whether message contains any header. 123 | */ 124 | public function hasHeaders() 125 | { 126 | if (is_object($this->_headers)) { 127 | return $this->_headers->getCount() > 0; 128 | } 129 | return !empty($this->_headers); 130 | } 131 | 132 | /** 133 | * Sets the cookies associated with HTTP message. 134 | * @param CookieCollection|Cookie[]|array $cookies cookie collection or cookies list. 135 | * @return $this self reference. 136 | */ 137 | public function setCookies($cookies) 138 | { 139 | $this->_cookies = $cookies; 140 | return $this; 141 | } 142 | 143 | /** 144 | * Returns the cookie collection. 145 | * The cookie collection contains the cookies associated with HTTP message. 146 | * @return CookieCollection|Cookie[] the cookie collection. 147 | */ 148 | public function getCookies() 149 | { 150 | if (!is_object($this->_cookies)) { 151 | $cookieCollection = new CookieCollection(); 152 | if (is_array($this->_cookies)) { 153 | foreach ($this->_cookies as $cookie) { 154 | if (!is_object($cookie)) { 155 | $cookie = new Cookie($cookie); 156 | } 157 | $cookieCollection->add($cookie); 158 | } 159 | } 160 | $this->_cookies = $cookieCollection; 161 | } 162 | return $this->_cookies; 163 | } 164 | 165 | /** 166 | * Adds more cookies to the already defined ones. 167 | * @param Cookie[]|array $cookies additional cookies. 168 | * @return $this self reference. 169 | */ 170 | public function addCookies(array $cookies) 171 | { 172 | $cookieCollection = $this->getCookies(); 173 | foreach ($cookies as $cookie) { 174 | if (!is_object($cookie)) { 175 | $cookie = new Cookie($cookie); 176 | } 177 | $cookieCollection->add($cookie); 178 | } 179 | return $this; 180 | } 181 | 182 | /** 183 | * Checks of HTTP message contains any cookie. 184 | * Using this method you are able to check cookie presence without instantiating [[CookieCollection]]. 185 | * @return bool whether message contains any cookie. 186 | */ 187 | public function hasCookies() 188 | { 189 | if (is_object($this->_cookies)) { 190 | return $this->_cookies->getCount() > 0; 191 | } 192 | return !empty($this->_cookies); 193 | } 194 | 195 | /** 196 | * Sets the HTTP message raw content. 197 | * @param string $content raw content. 198 | * @return $this self reference. 199 | */ 200 | public function setContent($content) 201 | { 202 | $this->_content = $content; 203 | return $this; 204 | } 205 | 206 | /** 207 | * Returns HTTP message raw content. 208 | * @return string raw body. 209 | */ 210 | public function getContent() 211 | { 212 | return $this->_content; 213 | } 214 | 215 | /** 216 | * Checks if content with provided name exists 217 | * @param $key string Name of the content parameter 218 | * @return bool 219 | * @since 2.0.10 220 | */ 221 | public function hasContent($key) 222 | { 223 | $content = $this->getContent(); 224 | return is_array($content) && isset($content[$key]); 225 | } 226 | 227 | 228 | 229 | /** 230 | * Sets the data fields, which composes message content. 231 | * @param mixed $data content data fields. 232 | * @return $this self reference. 233 | */ 234 | public function setData($data) 235 | { 236 | $this->_data = $data; 237 | return $this; 238 | } 239 | 240 | /** 241 | * Returns the data fields, parsed from raw content. 242 | * @return mixed content data fields. 243 | */ 244 | public function getData() 245 | { 246 | return $this->_data; 247 | } 248 | 249 | /** 250 | * Adds data fields to the existing ones. 251 | * @param array $data additional content data fields. 252 | * @return $this self reference. 253 | * @since 2.0.1 254 | */ 255 | public function addData($data) 256 | { 257 | if (empty($this->_data)) { 258 | $this->_data = $data; 259 | } else { 260 | if (!is_array($this->_data)) { 261 | throw new \yii\base\Exception('Unable to merge existing data with new data. Existing data is not an array.'); 262 | } 263 | $this->_data = array_merge($this->_data, $data); 264 | } 265 | return $this; 266 | } 267 | 268 | /** 269 | * Sets body format. 270 | * @param string $format body format name. 271 | * @return $this self reference. 272 | */ 273 | public function setFormat($format) 274 | { 275 | $this->_format = $format; 276 | return $this; 277 | } 278 | 279 | /** 280 | * Returns body format. 281 | * @return string body format name. 282 | */ 283 | public function getFormat() 284 | { 285 | if ($this->_format === null) { 286 | $this->_format = $this->defaultFormat(); 287 | } 288 | return $this->_format; 289 | } 290 | 291 | /** 292 | * Returns default format name. 293 | * @return string default format name. 294 | */ 295 | protected function defaultFormat() 296 | { 297 | return Client::FORMAT_URLENCODED; 298 | } 299 | 300 | /** 301 | * Composes raw header lines from [[headers]]. 302 | * Each line will be a string in format: 'header-name: value'. 303 | * @return array raw header lines. 304 | */ 305 | public function composeHeaderLines() 306 | { 307 | if (!$this->hasHeaders()) { 308 | return []; 309 | } 310 | $headers = []; 311 | foreach ($this->getHeaders() as $name => $values) { 312 | $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name))); 313 | foreach ($values as $value) { 314 | $headers[] = "$name: $value"; 315 | } 316 | } 317 | return $headers; 318 | } 319 | 320 | /** 321 | * Returns string representation of this HTTP message. 322 | * @return string the string representation of this HTTP message. 323 | */ 324 | public function toString() 325 | { 326 | $result = ''; 327 | if ($this->hasHeaders()) { 328 | $headers = $this->composeHeaderLines(); 329 | $result .= implode("\n", $headers); 330 | } 331 | 332 | $content = $this->getContent(); 333 | if ($content !== null) { 334 | $result .= "\n\n" . $content; 335 | } 336 | 337 | return $result; 338 | } 339 | 340 | /** 341 | * PHP magic method that returns the string representation of this object. 342 | * @return string the string representation of this object. 343 | */ 344 | public function __toString() 345 | { 346 | // __toString cannot throw exception 347 | // use trigger_error to bypass this limitation 348 | try { 349 | return $this->toString(); 350 | } catch (\Exception $e) { 351 | ErrorHandler::convertExceptionToError($e); 352 | return ''; 353 | } 354 | } 355 | } -------------------------------------------------------------------------------- /src/MockTransport.php: -------------------------------------------------------------------------------- 1 | responses[] = $response; 30 | } 31 | 32 | /** 33 | * @return Request[] 34 | */ 35 | public function flushRequests() 36 | { 37 | $requests = $this->requests; 38 | $this->requests = []; 39 | 40 | return $requests; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function send($request) 47 | { 48 | if (empty($this->responses)) { 49 | throw new Exception('No Response available'); 50 | } 51 | 52 | $nextResponse = array_shift($this->responses); 53 | if (null === $nextResponse->client) { 54 | $nextResponse->client = $request->client; 55 | } 56 | 57 | $this->requests[] = $request; 58 | 59 | return $nextResponse; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ParserInterface.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 2.0 15 | */ 16 | interface ParserInterface 17 | { 18 | /** 19 | * Parses given HTTP response instance. 20 | * @param Response $response HTTP response instance. 21 | * @return mixed parsed content data. 22 | */ 23 | public function parse(Response $response); 24 | } -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | 24 | * @since 2.0 25 | */ 26 | class Request extends Message 27 | { 28 | /** 29 | * @event RequestEvent an event raised right before sending request. 30 | */ 31 | const EVENT_BEFORE_SEND = 'beforeSend'; 32 | /** 33 | * @event RequestEvent an event raised right after request has been sent. 34 | */ 35 | const EVENT_AFTER_SEND = 'afterSend'; 36 | 37 | /** 38 | * @var string|array target URL. 39 | */ 40 | private $_url; 41 | /** 42 | * @var string|null full target URL. 43 | */ 44 | private $_fullUrl; 45 | /** 46 | * @var string request method. 47 | */ 48 | private $_method = 'GET'; 49 | /** 50 | * @var array request options. 51 | */ 52 | private $_options = []; 53 | /** 54 | * @var bool whether request object has been prepared for sending or not. 55 | * @see prepare() 56 | */ 57 | private $isPrepared = false; 58 | /** 59 | * @var resource The file that the transfer should be written to. 60 | */ 61 | private $_outputFile; 62 | /** 63 | * @var array Stores map (alias => name) of the content parameters 64 | */ 65 | private $_contentMap = []; 66 | /** 67 | * @var float stores the starttime of the current request with microsecond-precession 68 | */ 69 | private $_startTime; 70 | /** 71 | * @var float stores the seconds of how long does it take to get a response 72 | */ 73 | private $_timeElapsed; 74 | 75 | 76 | /** 77 | * Sets target URL. 78 | * @param string|array $url use a string to represent a URL (e.g. `http://some-domain.com`, `item/list`), 79 | * or an array to represent a URL with query parameters (e.g. `['item/list', 'param1' => 'value1']`). 80 | * @return $this self reference. 81 | */ 82 | public function setUrl($url) 83 | { 84 | $this->_url = $url; 85 | $this->_fullUrl = null; 86 | return $this; 87 | } 88 | 89 | /** 90 | * Returns target URL. 91 | * @return string|array|null target URL or URL parameters 92 | */ 93 | public function getUrl() 94 | { 95 | return $this->_url; 96 | } 97 | 98 | /** 99 | * Sets full target URL. 100 | * This method can be used during request formatting and preparation. 101 | * Do not use it for the target URL specification, use [[setUrl()]] instead. 102 | * @param string $fullUrl full target URL. 103 | * @return $this self reference. 104 | * @since 2.0.3 105 | */ 106 | public function setFullUrl($fullUrl) 107 | { 108 | $this->_fullUrl = $fullUrl; 109 | return $this; 110 | } 111 | 112 | /** 113 | * Returns full target URL, including [[Client::baseUrl]] as a string. 114 | * @return string full target URL. 115 | */ 116 | public function getFullUrl() 117 | { 118 | if ($this->_fullUrl === null) { 119 | $this->_fullUrl = $this->createFullUrl($this->getUrl()); 120 | } 121 | return $this->_fullUrl; 122 | } 123 | 124 | /** 125 | * @param string $method request method 126 | * @return $this self reference. 127 | */ 128 | public function setMethod($method) 129 | { 130 | $this->_method = $method; 131 | return $this; 132 | } 133 | 134 | /** 135 | * @return string request method 136 | */ 137 | public function getMethod() 138 | { 139 | return $this->_method; 140 | } 141 | 142 | /** 143 | * Following options are supported: 144 | * - timeout: int, the maximum number of seconds to allow request to be executed. 145 | * - proxy: string, URI specifying address of proxy server. (e.g. tcp://proxy.example.com:5100). 146 | * - userAgent: string, the contents of the "User-Agent: " header to be used in a HTTP request. 147 | * - followLocation: bool, whether to follow any "Location: " header that the server sends as part of the HTTP header. 148 | * - maxRedirects: int, the max number of redirects to follow. 149 | * - protocolVersion: float|string, HTTP protocol version. 150 | * - sslVerifyPeer: bool, whether verification of the peer's certificate should be performed. 151 | * - sslCafile: string, location of Certificate Authority file on local filesystem which should be used with 152 | * the 'sslVerifyPeer' option to authenticate the identity of the remote peer. 153 | * - sslCapath: string, a directory that holds multiple CA certificates. 154 | * 155 | * You may set options using keys, which are specific to particular transport, like `[CURLOPT_VERBOSE => true]` in case 156 | * there is a necessity for it. 157 | * 158 | * @param array $options request options. 159 | * @return $this self reference. 160 | */ 161 | public function setOptions(array $options) 162 | { 163 | $this->_options = $options; 164 | return $this; 165 | } 166 | 167 | /** 168 | * @return array request options. 169 | */ 170 | public function getOptions() 171 | { 172 | return $this->_options; 173 | } 174 | 175 | /** 176 | * Adds more options to already defined ones. 177 | * Please refer to [[setOptions()]] on how to specify options. 178 | * @param array $options additional options 179 | * @return $this self reference. 180 | */ 181 | public function addOptions(array $options) 182 | { 183 | // `array_merge()` will produce invalid result for cURL options, 184 | // while `ArrayHelper::merge()` is unable to override cURL options 185 | foreach ($options as $key => $value) { 186 | if (is_array($value) && isset($this->_options[$key])) { 187 | $value = ArrayHelper::merge($this->_options[$key], $value); 188 | } 189 | $this->_options[$key] = $value; 190 | } 191 | return $this; 192 | } 193 | 194 | /** 195 | * {@inheritdoc} 196 | */ 197 | public function setData($data) 198 | { 199 | if ($this->isPrepared) { 200 | $this->setContent(null); 201 | $this->isPrepared = false; 202 | } 203 | 204 | return parent::setData($data); 205 | } 206 | 207 | /** 208 | * {@inheritdoc} 209 | */ 210 | public function addData($data) 211 | { 212 | if ($this->isPrepared) { 213 | $this->setContent(null); 214 | $this->isPrepared = false; 215 | } 216 | 217 | return parent::addData($data); 218 | } 219 | 220 | /** 221 | * Adds a content part for multi-part content request. 222 | * @param string $name part (form input) name. 223 | * @param string $content content. 224 | * @param array $options content part options, valid options are: 225 | * - contentType - string, part content type 226 | * - fileName - string, name of the uploading file 227 | * - mimeType - string, part content type in case of file uploading 228 | * @return $this self reference. 229 | */ 230 | public function addContent($name, $content, $options = []) 231 | { 232 | $multiPartContent = $this->getContent(); 233 | if (!is_array($multiPartContent)) { 234 | $multiPartContent = []; 235 | } 236 | $options['content'] = $content; 237 | $alias = $this->generateContentAlias($name); 238 | $this->addAliasToContentMap($name, $alias); 239 | $multiPartContent[$alias] = $options; 240 | $this->setContent($multiPartContent); 241 | return $this; 242 | } 243 | 244 | /** 245 | * Adds a file for upload as multi-part content. 246 | * @see addContent() 247 | * @param string $name part (form input) name 248 | * @param string $fileName full name of the source file. 249 | * @param array $options content part options, valid options are: 250 | * - fileName - string, base name of the uploading file, if not set it base name of the source file will be used. 251 | * - mimeType - string, file mime type, if not set it will be determine automatically from source file. 252 | * @return $this 253 | * @throws \yii\base\InvalidConfigException 254 | */ 255 | public function addFile($name, $fileName, $options = []) 256 | { 257 | $content = file_get_contents($fileName); 258 | if (!isset($options['mimeType'])) { 259 | $options['mimeType'] = FileHelper::getMimeType($fileName); 260 | } 261 | if (!isset($options['fileName'])) { 262 | $options['fileName'] = basename($fileName); 263 | } 264 | return $this->addContent($name, $content, $options); 265 | } 266 | 267 | /** 268 | * Adds a string as a file upload. 269 | * @see addContent() 270 | * @param string $name part (form input) name 271 | * @param string $content file content. 272 | * @param array $options content part options, valid options are: 273 | * - fileName - string, base name of the uploading file. 274 | * - mimeType - string, file mime type, if not set it 'application/octet-stream' will be used. 275 | * @return $this 276 | */ 277 | public function addFileContent($name, $content, $options = []) 278 | { 279 | if (!isset($options['mimeType'])) { 280 | $options['mimeType'] = 'application/octet-stream'; 281 | } 282 | if (!isset($options['fileName'])) { 283 | $options['fileName'] = $name . '.dat'; 284 | } 285 | return $this->addContent($name, $content, $options); 286 | } 287 | 288 | /** 289 | * Prepares this request instance for sending. 290 | * This method should be invoked by transport before sending a request. 291 | * Do not call this method unless you know what you are doing. 292 | * @return $this self reference. 293 | */ 294 | public function prepare() 295 | { 296 | $content = $this->getContent(); 297 | if ($content === null) { 298 | $this->getFormatter()->format($this); 299 | } elseif (is_array($content)) { 300 | $this->prepareMultiPartContent($content); 301 | } 302 | 303 | $this->isPrepared = true; 304 | 305 | return $this; 306 | } 307 | 308 | /** 309 | * Normalizes given URL value, filling it with actual string URL value. 310 | * @param array|string $url raw URL, 311 | * @return string full URL 312 | */ 313 | private function createFullUrl($url) 314 | { 315 | if (is_array($url)) { 316 | $params = $url; 317 | if (isset($params[0])) { 318 | $url = (string)$params[0]; 319 | unset($params[0]); 320 | } else { 321 | $url = ''; 322 | } 323 | } 324 | 325 | if (!empty($this->client->baseUrl)) { 326 | if (empty($url)) { 327 | $url = $this->client->baseUrl; 328 | } elseif (!preg_match('/^https?:\\/\\//i', $url)) { 329 | $url = rtrim($this->client->baseUrl, '/') . '/' . ltrim($url, '/'); 330 | } 331 | } 332 | 333 | if (!empty($params)) { 334 | if (strpos($url, '?') === false) { 335 | $url .= '?'; 336 | } else { 337 | $url .= '&'; 338 | } 339 | $url .= http_build_query($params); 340 | } 341 | 342 | if ($url === null) { 343 | throw new InvalidCallException('Either the $url or the $client->baseUrl must be set.'); 344 | } 345 | 346 | return $url; 347 | } 348 | 349 | /** 350 | * Prepares multi-part content. 351 | * @param array $content multi part content. 352 | * @see https://tools.ietf.org/html/rfc7578 353 | * @see https://tools.ietf.org/html/rfc2616#section-19.5.1 for the Content-Disposition header 354 | * @see https://tools.ietf.org/html/rfc6266 for more details on the Content-Disposition header 355 | */ 356 | private function prepareMultiPartContent(array $content) 357 | { 358 | static $disallowedChars = ["\0", '"', "\r", "\n"]; 359 | 360 | $contentParts = []; 361 | 362 | $data = $this->getData(); 363 | if (!empty($data)) { 364 | foreach ($this->composeFormInputs($data) as $name => $value) { 365 | $name = str_replace($disallowedChars, '_', $name); 366 | $contentDisposition = 'Content-Disposition: form-data; name="' . $name . '"'; 367 | $contentParts[] = implode("\r\n", [$contentDisposition, '', $value]); 368 | } 369 | } 370 | 371 | // process content parts : 372 | foreach ($content as $name => $contentParams) { 373 | $headers = []; 374 | $name = $this->getNameByAlias($name); 375 | $name = str_replace($disallowedChars, '_', $name); 376 | $contentDisposition = 'Content-Disposition: form-data; name="' . $name . '"'; 377 | if (isset($contentParams['fileName'])) { 378 | $fileName = str_replace($disallowedChars, '_', $contentParams['fileName']); 379 | $contentDisposition .= '; filename="' . $fileName . '"'; 380 | } 381 | $headers[] = $contentDisposition; 382 | if (isset($contentParams['contentType'])) { 383 | $headers[] = 'Content-Type: ' . $contentParams['contentType']; 384 | } elseif (isset($contentParams['mimeType'])) { 385 | $headers[] = 'Content-Type: ' . $contentParams['mimeType']; 386 | } 387 | $contentParts[] = implode("\r\n", [implode("\r\n", $headers), '', $contentParams['content']]); 388 | } 389 | 390 | // generate safe boundary : 391 | do { 392 | 393 | $boundary = '---------------------' . md5(random_int(0, PHP_INT_MAX) . microtime()); 394 | } while (preg_grep("/{$boundary}/", $contentParts)); 395 | 396 | // add boundary for each part : 397 | array_walk($contentParts, function (&$part) use ($boundary) { 398 | $part = "--{$boundary}\r\n{$part}"; 399 | }); 400 | 401 | // add final boundary : 402 | $contentParts[] = "--{$boundary}--"; 403 | $contentParts[] = ''; 404 | 405 | $this->getHeaders()->set('content-type', "multipart/form-data; boundary={$boundary}"); 406 | $this->setContent(implode("\r\n", $contentParts)); 407 | } 408 | 409 | /** 410 | * Composes given data as form inputs submitted values, taking in account nested arrays. 411 | * Converts `['form' => ['name' => 'value']]` to `['form[name]' => 'value']`. 412 | * @param array $data 413 | * @param string $baseKey 414 | * @return array 415 | */ 416 | private function composeFormInputs(array $data, $baseKey = '') 417 | { 418 | $result = []; 419 | foreach ($data as $key => $value) { 420 | if (!empty($baseKey)) { 421 | $key = $baseKey . '[' . $key . ']'; 422 | } 423 | if (is_array($value)) { 424 | $result = array_merge($result, $this->composeFormInputs($value, $key)); 425 | } else { 426 | $result[$key] = $value; 427 | } 428 | } 429 | return $result; 430 | } 431 | 432 | /** 433 | * {@inheritdoc} 434 | */ 435 | public function composeHeaderLines() 436 | { 437 | $headers = parent::composeHeaderLines(); 438 | if ($this->hasCookies()) { 439 | $headers[] = $this->composeCookieHeader(); 440 | } 441 | return $headers; 442 | } 443 | 444 | /** 445 | * Sends this request. 446 | * @return Response response instance. 447 | * @throws Exception 448 | */ 449 | public function send() 450 | { 451 | return $this->client->send($this); 452 | } 453 | 454 | /** 455 | * This method is invoked right before this request is sent. 456 | * The method will invoke [[Client::beforeSend()]] and trigger the [[EVENT_BEFORE_SEND]] event. 457 | * @since 2.0.1 458 | */ 459 | public function beforeSend() 460 | { 461 | $this->client->beforeSend($this); 462 | 463 | $event = new RequestEvent(); 464 | $event->request = $this; 465 | $this->trigger(self::EVENT_BEFORE_SEND, $event); 466 | $this->_startTime = microtime(true); 467 | } 468 | 469 | /** 470 | * This method is invoked right after this request is sent. 471 | * The method will invoke [[Client::afterSend()]] and trigger the [[EVENT_AFTER_SEND]] event. 472 | * @param Response $response received response instance. 473 | * @since 2.0.1 474 | */ 475 | public function afterSend($response) 476 | { 477 | $this->_timeElapsed = microtime(true)-$this->_startTime; 478 | $this->client->afterSend($this, $response); 479 | 480 | $event = new RequestEvent(); 481 | $event->request = $this; 482 | $event->response = $response; 483 | $this->trigger(self::EVENT_AFTER_SEND, $event); 484 | } 485 | 486 | /** 487 | * Return the response time in seconds 488 | * 489 | * @return float the seconds elapsed from request to response 490 | * @since 2.0.12 491 | */ 492 | public function responseTime() 493 | { 494 | return $this->_timeElapsed; 495 | } 496 | 497 | /** 498 | * {@inheritdoc} 499 | */ 500 | public function toString() 501 | { 502 | if (!$this->isPrepared) { 503 | $this->prepare(); 504 | } 505 | 506 | $result = strtoupper($this->getMethod()) . ' ' . $this->getFullUrl(); 507 | 508 | $parentResult = parent::toString(); 509 | if ($parentResult !== '') { 510 | $result .= "\n" . $parentResult; 511 | } 512 | 513 | return $result; 514 | } 515 | 516 | /** 517 | * @return string cookie header value. 518 | */ 519 | private function composeCookieHeader() 520 | { 521 | $parts = []; 522 | foreach ($this->getCookies() as $cookie) { 523 | $parts[] = $cookie->name . '=' . $cookie->value; 524 | } 525 | return 'Cookie: ' . implode(';', $parts); 526 | } 527 | 528 | /** 529 | * @return FormatterInterface message formatter instance. 530 | * @throws \yii\base\InvalidConfigException 531 | */ 532 | private function getFormatter() 533 | { 534 | return $this->client->getFormatter($this->getFormat()); 535 | } 536 | 537 | /** 538 | * Gets the outputFile property 539 | * @return resource 540 | * @since 2.0.9 541 | */ 542 | public function getOutputFile() 543 | { 544 | return $this->_outputFile; 545 | } 546 | 547 | /** 548 | * Used with [[CurlTransport]] to set the file that the transfer should be written to 549 | * @see CURLOPT_FILE 550 | * @param resource $file 551 | * @return $this self reference. 552 | * @since 2.0.9 553 | */ 554 | public function setOutputFile($file) 555 | { 556 | $this->_outputFile = $file; 557 | 558 | return $this; 559 | } 560 | 561 | /** 562 | * Generates unique alias for the content 563 | * @param $name string 564 | * @return string 565 | */ 566 | private function generateContentAlias($name) 567 | { 568 | $alias = $name; 569 | while ($this->hasContent($alias)) { 570 | $alias = uniqid($name . '_'); 571 | } 572 | 573 | return $alias; 574 | } 575 | 576 | /** 577 | * Adds alias to the content map 578 | * @param $name string 579 | * @param $alias string 580 | */ 581 | private function addAliasToContentMap($name, $alias) 582 | { 583 | $this->_contentMap[$alias] = $name; 584 | } 585 | 586 | /** 587 | * Returns name by alias from the content map 588 | * @param $alias string 589 | * @return string 590 | */ 591 | private function getNameByAlias($alias) 592 | { 593 | return isset($this->_contentMap[$alias]) ? $this->_contentMap[$alias] : $alias; 594 | } 595 | } 596 | -------------------------------------------------------------------------------- /src/RequestEvent.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 2.0.1 17 | */ 18 | class RequestEvent extends Event 19 | { 20 | /** 21 | * @var Request related HTTP request instance. 22 | */ 23 | public $request; 24 | /** 25 | * @var Response|null related HTTP response. 26 | * This field will be filled only in case some response is already received, e.g. after request is sent. 27 | */ 28 | public $response; 29 | } -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | 20 | * @since 2.0 21 | */ 22 | class Response extends Message 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getData() 28 | { 29 | $data = parent::getData(); 30 | if ($data === null) { 31 | $content = $this->getContent(); 32 | if (is_string($content) && strlen($content) > 0) { 33 | $data = $this->getParser()->parse($this); 34 | $this->setData($data); 35 | } 36 | } 37 | return $data; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function getCookies() 44 | { 45 | $cookieCollection = parent::getCookies(); 46 | if ($cookieCollection->getCount() === 0 && $this->getHeaders()->has('set-cookie')) { 47 | $cookieStrings = $this->getHeaders()->get('set-cookie', [], false); 48 | foreach ($cookieStrings as $cookieString) { 49 | $cookieCollection->add($this->parseCookie($cookieString)); 50 | } 51 | } 52 | return $cookieCollection; 53 | } 54 | 55 | /** 56 | * Returns status code. 57 | * @throws Exception on failure. 58 | * @return string status code. 59 | */ 60 | public function getStatusCode() 61 | { 62 | $headers = $this->getHeaders(); 63 | if ($headers->has('http-code')) { 64 | // take into account possible 'follow location' 65 | $statusCodeHeaders = $headers->get('http-code', null, false); 66 | return empty($statusCodeHeaders) ? null : end($statusCodeHeaders); 67 | } 68 | throw new Exception('Unable to get status code: referred header information is missing.'); 69 | } 70 | 71 | /** 72 | * Checks if response status code is OK (status code = 2xx) 73 | * @return bool whether response is OK. 74 | * @throws Exception 75 | */ 76 | public function getIsOk() 77 | { 78 | $statusCode = (int)$this->getStatusCode(); 79 | return $statusCode >= 200 && $statusCode < 300; 80 | } 81 | 82 | /** 83 | * Returns default format automatically detected from headers and content. 84 | * @return string|null format name, 'null' - if detection failed. 85 | */ 86 | protected function defaultFormat() 87 | { 88 | $format = $this->detectFormatByHeaders($this->getHeaders()); 89 | if ($format === null) { 90 | $format = $this->detectFormatByContent($this->getContent()); 91 | } 92 | 93 | return $format; 94 | } 95 | 96 | /** 97 | * Detects format from headers. 98 | * @param HeaderCollection $headers source headers. 99 | * @return null|string format name, 'null' - if detection failed. 100 | */ 101 | protected function detectFormatByHeaders(HeaderCollection $headers) 102 | { 103 | $contentTypeHeaders = $headers->get('content-type', null, false); 104 | 105 | if (!empty($contentTypeHeaders)) { 106 | $contentType = end($contentTypeHeaders); 107 | if (stripos($contentType, 'json') !== false) { 108 | return Client::FORMAT_JSON; 109 | } 110 | if (stripos($contentType, 'urlencoded') !== false) { 111 | return Client::FORMAT_URLENCODED; 112 | } 113 | if (stripos($contentType, 'xml') !== false) { 114 | return Client::FORMAT_XML; 115 | } 116 | } 117 | 118 | return null; 119 | } 120 | 121 | /** 122 | * Detects response format from raw content. 123 | * @param string $content raw response content. 124 | * @return null|string format name, 'null' - if detection failed. 125 | */ 126 | protected function detectFormatByContent($content) 127 | { 128 | if (preg_match('/^(\\{|\\[\\{).*(\\}|\\}\\])$/is', $content)) { 129 | return Client::FORMAT_JSON; 130 | } 131 | if (preg_match('/^([^=&])+=[^=&]+(&[^=&]+=[^=&]+)*$/', $content)) { 132 | return Client::FORMAT_URLENCODED; 133 | } 134 | if (preg_match('/^<\?xml.*>$/s', $content)) { 135 | return Client::FORMAT_XML; 136 | } 137 | return null; 138 | } 139 | 140 | /** 141 | * Parses cookie value string, creating a [[Cookie]] instance. 142 | * @param string $cookieString cookie header string. 143 | * @return Cookie cookie object. 144 | */ 145 | private function parseCookie($cookieString) 146 | { 147 | $params = []; 148 | $pairs = explode(';', $cookieString); 149 | foreach ($pairs as $number => $pair) { 150 | $pair = trim($pair); 151 | if (strpos($pair, '=') === false) { 152 | $params[$this->normalizeCookieParamName($pair)] = true; 153 | } else { 154 | list($name, $value) = explode('=', $pair, 2); 155 | if ($number === 0) { 156 | $params['name'] = $name; 157 | $params['value'] = urldecode($value); 158 | } else { 159 | $params[$this->normalizeCookieParamName($name)] = urldecode($value); 160 | } 161 | } 162 | } 163 | 164 | $cookie = new Cookie(); 165 | foreach ($params as $name => $value) { 166 | if ($cookie->canSetProperty($name)) { 167 | // Cookie string may contain custom unsupported params 168 | $cookie->$name = $value; 169 | } 170 | } 171 | return $cookie; 172 | } 173 | 174 | /** 175 | * @param string $rawName raw cookie parameter name. 176 | * @return string name of [[Cookie]] field. 177 | */ 178 | private function normalizeCookieParamName($rawName) 179 | { 180 | static $nameMap = [ 181 | 'expires' => 'expire', 182 | 'httponly' => 'httpOnly', 183 | 'max-age' => 'maxAge', 184 | ]; 185 | $name = strtolower($rawName); 186 | if (isset($nameMap[$name])) { 187 | $name = $nameMap[$name]; 188 | } 189 | return $name; 190 | } 191 | 192 | /** 193 | * @return ParserInterface message parser instance. 194 | * @throws Exception if unable to detect parser. 195 | * @throws \yii\base\InvalidConfigException 196 | */ 197 | private function getParser() 198 | { 199 | $format = $this->getFormat(); 200 | return $this->client->getParser($format); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/StreamTransport.php: -------------------------------------------------------------------------------- 1 | 20 | * @since 2.0 21 | */ 22 | class StreamTransport extends Transport 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function send($request) 28 | { 29 | $request->beforeSend(); 30 | 31 | $request->prepare(); 32 | 33 | $url = $request->getFullUrl(); 34 | $method = strtoupper($request->getMethod()); 35 | 36 | $contextOptions = [ 37 | 'http' => [ 38 | 'method' => $method, 39 | 'ignore_errors' => true, 40 | ], 41 | 'ssl' => [ 42 | 'verify_peer' => false, 43 | ], 44 | ]; 45 | 46 | $content = $request->getContent(); 47 | if ($content !== null) { 48 | $contextOptions['http']['content'] = $content; 49 | } 50 | $headers = $request->composeHeaderLines(); 51 | $contextOptions['http']['header'] = $headers; 52 | 53 | $contextOptions = ArrayHelper::merge($contextOptions, $this->composeContextOptions($request->getOptions())); 54 | 55 | $token = $request->client->createRequestLogToken($method, $url, $headers, $content); 56 | Yii::info($token, __METHOD__); 57 | Yii::beginProfile($token, __METHOD__); 58 | 59 | try { 60 | $context = stream_context_create($contextOptions); 61 | $stream = fopen($url, 'rb', false, $context); 62 | $responseContent = stream_get_contents($stream); 63 | // see https://php.net/manual/en/reserved.variables.httpresponseheader.php 64 | $responseHeaders = (array)$http_response_header; 65 | fclose($stream); 66 | } catch (\Exception $e) { 67 | Yii::endProfile($token, __METHOD__); 68 | throw new Exception($e->getMessage(), $e->getCode(), $e); 69 | } 70 | 71 | Yii::endProfile($token, __METHOD__); 72 | 73 | $response = $request->client->createResponse($responseContent, $responseHeaders); 74 | 75 | $request->afterSend($response); 76 | 77 | return $response; 78 | } 79 | 80 | /** 81 | * Composes stream context options from raw request options. 82 | * @param array $options raw request options. 83 | * @return array stream context options. 84 | */ 85 | private function composeContextOptions(array $options) 86 | { 87 | $contextOptions = []; 88 | foreach ($options as $key => $value) { 89 | $section = 'http'; 90 | if (strpos($key, 'ssl') === 0) { 91 | $section = 'ssl'; 92 | $key = substr($key, 3); 93 | } 94 | $key = Inflector::underscore($key); 95 | $contextOptions[$section][$key] = $value; 96 | } 97 | return $contextOptions; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Transport.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 2.0 17 | */ 18 | abstract class Transport extends Component 19 | { 20 | /** 21 | * Performs given request. 22 | * @param Request $request request to be sent. 23 | * @return Response response instance. 24 | * @throws Exception on failure. 25 | */ 26 | abstract public function send($request); 27 | 28 | /** 29 | * Performs multiple HTTP requests. 30 | * Particular transport may benefit from this method, allowing sending requests in parallel. 31 | * This method accepts an array of the [[Request]] objects and returns an array of the [[Response]] objects. 32 | * Keys of the response array correspond the ones from request array. 33 | * @param Request[] $requests requests to perform. 34 | * @return Response[] responses list. 35 | * @throws Exception 36 | */ 37 | public function batchSend(array $requests) 38 | { 39 | $responses = []; 40 | foreach ($requests as $key => $request) { 41 | $responses[$key] = $this->send($request); 42 | } 43 | return $responses; 44 | } 45 | } -------------------------------------------------------------------------------- /src/UrlEncodedFormatter.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 2.0 18 | */ 19 | class UrlEncodedFormatter extends BaseObject implements FormatterInterface 20 | { 21 | /** 22 | * @var int URL encoding type. 23 | * Possible values are: 24 | * - PHP_QUERY_RFC1738 - encoding is performed per 'RFC 1738' and the 'application/x-www-form-urlencoded' media type, 25 | * which implies that spaces are encoded as plus (+) signs. This is most common encoding type used by most web 26 | * applications. 27 | * - PHP_QUERY_RFC3986 - then encoding is performed according to 'RFC 3986', and spaces will be percent encoded (%20). 28 | * This encoding type is required by OpenID and OAuth protocols. 29 | */ 30 | public $encodingType = PHP_QUERY_RFC1738; 31 | /** 32 | * @var string the content charset. If not set, it will use the value of [[\yii\base\Application::charset]]. 33 | * @since 2.0.1 34 | */ 35 | public $charset; 36 | 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function format(Request $request) 42 | { 43 | if (($data = $request->getData()) !== null) { 44 | $content = http_build_query((array)$data, '', '&', $this->encodingType); 45 | } 46 | 47 | if (strcasecmp('GET', $request->getMethod()) === 0) { 48 | if (!empty($content)) { 49 | $request->setFullUrl(null); 50 | $url = $request->getFullUrl(); 51 | $url .= (strpos($url, '?') === false) ? '?' : '&'; 52 | $url .= $content; 53 | $request->setFullUrl($url); 54 | } 55 | return $request; 56 | } 57 | 58 | $charset = $this->charset === null ? Yii::$app->charset : $this->charset; 59 | $charset = $charset ? '; charset=' . $charset : ''; 60 | $request->getHeaders()->set('Content-Type', 'application/x-www-form-urlencoded' . $charset); 61 | 62 | if (isset($content)) { 63 | $request->setContent($content); 64 | } 65 | 66 | if (!isset($content) && !isset($request->getOptions()[CURLOPT_INFILE])) { 67 | $request->getHeaders()->set('Content-Length', '0'); 68 | } 69 | 70 | return $request; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/UrlEncodedParser.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 2.0 17 | */ 18 | class UrlEncodedParser extends BaseObject implements ParserInterface 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function parse(Response $response) 24 | { 25 | $data = []; 26 | parse_str($response->getContent(), $data); 27 | return $data; 28 | } 29 | } -------------------------------------------------------------------------------- /src/XmlFormatter.php: -------------------------------------------------------------------------------- 1 | 23 | * @since 2.0 24 | */ 25 | class XmlFormatter extends BaseObject implements FormatterInterface 26 | { 27 | /** 28 | * @var string the Content-Type header for the response 29 | */ 30 | public $contentType = 'application/xml'; 31 | /** 32 | * @var string the XML version 33 | */ 34 | public $version = '1.0'; 35 | /** 36 | * @var string the XML encoding. If not set, it will use the value of [[\yii\base\Application::charset]]. 37 | */ 38 | public $encoding; 39 | /** 40 | * @var string the name of the root element. 41 | */ 42 | public $rootTag = 'request'; 43 | /** 44 | * @var string the name of the elements that represent the array elements with numeric keys. 45 | * @since 2.0.1 46 | */ 47 | public $itemTag = 'item'; 48 | /** 49 | * @var bool whether to interpret objects implementing the [[\Traversable]] interface as arrays. 50 | * Defaults to `true`. 51 | * @since 2.0.1 52 | */ 53 | public $useTraversableAsArray = true; 54 | 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function format(Request $request) 60 | { 61 | $contentType = $this->contentType; 62 | $charset = $this->encoding === null ? Yii::$app->charset : $this->encoding; 63 | if (stripos($contentType, 'charset') === false) { 64 | $contentType .= '; charset=' . $charset; 65 | } 66 | $request->getHeaders()->set('Content-Type', $contentType); 67 | 68 | $data = $request->getData(); 69 | if ($data !== null) { 70 | if ($data instanceof DOMDocument) { 71 | $content = $data->saveXML(); 72 | } elseif ($data instanceof SimpleXMLElement) { 73 | $content = $data->saveXML(); 74 | } else { 75 | $dom = new DOMDocument($this->version, $charset); 76 | $root = new DOMElement($this->rootTag); 77 | $dom->appendChild($root); 78 | $this->buildXml($root, $data); 79 | $content = $dom->saveXML(); 80 | } 81 | $request->setContent($content); 82 | } 83 | 84 | return $request; 85 | } 86 | 87 | /** 88 | * @param DOMElement $element 89 | * @param mixed $data 90 | */ 91 | protected function buildXml($element, $data) 92 | { 93 | if (is_array($data) || 94 | ($data instanceof \Traversable && $this->useTraversableAsArray && !$data instanceof Arrayable) 95 | ) { 96 | foreach ($data as $name => $value) { 97 | if (is_int($name) && is_object($value)) { 98 | $this->buildXml($element, $value); 99 | } elseif (is_array($value) || is_object($value)) { 100 | $child = new DOMElement(is_int($name) ? $this->itemTag : $name); 101 | $element->appendChild($child); 102 | $this->buildXml($child, $value); 103 | } else { 104 | $child = new DOMElement(is_int($name) ? $this->itemTag : $name); 105 | $element->appendChild($child); 106 | $child->appendChild(new DOMText((string) $value)); 107 | } 108 | } 109 | } elseif (is_object($data)) { 110 | $child = new DOMElement(StringHelper::basename(get_class($data))); 111 | $element->appendChild($child); 112 | if ($data instanceof Arrayable) { 113 | $this->buildXml($child, $data->toArray()); 114 | } else { 115 | $array = []; 116 | foreach ($data as $name => $value) { 117 | $array[$name] = $value; 118 | } 119 | $this->buildXml($child, $array); 120 | } 121 | } else { 122 | $element->appendChild(new DOMText((string) $data)); 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /src/XmlParser.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 2.0 17 | */ 18 | class XmlParser extends BaseObject implements ParserInterface 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function parse(Response $response) 24 | { 25 | $contentType = $response->getHeaders()->get('content-type', ''); 26 | if (preg_match('/charset=(.*)/i', $contentType, $matches)) { 27 | $encoding = $matches[1]; 28 | } else { 29 | $encoding = 'UTF-8'; 30 | } 31 | 32 | $dom = new \DOMDocument('1.0', $encoding); 33 | $dom->loadXML($response->getContent(), LIBXML_NOCDATA); 34 | return $this->convertXmlToArray(simplexml_import_dom($dom->documentElement)); 35 | } 36 | 37 | /** 38 | * Converts XML document to array. 39 | * @param string|\SimpleXMLElement $xml xml to process. 40 | * @return array XML array representation. 41 | */ 42 | protected function convertXmlToArray($xml) 43 | { 44 | if (is_string($xml)) { 45 | $xml = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA); 46 | } 47 | $result = (array) $xml; 48 | foreach ($result as $key => $value) { 49 | if (!is_scalar($value)) { 50 | $result[$key] = $this->convertXmlToArray($value); 51 | } 52 | } 53 | return $result; 54 | } 55 | } -------------------------------------------------------------------------------- /src/debug/HttpClientPanel.php: -------------------------------------------------------------------------------- 1 | 25 | * @since 2.0 26 | */ 27 | class HttpClientPanel extends Panel 28 | { 29 | /** 30 | * @var array current HTTP request timings 31 | */ 32 | private $_timings; 33 | /** 34 | * @var array HTTP requests info extracted to array as models, to use with data provider. 35 | */ 36 | private $_models; 37 | /** 38 | * @var \yii\httpclient\Client|array|string 39 | */ 40 | private $_httpClient = 'yii\httpclient\Client'; 41 | 42 | 43 | /** 44 | * @param array $httpClient 45 | */ 46 | public function setHttpClient($httpClient) 47 | { 48 | $this->_httpClient = $httpClient; 49 | } 50 | 51 | /** 52 | * @return \yii\httpclient\Client 53 | * @throws \yii\base\InvalidConfigException 54 | */ 55 | public function getHttpClient() 56 | { 57 | if (!is_object($this->_httpClient)) { 58 | $this->_httpClient = Instance::ensure($this->_httpClient, Client::className()); 59 | } 60 | return $this->_httpClient; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function init() 67 | { 68 | $this->actions['request-execute'] = [ 69 | 'class' => 'yii\httpclient\debug\RequestExecuteAction', 70 | 'panel' => $this, 71 | ]; 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function getName() 78 | { 79 | return 'HTTP Client'; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function getSummary() 86 | { 87 | $timings = $this->calculateTimings(); 88 | $queryCount = count($timings); 89 | if ($queryCount === 0) { 90 | return ''; 91 | } 92 | 93 | $queryTime = number_format($this->getTotalRequestTime($timings) * 1000) . ' ms'; 94 | 95 | return Yii::$app->view->render('@yii/httpclient/debug/views/summary', [ 96 | 'timings' => $this->calculateTimings(), 97 | 'panel' => $this, 98 | 'queryCount' => $queryCount, 99 | 'queryTime' => $queryTime, 100 | ]); 101 | } 102 | 103 | /** 104 | * {@inheritdoc} 105 | */ 106 | public function getDetail() 107 | { 108 | $searchModel = new SearchModel(); 109 | $dataProvider = $searchModel->search(Yii::$app->request->getQueryParams(), $this->getModels()); 110 | 111 | return Yii::$app->view->render('@yii/httpclient/debug/views/detail', [ 112 | 'panel' => $this, 113 | 'dataProvider' => $dataProvider, 114 | 'searchModel' => $searchModel, 115 | ]); 116 | } 117 | 118 | /** 119 | * Calculates given request profile timings. 120 | * 121 | * @return array timings [token, category, timestamp, traces, nesting level, elapsed time] 122 | */ 123 | public function calculateTimings() 124 | { 125 | if ($this->_timings === null) { 126 | $this->_timings = Yii::getLogger()->calculateTimings(isset($this->data['messages']) ? $this->data['messages'] : []); 127 | } 128 | 129 | return $this->_timings; 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | */ 135 | public function save() 136 | { 137 | $target = $this->module->logTarget; 138 | $messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, [ 139 | 'yii\httpclient\Transport::*', 140 | 'yii\httpclient\CurlTransport::*', 141 | 'yii\httpclient\StreamTransport::*', 142 | ]); 143 | return ['messages' => $messages]; 144 | } 145 | 146 | /** 147 | * Returns an array of models that represents logs of the current request. 148 | * Can be used with data providers such as \yii\data\ArrayDataProvider. 149 | * @return array models 150 | */ 151 | protected function getModels() 152 | { 153 | if ($this->_models === null) { 154 | $this->_models = []; 155 | $timings = $this->calculateTimings(); 156 | 157 | foreach ($timings as $seq => $dbTiming) { 158 | $this->_models[] = [ 159 | 'method' => $this->getRequestMethod($dbTiming['info']), 160 | 'type' => $this->getRequestType($dbTiming['category']), 161 | 'request' => $dbTiming['info'], 162 | 'duration' => ($dbTiming['duration'] * 1000), // in milliseconds 163 | 'trace' => $dbTiming['trace'], 164 | 'timestamp' => ($dbTiming['timestamp'] * 1000), // in milliseconds 165 | 'seq' => $seq, 166 | ]; 167 | } 168 | } 169 | 170 | return $this->_models; 171 | } 172 | 173 | /** 174 | * Returns HTTP request method. 175 | * 176 | * @param string $timing timing procedure string 177 | * @return string request method such as GET, POST, PUT, etc. 178 | */ 179 | protected function getRequestMethod($timing) 180 | { 181 | $timing = ltrim($timing); 182 | preg_match('/^([a-zA-z]*)/', $timing, $matches); 183 | 184 | return count($matches) ? $matches[0] : ''; 185 | } 186 | 187 | /** 188 | * Returns request type. 189 | * 190 | * @param string $category 191 | * @return string request type such as 'normal', 'batch' 192 | */ 193 | protected function getRequestType($category) 194 | { 195 | return (stripos($category, '::batchSend') === false) ? 'normal' : 'batch'; 196 | } 197 | 198 | /** 199 | * Returns total request time. 200 | * 201 | * @param array $timings 202 | * @return int total time 203 | */ 204 | protected function getTotalRequestTime($timings) 205 | { 206 | $queryTime = 0; 207 | 208 | foreach ($timings as $timing) { 209 | $queryTime += $timing['duration']; 210 | } 211 | 212 | return $queryTime; 213 | } 214 | 215 | /** 216 | * Returns array request methods 217 | * 218 | * @return array 219 | */ 220 | public function getMethods() 221 | { 222 | return array_reduce( 223 | $this->_models, 224 | function ($result, $item) { 225 | $result[$item['method']] = $item['method']; 226 | return $result; 227 | }, 228 | [] 229 | ); 230 | } 231 | 232 | /** 233 | * Returns array request types 234 | * 235 | * @return array 236 | */ 237 | public function getTypes() 238 | { 239 | return [ 240 | 'normal' => 'Normal', 241 | 'batch' => 'Batch', 242 | ]; 243 | } 244 | } -------------------------------------------------------------------------------- /src/debug/RequestExecuteAction.php: -------------------------------------------------------------------------------- 1 | 18 | * @since 2.0 19 | */ 20 | class RequestExecuteAction extends Action 21 | { 22 | /** 23 | * @var HttpClientPanel 24 | */ 25 | public $panel; 26 | 27 | 28 | /** 29 | * @param string $seq 30 | * @param string $tag 31 | * @param bool $passthru whether to send response to the browser or render it as plain text 32 | * @return Response 33 | * @throws HttpException 34 | * @throws \yii\base\InvalidConfigException 35 | * @throws \yii\httpclient\Exception 36 | */ 37 | public function run($seq, $tag, $passthru = false) 38 | { 39 | $this->controller->loadData($tag); 40 | 41 | $timings = $this->panel->calculateTimings(); 42 | 43 | if (!isset($timings[$seq])) { 44 | throw new HttpException(404, 'Log message not found.'); 45 | } 46 | 47 | $requestInfo = $timings[$seq]['info']; 48 | 49 | $httpRequest = $this->createRequestFromLog($requestInfo); 50 | $httpResponse = $httpRequest->send(); 51 | $httpResponse->getHeaders()->get('content-type'); 52 | 53 | $response = new Response([ 54 | 'format' => Response::FORMAT_RAW, 55 | ]); 56 | 57 | if ($passthru) { 58 | foreach ($httpResponse->getHeaders() as $name => $value) { 59 | $response->getHeaders()->set($name, $value); 60 | } 61 | $response->content = $httpResponse->content; 62 | return $response; 63 | } 64 | 65 | $response->getHeaders()->add('content-type', 'text/plain'); 66 | $response->content = $httpResponse->toString(); 67 | return $response; 68 | } 69 | 70 | /** 71 | * Creates an HTTP request instance from log entry. 72 | * @param string $requestLog HTTP request log entry 73 | * @return \yii\httpclient\Request request instance. 74 | * @throws \yii\base\InvalidConfigException 75 | */ 76 | protected function createRequestFromLog($requestLog) 77 | { 78 | if (strpos($requestLog, "\n\n")) { 79 | list($head, $content) = explode("\n\n", $requestLog, 2); 80 | } else { 81 | $head = $requestLog; 82 | $content = null; 83 | } 84 | 85 | $headers = explode("\n", $head); 86 | $main = array_shift($headers); 87 | list($method, $url) = explode(' ', $main, 2); 88 | 89 | return $this->panel->getHttpClient()->createRequest() 90 | ->setMethod($method) 91 | ->setUrl($url) 92 | ->setHeaders($headers) 93 | ->setContent($content); 94 | } 95 | } -------------------------------------------------------------------------------- /src/debug/SearchModel.php: -------------------------------------------------------------------------------- 1 | 18 | * @since 2.0 19 | */ 20 | class SearchModel extends Base 21 | { 22 | /** 23 | * @var string type of the input search value 24 | */ 25 | public $type; 26 | /** 27 | * @var string method of the input search value 28 | */ 29 | public $method; 30 | /** 31 | * @var int request attribute input search value 32 | */ 33 | public $request; 34 | 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function rules() 40 | { 41 | return [ 42 | [['type', 'method', 'request'], 'safe'], 43 | ]; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function attributeLabels() 50 | { 51 | return [ 52 | 'type' => 'Type', 53 | 'method' => 'Method', 54 | 'request' => 'Request', 55 | ]; 56 | } 57 | 58 | /** 59 | * Returns data provider with filled models. Filter applied if needed. 60 | * 61 | * @param array $params an array of parameter values indexed by parameter names 62 | * @param array $models data to return provider for 63 | * @return \yii\data\ArrayDataProvider 64 | */ 65 | public function search($params, $models) 66 | { 67 | $dataProvider = new ArrayDataProvider([ 68 | 'allModels' => $models, 69 | 'pagination' => false, 70 | 'sort' => [ 71 | 'attributes' => ['duration', 'seq', 'type', 'method', 'request'], 72 | 'defaultOrder' => [ 73 | 'duration' => SORT_DESC, 74 | ], 75 | ], 76 | ]); 77 | 78 | if (!($this->load($params) && $this->validate())) { 79 | return $dataProvider; 80 | } 81 | 82 | $filter = new Filter(); 83 | $this->addCondition($filter, 'type', true); 84 | $this->addCondition($filter, 'method', true); 85 | $this->addCondition($filter, 'request', true); 86 | $dataProvider->allModels = $filter->filter($models); 87 | 88 | return $dataProvider; 89 | } 90 | } -------------------------------------------------------------------------------- /src/debug/views/detail.php: -------------------------------------------------------------------------------- 1 | 10 |

getName(); ?> Requests

11 | 12 | $dataProvider, 16 | 'id' => 'db-panel-detailed-grid', 17 | 'options' => ['class' => 'detail-grid-view table-responsive'], 18 | 'filterModel' => $searchModel, 19 | 'filterUrl' => $panel->getUrl(), 20 | 'columns' => [ 21 | ['class' => 'yii\grid\SerialColumn'], 22 | [ 23 | 'attribute' => 'seq', 24 | 'label' => 'Time', 25 | 'value' => function ($data) { 26 | $timeInSeconds = $data['timestamp'] / 1000; 27 | $millisecondsDiff = (int) (($timeInSeconds - (int) $timeInSeconds) * 1000); 28 | 29 | return date('H:i:s.', (int) $timeInSeconds) . sprintf('%03d', $millisecondsDiff); 30 | }, 31 | 'headerOptions' => [ 32 | 'class' => 'sort-numerical' 33 | ] 34 | ], 35 | [ 36 | 'attribute' => 'duration', 37 | 'value' => function ($data) { 38 | return sprintf('%.1f ms', $data['duration']); 39 | }, 40 | 'options' => [ 41 | 'width' => '10%', 42 | ], 43 | 'headerOptions' => [ 44 | 'class' => 'sort-numerical' 45 | ] 46 | ], 47 | [ 48 | 'attribute' => 'type', 49 | 'value' => function ($data) { 50 | return Html::encode($data['type']); 51 | }, 52 | 'filter' => $panel->getTypes(), 53 | ], 54 | [ 55 | 'attribute' => 'method', 56 | 'value' => function ($data) { 57 | return Html::encode(mb_strtoupper($data['method'], 'utf8')); 58 | }, 59 | 'filter' => $panel->getMethods(), 60 | ], 61 | [ 62 | 'attribute' => 'request', 63 | 'value' => function ($data) { 64 | $query = Html::encode($data['request']); 65 | 66 | if (!empty($data['trace'])) { 67 | $query .= Html::ul($data['trace'], [ 68 | 'class' => 'trace', 69 | 'item' => function ($trace) { 70 | return "
  • {$trace['file']} ({$trace['line']})
  • "; 71 | }, 72 | ]); 73 | } 74 | 75 | if ($data['type'] !== 'batch') { 76 | $query .= Html::tag( 77 | 'div', 78 | implode('
    ', [ 79 | Html::a('>> Execute', ['request-execute', 'seq' => $data['seq'], 'tag' => Yii::$app->controller->summary['tag']], ['target' => '_blank']), 80 | Html::a('>> Pass Through', ['request-execute', 'seq' => $data['seq'], 'tag' => Yii::$app->controller->summary['tag'], 'passthru' => true], ['target' => '_blank']), 81 | ]), 82 | ['class' => 'db-explain'] 83 | ); 84 | } 85 | 86 | return $query; 87 | }, 88 | 'format' => 'raw', 89 | 'options' => [ 90 | 'width' => '60%', 91 | ], 92 | ] 93 | ], 94 | ]); 95 | ?> 96 | -------------------------------------------------------------------------------- /src/debug/views/summary.php: -------------------------------------------------------------------------------- 1 | 6 | 7 |
    8 | 9 | HTTP Requests 10 | 11 |
    12 | 13 | --------------------------------------------------------------------------------