1
|
|
|
<?php |
2
|
|
|
namespace GuzzleHttp\Handler; |
3
|
|
|
|
4
|
|
|
use GuzzleHttp\Exception\RequestException; |
5
|
|
|
use GuzzleHttp\Exception\ConnectException; |
6
|
|
|
use GuzzleHttp\Promise\FulfilledPromise; |
7
|
|
|
use GuzzleHttp\Promise\RejectedPromise; |
8
|
|
|
use GuzzleHttp\Promise\PromiseInterface; |
9
|
|
|
use GuzzleHttp\Psr7; |
10
|
|
|
use GuzzleHttp\TransferStats; |
11
|
|
|
use Psr\Http\Message\RequestInterface; |
12
|
|
|
use Psr\Http\Message\ResponseInterface; |
13
|
|
|
use Psr\Http\Message\StreamInterface; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* HTTP handler that uses PHP's HTTP stream wrapper. |
17
|
|
|
*/ |
18
|
|
|
class StreamHandler |
19
|
|
|
{ |
20
|
|
|
private $lastHeaders = []; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Sends an HTTP request. |
24
|
|
|
* |
25
|
|
|
* @param RequestInterface $request Request to send. |
26
|
|
|
* @param array $options Request transfer options. |
27
|
|
|
* |
28
|
|
|
* @return PromiseInterface |
29
|
|
|
*/ |
30
|
|
|
public function __invoke(RequestInterface $request, array $options) |
31
|
|
|
{ |
32
|
|
|
// Sleep if there is a delay specified. |
33
|
|
|
if (isset($options['delay'])) { |
34
|
|
|
usleep($options['delay'] * 1000); |
35
|
|
|
} |
36
|
|
|
|
37
|
|
|
$startTime = isset($options['on_stats']) ? microtime(true) : null; |
38
|
|
|
|
39
|
|
|
try { |
40
|
|
|
// Does not support the expect header. |
41
|
|
|
$request = $request->withoutHeader('Expect'); |
42
|
|
|
|
43
|
|
|
// Append a content-length header if body size is zero to match |
44
|
|
|
// cURL's behavior. |
45
|
|
|
if (0 === $request->getBody()->getSize()) { |
46
|
|
|
$request = $request->withHeader('Content-Length', 0); |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
return $this->createResponse( |
50
|
|
|
$request, |
51
|
|
|
$options, |
52
|
|
|
$this->createStream($request, $options), |
53
|
|
|
$startTime |
54
|
|
|
); |
55
|
|
|
} catch (\InvalidArgumentException $e) { |
56
|
|
|
throw $e; |
57
|
|
|
} catch (\Exception $e) { |
58
|
|
|
// Determine if the error was a networking error. |
59
|
|
|
$message = $e->getMessage(); |
60
|
|
|
// This list can probably get more comprehensive. |
61
|
|
|
if (strpos($message, 'getaddrinfo') // DNS lookup failed |
62
|
|
|
|| strpos($message, 'Connection refused') |
63
|
|
|
|| strpos($message, "couldn't connect to host") // error on HHVM |
64
|
|
|
) { |
65
|
|
|
$e = new ConnectException($e->getMessage(), $request, $e); |
66
|
|
|
} |
67
|
|
|
$e = RequestException::wrapException($request, $e); |
68
|
|
|
$this->invokeStats($options, $request, $startTime, null, $e); |
69
|
|
|
|
70
|
|
|
return new RejectedPromise($e); |
71
|
|
|
} |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
private function invokeStats( |
75
|
|
|
array $options, |
76
|
|
|
RequestInterface $request, |
77
|
|
|
$startTime, |
78
|
|
|
ResponseInterface $response = null, |
79
|
|
|
$error = null |
80
|
|
|
) { |
81
|
|
|
if (isset($options['on_stats'])) { |
82
|
|
|
$stats = new TransferStats( |
83
|
|
|
$request, |
84
|
|
|
$response, |
85
|
|
|
microtime(true) - $startTime, |
86
|
|
|
$error, |
87
|
|
|
[] |
88
|
|
|
); |
89
|
|
|
call_user_func($options['on_stats'], $stats); |
90
|
|
|
} |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
private function createResponse( |
94
|
|
|
RequestInterface $request, |
95
|
|
|
array $options, |
96
|
|
|
$stream, |
97
|
|
|
$startTime |
98
|
|
|
) { |
99
|
|
|
$hdrs = $this->lastHeaders; |
100
|
|
|
$this->lastHeaders = []; |
101
|
|
|
$parts = explode(' ', array_shift($hdrs), 3); |
102
|
|
|
$ver = explode('/', $parts[0])[1]; |
103
|
|
|
$status = $parts[1]; |
104
|
|
|
$reason = isset($parts[2]) ? $parts[2] : null; |
105
|
|
|
$headers = \GuzzleHttp\headers_from_lines($hdrs); |
106
|
|
|
list ($stream, $headers) = $this->checkDecode($options, $headers, $stream); |
107
|
|
|
$stream = Psr7\stream_for($stream); |
108
|
|
|
$sink = $stream; |
109
|
|
|
|
110
|
|
|
if (strcasecmp('HEAD', $request->getMethod())) { |
111
|
|
|
$sink = $this->createSink($stream, $options); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
$response = new Psr7\Response($status, $headers, $sink, $ver, $reason); |
115
|
|
|
|
116
|
|
|
if (isset($options['on_headers'])) { |
117
|
|
|
try { |
118
|
|
|
$options['on_headers']($response); |
119
|
|
|
} catch (\Exception $e) { |
120
|
|
|
$msg = 'An error was encountered during the on_headers event'; |
121
|
|
|
$ex = new RequestException($msg, $request, $response, $e); |
122
|
|
|
return new RejectedPromise($ex); |
123
|
|
|
} |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
// Do not drain when the request is a HEAD request because they have |
127
|
|
|
// no body. |
128
|
|
|
if ($sink !== $stream) { |
129
|
|
|
$this->drain( |
130
|
|
|
$stream, |
131
|
|
|
$sink, |
132
|
|
|
$response->getHeaderLine('Content-Length') |
133
|
|
|
); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
$this->invokeStats($options, $request, $startTime, $response, null); |
137
|
|
|
|
138
|
|
|
return new FulfilledPromise($response); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
private function createSink(StreamInterface $stream, array $options) |
142
|
|
|
{ |
143
|
|
|
if (!empty($options['stream'])) { |
144
|
|
|
return $stream; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
$sink = isset($options['sink']) |
148
|
|
|
? $options['sink'] |
149
|
|
|
: fopen('php://temp', 'r+'); |
150
|
|
|
|
151
|
|
|
return is_string($sink) |
152
|
|
|
? new Psr7\LazyOpenStream($sink, 'w+') |
153
|
|
|
: Psr7\stream_for($sink); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
private function checkDecode(array $options, array $headers, $stream) |
157
|
|
|
{ |
158
|
|
|
// Automatically decode responses when instructed. |
159
|
|
|
if (!empty($options['decode_content'])) { |
160
|
|
|
$normalizedKeys = \GuzzleHttp\normalize_header_keys($headers); |
161
|
|
|
if (isset($normalizedKeys['content-encoding'])) { |
162
|
|
|
$encoding = $headers[$normalizedKeys['content-encoding']]; |
163
|
|
|
if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { |
164
|
|
|
$stream = new Psr7\InflateStream( |
165
|
|
|
Psr7\stream_for($stream) |
166
|
|
|
); |
167
|
|
|
$headers['x-encoded-content-encoding'] |
168
|
|
|
= $headers[$normalizedKeys['content-encoding']]; |
169
|
|
|
// Remove content-encoding header |
170
|
|
|
unset($headers[$normalizedKeys['content-encoding']]); |
171
|
|
|
// Fix content-length header |
172
|
|
View Code Duplication |
if (isset($normalizedKeys['content-length'])) { |
|
|
|
|
173
|
|
|
$headers['x-encoded-content-length'] |
174
|
|
|
= $headers[$normalizedKeys['content-length']]; |
175
|
|
|
|
176
|
|
|
$length = (int) $stream->getSize(); |
177
|
|
|
if ($length === 0) { |
178
|
|
|
unset($headers[$normalizedKeys['content-length']]); |
179
|
|
|
} else { |
180
|
|
|
$headers[$normalizedKeys['content-length']] = [$length]; |
181
|
|
|
} |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
return [$stream, $headers]; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Drains the source stream into the "sink" client option. |
192
|
|
|
* |
193
|
|
|
* @param StreamInterface $source |
194
|
|
|
* @param StreamInterface $sink |
195
|
|
|
* @param string $contentLength Header specifying the amount of |
196
|
|
|
* data to read. |
197
|
|
|
* |
198
|
|
|
* @return StreamInterface |
199
|
|
|
* @throws \RuntimeException when the sink option is invalid. |
200
|
|
|
*/ |
201
|
|
|
private function drain( |
202
|
|
|
StreamInterface $source, |
203
|
|
|
StreamInterface $sink, |
204
|
|
|
$contentLength |
205
|
|
|
) { |
206
|
|
|
// If a content-length header is provided, then stop reading once |
207
|
|
|
// that number of bytes has been read. This can prevent infinitely |
208
|
|
|
// reading from a stream when dealing with servers that do not honor |
209
|
|
|
// Connection: Close headers. |
210
|
|
|
Psr7\copy_to_stream( |
211
|
|
|
$source, |
212
|
|
|
$sink, |
213
|
|
|
(strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1 |
214
|
|
|
); |
215
|
|
|
|
216
|
|
|
$sink->seek(0); |
217
|
|
|
$source->close(); |
218
|
|
|
|
219
|
|
|
return $sink; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Create a resource and check to ensure it was created successfully |
224
|
|
|
* |
225
|
|
|
* @param callable $callback Callable that returns stream resource |
226
|
|
|
* |
227
|
|
|
* @return resource |
228
|
|
|
* @throws \RuntimeException on error |
229
|
|
|
*/ |
230
|
|
|
private function createResource(callable $callback) |
231
|
|
|
{ |
232
|
|
|
$errors = null; |
233
|
|
|
set_error_handler(function ($_, $msg, $file, $line) use (&$errors) { |
234
|
|
|
$errors[] = [ |
235
|
|
|
'message' => $msg, |
236
|
|
|
'file' => $file, |
237
|
|
|
'line' => $line |
238
|
|
|
]; |
239
|
|
|
return true; |
240
|
|
|
}); |
241
|
|
|
|
242
|
|
|
$resource = $callback(); |
243
|
|
|
restore_error_handler(); |
244
|
|
|
|
245
|
|
|
if (!$resource) { |
246
|
|
|
$message = 'Error creating resource: '; |
247
|
|
|
foreach ($errors as $err) { |
248
|
|
|
foreach ($err as $key => $value) { |
249
|
|
|
$message .= "[$key] $value" . PHP_EOL; |
250
|
|
|
} |
251
|
|
|
} |
252
|
|
|
throw new \RuntimeException(trim($message)); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
return $resource; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
private function createStream(RequestInterface $request, array $options) |
259
|
|
|
{ |
260
|
|
|
static $methods; |
261
|
|
|
if (!$methods) { |
262
|
|
|
$methods = array_flip(get_class_methods(__CLASS__)); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
// HTTP/1.1 streams using the PHP stream wrapper require a |
266
|
|
|
// Connection: close header |
267
|
|
|
if ($request->getProtocolVersion() == '1.1' |
268
|
|
|
&& !$request->hasHeader('Connection') |
269
|
|
|
) { |
270
|
|
|
$request = $request->withHeader('Connection', 'close'); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
// Ensure SSL is verified by default |
274
|
|
|
if (!isset($options['verify'])) { |
275
|
|
|
$options['verify'] = true; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
$params = []; |
279
|
|
|
$context = $this->getDefaultContext($request, $options); |
|
|
|
|
280
|
|
|
|
281
|
|
|
if (isset($options['on_headers']) && !is_callable($options['on_headers'])) { |
282
|
|
|
throw new \InvalidArgumentException('on_headers must be callable'); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
if (!empty($options)) { |
286
|
|
|
foreach ($options as $key => $value) { |
287
|
|
|
$method = "add_{$key}"; |
288
|
|
|
if (isset($methods[$method])) { |
289
|
|
|
$this->{$method}($request, $context, $value, $params); |
290
|
|
|
} |
291
|
|
|
} |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
if (isset($options['stream_context'])) { |
295
|
|
|
if (!is_array($options['stream_context'])) { |
296
|
|
|
throw new \InvalidArgumentException('stream_context must be an array'); |
297
|
|
|
} |
298
|
|
|
$context = array_replace_recursive( |
299
|
|
|
$context, |
300
|
|
|
$options['stream_context'] |
301
|
|
|
); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
// Microsoft NTLM authentication only supported with curl handler |
305
|
|
|
if (isset($options['auth']) |
306
|
|
|
&& is_array($options['auth']) |
307
|
|
|
&& isset($options['auth'][2]) |
308
|
|
|
&& 'ntlm' == $options['auth'][2] |
309
|
|
|
) { |
310
|
|
|
|
311
|
|
|
throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
$uri = $this->resolveHost($request, $options); |
315
|
|
|
|
316
|
|
|
$context = $this->createResource( |
317
|
|
|
function () use ($context, $params) { |
318
|
|
|
return stream_context_create($context, $params); |
319
|
|
|
} |
320
|
|
|
); |
321
|
|
|
|
322
|
|
|
return $this->createResource( |
323
|
|
|
function () use ($uri, &$http_response_header, $context, $options) { |
324
|
|
|
$resource = fopen((string) $uri, 'r', null, $context); |
325
|
|
|
$this->lastHeaders = $http_response_header; |
326
|
|
|
|
327
|
|
|
if (isset($options['read_timeout'])) { |
328
|
|
|
$readTimeout = $options['read_timeout']; |
329
|
|
|
$sec = (int) $readTimeout; |
330
|
|
|
$usec = ($readTimeout - $sec) * 100000; |
331
|
|
|
stream_set_timeout($resource, $sec, $usec); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
return $resource; |
335
|
|
|
} |
336
|
|
|
); |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
private function resolveHost(RequestInterface $request, array $options) |
340
|
|
|
{ |
341
|
|
|
$uri = $request->getUri(); |
342
|
|
|
|
343
|
|
|
if (isset($options['force_ip_resolve']) && !filter_var($uri->getHost(), FILTER_VALIDATE_IP)) { |
344
|
|
|
if ('v4' === $options['force_ip_resolve']) { |
345
|
|
|
$records = dns_get_record($uri->getHost(), DNS_A); |
346
|
|
View Code Duplication |
if (!isset($records[0]['ip'])) { |
|
|
|
|
347
|
|
|
throw new ConnectException(sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); |
348
|
|
|
} |
349
|
|
|
$uri = $uri->withHost($records[0]['ip']); |
350
|
|
|
} elseif ('v6' === $options['force_ip_resolve']) { |
351
|
|
|
$records = dns_get_record($uri->getHost(), DNS_AAAA); |
352
|
|
View Code Duplication |
if (!isset($records[0]['ipv6'])) { |
|
|
|
|
353
|
|
|
throw new ConnectException(sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); |
354
|
|
|
} |
355
|
|
|
$uri = $uri->withHost('[' . $records[0]['ipv6'] . ']'); |
356
|
|
|
} |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
return $uri; |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
private function getDefaultContext(RequestInterface $request) |
363
|
|
|
{ |
364
|
|
|
$headers = ''; |
365
|
|
|
foreach ($request->getHeaders() as $name => $value) { |
366
|
|
|
foreach ($value as $val) { |
367
|
|
|
$headers .= "$name: $val\r\n"; |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
$context = [ |
372
|
|
|
'http' => [ |
373
|
|
|
'method' => $request->getMethod(), |
374
|
|
|
'header' => $headers, |
375
|
|
|
'protocol_version' => $request->getProtocolVersion(), |
376
|
|
|
'ignore_errors' => true, |
377
|
|
|
'follow_location' => 0, |
378
|
|
|
], |
379
|
|
|
]; |
380
|
|
|
|
381
|
|
|
$body = (string) $request->getBody(); |
382
|
|
|
|
383
|
|
|
if (!empty($body)) { |
384
|
|
|
$context['http']['content'] = $body; |
385
|
|
|
// Prevent the HTTP handler from adding a Content-Type header. |
386
|
|
|
if (!$request->hasHeader('Content-Type')) { |
387
|
|
|
$context['http']['header'] .= "Content-Type:\r\n"; |
388
|
|
|
} |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
$context['http']['header'] = rtrim($context['http']['header']); |
392
|
|
|
|
393
|
|
|
return $context; |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
private function add_proxy(RequestInterface $request, &$options, $value, &$params) |
|
|
|
|
397
|
|
|
{ |
398
|
|
|
if (!is_array($value)) { |
399
|
|
|
$options['http']['proxy'] = $value; |
400
|
|
|
} else { |
401
|
|
|
$scheme = $request->getUri()->getScheme(); |
402
|
|
|
if (isset($value[$scheme])) { |
403
|
|
|
if (!isset($value['no']) |
404
|
|
|
|| !\GuzzleHttp\is_host_in_noproxy( |
405
|
|
|
$request->getUri()->getHost(), |
406
|
|
|
$value['no'] |
407
|
|
|
) |
408
|
|
|
) { |
409
|
|
|
$options['http']['proxy'] = $value[$scheme]; |
410
|
|
|
} |
411
|
|
|
} |
412
|
|
|
} |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
private function add_timeout(RequestInterface $request, &$options, $value, &$params) |
|
|
|
|
416
|
|
|
{ |
417
|
|
|
if ($value > 0) { |
418
|
|
|
$options['http']['timeout'] = $value; |
419
|
|
|
} |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
private function add_verify(RequestInterface $request, &$options, $value, &$params) |
|
|
|
|
423
|
|
|
{ |
424
|
|
|
if ($value === true) { |
425
|
|
|
// PHP 5.6 or greater will find the system cert by default. When |
426
|
|
|
// < 5.6, use the Guzzle bundled cacert. |
427
|
|
|
if (PHP_VERSION_ID < 50600) { |
428
|
|
|
$options['ssl']['cafile'] = \GuzzleHttp\default_ca_bundle(); |
429
|
|
|
} |
430
|
|
|
} elseif (is_string($value)) { |
431
|
|
|
$options['ssl']['cafile'] = $value; |
432
|
|
|
if (!file_exists($value)) { |
433
|
|
|
throw new \RuntimeException("SSL CA bundle not found: $value"); |
434
|
|
|
} |
435
|
|
|
} elseif ($value === false) { |
436
|
|
|
$options['ssl']['verify_peer'] = false; |
437
|
|
|
$options['ssl']['verify_peer_name'] = false; |
438
|
|
|
return; |
439
|
|
|
} else { |
440
|
|
|
throw new \InvalidArgumentException('Invalid verify request option'); |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
$options['ssl']['verify_peer'] = true; |
444
|
|
|
$options['ssl']['verify_peer_name'] = true; |
445
|
|
|
$options['ssl']['allow_self_signed'] = false; |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
private function add_cert(RequestInterface $request, &$options, $value, &$params) |
|
|
|
|
449
|
|
|
{ |
450
|
|
|
if (is_array($value)) { |
451
|
|
|
$options['ssl']['passphrase'] = $value[1]; |
452
|
|
|
$value = $value[0]; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
if (!file_exists($value)) { |
456
|
|
|
throw new \RuntimeException("SSL certificate not found: {$value}"); |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
$options['ssl']['local_cert'] = $value; |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
private function add_progress(RequestInterface $request, &$options, $value, &$params) |
|
|
|
|
463
|
|
|
{ |
464
|
|
|
$this->addNotification( |
465
|
|
|
$params, |
466
|
|
|
function ($code, $a, $b, $c, $transferred, $total) use ($value) { |
467
|
|
|
if ($code == STREAM_NOTIFY_PROGRESS) { |
468
|
|
|
$value($total, $transferred, null, null); |
469
|
|
|
} |
470
|
|
|
} |
471
|
|
|
); |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
private function add_debug(RequestInterface $request, &$options, $value, &$params) |
|
|
|
|
475
|
|
|
{ |
476
|
|
|
if ($value === false) { |
477
|
|
|
return; |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
static $map = [ |
481
|
|
|
STREAM_NOTIFY_CONNECT => 'CONNECT', |
482
|
|
|
STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', |
483
|
|
|
STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', |
484
|
|
|
STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', |
485
|
|
|
STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', |
486
|
|
|
STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', |
487
|
|
|
STREAM_NOTIFY_PROGRESS => 'PROGRESS', |
488
|
|
|
STREAM_NOTIFY_FAILURE => 'FAILURE', |
489
|
|
|
STREAM_NOTIFY_COMPLETED => 'COMPLETED', |
490
|
|
|
STREAM_NOTIFY_RESOLVE => 'RESOLVE', |
491
|
|
|
]; |
492
|
|
|
static $args = ['severity', 'message', 'message_code', |
493
|
|
|
'bytes_transferred', 'bytes_max']; |
494
|
|
|
|
495
|
|
|
$value = \GuzzleHttp\debug_resource($value); |
496
|
|
|
$ident = $request->getMethod() . ' ' . $request->getUri()->withFragment(''); |
497
|
|
|
$this->addNotification( |
498
|
|
|
$params, |
499
|
|
|
function () use ($ident, $value, $map, $args) { |
500
|
|
|
$passed = func_get_args(); |
501
|
|
|
$code = array_shift($passed); |
502
|
|
|
fprintf($value, '<%s> [%s] ', $ident, $map[$code]); |
503
|
|
|
foreach (array_filter($passed) as $i => $v) { |
504
|
|
|
fwrite($value, $args[$i] . ': "' . $v . '" '); |
505
|
|
|
} |
506
|
|
|
fwrite($value, "\n"); |
507
|
|
|
} |
508
|
|
|
); |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
private function addNotification(array &$params, callable $notify) |
512
|
|
|
{ |
513
|
|
|
// Wrap the existing function if needed. |
514
|
|
|
if (!isset($params['notification'])) { |
515
|
|
|
$params['notification'] = $notify; |
516
|
|
|
} else { |
517
|
|
|
$params['notification'] = $this->callArray([ |
518
|
|
|
$params['notification'], |
519
|
|
|
$notify |
520
|
|
|
]); |
521
|
|
|
} |
522
|
|
|
} |
523
|
|
|
|
524
|
|
|
private function callArray(array $functions) |
525
|
|
|
{ |
526
|
|
|
return function () use ($functions) { |
527
|
|
|
$args = func_get_args(); |
528
|
|
|
foreach ($functions as $fn) { |
529
|
|
|
call_user_func_array($fn, $args); |
530
|
|
|
} |
531
|
|
|
}; |
532
|
|
|
} |
533
|
|
|
} |
534
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.