HttpOptions   B
last analyzed

Complexity

Total Complexity 44

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 88
dl 0
loc 293
rs 8.8798
c 1
b 0
f 0
wmc 44

19 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 7 2
A __construct() 0 3 1
A body() 0 16 4
D addQueryToUrl() 0 24 10
A parameters() 0 4 2
A asAjax() 0 5 1
B server() 0 33 9
A asJson() 0 5 1
A withServer() 0 5 1
A withHeader() 0 5 1
A jsonAjax() 0 3 1
A withQuery() 0 5 1
A withBody() 0 5 1
A ajax() 0 3 1
A files() 0 3 1
A json() 0 3 1
A withFiles() 0 5 1
A withHeaders() 0 5 1
A merge() 0 24 4

How to fix   Complexity   

Complex Class

Complex classes like HttpOptions often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HttpOptions, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Zenstruck\Browser;
4
5
/**
6
 * @author Kevin Bond <[email protected]>
7
 */
8
class HttpOptions
9
{
10
    private const EMPTY_JSON_TRIGGER = '__JSON__';
11
    private const DEFAULT_OPTIONS = [
12
        // request headers
13
        'headers' => [],
14
15
        // query parameters
16
        'query' => [],
17
18
        // files to include
19
        'files' => [],
20
21
        // server variables
22
        'server' => [],
23
24
        // request body
25
        'body' => null,
26
27
        // if set, will json_encode and use as the body and
28
        // set the Content-Type/Accept request headers to application/json
29
        'json' => null,
30
31
        // if true will set the X-Requested-With request header to XMLHttpRequest
32
        'ajax' => false,
33
    ];
34
35
    private array $options;
36
37
    final public function __construct(array $options = [])
38
    {
39
        $this->options = \array_merge(self::DEFAULT_OPTIONS, $options);
40
    }
41
42
    /**
43
     * @param self|array $options
44
     *
45
     * @return static
46
     */
47
    final public static function create($options = []): self
48
    {
49
        if ($options instanceof static) {
50
            return $options;
51
        }
52
53
        return new static($options);
54
    }
55
56
    /**
57
     * @param mixed $body
58
     *
59
     * @return static
60
     */
61
    final public static function json($body = null): self
62
    {
63
        return static::create()->asJson($body);
64
    }
65
66
    /**
67
     * @return static
68
     */
69
    final public static function ajax(): self
70
    {
71
        return static::create()->asAjax();
72
    }
73
74
    /**
75
     * @param mixed $body
76
     *
77
     * @return static
78
     */
79
    final public static function jsonAjax($body = null): self
80
    {
81
        return static::json($body)->asAjax();
82
    }
83
84
    /**
85
     * @param self|array $options
86
     *
87
     * @return static
88
     */
89
    final public function merge($options = []): self
90
    {
91
        $other = self::create($options);
92
93
        // merge array options
94
        $this->options['headers'] = \array_merge($this->options['headers'], $other->options['headers']);
95
        $this->options['query'] = \array_merge($this->options['query'], $other->options['query']);
96
        $this->options['files'] = \array_merge($this->options['files'], $other->options['files']);
97
        $this->options['server'] = \array_merge($this->options['server'], $other->options['server']);
98
99
        // override value options only if different from default
100
        if ($other->options['body'] !== self::DEFAULT_OPTIONS['body']) {
101
            $this->options['body'] = $other->options['body'];
102
        }
103
104
        if ($other->options['json'] !== self::DEFAULT_OPTIONS['json']) {
105
            $this->options['json'] = $other->options['json'];
106
        }
107
108
        if ($other->options['ajax'] !== self::DEFAULT_OPTIONS['ajax']) {
109
            $this->options['ajax'] = $other->options['ajax'];
110
        }
111
112
        return $this;
113
    }
114
115
    /**
116
     * @return static
117
     */
118
    final public function withHeader(string $header, string $value): self
119
    {
120
        $this->options['headers'][$header] = $value;
121
122
        return $this;
123
    }
124
125
    /**
126
     * @return static
127
     */
128
    final public function withHeaders(array $headers): self
129
    {
130
        $this->options['headers'] = $headers;
131
132
        return $this;
133
    }
134
135
    /**
136
     * @return static
137
     */
138
    final public function withQuery(array $query): self
139
    {
140
        $this->options['query'] = $query;
141
142
        return $this;
143
    }
144
145
    /**
146
     * @return static
147
     */
148
    final public function withServer(array $server): self
149
    {
150
        $this->options['server'] = $server;
151
152
        return $this;
153
    }
154
155
    /**
156
     * @return static
157
     */
158
    final public function withFiles(array $files): self
159
    {
160
        $this->options['files'] = $files;
161
162
        return $this;
163
    }
164
165
    /**
166
     * @param string|array|null $body
167
     *
168
     * @return static
169
     */
170
    final public function withBody($body): self
171
    {
172
        $this->options['body'] = $body;
173
174
        return $this;
175
    }
176
177
    /**
178
     * @param mixed $body Any value that can be json encoded
179
     *
180
     * @return static
181
     */
182
    final public function asJson($body = null): self
183
    {
184
        $this->options['json'] = $body ?? self::EMPTY_JSON_TRIGGER;
185
186
        return $this;
187
    }
188
189
    /**
190
     * @return static
191
     */
192
    final public function asAjax(): self
193
    {
194
        $this->options['ajax'] = true;
195
196
        return $this;
197
    }
198
199
    final public function addQueryToUrl(string $url): string
200
    {
201
        $parts = \parse_url($url);
202
203
        if (isset($parts['query'])) {
204
            \parse_str($parts['query'], $query);
205
        } else {
206
            $query = [];
207
        }
208
209
        // merge query on url with the query option
210
        $parts['query'] = \http_build_query(\array_merge($query, $this->options['query']));
211
212
        $scheme = isset($parts['scheme']) ? $parts['scheme'].'://' : '';
213
        $host = $parts['host'] ?? '';
214
        $port = isset($parts['port']) ? ':'.$parts['port'] : '';
215
        $user = $parts['user'] ?? '';
216
        $pass = isset($parts['pass']) ? ':'.$parts['pass'] : '';
217
        $pass = ($user || $pass) ? "{$pass}@" : '';
218
        $path = $parts['path'] ?? '';
219
        $query = isset($parts['query']) && $parts['query'] ? '?'.$parts['query'] : '';
220
        $fragment = isset($parts['fragment']) ? '#'.$parts['fragment'] : '';
221
222
        return $scheme.$user.$pass.$host.$port.$path.$query.$fragment;
223
    }
224
225
    /**
226
     * @internal
227
     */
228
    final public function parameters(): array
229
    {
230
        // when body is array, use as request parameters
231
        return \is_array($this->options['body']) ? $this->options['body'] : [];
232
    }
233
234
    /**
235
     * @internal
236
     */
237
    final public function files(): array
238
    {
239
        return $this->options['files'];
240
    }
241
242
    /**
243
     * @co-author Kévin Dunglas <[email protected]>
244
     *
245
     * @internal
246
     */
247
    final public function server(): array
248
    {
249
        $server = $this->options['server'];
250
        $headers = \array_combine(
251
            \array_map(
252
                static fn($header) => \mb_strtoupper(\str_replace('-', '_', $header)),
253
                \array_keys($this->options['headers'])
254
            ),
255
            $this->options['headers']
256
        );
257
258
        if (null !== $this->options['json'] && !\array_key_exists('ACCEPT', $headers)) {
259
            $headers['ACCEPT'] = 'application/json';
260
        }
261
262
        if (null !== $this->options['json'] && !\array_key_exists('CONTENT_TYPE', $headers)) {
263
            $headers['CONTENT_TYPE'] = 'application/json';
264
        }
265
266
        if (false !== $this->options['ajax'] && !\array_key_exists('X_REQUESTED_WITH', $headers)) {
267
            $headers['X_REQUESTED_WITH'] = 'XMLHttpRequest';
268
        }
269
270
        foreach ($headers as $header => $value) {
271
            // content type header cannot have HTTP_ prefix
272
            if ('CONTENT_TYPE' !== $header) {
273
                $header = "HTTP_{$header}";
274
            }
275
276
            $server[$header] = $value;
277
        }
278
279
        return $server;
280
    }
281
282
    /**
283
     * @internal
284
     */
285
    final public function body(): ?string
286
    {
287
        if (\is_array($this->options['body'])) {
288
            // when body is array, it's used as the request parameters
289
            return null;
290
        }
291
292
        if (null === $this->options['json']) {
293
            return $this->options['body'];
294
        }
295
296
        if (self::EMPTY_JSON_TRIGGER === $this->options['json']) {
297
            return null;
298
        }
299
300
        return \json_encode($this->options['json'], \JSON_THROW_ON_ERROR);
301
    }
302
}
303