Cookie   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 432
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 62
eloc 122
c 3
b 0
f 0
dl 0
loc 432
ccs 147
cts 147
cp 1
rs 3.44

29 Methods

Rating   Name   Duplication   Size   Complexity  
A withPath() 0 9 2
A expire() 0 9 2
A getPath() 0 3 1
A withSameSite() 0 9 2
B __toString() 0 30 7
A withHttpOnly() 0 9 2
A isSecure() 0 3 1
A __construct() 0 18 1
A withDomain() 0 9 2
A withSecure() 0 9 2
A withExpires() 0 9 2
A getExpires() 0 3 1
A getValue() 0 3 1
A isHttpOnly() 0 3 1
A getMaxAge() 0 4 2
A isExpired() 0 3 2
A getDomain() 0 3 1
A getSameSite() 0 3 1
A isSession() 0 3 1
A setValue() 0 3 1
A withValue() 0 9 2
A getName() 0 3 1
A setName() 0 15 3
A setSameSite() 0 14 4
A setHttpOnly() 0 3 1
A setDomain() 0 3 2
A setPath() 0 3 2
B setExpires() 0 30 11
A setSecure() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Cookie 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 Cookie, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace HttpSoft\Cookie;
6
7
use DateTimeInterface;
8
use InvalidArgumentException;
9
10
use function array_map;
11
use function get_class;
12
use function gettype;
13
use function gmdate;
14
use function implode;
15
use function in_array;
16
use function is_int;
17
use function is_numeric;
18
use function is_object;
19
use function is_string;
20
use function preg_match;
21
use function rawurlencode;
22
use function sprintf;
23
use function strtolower;
24
use function strtotime;
25
use function time;
26
use function ucfirst;
27
28
final class Cookie implements CookieInterface
29
{
30
    /**
31
     * @var string
32
     */
33
    private string $name;
34
35
    /**
36
     * @var string
37
     */
38
    private string $value;
39
40
    /**
41
     * @var int
42
     */
43
    private int $expires;
44
45
    /**
46
     * @var string|null
47
     */
48
    private ?string $domain;
49
50
    /**
51
     * @var string|null
52
     */
53
    private ?string $path;
54
55
    /**
56
     * @var bool|null
57
     */
58
    private ?bool $secure;
59
60
    /**
61
     * @var bool|null
62
     */
63
    private ?bool $httpOnly;
64
65
    /**
66
     * @var string|null
67
     */
68
    private ?string $sameSite;
69
70
    /**
71
     * @param string $name the name of the cookie.
72
     * @param string $value the value of the cookie.
73
     * @param DateTimeInterface|int|string|null $expire the time the cookie expire.
74
     * @param string|null $path the set of paths for the cookie.
75
     * @param string|null $domain the set of domains for the cookie.
76
     * @param bool|null $secure whether the cookie should only be transmitted over a secure HTTPS connection.
77
     * @param bool|null $httpOnly whether the cookie can be accessed only through the HTTP protocol.
78
     * @param string|null $sameSite whether the cookie will be available for cross-site requests.
79
     * @throws InvalidArgumentException if one or more arguments are not valid.
80
     */
81 99
    public function __construct(
82
        string $name,
83
        string $value = '',
84
        $expire = null,
85
        ?string $domain = null,
86
        ?string $path = '/',
87
        ?bool $secure = true,
88
        ?bool $httpOnly = true,
89
        ?string $sameSite = self::SAME_SITE_LAX
90
    ) {
91 99
        $this->setName($name);
92 98
        $this->setValue($value);
93 98
        $this->setExpires($expire);
94 98
        $this->setDomain($domain);
95 98
        $this->setPath($path);
96 98
        $this->setSecure($secure);
97 98
        $this->setHttpOnly($httpOnly);
98 98
        $this->setSameSite($sameSite);
99
    }
100
101
    /**
102
     * {@inheritDoc}
103
     */
104 37
    public function getName(): string
105
    {
106 37
        return $this->name;
107
    }
108
109
    /**
110
     * {@inheritDoc}
111
     */
112 7
    public function getValue(): string
113
    {
114 7
        return $this->value;
115
    }
116
117
    /**
118
     * {@inheritDoc}
119
     */
120 2
    public function withValue(string $value): CookieInterface
121
    {
122 2
        if ($value === $this->value) {
123 1
            return $this;
124
        }
125
126 2
        $new = clone $this;
127 2
        $new->setValue($value);
128 2
        return $new;
129
    }
130
131
    /**
132
     * {@inheritDoc}
133
     */
134 7
    public function getMaxAge(): int
135
    {
136 7
        $maxAge = $this->expires - time();
137 7
        return $maxAge > 0 ? $maxAge : 0;
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143 8
    public function getExpires(): int
144
    {
145 8
        return $this->expires;
146
    }
147
148
    /**
149
     * {@inheritDoc}
150
     */
151 5
    public function isExpired(): bool
152
    {
153 5
        return (!$this->isSession() && $this->expires < time());
154
    }
155
156
    /**
157
     * {@inheritDoc}
158
     */
159 2
    public function expire(): CookieInterface
160
    {
161 2
        if ($this->isExpired()) {
162 1
            return $this;
163
        }
164
165 2
        $new = clone $this;
166 2
        $new->expires = time() - 31536001;
167 2
        return $new;
168
    }
169
170
    /**
171
     * {@inheritDoc}
172
     *
173
     * @throws InvalidArgumentException if the expire time is not valid.
174
     */
175 12
    public function withExpires($expire = null): CookieInterface
176
    {
177 12
        if ($expire === $this->expires) {
178 2
            return $this;
179
        }
180
181 11
        $new = clone $this;
182 11
        $new->setExpires($expire);
183 4
        return $new;
184
    }
185
186
    /**
187
     * {@inheritDoc}
188
     */
189 6
    public function getDomain(): ?string
190
    {
191 6
        return $this->domain;
192
    }
193
194
    /**
195
     * {@inheritDoc}
196
     */
197 3
    public function withDomain(?string $domain): CookieInterface
198
    {
199 3
        if ($domain === $this->domain) {
200 2
            return $this;
201
        }
202
203 3
        $new = clone $this;
204 3
        $new->setDomain($domain);
205 3
        return $new;
206
    }
207
208
    /**
209
     * {@inheritDoc}
210
     */
211 6
    public function getPath(): ?string
212
    {
213 6
        return $this->path;
214
    }
215
216
    /**
217
     * {@inheritDoc}
218
     */
219 3
    public function withPath(?string $path): CookieInterface
220
    {
221 3
        if ($path === $this->path) {
222 1
            return $this;
223
        }
224
225 3
        $new = clone $this;
226 3
        $new->setPath($path);
227 3
        return $new;
228
    }
229
230
    /**
231
     * {@inheritDoc}
232
     */
233 5
    public function isSecure(): bool
234
    {
235 5
        return $this->secure ?? false;
236
    }
237
238
    /**
239
     * {@inheritDoc}
240
     */
241 2
    public function withSecure(bool $secure = true): CookieInterface
242
    {
243 2
        if ($secure === $this->secure) {
244 1
            return $this;
245
        }
246
247 1
        $new = clone $this;
248 1
        $new->setSecure($secure);
249 1
        return $new;
250
    }
251
252
    /**
253
     * {@inheritDoc}
254
     */
255 5
    public function isHttpOnly(): bool
256
    {
257 5
        return $this->httpOnly ?? false;
258
    }
259
260
    /**
261
     * {@inheritDoc}
262
     */
263 2
    public function withHttpOnly(bool $httpOnly = true): CookieInterface
264
    {
265 2
        if ($httpOnly === $this->httpOnly) {
266 1
            return $this;
267
        }
268
269 1
        $new = clone $this;
270 1
        $new->setHttpOnly($httpOnly);
271 1
        return $new;
272
    }
273
274
    /**
275
     * {@inheritDoc}
276
     */
277 6
    public function getSameSite(): ?string
278
    {
279 6
        return $this->sameSite;
280
    }
281
282
    /**
283
     * {@inheritDoc}
284
     *
285
     * @throws InvalidArgumentException if the sameSite is not valid.
286
     */
287 4
    public function withSameSite(?string $sameSite): CookieInterface
288
    {
289 4
        if ($sameSite === $this->sameSite) {
290 1
            return $this;
291
        }
292
293 3
        $new = clone $this;
294 3
        $new->setSameSite($sameSite);
295 2
        return $new;
296
    }
297
298
    /**
299
     * {@inheritDoc}
300
     */
301 19
    public function isSession(): bool
302
    {
303 19
        return $this->expires === 0;
304
    }
305
306
    /**
307
     * {@inheritDoc}
308
     */
309 19
    public function __toString(): string
310
    {
311 19
        $cookie = $this->name . '=' . rawurlencode($this->value);
312
313 19
        if (!$this->isSession()) {
314 6
            $cookie .= '; Expires=' . gmdate('D, d-M-Y H:i:s T', $this->expires);
315 6
            $cookie .= '; Max-Age=' . $this->getMaxAge();
316
        }
317
318 19
        if ($this->domain !== null) {
319 3
            $cookie .= '; Domain=' . $this->domain;
320
        }
321
322 19
        if ($this->path !== null) {
323 19
            $cookie .= '; Path=' . $this->path;
324
        }
325
326 19
        if ($this->secure === true) {
327 18
            $cookie .= '; Secure';
328
        }
329
330 19
        if ($this->httpOnly === true) {
331 16
            $cookie .= '; HttpOnly';
332
        }
333
334 19
        if ($this->sameSite !== null) {
335 19
            $cookie .= '; SameSite=' . $this->sameSite;
336
        }
337
338 19
        return $cookie;
339
    }
340
341
    /**
342
     * @param string $name
343
     * @throws InvalidArgumentException if the name is not valid.
344
     */
345 99
    private function setName(string $name): void
346
    {
347 99
        if (empty($name)) {
348 2
            throw new InvalidArgumentException('The cookie name cannot be empty.');
349
        }
350
351 98
        if (!preg_match('/^[a-zA-Z0-9!#$%&\' *+\-.^_`|~]+$/', $name)) {
352 20
            throw new InvalidArgumentException(sprintf(
353 20
                'The cookie name `%s` contains invalid characters; must contain any US-ASCII'
354 20
                . ' characters, except control and separator characters, spaces, or tabs.',
355 20
                $name
356 20
            ));
357
        }
358
359 98
        $this->name = $name;
360
    }
361
362
    /**
363
     * @param string $value
364
     */
365 98
    private function setValue(string $value): void
366
    {
367 98
        $this->value = $value;
368
    }
369
370
    /**
371
     * @param mixed $expire
372
     * @throws InvalidArgumentException if the expire time is not valid.
373
     * @psalm-suppress RiskyTruthyFalsyComparison
374
     */
375 98
    private function setExpires($expire): void
376
    {
377 98
        if ($expire !== null && !is_int($expire) && !is_string($expire) && !$expire instanceof DateTimeInterface) {
378 12
            throw new InvalidArgumentException(sprintf(
379 12
                'The cookie expire time is not valid; must be null, or string,'
380 12
                . ' or integer, or DateTimeInterface instance; received `%s`.',
381 12
                (is_object($expire) ? get_class($expire) : gettype($expire))
382 12
            ));
383
        }
384
385 98
        if (empty($expire)) {
386 95
            $this->expires = 0;
387 95
            return;
388
        }
389
390 9
        if ($expire instanceof DateTimeInterface) {
391 3
            $expire = $expire->format('U');
392 8
        } elseif (!is_numeric($expire)) {
393 4
            $stringExpire = $expire;
394 4
            $expire = strtotime($expire);
395
396 4
            if ($expire === false) {
397 2
                throw new InvalidArgumentException(sprintf(
398 2
                    'The string representation of the cookie expire time `%s` is not valid.',
399 2
                    $stringExpire
400 2
                ));
401
            }
402
        }
403
404 7
        $this->expires = ($expire > 0) ? (int) $expire : 0;
405
    }
406
407
    /**
408
     * @param string|null $domain
409
     * @psalm-suppress RiskyTruthyFalsyComparison
410
     */
411 98
    private function setDomain(?string $domain): void
412
    {
413 98
        $this->domain = empty($domain) ? null : $domain;
414
    }
415
416
    /**
417
     * @param string|null $path
418
     * @psalm-suppress RiskyTruthyFalsyComparison
419
     */
420 98
    private function setPath(?string $path): void
421
    {
422 98
        $this->path = empty($path) ? null : $path;
423
    }
424
425
    /**
426
     * @param bool|null $secure
427
     */
428 98
    private function setSecure(?bool $secure): void
429
    {
430 98
        $this->secure = $secure;
431
    }
432
433
    /**
434
     * @param bool|null $httpOnly
435
     */
436 98
    private function setHttpOnly(?bool $httpOnly): void
437
    {
438 98
        $this->httpOnly = $httpOnly;
439
    }
440
441
    /**
442
     * @param string|null $sameSite
443
     * @throws InvalidArgumentException if the sameSite is not valid.
444
     * @psalm-suppress RiskyTruthyFalsyComparison
445
     */
446 98
    private function setSameSite(?string $sameSite): void
447
    {
448 98
        $sameSite = empty($sameSite) ? null : ucfirst(strtolower($sameSite));
449 98
        $sameSiteValues = [self::SAME_SITE_NONE, self::SAME_SITE_LAX, self::SAME_SITE_STRICT];
450
451 98
        if ($sameSite !== null && !in_array($sameSite, $sameSiteValues, true)) {
452 2
            throw new InvalidArgumentException(sprintf(
453 2
                'The sameSite attribute `%s` is not valid; must be one of (%s).',
454 2
                $sameSite,
455 2
                implode(', ', array_map(static fn($item) => "\"$item\"", $sameSiteValues)),
456 2
            ));
457
        }
458
459 98
        $this->sameSite = $sameSite;
460
    }
461
}
462