1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Http\Client\Socket; |
4
|
|
|
|
5
|
|
|
use Http\Client\Socket\Exception\BrokenPipeException; |
6
|
|
|
use Psr\Http\Message\RequestInterface; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* Method for writing request. |
10
|
|
|
* |
11
|
|
|
* Mainly used by SocketHttpClient |
12
|
|
|
* |
13
|
|
|
* @author Joel Wurtz <[email protected]> |
14
|
|
|
*/ |
15
|
|
|
trait RequestWriter |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* Write a request to a socket. |
19
|
|
|
* |
20
|
|
|
* @param resource $socket |
21
|
|
|
* |
22
|
|
|
* @throws BrokenPipeException |
23
|
|
|
*/ |
24
|
59 |
|
protected function writeRequest($socket, RequestInterface $request, int $bufferSize = 8192) |
25
|
|
|
{ |
26
|
59 |
|
if (false === $this->fwrite($socket, $this->transformRequestHeadersToString($request))) { |
27
|
|
|
throw new BrokenPipeException('Failed to send request, underlying socket not accessible, (BROKEN EPIPE)', $request); |
28
|
|
|
} |
29
|
|
|
|
30
|
59 |
|
if ($request->getBody()->isReadable()) { |
31
|
59 |
|
$this->writeBody($socket, $request, $bufferSize); |
32
|
|
|
} |
33
|
59 |
|
} |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Write Body of the request. |
37
|
|
|
* |
38
|
|
|
* @param resource $socket |
39
|
|
|
* |
40
|
|
|
* @throws BrokenPipeException |
41
|
|
|
*/ |
42
|
59 |
|
protected function writeBody($socket, RequestInterface $request, int $bufferSize = 8192) |
43
|
|
|
{ |
44
|
59 |
|
$body = $request->getBody(); |
45
|
|
|
|
46
|
59 |
|
if ($body->isSeekable()) { |
47
|
59 |
|
$body->rewind(); |
48
|
|
|
} |
49
|
|
|
|
50
|
59 |
|
while (!$body->eof()) { |
51
|
59 |
|
$buffer = $body->read($bufferSize); |
52
|
|
|
|
53
|
59 |
|
if (false === $this->fwrite($socket, $buffer)) { |
54
|
|
|
throw new BrokenPipeException('An error occur when writing request to client (BROKEN EPIPE)', $request); |
55
|
|
|
} |
56
|
|
|
} |
57
|
59 |
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Produce the header of request as a string based on a PSR Request. |
61
|
|
|
*/ |
62
|
59 |
|
protected function transformRequestHeadersToString(RequestInterface $request): string |
63
|
|
|
{ |
64
|
59 |
|
$message = vsprintf('%s %s HTTP/%s', [ |
65
|
59 |
|
strtoupper($request->getMethod()), |
66
|
59 |
|
$request->getRequestTarget(), |
67
|
59 |
|
$request->getProtocolVersion(), |
68
|
59 |
|
])."\r\n"; |
69
|
|
|
|
70
|
59 |
|
foreach ($request->getHeaders() as $name => $values) { |
71
|
59 |
|
$message .= $name.': '.implode(', ', $values)."\r\n"; |
72
|
|
|
} |
73
|
|
|
|
74
|
59 |
|
$message .= "\r\n"; |
75
|
|
|
|
76
|
59 |
|
return $message; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Replace fwrite behavior as api is broken in PHP. |
81
|
|
|
* |
82
|
|
|
* @see https://secure.phabricator.com/rPHU69490c53c9c2ef2002bc2dd4cecfe9a4b080b497 |
83
|
|
|
* |
84
|
|
|
* @param resource $stream The stream resource |
85
|
|
|
* |
86
|
|
|
* @return bool|int false if pipe is broken, number of bytes written otherwise |
87
|
|
|
*/ |
88
|
59 |
|
private function fwrite($stream, string $bytes) |
89
|
|
|
{ |
90
|
59 |
|
if (!strlen($bytes)) { |
91
|
41 |
|
return 0; |
92
|
|
|
} |
93
|
59 |
|
$result = @fwrite($stream, $bytes); |
94
|
59 |
|
if (0 !== $result) { |
95
|
|
|
// In cases where some bytes are witten (`$result > 0`) or |
96
|
|
|
// an error occurs (`$result === false`), the behavior of fwrite() is |
97
|
|
|
// correct. We can return the value as-is. |
98
|
59 |
|
return $result; |
99
|
|
|
} |
100
|
|
|
// If we make it here, we performed a 0-length write. Try to distinguish |
101
|
|
|
// between EAGAIN and EPIPE. To do this, we're going to `stream_select()` |
102
|
|
|
// the stream, write to it again if PHP claims that it's writable, and |
103
|
|
|
// consider the pipe broken if the write fails. |
104
|
|
|
$read = []; |
105
|
|
|
$write = [$stream]; |
106
|
|
|
$except = []; |
107
|
|
|
@stream_select($read, $write, $except, 0); |
108
|
|
|
if (!$write) { |
|
|
|
|
109
|
|
|
// The stream isn't writable, so we conclude that it probably really is |
110
|
|
|
// blocked and the underlying error was EAGAIN. Return 0 to indicate that |
111
|
|
|
// no data could be written yet. |
112
|
|
|
return 0; |
113
|
|
|
} |
114
|
|
|
// If we make it here, PHP **just** claimed that this stream is writable, so |
115
|
|
|
// perform a write. If the write also fails, conclude that these failures are |
116
|
|
|
// EPIPE or some other permanent failure. |
117
|
|
|
$result = @fwrite($stream, $bytes); |
118
|
|
|
if (0 !== $result) { |
119
|
|
|
// The write worked or failed explicitly. This value is fine to return. |
120
|
|
|
return $result; |
121
|
|
|
} |
122
|
|
|
// We performed a 0-length write, were told that the stream was writable, and |
123
|
|
|
// then immediately performed another 0-length write. Conclude that the pipe |
124
|
|
|
// is broken and return `false`. |
125
|
|
|
return false; |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.