Completed
Push — master ( 66e423...45459e )
by Joel
08:39
created

Client::getConnection()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 47
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5.3073

Importance

Changes 0
Metric Value
cc 5
eloc 25
nc 5
nop 0
dl 0
loc 47
rs 9.2088
c 0
b 0
f 0
ccs 20
cts 26
cp 0.7692
crap 5.3073
1
<?php
2
3
namespace RedirectionIO\Client\Sdk;
4
5
use Psr\Log\LoggerInterface;
6
use Psr\Log\NullLogger;
7
use RedirectionIO\Client\Sdk\Command\LogCommand;
8
use RedirectionIO\Client\Sdk\Command\MatchCommand;
9
use RedirectionIO\Client\Sdk\Exception\AgentNotFoundException;
10
use RedirectionIO\Client\Sdk\Exception\BadConfigurationException;
11
use RedirectionIO\Client\Sdk\Exception\ExceptionInterface;
12
use RedirectionIO\Client\Sdk\HttpMessage\Request;
13
use RedirectionIO\Client\Sdk\HttpMessage\Response;
14
15
class Client
16
{
17
    private $connections;
18
    private $timeout;
19
    private $debug;
20
    private $logger;
21
    private $currentConnection;
22
    private $currentConnectionName;
23
24
    /**
25
     * @param int  $timeout
26 13
     * @param bool $debug
27
     */
28 13
    public function __construct(array $connections, $timeout = 10000, $debug = false, LoggerInterface $logger = null)
29 1
    {
30
        if (!$connections) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $connections of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
31
            throw new BadConfigurationException('At least one connection is required.');
32 13
        }
33 13
34 13
        foreach ($connections as $name => $connection) {
35 13
            $this->connections[$name] = [
36
                'remote_socket' => $connection,
37 13
                'retries' => 2,
38
            ];
39 13
        }
40 13
41 13
        $this->timeout = $timeout;
42 13
        $this->debug = $debug;
43
        $this->logger = $logger ?: new NullLogger();
44 9
    }
45
46
    /**
47 9
     * @deprecated findRedirect() is deprecated since version 0.2 and will be removed in 0.3. Use request(new MatchCommand()) instead.
48 9
     */
49 9
    public function findRedirect(Request $request)
50 9
    {
51 9
        @trigger_error('findRedirect() is deprecated since version 0.2 and will be removed in 0.3. Use request(new MatchCommand()) instead.', E_USER_DEPRECATED);
52 9
53 9
        return $this->request(new MatchCommand($request));
54
    }
55
56 9
    /**
57 9
     * @deprecated log() is deprecated since version 0.2 and will be removed in 0.3. Use request(new LogCommand()) instead.
58 3
     */
59 1
    public function log(Request $request, Response $response)
60
    {
61
        @trigger_error('log() is deprecated since version 0.2 and will be removed in 0.3. Use request(new LogCommand()) instead.', E_USER_DEPRECATED);
62 2
63
        $this->request(new LogCommand($request, $response));
64
65 7
        return true;
66 1
    }
67
68
    public function request(Command\CommandInterface $command)
69 6
    {
70
        try {
71 6
            return $this->doRequest($command);
72
        } catch (ExceptionInterface $exception) {
73
            if ($this->debug) {
74
                throw $exception;
75
            }
76
77
            return null;
78
        }
79 6
    }
80 6
81
    private function doRequest(Command\CommandInterface $command)
82 6
    {
83
        $connection = $this->getConnection();
84
85
        $toSend = $command->getName() . "\0" . $command->getRequest() . "\0";
86 6
        $sent = $this->box('doSend', false, [$connection, $toSend]);
87 6
88 6
        if (false === $sent) {
89
            $this->logger->debug('Impossible to send content to the connection.', [
90 6
                'options' => $this->connections[$this->currentConnectionName],
91
            ]);
92
93 3
            --$this->connections[$this->currentConnectionName]['retries'];
94
            $this->currentConnection = null;
95
            $this->box('disconnect', null, [$connection]);
96 3
97 3
            return $this->doRequest($command);
98 3
        }
99 3
100 3
        if (!$command->hasResponse()) {
101 3
            return null;
102 3
        }
103 3
104
        $received = $this->box('doGet', false, [$connection]);
105 3
106
        // false: the persistent connection is stale
107
        if (false === $received) {
108
            $this->logger->debug('Impossible to get content from the connection.', [
109 3
                'options' => $this->connections[$this->currentConnectionName],
110
            ]);
111
112
            --$this->connections[$this->currentConnectionName]['retries'];
113
            $this->currentConnection = null;
114 3
            $this->box('disconnect', null, [$connection]);
115 2
116 2
            return $this->doRequest($command);
117 1
        }
118 1
119
        return $command->parseResponse(trim($received));
120 1
    }
121
122
    private function getConnection()
123
    {
124 12
        if (null !== $this->currentConnection) {
125
            return $this->currentConnection;
126 12
        }
127
128 8
        foreach ($this->connections as $name => $connection) {
129 8
            if ($connection['retries'] <= 0) {
130
                continue;
131
            }
132 8
133
            $this->logger->debug('New connection chosen. Trying to connect.', [
134
                'connection' => $connection,
135
                'name' => $name,
136
            ]);
137
138
            $connection = $this->box('doConnect', false, [$connection]);
139
140
            if (false === $connection) {
141
                $this->logger->debug('Impossible to connect to the connection.', [
142
                    'connection' => $connection,
143 8
                    'name' => $name,
144
                ]);
145
146 8
                $this->connections[$name]['retries'] = 0;
147 1
148 1
                continue;
149 1
            }
150
151 1
            $this->logger->debug('New connection approved.', [
152 1
                'connection' => $connection,
153
                'name' => $name,
154 1
            ]);
155
156
            stream_set_timeout($connection, 0, $this->timeout);
157 8
158
            $this->currentConnection = $connection;
159
            $this->currentConnectionName = $name;
160 12
161
            return $connection;
162 12
        }
163 2
164
        $this->logger->error('Can not find an agent.', [
165
            'connections_options' => $this->connections,
166 12
        ]);
167 12
168 1
        throw new AgentNotFoundException();
169
    }
170
171 12
    private function doConnect($options)
172 12
    {
173 12
        return stream_socket_client(
174 12
            $options['remote_socket'],
175
            $errNo,
176 12
            $errMsg,
177
            1, // This value is not used but it should not be 0
178 12
            STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT
179 6
        );
180 6
    }
181 6
182 6
    private function disconnect($connection)
183
    {
184 6
        fclose($connection);
185
    }
186 6
187
    private function doSend($connection, $content)
188
    {
189 8
        return $this->fwrite($connection, $content);
190 8
    }
191 8
192 8
    private function doGet($connection)
193
    {
194 8
        $buffer = '';
195
196 8
        while (true) {
197 8
            if (feof($connection)) {
198
                return false;
199 8
            }
200 5
201
            $char = fread($connection, 1);
202 5
203 5
            if ($char === false) {
204 5
                return false;
205
            }
206 5
207
            // On timeout char is empty
208
            if ($char === '') {
209 12
                return false;
210
            }
211 12
212 12
            if ($char === "\0") {
213 12
                return $buffer;
214 12
            }
215 12
216 12
            $buffer .= $char;
217 12
        }
218
    }
219
220 8
    private function box($method, $defaultReturnValue = null, array $args = [])
221
    {
222 8
        set_error_handler(__CLASS__.'::handleInternalError');
223
224
        try {
225 8
            $returnValue = \call_user_func_array([$this, $method], $args);
226
        } catch (\ErrorException $exception) {
227 8
            $returnValue = $defaultReturnValue;
228
229
            $this->logger->warning('Impossible to execute a boxed call.', [
230 12
                'method' => $method,
231
                'default_return_value' => $defaultReturnValue,
232 12
                'args' => $args,
233
                'exception' => $exception,
234
            ]);
235 12
        }
236 12
237 6
        restore_error_handler();
238
239 6
        return $returnValue;
240 6
    }
241 6
242 6
    private static function handleInternalError($type, $message, $file, $line)
243 6
    {
244 6
        throw new \ErrorException($message, 0, $type, $file, $line);
245
    }
246
247 12
    /**
248
     * Replace fwrite behavior as API is broken in PHP.
249 12
     *
250
     * @see https://secure.phabricator.com/rPHU69490c53c9c2ef2002bc2dd4cecfe9a4b080b497
251
     *
252 6
     * @param resource $stream The stream resource
253
     * @param string   $bytes  Bytes written in the stream
254 6
     *
255
     * @return bool|int false if pipe is broken, number of bytes written otherwise
256
     */
257
    private function fwrite($stream, $bytes)
258
    {
259
        if (!\strlen($bytes)) {
260
            return 0;
261
        }
262
        $result = @fwrite($stream, $bytes);
263
        if (0 !== $result) {
264
            // In cases where some bytes are witten (`$result > 0`) or
265
            // an error occurs (`$result === false`), the behavior of fwrite() is
266
            // correct. We can return the value as-is.
267 8
            return $result;
268
        }
269 8
        // If we make it here, we performed a 0-length write. Try to distinguish
270
        // between EAGAIN and EPIPE. To do this, we're going to `stream_select()`
271
        // the stream, write to it again if PHP claims that it's writable, and
272 8
        // consider the pipe broken if the write fails.
273 8
        $read = [];
274
        $write = [$stream];
275
        $except = [];
276
        @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

276
        /** @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...
277 8
        if (!$write) {
278
            // The stream isn't writable, so we conclude that it probably really is
279
            // blocked and the underlying error was EAGAIN. Return 0 to indicate that
280
            // no data could be written yet.
281
            return 0;
282
        }
283
        // If we make it here, PHP **just** claimed that this stream is writable, so
284
        // perform a write. If the write also fails, conclude that these failures are
285
        // EPIPE or some other permanent failure.
286
        $result = @fwrite($stream, $bytes);
287
        if (0 !== $result) {
288
            // The write worked or failed explicitly. This value is fine to return.
289
            return $result;
290
        }
291
        // We performed a 0-length write, were told that the stream was writable, and
292
        // then immediately performed another 0-length write. Conclude that the pipe
293
        // is broken and return `false`.
294
        return false;
295
    }
296
}
297