Passed
Push — master ( 401ce9...01f538 )
by Igor
01:14 queued 10s
created

SocketClient   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 165
Duplicated Lines 0 %

Test Coverage

Coverage 78.85%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 44
c 1
b 1
f 0
dl 0
loc 165
ccs 41
cts 52
cp 0.7885
rs 10
wmc 20

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getConnection() 0 11 3
A createConnection() 0 26 6
A __destruct() 0 4 2
A connect() 0 3 1
A send() 0 6 1
A __construct() 0 5 1
A checkForTimeout() 0 8 2
A receiveResponse() 0 11 2
A sendRequest() 0 7 2
1
<?php
2
/*
3
 * This file is part of JSON RPC Client.
4
 *
5
 * (c) Igor Lazarev <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Strider2038\JsonRpcClient\Transport\Socket;
12
13
use Strider2038\JsonRpcClient\Exception\ConnectionFailedException;
14
use Strider2038\JsonRpcClient\Exception\ConnectionLostException;
15
use Strider2038\JsonRpcClient\Exception\RemoteProcedureCallFailedException;
16
17
/**
18
 * @author Igor Lazarev <[email protected]>
19
 */
20
class SocketClient
21
{
22
    /**
23
     * Connection in URL format.
24
     *
25
     * @var string
26
     */
27
    private $url;
28
29
    /**
30
     * Connection timeout in seconds.
31
     *
32
     * @var float
33
     */
34
    private $connectionTimeoutS;
35
36
    /**
37
     * Request timeout in microseconds.
38
     *
39
     * @var int
40
     */
41
    private $requestTimeoutUs;
42
43
    /** @var resource|null */
44
    private $connection;
45
46 9
    public function __construct(string $url, int $connectionTimeoutUs, int $requestTimeoutUs)
47
    {
48 9
        $this->url = $url;
49 9
        $this->connectionTimeoutS = ((float) $connectionTimeoutUs) / 1000000;
50 9
        $this->requestTimeoutUs = $requestTimeoutUs;
51 9
    }
52
53 9
    public function __destruct()
54
    {
55 9
        if (is_resource($this->connection)) {
56 5
            fclose($this->connection);
57
        }
58 9
    }
59
60
    /**
61
     * Sends request under socket client and returns response.
62
     *
63
     * @throws ConnectionFailedException          when connection to server cannot be established
64
     * @throws ConnectionLostException            if connection to server was lost and request cannot be sent
65
     * @throws RemoteProcedureCallFailedException if request to server was sent and response cannot be received.
66
     *                                            Be aware that request may be successfully processed by server,
67
     *                                            so resending the request may lead to inconsistent state
68
     *                                            of the server data.
69
     */
70 5
    public function send(string $request): string
71
    {
72 5
        $connection = $this->getConnection();
73 5
        $this->sendRequest($connection, $request);
74
75 5
        return $this->receiveResponse($connection);
76
    }
77
78
    /**
79
     * Recreates connection. Can be used to reconnect.
80
     *
81
     * @throws ConnectionFailedException
82
     */
83
    public function connect(): void
84
    {
85
        $this->connection = $this->createConnection();
86
    }
87
88
    /**
89
     * @throws ConnectionFailedException
90
     * @throws ConnectionLostException
91
     *
92
     * @return resource
93
     */
94 5
    private function getConnection()
95
    {
96 5
        if (null === $this->connection) {
97 5
            $this->connection = $this->createConnection();
98
        }
99
100 5
        if (feof($this->connection)) {
101
            throw new ConnectionLostException($this->url, 'eof received');
102
        }
103
104 5
        return $this->connection;
105
    }
106
107
    /**
108
     * @throws ConnectionFailedException
109
     *
110
     * @return resource
111
     */
112 5
    private function createConnection()
113
    {
114 5
        $start = microtime(true);
115
116 5
        while (true) {
117 5
            $connection = @stream_socket_client($this->url, $errno, $errstr, $this->connectionTimeoutS);
118 5
            if (is_resource($connection)) {
119 5
                break;
120
            }
121
122
            if (microtime(true) - $start >= $this->connectionTimeoutS) {
123
                break;
124
            }
125
126
            usleep(100);
127
        }
128
129 5
        if (!is_resource($connection)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $connection does not seem to be defined for all execution paths leading up to this point.
Loading history...
130
            throw new ConnectionFailedException($this->url, sprintf('%d: %s', $errno, $errstr));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $errstr does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $errno does not seem to be defined for all execution paths leading up to this point.
Loading history...
131
        }
132
133 5
        if (false === stream_set_timeout($connection, 0, $this->requestTimeoutUs)) {
134
            throw new ConnectionFailedException($this->url, 'failed to set request timeout');
135
        }
136
137 5
        return $connection;
138
    }
139
140
    /**
141
     * @param resource $connection
142
     *
143
     * @throws ConnectionLostException
144
     */
145 5
    private function sendRequest($connection, string $request): void
146
    {
147 5
        if (false === fwrite($connection, $request."\n")) {
148
            throw new ConnectionLostException($this->url, 'failed to write data into stream');
149
        }
150
151 5
        fflush($connection);
152 5
    }
153
154
    /**
155
     * @param resource $connection
156
     *
157
     * @throws RemoteProcedureCallFailedException
158
     */
159 5
    private function receiveResponse($connection): string
160
    {
161 5
        $response = fgets($connection);
162
163 5
        if (false === $response) {
164 1
            $this->checkForTimeout($connection);
165
166
            throw new RemoteProcedureCallFailedException(sprintf('Failed to get response from %s.', $this->url));
167
        }
168
169 4
        return $response;
170
    }
171
172
    /**
173
     * @param resource $stream
174
     *
175
     * @throws RemoteProcedureCallFailedException
176
     */
177 1
    private function checkForTimeout($stream): void
178
    {
179 1
        $info = stream_get_meta_data($stream);
180
181 1
        if ($info['timed_out']) {
182 1
            $error = sprintf('Request to %s failed by timeout %d us.', $this->url, $this->requestTimeoutUs);
183
184 1
            throw new RemoteProcedureCallFailedException($error);
185
        }
186
    }
187
}
188