Issues (8)

lib/Browser.php (1 issue)

Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Buzz;
6
7
use Buzz\Client\BuzzClientInterface;
8
use Buzz\Exception\ClientException;
9
use Buzz\Exception\InvalidArgumentException;
10
use Buzz\Exception\LogicException;
11
use Buzz\Middleware\MiddlewareInterface;
12
use Http\Message\RequestFactory;
13
use Psr\Http\Message\RequestFactoryInterface;
14
use Psr\Http\Message\RequestInterface;
15
use Psr\Http\Message\ResponseInterface;
16
17
class Browser implements BuzzClientInterface
18
{
19
    /** @var BuzzClientInterface */
20
    private $client;
21
22
    /** @var RequestFactoryInterface|RequestFactory */
23
    private $requestFactory;
24
25
    /**
26
     * @var MiddlewareInterface[]
27
     */
28
    private $middleware = [];
29
30
    /** @var RequestInterface */
31
    private $lastRequest;
32
33
    /** @var ResponseInterface */
34
    private $lastResponse;
35
36
    /**
37
     * @param RequestFactoryInterface|RequestFactory $requestFactory
38
     */
39 72
    public function __construct(BuzzClientInterface $client, $requestFactory)
40
    {
41 72
        if (!$requestFactory instanceof RequestFactoryInterface && !$requestFactory instanceof RequestFactory) {
0 ignored issues
show
$requestFactory is always a sub-type of Http\Message\RequestFactory.
Loading history...
42
            throw new InvalidArgumentException(sprintf('Second argument of %s must be an instance of %s or %s.', __CLASS__, RequestFactoryInterface::class, RequestFactory::class));
43
        }
44
45 72
        $this->client = $client;
46 72
        $this->requestFactory = $requestFactory;
47 72
    }
48
49 1
    public function get(string $url, array $headers = []): ResponseInterface
50
    {
51 1
        return $this->request('GET', $url, $headers);
52
    }
53
54 1
    public function post(string $url, array $headers = [], string $body = ''): ResponseInterface
55
    {
56 1
        return $this->request('POST', $url, $headers, $body);
57
    }
58
59 1
    public function head(string $url, array $headers = []): ResponseInterface
60
    {
61 1
        return $this->request('HEAD', $url, $headers);
62
    }
63
64 1
    public function patch(string $url, array $headers = [], string $body = ''): ResponseInterface
65
    {
66 1
        return $this->request('PATCH', $url, $headers, $body);
67
    }
68
69 1
    public function put(string $url, array $headers = [], string $body = ''): ResponseInterface
70
    {
71 1
        return $this->request('PUT', $url, $headers, $body);
72
    }
73
74 1
    public function delete(string $url, array $headers = [], string $body = ''): ResponseInterface
75
    {
76 1
        return $this->request('DELETE', $url, $headers, $body);
77
    }
78
79
    /**
80
     * Sends a request.
81
     *
82
     * @param string $method  The request method to use
83
     * @param string $url     The URL to call
84
     * @param array  $headers An array of request headers
85
     * @param string $body    The request content
86
     *
87
     * @return ResponseInterface The response object
88
     */
89 6
    public function request(string $method, string $url, array $headers = [], string $body = ''): ResponseInterface
90
    {
91 6
        $request = $this->createRequest($method, $url, $headers, $body);
92
93 6
        return $this->sendRequest($request);
94
    }
95
96
    /**
97
     * Submit a form.
98
     *
99
     * @throws ClientException
100
     * @throws LogicException
101
     * @throws InvalidArgumentException
102
     */
103 9
    public function submitForm(string $url, array $fields, string $method = 'POST', array $headers = []): ResponseInterface
104
    {
105 9
        $body = [];
106 9
        $files = '';
107 9
        $boundary = uniqid('', true);
108 9
        foreach ($fields as $name => $field) {
109 9
            if (!isset($field['path'])) {
110 6
                $body[$name] = $field;
111
            } else {
112
                // This is a file
113 8
                $fileContent = file_get_contents($field['path']);
114 8
                if (false !== $fileContent) {
115 8
                    $files .= $this->prepareMultipart($name, $fileContent, $boundary, $field);
116
                }
117
            }
118
        }
119
120 9
        if (empty($files)) {
121 1
            $headers['Content-Type'] = 'application/x-www-form-urlencoded';
122 1
            $body = http_build_query($body);
123
        } else {
124 8
            $headers['Content-Type'] = 'multipart/form-data; boundary="'.$boundary.'"';
125
126 8
            foreach ($body as $name => $value) {
127 5
                $files .= $this->prepareMultipart((string) $name, $value, $boundary);
128
            }
129 8
            $body = "$files--{$boundary}--\r\n";
130
        }
131
132 9
        $request = $this->createRequest($method, $url, $headers, $body);
133
134 9
        return $this->sendRequest($request);
135
    }
136
137
    /**
138
     * Send a PSR7 request.
139
     *
140
     * @throws ClientException
141
     * @throws LogicException
142
     * @throws InvalidArgumentException
143
     */
144 68
    public function sendRequest(RequestInterface $request, array $options = []): ResponseInterface
145
    {
146 68
        $chain = $this->createMiddlewareChain($this->middleware, function (RequestInterface $request, callable $responseChain) use ($options) {
147 68
            $response = $this->client->sendRequest($request, $options);
148 67
            $responseChain($request, $response);
149 68
        }, function (RequestInterface $request, ResponseInterface $response) {
150 67
            $this->lastRequest = $request;
151 67
            $this->lastResponse = $response;
152 68
        });
153
154
        // Call the chain
155 68
        $chain($request);
156
157 67
        return $this->lastResponse;
158
    }
159
160
    /**
161
     * @param MiddlewareInterface[] $middleware
162
     */
163 68
    private function createMiddlewareChain(array $middleware, callable $requestChainLast, callable $responseChainLast): callable
164
    {
165 68
        $responseChainNext = $responseChainLast;
166
167
        // Build response chain
168 68
        foreach ($middleware as $m) {
169 3
            $lastCallable = function (RequestInterface $request, ResponseInterface $response) use ($m, $responseChainNext) {
170 3
                return $m->handleResponse($request, $response, $responseChainNext);
171 3
            };
172
173 3
            $responseChainNext = $lastCallable;
174
        }
175
176 68
        $requestChainLast = function (RequestInterface $request) use ($requestChainLast, $responseChainNext) {
177
            // Send the actual request and get the response
178 68
            $requestChainLast($request, $responseChainNext);
179 68
        };
180
181 68
        $middleware = array_reverse($middleware);
182
183
        // Build request chain
184 68
        $requestChainNext = $requestChainLast;
185 68
        foreach ($middleware as $m) {
186 3
            $lastCallable = function (RequestInterface $request) use ($m, $requestChainNext) {
187 3
                return $m->handleRequest($request, $requestChainNext);
188 3
            };
189
190 3
            $requestChainNext = $lastCallable;
191
        }
192
193 68
        return $requestChainNext;
194
    }
195
196 1
    public function getLastRequest(): ?RequestInterface
197
    {
198 1
        return $this->lastRequest;
199
    }
200
201 1
    public function getLastResponse(): ?ResponseInterface
202
    {
203 1
        return $this->lastResponse;
204
    }
205
206 1
    public function getClient(): BuzzClientInterface
207
    {
208 1
        return $this->client;
209
    }
210
211
    /**
212
     * Add a new middleware to the stack.
213
     */
214 3
    public function addMiddleware(MiddlewareInterface $middleware): void
215
    {
216 3
        $this->middleware[] = $middleware;
217 3
    }
218
219 8
    private function prepareMultipart(string $name, string $content, string $boundary, array $data = []): string
220
    {
221 8
        $output = '';
222 8
        $fileHeaders = [];
223
224
        // Set a default content-disposition header
225 8
        $fileHeaders['Content-Disposition'] = sprintf('form-data; name="%s"', $name);
226 8
        if (isset($data['filename'])) {
227 7
            $fileHeaders['Content-Disposition'] .= sprintf('; filename="%s"', $data['filename']);
228
        }
229
230
        // Set a default content-length header
231 8
        if ($length = \strlen($content)) {
232 8
            $fileHeaders['Content-Length'] = (string) $length;
233
        }
234
235 8
        if (isset($data['contentType'])) {
236 7
            $fileHeaders['Content-Type'] = $data['contentType'];
237
        }
238
239
        // Add start
240 8
        $output .= "--$boundary\r\n";
241 8
        foreach ($fileHeaders as $key => $value) {
242 8
            $output .= sprintf("%s: %s\r\n", $key, $value);
243
        }
244 8
        $output .= "\r\n";
245 8
        $output .= $content;
246 8
        $output .= "\r\n";
247
248 8
        return $output;
249
    }
250
251 12
    protected function createRequest(string $method, string $url, array $headers, string $body): RequestInterface
252
    {
253 12
        $request = $this->requestFactory->createRequest($method, $url);
254 12
        $request->getBody()->write($body);
255 12
        foreach ($headers as $name => $value) {
256 12
            $request = $request->withAddedHeader($name, $value);
257
        }
258
259 12
        return $request;
260
    }
261
}
262