Passed
Push — master ( 4ac306...d2aa89 )
by Mihail
05:30
created

PhpClient::maxRedirects()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded\Http\Client;
14
15
use Koded\Http\{ClientRequest, ServerResponse};
16
use Koded\Http\Interfaces\{HttpRequestClient, HttpStatus, Response};
17
use Throwable;
18
use function Koded\Http\create_stream;
19
20
/**
21
 * @link http://php.net/manual/en/context.http.php
22
 */
23
class PhpClient extends ClientRequest implements HttpRequestClient
24
{
25
    use EncodingTrait, Psr18ClientTrait;
26
27
    /** @var array Stream context options */
28
    private $options = [
29
        'protocol_version' => 1.1,
30
        'user_agent'       => self::USER_AGENT,
31
        'method'           => 'GET',
32
        'max_redirects'    => 20,
33
        'follow_location'  => 1,
34
        'ignore_errors'    => true,
35
        'request_fulluri'  => true,
36
        'ssl'              => [
37
            'verify_peer'       => true,
38
            'allow_self_signed' => false,
39
        ]
40
    ];
41
42 9
    public function __construct(string $method, $uri, $body = null, array $headers = [])
43
    {
44 9
        parent::__construct($method, $uri, $body, $headers);
45 9
        $this->options['timeout'] = (ini_get('default_socket_timeout') ?: 10.0) * 1.0;
46 9
    }
47
48 13
    public function read(): Response
49
    {
50 13
        if ($resource = $this->assertSafeMethod()) {
51 1
            return $resource;
52
        }
53
54 12
        $this->prepareRequestBody();
55 12
        $this->prepareOptions();
56
57
        try {
58 12
            if (false === $resource = $this->createResource(stream_context_create(['http' => $this->options]))) {
59 4
                return new ServerResponse(error_get_last()['message'], HttpStatus::FAILED_DEPENDENCY);
60
            }
61
62 7
            $this->extractFromResponseHeaders($resource, $headers, $statusCode);
63
64 7
            return new ServerResponse(
65 7
                stream_get_contents($resource),
66
                $statusCode,
67
                $headers
68
            );
69 1
        } catch (Throwable $e) {
70 1
            return new ServerResponse($e->getMessage(), HttpStatus::INTERNAL_SERVER_ERROR);
71
        } finally {
72 12
            if (is_resource($resource)) {
73 12
                fclose($resource);
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return Koded\Http\Interfaces\Response. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
74
            }
75
        }
76
    }
77
78 1
    public function userAgent(string $value): HttpRequestClient
79
    {
80 1
        $this->options['user_agent'] = $value;
81
82 1
        return $this;
83
    }
84
85 1
    public function followLocation(bool $value): HttpRequestClient
86
    {
87 1
        $this->options['follow_location'] = (int)$value;
88
89 1
        return $this;
90
    }
91
92 1
    public function maxRedirects(int $value): HttpRequestClient
93
    {
94 1
        $this->options['max_redirects'] = $value;
95
96 1
        return $this;
97
    }
98
99 8
    public function timeout(float $value): HttpRequestClient
100
    {
101 8
        $this->options['timeout'] = $value * 1.0;
102
103 8
        return $this;
104
    }
105
106 1
    public function ignoreErrors(bool $value): HttpRequestClient
107
    {
108 1
        $this->options['ignore_errors'] = $value;
109
110 1
        return $this;
111
    }
112
113 1
    public function verifySslHost(bool $value): HttpRequestClient
114
    {
115 1
        $this->options['ssl']['allow_self_signed'] = $value;
116
117 1
        return $this;
118
    }
119
120 1
    public function verifySslPeer(bool $value): HttpRequestClient
121
    {
122 1
        $this->options['ssl']['verify_peer'] = $value;
123
124 1
        return $this;
125
    }
126
127
    /**
128
     * @param resource $context from stream_context_create()
129
     *
130
     * @return bool|resource
131
     */
132 10
    protected function createResource($context)
133
    {
134 10
        return @fopen((string)$this->getUri(), 'r', false, $context);
135
    }
136
137 12
    protected function prepareRequestBody(): void
138
    {
139 12
        if (!$this->stream->getSize()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->stream->getSize() of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
140 9
            return;
141
        }
142
143 3
        $this->stream->rewind();
144
145 3
        if (0 === $this->encoding) {
146 1
            $this->options['content'] = $this->stream->getContents();
147 2
        } elseif ($content = json_decode($this->stream->getContents() ?: '[]', true)) {
148 2
            $this->normalizeHeader('Content-Type', self::X_WWW_FORM_URLENCODED, true);
149 2
            $this->options['content'] = http_build_query($content, null, '&', $this->encoding);
150
        }
151
152 3
        $this->stream = create_stream($this->options['content']);
153 3
    }
154
155 12
    protected function prepareOptions(): void
156
    {
157 12
        $this->options['method'] = $this->getMethod();
158 12
        $this->options['header'] = $this->getFlattenedHeaders();
159 12
        unset($this->options['header'][0]); // Host header is always present and first
160 12
    }
161
162
    /**
163
     * Extracts the headers and status code from the response.
164
     *
165
     * @param resource $response   The resource from fopen()
166
     * @param array    $headers    Parsed response headers
167
     * @param int      $statusCode Response status code
168
     */
169 7
    protected function extractFromResponseHeaders($response, &$headers, &$statusCode): void
170
    {
171
        try {
172 7
            $_headers   = stream_get_meta_data($response)['wrapper_data'];
173
            $statusCode = array_filter($_headers, function(string $header) {
174 7
                return false !== stripos($header, 'HTTP/', 0);
175 7
            });
176 7
            $statusCode = array_pop($statusCode) ?: 'HTTP/1.1 200 OK';
177 7
            $statusCode = (int)(explode(' ', $statusCode)[1] ?? HttpStatus::OK);
178
179 7
            foreach ($_headers as $header) {
180 7
                [$k, $v] = explode(':', $header, 2) + [1 => null];
181 7
                if (null === $v) {
182 7
                    continue;
183
                }
184 7
                $headers[$k] = $v;
185
            }
186 7
        } finally {
187 7
            unset($_headers, $header, $k, $v);
188
        }
189 7
    }
190
}
191