Query::getPathAndParams()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 3
nop 0
dl 0
loc 8
rs 10
c 0
b 0
f 0
ccs 6
cts 6
cp 1
crap 4
1
<?php
2
3
namespace kalanis\RemoteRequest\Protocols\Http;
4
5
6
use kalanis\RemoteRequest\Interfaces;
7
use kalanis\RemoteRequest\Protocols;
8
use kalanis\RemoteRequest\Protocols\Http;
9
use kalanis\RemoteRequest\RequestException;
10
11
12
/**
13
 * Class Query
14
 * @package kalanis\RemoteRequest\Protocols\Http
15
 * Message to the remote server compilation - protocol http
16
 */
17
class Query extends Protocols\Dummy\Query implements Interfaces\ITarget
18
{
19
    protected string $host = '';
20
    protected int $port = 80;
21
    protected string $path = '';
22
    protected string $method = 'POST';
23
    /** @var string[] */
24
    protected array $availableMethods = ['GET', 'POST', 'PUT', 'DELETE'];
25
    /** @var string[] */
26
    protected array $multipartMethods = ['POST', 'PUT'];
27
    /** @var Query\Value[] */
28
    protected array $content = [];
29
    protected bool $inline = false;
30
    protected string $userAgent = 'php-agent/1.3';
31
    /** @var string[] */
32
    protected array $headers = [];
33
    /** @var resource */
34
    protected $contentStream = null;
35
    protected int $contentLength = 0;
36
    protected ?string $boundary = null;
37
38 17
    public function __construct()
39
    {
40 17
        $this->addHeader('Host', 'dummy.example');
41 17
        $this->addHeader('Accept', '*/*');
42 17
        $this->addHeader('User-Agent', $this->userAgent);
43 17
        $this->addHeader('Connection', 'close');
44
    }
45
46 11
    public function setMethod(string $method): self
47
    {
48 11
        $method = strtoupper($method);
49 11
        if (in_array($method, $this->availableMethods)) {
50 11
            $this->method = $method;
51
        }
52 11
        return $this;
53
    }
54
55 11
    public function getMethod(): string
56
    {
57 11
        return $this->method;
58
    }
59
60 11
    public function setInline(bool $inline): self
61
    {
62 11
        $this->inline = $inline;
63 11
        return $this;
64
    }
65
66 7
    public function setHost(string $host): self
67
    {
68 7
        $this->host = $host;
69 7
        return $this;
70
    }
71
72 2
    public function getHost(): string
73
    {
74 2
        return $this->host;
75
    }
76
77 7
    public function setPort(int $port): self
78
    {
79 7
        $this->port = $port;
80 7
        return $this;
81
    }
82
83 2
    public function getPort(): ?int
84
    {
85 2
        return $this->port;
86
    }
87
88 6
    public function setRequestSettings(Interfaces\ITarget $request): self
89
    {
90 6
        $this->host = $request->getHost();
91 6
        $this->port = $request->getPort() ?? $this->port;
92 6
        return $this;
93
    }
94
95 11
    public function setPath(string $path): self
96
    {
97 11
        $this->path = $path;
98 11
        return $this;
99
    }
100
101 1
    public function getPath(): string
102
    {
103 1
        return $this->path;
104
    }
105
106
    /**
107
     * Is current query set as inline?
108
     * @return bool
109
     */
110 7
    public function isInline(): bool
111
    {
112 7
        return $this->inline;
113
    }
114
115
    /**
116
     * Add HTTP header
117
     * @param string $name
118
     * @param string $value
119
     * @return $this
120
     */
121 17
    public function addHeader(string $name, string $value): self
122
    {
123 17
        $this->headers[$name] = $value;
124 17
        return $this;
125
    }
126
127
    /**
128
     * Remove HTTP header
129
     * @param string $name
130
     * @return $this
131
     */
132 11
    public function removeHeader(string $name): self
133
    {
134 11
        unset($this->headers[$name]);
135 11
        return $this;
136
    }
137
138
    /**
139
     * Add HTTP variables
140
     * @param string[] $array
141
     * @return $this
142
     */
143 7
    public function addValues($array): self
144
    {
145 7
        array_walk($array, function ($value, $key) {
146 7
            $this->addValue($key, $value);
147 7
        });
148 7
        return $this;
149
    }
150
151
    /**
152
     * Add HTTP variable
153
     * @param string $key
154
     * @param string|Query\Value $value
155
     * @return $this
156
     */
157 9
    public function addValue(string $key, $value): self
158
    {
159 9
        $this->content[$key] = ($value instanceof Query\Value) ? $value : new Query\Value(strval($value));
160 9
        return $this;
161
    }
162
163
    /**
164
     * Remove HTTP header
165
     * @param string $key
166
     * @return $this
167
     */
168 1
    public function removeValue(string $key): self
169
    {
170 1
        unset($this->content[$key]);
171 1
        return $this;
172
    }
173
174 11
    public function getData()
175
    {
176 11
        $this->contentStream = Protocols\Helper::getTempStorage();
177 11
        $this->contentLength = 0;
178 11
        $this->addHeader('Host', $this->getHostAndPort());
179
180 11
        $this->checkForMethod();
181 11
        $this->checkForFiles();
182 11
        $this->prepareBoundary();
183 11
        $this->prepareQuery();
184
185 11
        $this->contentLengthHeader();
186 11
        $this->contentTypeHeader();
187
188 11
        $storage = Protocols\Helper::getTempStorage();
189 11
        fwrite($storage, $this->renderRequestHeader());
190 11
        rewind($this->contentStream);
191 11
        stream_copy_to_stream($this->contentStream, $storage);
192 11
        rewind($storage);
193 11
        return $storage;
194
    }
195
196 11
    protected function getHostAndPort(): string
197
    {
198 11
        $portPart = '';
199 11
        if (is_int($this->port) && (80 != $this->port)) {
200 7
            $portPart .= ':' . $this->port;
201
        }
202 11
        return $this->host . $portPart;
203
    }
204
205 11
    protected function checkForMethod(): self
206
    {
207 11
        if (in_array($this->getMethod(), $this->multipartMethods)) {
208 2
            $this->inline = false;
209
        }
210 11
        return $this;
211
    }
212
213 11
    protected function checkForFiles(): self
214
    {
215 11
        if (!empty(array_filter($this->content, [$this, 'fileAnywhere']))) { // for files
216 3
            $this->inline = false;
217
        }
218 11
        return $this;
219
    }
220
221 11
    protected function prepareBoundary(): self
222
    {
223 11
        $this->boundary = $this->isMultipart() ? $this->generateBoundary() : null ;
224 11
        return $this;
225
    }
226
227 1
    protected function generateBoundary(): string
228
    {
229 1
        return 'PHPFsock------------------' . $this->generateRandomString();
230
    }
231
232
    /**
233
     * @throws RequestException
234
     * @return $this
235
     */
236 9
    protected function prepareQuery(): self
237
    {
238 9
        if (!$this->isInline()) {
239 7
            if ($this->isMultipart()) {
240 2
                $this->createMultipartRequest();
241
            } else {
242 5
                $this->contentLength += intval(fwrite($this->contentStream, $this->getSimpleRequest()));
243
            }
244
        }
245 9
        return $this;
246
    }
247
248
    /**
249
     * Is current query set as multipart? (Know how to work with content values?)
250
     * @return bool
251
     */
252 11
    public function isMultipart(): bool
253
    {
254 11
        return !empty(array_filter($this->content, [$this, 'fileAnywhere']));
255
    }
256
257
    /**
258
     * @param mixed $variable
259
     * @return bool
260
     */
261 9
    public function fileAnywhere($variable): bool
262
    {
263 9
        return is_object($variable) && ($variable instanceof Query\File);
264
    }
265
266 11
    protected function contentLengthHeader(): self
267
    {
268 11
        if (empty($this->contentLength)) {
269 4
            $this->removeHeader('Content-Length');
270
        } else {
271 7
            $this->addHeader('Content-Length', strval($this->contentLength));
272
        }
273 11
        return $this;
274
    }
275
276 11
    protected function contentTypeHeader(): self
277
    {
278 11
        if (in_array($this->getMethod(), $this->multipartMethods)) {
279 2
            if (is_null($this->boundary)) {
280 2
                $this->addHeader('Content-Type', 'application/x-www-form-urlencoded');
281
            } else {
282 2
                $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $this->boundary);
283
            }
284
        } else {
285 10
            $this->removeHeader('Content-Type');
286
        }
287 11
        return $this;
288
    }
289
290
    /**
291
     * Generate and returns random string with combination of numbers and chars with specified length
292
     * @param int $stringLength
293
     * @return string
294
     */
295 1
    protected function generateRandomString(int $stringLength = 16): string
296
    {
297 1
        $all = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
298 1
        $string = "";
299 1
        for ($i = 0; $i < $stringLength; $i++) {
300 1
            $rand = mt_rand(0, count($all) - 1);
301 1
            $string .= $all[$rand];
302
        }
303 1
        return $string;
304
    }
305
306
    /**
307
     * @throws RequestException
308
     */
309 2
    protected function createMultipartRequest(): void
310
    {
311 2
        foreach ($this->content as $key => $value) {
312 2
            $this->contentLength += intval(fwrite($this->contentStream, '--' . $this->boundary . Http::DELIMITER));
313 2
            if ($value instanceof Query\File) {
314 2
                $filename = empty($value->getFilename()) ? '' : '; filename="' . urlencode($value->getFilename()) . '"';
315 2
                $this->contentLength += intval(fwrite($this->contentStream, 'Content-Disposition: form-data; name="' . urlencode($key) . '"' . $filename . Http::DELIMITER));
316 2
                $this->contentLength += intval(fwrite($this->contentStream, 'Content-Type: ' . $value->getMimeType() . Http::DELIMITER . Http::DELIMITER));
317 2
                $source = $value->getStream();
318 2
                rewind($source);
319 2
                $this->contentLength += intval(stream_copy_to_stream($source, $this->contentStream));
320 2
                $this->contentLength += intval(fwrite($this->contentStream, Http::DELIMITER));
321
            } else {
322 2
                $this->contentLength += intval(fwrite($this->contentStream, 'Content-Disposition: form-data; name="' . urlencode($key) . '"' . Http::DELIMITER . Http::DELIMITER));
323 2
                $this->contentLength += intval(fwrite($this->contentStream, $value->getContent() . Http::DELIMITER));
324
            }
325
        }
326 2
        $this->contentLength += intval(fwrite($this->contentStream, '--' . $this->boundary . '--' . Http::DELIMITER));
327
    }
328
329
    /**
330
     * From defined headers make string as defined in RFC
331
     * @return string
332
     */
333 11
    protected function renderRequestHeader(): string
334
    {
335 11
        $header = $this->getQueryHeaders();
336 11
        return sprintf('%s%s%s',
337 11
            $this->getQueryTarget() . Http::DELIMITER,
338 11
            (mb_strlen($header) ? $header . Http::DELIMITER : ''),
339 11
            Http::DELIMITER
340 11
        );
341
    }
342
343
    /**
344
     * Get headers itself as string param to query
345
     * @return string
346
     */
347 11
    protected function getQueryHeaders(): string
348
    {
349 11
        return implode(Http::DELIMITER, array_map(function ($key, $value) {
350 11
            return sprintf('%s: %s', $key, $value);
351 11
        }, array_keys($this->headers), array_values($this->headers)));
352
    }
353
354
    /**
355
     * Top line, what server is a target
356
     * @return string
357
     */
358 11
    protected function getQueryTarget(): string
359
    {
360 11
        return sprintf('%1$s %2$s HTTP/1.1', $this->getMethod(), $this->getPathAndParams());
361
    }
362
363
    /**
364
     * Which path is target
365
     * @return string
366
     */
367 11
    protected function getPathAndParams(): string
368
    {
369 11
        $requestPart = '';
370 11
        if ($this->isInline() && !empty($this->content)) {
371 2
            $requestPart .= (false === mb_strpos($this->path, '?')) ? '?' : '&' ;
372 2
            $requestPart .= $this->getSimpleRequest();
373
        }
374 11
        return $this->path . $requestPart;
375
    }
376
377
    /**
378
     * Get pairs of content into encoded string
379
     * @return string
380
     */
381 7
    protected function getSimpleRequest(): string
382
    {
383 7
        return implode('&', array_map(function ($key, Http\Query\Value $value) {
384 5
            return sprintf('%s=%s', urlencode($key), urlencode($value->getContent()));
385 7
        }, array_keys($this->content), array_values($this->content)));
386
    }
387
}
388