Passed
Push — master ( fe4064...fbe9f7 )
by Joel
02:31
created

Client::resolveConnectionOptions()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7.0957

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 5
nop 1
dl 0
loc 28
ccs 14
cts 16
cp 0.875
crap 7.0957
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
namespace RedirectionIO\Client\Sdk;
4
5
use Psr\Log\LoggerInterface;
6
use Psr\Log\NullLogger;
7
use RedirectionIO\Client\Sdk\Exception\AgentNotFoundException;
8
use RedirectionIO\Client\Sdk\Exception\BadConfigurationException;
9
use RedirectionIO\Client\Sdk\Exception\ExceptionInterface;
10
use RedirectionIO\Client\Sdk\HttpMessage\RedirectResponse;
11
use RedirectionIO\Client\Sdk\HttpMessage\Request;
12
use RedirectionIO\Client\Sdk\HttpMessage\Response;
13
14
class Client
15
{
16
    private $connections;
17
    private $timeout;
18
    private $debug;
19
    private $logger;
20
    private $currentConnection;
21
    private $currentConnectionName;
22
23
    /**
24
     * @param int  $timeout
25
     * @param bool $debug
26
     */
27 12
    public function __construct(array $connections, $timeout = 10000, $debug = false, LoggerInterface $logger = null)
28
    {
29 12
        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...
30 1
            throw new BadConfigurationException('At least one connection is required.');
31
        }
32
33 12
        foreach ($connections as $name => $connection) {
34 12
            $this->connections[$name] = [
35 12
                'remote_socket' => $connection,
36 12
                'retries' => 2,
37
            ];
38 12
        }
39
40 12
        $this->timeout = $timeout;
41 12
        $this->debug = $debug;
42 12
        $this->logger = $logger ?: new NullLogger();
43 12
    }
44
45 8
    public function findRedirect(Request $request)
46
    {
47
        $requestContext = [
48 8
            'host' => $request->getHost(),
49 8
            'request_uri' => $request->getPath(),
50 8
            'user_agent' => $request->getUserAgent(),
51 8
            'referer' => $request->getReferer(),
52 8
            'scheme' => $request->getScheme(),
53 8
            'use_json' => true,
54 8
        ];
55
56
        try {
57 8
            $agentResponse = $this->request('GET', $requestContext);
58 8
        } catch (ExceptionInterface $exception) {
59 3
            if ($this->debug) {
60 1
                throw $exception;
61
            }
62
63 2
            return null;
64
        }
65
66 6
        if (0 === strlen($agentResponse)) {
67 1
            return null;
68
        }
69
70 5
        $agentResponse = json_decode($agentResponse);
71
72 5
        return new RedirectResponse($agentResponse->location, (int) $agentResponse->status_code);
73
    }
74
75 3
    public function log(Request $request, Response $response)
76
    {
77
        $responseContext = [
78 3
            'status_code' => $response->getStatusCode(),
79 3
            'host' => $request->getHost(),
80 3
            'request_uri' => $request->getPath(),
81 3
            'user_agent' => $request->getUserAgent(),
82 3
            'referer' => $request->getReferer(),
83 3
            'scheme' => $request->getScheme(),
84 3
            'use_json' => true,
85 3
        ];
86
87
        try {
88 3
            return (bool) $this->request('LOG', $responseContext);
89 2
        } catch (ExceptionInterface $exception) {
90 2
            if ($this->debug) {
91 1
                throw $exception;
92
            }
93
94 1
            return false;
95
        }
96
    }
97
98 11
    private function request($command, $context)
99
    {
100 11
        $connection = $this->getConnection();
101
102 7
        $content = $command.' '.json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)."\n";
103 7
        $sent = $this->box('doSend', false, [$connection, $content]);
104
105
        // if the pipe is broken, `fwrite` will throw a Notice
106 7
        if (false === $sent) {
107
            $this->logger->debug('Impossible to send content to the connection.', [
108
                'options' => $this->connections[$this->currentConnectionName],
109
            ]);
110
111
            --$this->connections[$this->currentConnectionName]['retries'];
112
            $this->currentConnection = null;
113
114
            return $this->request($command, $context);
115
        }
116
117 7
        $received = $this->box('doGet', false, [$connection]);
118
119
        // false: the persistent connection is stale
120 7
        if (false === $received) {
121 1
            $this->logger->debug('Impossible to get content from the connection.', [
122 1
                'options' => $this->connections[$this->currentConnectionName],
123 1
            ]);
124
125 1
            --$this->connections[$this->currentConnectionName]['retries'];
126 1
            $this->currentConnection = null;
127
128 1
            return $this->request($command, $context);
129
        }
130
131 7
        return trim($received);
132
    }
133
134 11
    private function getConnection()
135
    {
136 11
        if (null !== $this->currentConnection) {
137 2
            return $this->currentConnection;
138
        }
139
140 11
        foreach ($this->connections as $name => $connection) {
141 11
            if ($connection['retries'] <= 0) {
142 1
                continue;
143
            }
144
145 11
            $this->logger->debug('New connection chosen. Trying to connect.', [
146 11
                'connection' => $connection,
147 11
                'name' => $name,
148 11
            ]);
149
150 11
            $connection = $this->box('doConnect', false, [$connection]);
151
152 11
            if (false === $connection) {
153 6
                $this->logger->debug('Impossible to connect to the connection.', [
154 6
                    'connection' => $connection,
155 6
                    'name' => $name,
156 6
                ]);
157
158 6
                $this->connections[$name]['retries'] = 0;
159
160 6
                continue;
161
            }
162
163 7
            $this->logger->debug('New connection approved.', [
164 7
                'connection' => $connection,
165 7
                'name' => $name,
166 7
            ]);
167
168 7
            stream_set_timeout($connection, 0, $this->timeout);
169
170 7
            $this->currentConnection = $connection;
171 7
            $this->currentConnectionName = $name;
172
173 7
            return $connection;
174 5
        }
175
176 5
        $this->logger->error('Can not find an agent.', [
177 5
            'connections_options' => $this->connections,
178 5
        ]);
179
180 5
        throw new AgentNotFoundException();
181
    }
182
183 11
    private function doConnect($options)
184
    {
185 11
        return stream_socket_client(
186 11
            $options['remote_socket'],
187 11
            $errNo,
188 11
            $errMsg,
189 11
            1, // This value is not used but it should not be 0
190 11
            STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT
191 11
        );
192
    }
193
194 7
    private function doSend($connection, $content)
195
    {
196 7
        return fwrite($connection, $content);
197
    }
198
199 7
    private function doGet($connection)
200
    {
201 7
        return fgets($connection);
202
    }
203
204 11
    private function box($method, $defaultReturnValue = null, array $args = [])
205
    {
206 11
        set_error_handler(__CLASS__.'::handleInternalError');
207
208
        try {
209 11
            $returnValue = call_user_func_array([$this, $method], $args);
210 11
        } catch (\ErrorException $exception) {
211 6
            $returnValue = $defaultReturnValue;
212
213 6
            $this->logger->warning('Impossible to execute a boxed called.', [
214 6
                'method' => $method,
215 6
                'default_return_value' => $defaultReturnValue,
216 6
                'args' => $args,
217 6
                'exception' => $exception,
218 6
            ]);
219
        }
220
221 11
        restore_error_handler();
222
223 11
        return $returnValue;
224
    }
225
226 6
    private static function handleInternalError($type, $message, $file, $line)
227
    {
228 6
        throw new \ErrorException($message, 0, $type, $file, $line);
229
    }
230
}
231