Completed
Push — master ( 7bc8b4...84cfa0 )
by Joel
40:58
created

Client::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1

Importance

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