├── 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 | [](https://packagist.org/packages/yiisoft/yii2-httpclient)
16 | [](https://packagist.org/packages/yiisoft/yii2-httpclient)
17 | [](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 | = $panel->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 |
12 |
13 |
--------------------------------------------------------------------------------