Passed
Pull Request — master (#63)
by David
10:36
created

RequestWriter::fwrite()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 38
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 11.1035

Importance

Changes 0
Metric Value
cc 5
eloc 15
nc 5
nop 2
dl 0
loc 38
ccs 6
cts 16
cp 0.375
crap 11.1035
rs 9.4555
c 0
b 0
f 0
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);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for stream_select(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

107
        /** @scrutinizer ignore-unhandled */ @stream_select($read, $write, $except, 0);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
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