StreamClient::makeHeaders()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 18
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cakasim\Payone\Sdk\Http\StreamClient;
6
7
use Psr\Http\Client\ClientInterface;
8
use Psr\Http\Message\RequestInterface;
9
use Psr\Http\Message\ResponseFactoryInterface;
10
use Psr\Http\Message\ResponseInterface;
11
use Psr\Http\Message\StreamFactoryInterface;
12
13
/**
14
 * The StreamClient uses PHP core stream features in order to
15
 * make HTTP requests. This provides a client implementation
16
 * with zero dependencies.
17
 *
18
 * @author Fabian Böttcher <[email protected]>
19
 * @since 0.1.0
20
 */
21
class StreamClient implements ClientInterface
22
{
23
    /**
24
     * @var ResponseFactoryInterface Holds the concrete PSR-17 response factory
25
     * instance that is used to create responses for the sent requests.
26
     */
27
    protected $responseFactory;
28
29
    /**
30
     * @var StreamFactoryInterface Holds the concrete PSR-17 stream factory
31
     * instance that is used to create the body (stream) for created responses.
32
     */
33
    protected $streamFactory;
34
35
    /**
36
     * Constructs the client with a response factory.
37
     *
38
     * @param ResponseFactoryInterface $responseFactory The response factory instance to use.
39
     * @param StreamFactoryInterface $streamFactory The stream factory instance to use.
40
     */
41
    public function __construct(ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory)
42
    {
43
        $this->responseFactory = $responseFactory;
44
        $this->streamFactory = $streamFactory;
45
    }
46
47
    /**
48
     * @inheritDoc
49
     */
50
    public function sendRequest(RequestInterface $request): ResponseInterface
51
    {
52
        // Make stream context options.
53
        $options = $this->makeStreamContextOptions($request);
54
55
        // Create HTTP stream context.
56
        $context = stream_context_create(['http' => $options]);
57
58
        // Register temporary error handler to catch fopen warnings.
59
        $err = [];
60
        set_error_handler(function (int $code, string $message) use (&$err): bool {
61
            $err['code'] = $code;
62
            $err['message'] = $message;
63
            return true;
64
        });
65
66
        // Open the file and restore the error handler.
67
        $stream = fopen((string) $request->getUri(), 'r', false, $context);
68
        restore_error_handler();
69
70
        if (!empty($err)) {
71
            throw new ClientException("Failed to send HTTP request: [{$err['code']}] {$err['message']}");
72
        }
73
74
        if (!is_resource($stream)) {
75
            throw new ClientException("Failed to create HTTP request stream.");
76
        }
77
78
        // Create the HTTP response.
79
        $response = $this->responseFactory->createResponse();
80
81
        // Parse $http_response_header data.
82
        $response = $this->parseResponseHeaders($response, $http_response_header);
83
84
        // Create a response body stream.
85
        $responseBody = $this->streamFactory->createStreamFromResource($stream);
86
87
        return $response->withBody($responseBody);
88
    }
89
90
    /**
91
     * Makes the stream context options.
92
     *
93
     * @param RequestInterface $request The HTTP request.
94
     * @return array The stream context options.
95
     */
96
    protected function makeStreamContextOptions(RequestInterface $request): array
97
    {
98
        $options = [
99
            'method'        => $request->getMethod(),
100
            'header'        => $this->makeHeaders($request),
101
            'ignore_errors' => true, // Fetch the content even on failure status codes.
102
        ];
103
104
        $body = $request->getBody()->getContents();
105
106
        // Add content to options if the request body is not empty.
107
        if (!empty($body)) {
108
            $options['content'] = $body;
109
        }
110
111
        return $options;
112
    }
113
114
    /**
115
     * Makes the headers suitable for the stream context options.
116
     *
117
     * @param RequestInterface $request The HTTP request.
118
     * @return string The request headers.
119
     */
120
    protected function makeHeaders(RequestInterface $request): string
121
    {
122
        $headers = [];
123
124
        // Populate headers array.
125
        foreach ($request->getHeaders() as $headerName => $headerValues) {
126
            foreach ($headerValues as $headerValue) {
127
                $headers[] = [$headerName, $headerValue];
128
            }
129
        }
130
131
        // Transform headers array to valid header lines.
132
        $headers = array_map(function ($header) {
133
            return "{$header[0]}: {$header[1]}";
134
        }, $headers);
135
136
        // Join header lines by default HTTP header line feed
137
        return join("\r\n", $headers);
138
    }
139
140
    /**
141
     * Parses the response headers.
142
     *
143
     * @param ResponseInterface $response The HTTP response.
144
     * @param array $headers The response headers from $http_response_header.
145
     * @return ResponseInterface The HTTP response configured with headers.
146
     */
147
    protected function parseResponseHeaders(ResponseInterface $response, array $headers): ResponseInterface
148
    {
149
        // Parse response header lines.
150
        foreach ($headers as $line) {
151
            $line = explode(':', $line, 2);
152
            if (count($line) === 2) {
153
                $response = $response->withHeader(trim($line[0]), trim($line[1]));
154
            }
155
        }
156
157
        return $response;
158
    }
159
}
160