Completed
Pull Request — master (#24)
by Joel
03:25
created

Client::__construct()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4.5435

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 0
loc 13
ccs 4
cts 9
cp 0.4444
rs 9.4285
cc 3
eloc 8
nc 2
nop 2
crap 4.5435
1
<?php
2
3
namespace Http\Client\Socket;
4
5
use Http\Client\Exception\NetworkException;
6
use Http\Client\HttpClient;
7
use Http\Message\ResponseFactory;
8
use Psr\Http\Message\RequestInterface;
9
use Symfony\Component\OptionsResolver\Options;
10
use Symfony\Component\OptionsResolver\OptionsResolver;
11
12
/**
13
 * Socket Http Client.
14
 *
15
 * Use stream and socket capabilities of the core of PHP to send HTTP requests
16
 *
17
 * @author Joel Wurtz <[email protected]>
18
 */
19
class Client implements HttpClient
20
{
21
    use RequestWriter;
22
    use ResponseReader;
23
24
    /**
25
     * Config of this client.
26
     *
27
     * @var array
28
     */
29
    private $config = [
30
        'remote_socket' => null,
31
        'timeout' => null,
32
        'stream_context_options' => [],
33
        'stream_context_param' => [],
34
        'ssl' => null,
35
        'write_buffer_size' => 8192,
36
        'ssl_method' => STREAM_CRYPTO_METHOD_TLS_CLIENT,
37
    ];
38
39
    /**
40
     * Constructor.
41
     *
42
     * @param array $config {
43
     *
44
     *    @var string $remote_socket          Remote entrypoint (can be a tcp or unix domain address)
45
     *    @var int    $timeout                Timeout before canceling request
46
     *    @var array  $stream_context_options Context options as defined in the PHP documentation
47
     *    @var array  $stream_context_param   Context params as defined in the PHP documentation
48
     *    @var bool   $ssl                    Use ssl, default to scheme from request, false if not present
49
     *    @var int    $write_buffer_size      Buffer when writing the request body, defaults to 8192
50
     *    @var int    $ssl_method             Crypto method for ssl/tls, see PHP doc, defaults to STREAM_CRYPTO_METHOD_TLS_CLIENT
51
     * }
52
     *
53
     * @param array $deprecatedConfig Use for BC with old versions
54
     */
55 64
    public function __construct($config = [], array $deprecatedConfig = [])
56
    {
57 64
        if ($config instanceof ResponseFactory || $config === null) {
58
            // To remove in 2.0
59
            trigger_error(
60
                'Injecting a request factory is no longer required, as this lib directly use guzzlehttp/psr7 implementation',
61
                E_USER_DEPRECATED
62
            );
63
            $this->config = $this->configure($deprecatedConfig);
64
        } else {
65 64
            $this->config = $this->configure($config);
66
        }
67 64
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 64
    public function sendRequest(RequestInterface $request)
73
    {
74 64
        $remote = $this->config['remote_socket'];
75 64
        $useSsl = $this->config['ssl'];
76
77 64
        if (!$request->hasHeader('Connection')) {
78 11
            $request = $request->withHeader('Connection', 'close');
79 11
        }
80
81 64
        if (null === $remote) {
82 58
            $remote = $this->determineRemoteFromRequest($request);
83 57
        }
84
85 63
        if (null === $useSsl) {
86 61
            $useSsl = ($request->getUri()->getScheme() === 'https');
87 61
        }
88
89 63
        $socket = $this->createSocket($request, $remote, $useSsl);
90
91
        try {
92 59
            $this->writeRequest($socket, $request, $this->config['write_buffer_size']);
93 59
            $response = $this->readResponse($request, $socket);
94 59
        } catch (\Exception $e) {
95 1
            $this->closeSocket($socket);
96
97 1
            throw $e;
98
        }
99
100 58
        return $response;
101
    }
102
103
    /**
104
     * Create the socket to write request and read response on it.
105
     *
106
     * @param RequestInterface $request Request for
107
     * @param string           $remote  Entrypoint for the connection
108
     * @param bool             $useSsl  Whether to use ssl or not
109
     *
110
     * @throws NetworkException When the connection fail
111
     *
112
     * @return resource Socket resource
113
     */
114 63
    protected function createSocket(RequestInterface $request, $remote, $useSsl)
115
    {
116 63
        $errNo = null;
117 63
        $errMsg = null;
118 63
        $socket = @stream_socket_client($remote, $errNo, $errMsg, floor($this->config['timeout'] / 1000), STREAM_CLIENT_CONNECT, $this->config['stream_context']);
119
120 63
        if (false === $socket) {
121 3
            throw new NetworkException($errMsg, $request);
122
        }
123
124 60
        stream_set_timeout($socket, floor($this->config['timeout'] / 1000), $this->config['timeout'] % 1000);
125
126 60
        if ($useSsl) {
127 3
            if (false === @stream_socket_enable_crypto($socket, true, $this->config['ssl_method'])) {
128 1
                throw new NetworkException(sprintf('Cannot enable tls: %s', error_get_last()['message']), $request);
129
            }
130 2
        }
131
132 59
        return $socket;
133
    }
134
135
    /**
136
     * Close the socket, used when having an error.
137
     *
138
     * @param resource $socket
139
     */
140 1
    protected function closeSocket($socket)
141
    {
142 1
        fclose($socket);
143 1
    }
144
145
    /**
146
     * Return configuration for the socket client.
147
     *
148
     * @param array $config Configuration from user
149
     *
150
     * @return array Configuration resolved
151
     */
152 64
    protected function configure(array $config = [])
153
    {
154 64
        $resolver = new OptionsResolver();
155 64
        $resolver->setDefaults($this->config);
156 64
        $resolver->setDefault('stream_context', function (Options $options) {
157 64
            return stream_context_create($options['stream_context_options'], $options['stream_context_param']);
158 64
        });
159
160 64
        $resolver->setDefault('timeout', ini_get('default_socket_timeout') * 1000);
161
162 64
        $resolver->setAllowedTypes('stream_context_options', 'array');
163 64
        $resolver->setAllowedTypes('stream_context_param', 'array');
164 64
        $resolver->setAllowedTypes('stream_context', 'resource');
165 64
        $resolver->setAllowedTypes('ssl', ['bool', 'null']);
166
167 64
        return $resolver->resolve($config);
168
    }
169
170
    /**
171
     * Return remote socket from the request.
172
     *
173
     * @param RequestInterface $request
174
     *
175
     * @throws NetworkException When no remote can be determined from the request
176
     *
177
     * @return string
178
     */
179 58
    private function determineRemoteFromRequest(RequestInterface $request)
180
    {
181 58
        if ($request->getUri()->getHost() == '' && !$request->hasHeader('Host')) {
182 1
            throw new NetworkException('Cannot find connection endpoint for this request', $request);
183
        }
184
185 57
        $host = $request->getUri()->getHost();
186 57
        $port = $request->getUri()->getPort() ?: ($request->getUri()->getScheme() == 'https' ? 443 : 80);
187 57
        $endpoint = sprintf('%s:%s', $host, $port);
188
189
        // If use the host header if present for the endpoint
190 57
        if (empty($host) && $request->hasHeader('Host')) {
191 1
            $endpoint = $request->getHeaderLine('Host');
192 1
        }
193
194 57
        return sprintf('tcp://%s', $endpoint);
195
    }
196
}
197