Passed
Push — master ( c42834...98c675 )
by Joel
02:16
created

Client::findRedirect()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 38
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 7.1086

Importance

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