Completed
Push — main ( f06d77...6ba089 )
by Beau
17s queued 12s
created

SetCookie   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 385
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 91.19%

Importance

Changes 0
Metric Value
wmc 56
lcom 1
cbo 2
dl 0
loc 385
ccs 145
cts 159
cp 0.9119
rs 5.5199
c 0
b 0
f 0

34 Methods

Rating   Name   Duplication   Size   Complexity  
A getSameSite() 0 4 1
A withValue() 0 8 1
A rememberForever() 0 4 1
A expire() 0 4 1
A withHttpOnly() 0 8 1
A create() 0 4 1
A __construct() 0 5 1
A getName() 0 4 1
A getValue() 0 4 1
A getExpires() 0 4 1
A getMaxAge() 0 4 1
A getPath() 0 4 1
A getDomain() 0 4 1
A getSecure() 0 4 1
A getHttpOnly() 0 4 1
A resolveExpires() 0 22 5
A withExpires() 0 10 1
A withMaxAge() 0 8 1
A withPath() 0 8 1
A withDomain() 0 8 1
A withSecure() 0 8 1
A withSameSite() 0 8 1
A withoutSameSite() 0 8 1
A __toString() 0 16 1
A createRememberedForever() 0 4 1
A createExpired() 0 4 1
C fromSetCookieString() 0 56 12
A appendFormattedDomainPartIfSet() 0 8 2
A appendFormattedPathPartIfSet() 0 8 2
A appendFormattedExpiresPartIfSet() 0 8 2
A appendFormattedMaxAgePartIfSet() 0 8 2
A appendFormattedSecurePartIfSet() 0 8 2
A appendFormattedHttpOnlyPartIfSet() 0 8 2
A appendFormattedSameSitePartIfSet() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like SetCookie 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 SetCookie, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Dflydev\FigCookies;
6
7
use DateTime;
8
use DateTimeInterface;
9
use Dflydev\FigCookies\Modifier\SameSite;
10
use InvalidArgumentException;
11
12
use function array_shift;
13
use function count;
14
use function explode;
15
use function gmdate;
16
use function implode;
17
use function is_int;
18
use function is_numeric;
19
use function is_string;
20
use function sprintf;
21
use function strtolower;
22
use function strtotime;
23
use function urlencode;
24
25
class SetCookie
26
{
27
    /** @var string */
28
    private $name;
29
    /** @var string|null */
30
    private $value;
31
    /** @var int */
32
    private $expires = 0;
33
    /** @var int */
34
    private $maxAge = 0;
35
    /** @var string|null */
36
    private $path;
37
    /** @var string|null */
38
    private $domain;
39
    /** @var bool */
40
    private $secure = false;
41
    /** @var bool */
42
    private $httpOnly = false;
43
    /** @var SameSite|null */
44
    private $sameSite;
45
46 35
    private function __construct(string $name, ?string $value = null)
47
    {
48 35
        $this->name  = $name;
49 35
        $this->value = $value;
50 35
    }
51
52 19
    public function getName(): string
53
    {
54 19
        return $this->name;
55
    }
56
57 3
    public function getValue(): ?string
58
    {
59 3
        return $this->value;
60
    }
61
62 2
    public function getExpires(): int
63
    {
64 2
        return $this->expires;
65
    }
66
67
    public function getMaxAge(): int
68
    {
69
        return $this->maxAge;
70
    }
71
72
    public function getPath(): ?string
73
    {
74
        return $this->path;
75
    }
76
77
    public function getDomain(): ?string
78
    {
79
        return $this->domain;
80
    }
81
82
    public function getSecure(): bool
83
    {
84
        return $this->secure;
85
    }
86
87
    public function getHttpOnly(): bool
88
    {
89
        return $this->httpOnly;
90
    }
91
92 1
    public function getSameSite(): ?SameSite
93
    {
94 1
        return $this->sameSite;
95
    }
96
97 32
    public function withValue(?string $value = null): self
98
    {
99 32
        $clone = clone $this;
100
101 32
        $clone->value = $value;
102
103 32
        return $clone;
104
    }
105
106
    /** @param int|DateTimeInterface|string|null $expires */
107 16
    private function resolveExpires($expires = null): int
108
    {
109 16
        if ($expires === null) {
110
            return 0;
111
        }
112
113 16
        if ($expires instanceof DateTimeInterface) {
114 2
            return (int) $expires->getTimestamp();
115
        }
116
117 14
        if (is_numeric($expires)) {
118
            return (int) $expires;
119
        }
120
121 14
        $time = strtotime($expires);
122
123 14
        if (! is_int($time)) {
124 1
            throw new InvalidArgumentException(sprintf('Invalid expires "%s" provided', $expires));
125
        }
126
127 13
        return $time;
128
    }
129
130
    /** @param int|string|DateTimeInterface|null $expires */
131 16
    public function withExpires($expires = null): self
132
    {
133 16
        $expires = $this->resolveExpires($expires);
134
135 15
        $clone = clone $this;
136
137 15
        $clone->expires = $expires;
138
139 15
        return $clone;
140
    }
141
142 1
    public function rememberForever(): self
143
    {
144 1
        return $this->withExpires(new DateTime('+5 years'));
145
    }
146
147 1
    public function expire(): self
148
    {
149 1
        return $this->withExpires(new DateTime('-5 years'));
150
    }
151
152 6
    public function withMaxAge(?int $maxAge = null): self
153
    {
154 6
        $clone = clone $this;
155
156 6
        $clone->maxAge = (int) $maxAge;
157
158 6
        return $clone;
159
    }
160
161 15
    public function withPath(?string $path = null): self
162
    {
163 15
        $clone = clone $this;
164
165 15
        $clone->path = $path;
166
167 15
        return $clone;
168
    }
169
170 10
    public function withDomain(?string $domain = null): self
171
    {
172 10
        $clone = clone $this;
173
174 10
        $clone->domain = $domain;
175
176 10
        return $clone;
177
    }
178
179 12
    public function withSecure(bool $secure = true): self
180
    {
181 12
        $clone = clone $this;
182
183 12
        $clone->secure = $secure;
184
185 12
        return $clone;
186
    }
187
188 15
    public function withHttpOnly(bool $httpOnly = true): self
189
    {
190 15
        $clone = clone $this;
191
192 15
        $clone->httpOnly = $httpOnly;
193
194 15
        return $clone;
195
    }
196
197 3
    public function withSameSite(SameSite $sameSite): self
198
    {
199 3
        $clone = clone $this;
200
201 3
        $clone->sameSite = $sameSite;
202
203 3
        return $clone;
204
    }
205
206 1
    public function withoutSameSite(): self
207
    {
208 1
        $clone = clone $this;
209
210 1
        $clone->sameSite = null;
211
212 1
        return $clone;
213
    }
214
215 20
    public function __toString(): string
216
    {
217 20
        $cookieStringParts = [
218 20
            urlencode($this->name) . '=' . urlencode((string) $this->value),
219
        ];
220
221 20
        $cookieStringParts = $this->appendFormattedDomainPartIfSet($cookieStringParts);
222 20
        $cookieStringParts = $this->appendFormattedPathPartIfSet($cookieStringParts);
223 20
        $cookieStringParts = $this->appendFormattedExpiresPartIfSet($cookieStringParts);
224 20
        $cookieStringParts = $this->appendFormattedMaxAgePartIfSet($cookieStringParts);
225 20
        $cookieStringParts = $this->appendFormattedSecurePartIfSet($cookieStringParts);
226 20
        $cookieStringParts = $this->appendFormattedHttpOnlyPartIfSet($cookieStringParts);
227 20
        $cookieStringParts = $this->appendFormattedSameSitePartIfSet($cookieStringParts);
228
229 20
        return implode('; ', $cookieStringParts);
230
    }
231
232 10
    public static function create(string $name, ?string $value = null): self
233
    {
234 10
        return new static($name, $value);
235
    }
236
237 1
    public static function createRememberedForever(string $name, ?string $value = null): self
238
    {
239 1
        return static::create($name, $value)->rememberForever();
240
    }
241
242
    /**
243
     * @deprecated Do not use this method. Will be removed in v4.0.
244
     *
245
     * If you want to remove a cookie, create it normally and call ->expire()
246
     * on the SetCookie object.
247
     */
248
    public static function createExpired(string $name): self
249
    {
250
        return static::create($name)->expire();
251
    }
252
253 32
    public static function fromSetCookieString(string $string): self
254
    {
255 32
        $rawAttributes = StringUtil::splitOnAttributeDelimiter($string);
256
257 32
        $rawAttribute = array_shift($rawAttributes);
258
259 32
        if (! is_string($rawAttribute)) {
260 1
            throw new InvalidArgumentException(sprintf(
261 1
                'The provided cookie string "%s" must have at least one attribute',
262 1
                $string
263
            ));
264
        }
265
266 31
        [$cookieName, $cookieValue] = StringUtil::splitCookiePair($rawAttribute);
0 ignored issues
show
Bug introduced by
The variable $cookieName does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $cookieValue does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
267
268 31
        $setCookie = new static($cookieName);
269
270 31
        if ($cookieValue !== null) {
271 31
            $setCookie = $setCookie->withValue($cookieValue);
272
        }
273
274 31
        while ($rawAttribute = array_shift($rawAttributes)) {
275 14
            $rawAttributePair = explode('=', $rawAttribute, 2);
276
277 14
            $attributeKey   = $rawAttributePair[0];
278 14
            $attributeValue = count($rawAttributePair) > 1 ? $rawAttributePair[1] : null;
279
280 14
            $attributeKey = strtolower($attributeKey);
281
282 14
            switch ($attributeKey) {
283 14
                case 'expires':
284 13
                    $setCookie = $setCookie->withExpires($attributeValue);
285 13
                    break;
286 14
                case 'max-age':
287 6
                    $setCookie = $setCookie->withMaxAge((int) $attributeValue);
288 6
                    break;
289 14
                case 'domain':
290 9
                    $setCookie = $setCookie->withDomain($attributeValue);
291 9
                    break;
292 14
                case 'path':
293 14
                    $setCookie = $setCookie->withPath($attributeValue);
294 14
                    break;
295 14
                case 'secure':
296 12
                    $setCookie = $setCookie->withSecure(true);
297 12
                    break;
298 14
                case 'httponly':
299 14
                    $setCookie = $setCookie->withHttpOnly(true);
300 14
                    break;
301 2
                case 'samesite':
302 2
                    $setCookie = $setCookie->withSameSite(SameSite::fromString((string) $attributeValue));
303 2
                    break;
304
            }
305
        }
306
307 31
        return $setCookie;
308
    }
309
310
    /**
311
     * @param string[] $cookieStringParts
312
     *
313
     * @return string[]
314
     */
315 20
    private function appendFormattedDomainPartIfSet(array $cookieStringParts): array
316
    {
317 20
        if ($this->domain) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->domain of type string|null is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
318 9
            $cookieStringParts[] = sprintf('Domain=%s', $this->domain);
319
        }
320
321 20
        return $cookieStringParts;
322
    }
323
324
    /**
325
     * @param string[] $cookieStringParts
326
     *
327
     * @return string[]
328
     */
329 20
    private function appendFormattedPathPartIfSet(array $cookieStringParts): array
330
    {
331 20
        if ($this->path) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->path of type string|null is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
332 10
            $cookieStringParts[] = sprintf('Path=%s', $this->path);
333
        }
334
335 20
        return $cookieStringParts;
336
    }
337
338
    /**
339
     * @param string[] $cookieStringParts
340
     *
341
     * @return string[]
342
     */
343 20
    private function appendFormattedExpiresPartIfSet(array $cookieStringParts): array
344
    {
345 20
        if ($this->expires) {
346 9
            $cookieStringParts[] = sprintf('Expires=%s', gmdate('D, d M Y H:i:s T', $this->expires));
347
        }
348
349 20
        return $cookieStringParts;
350
    }
351
352
    /**
353
     * @param string[] $cookieStringParts
354
     *
355
     * @return string[]
356
     */
357 20
    private function appendFormattedMaxAgePartIfSet(array $cookieStringParts): array
358
    {
359 20
        if ($this->maxAge) {
360 6
            $cookieStringParts[] = sprintf('Max-Age=%s', $this->maxAge);
361
        }
362
363 20
        return $cookieStringParts;
364
    }
365
366
    /**
367
     * @param string[] $cookieStringParts
368
     *
369
     * @return string[]
370
     */
371 20
    private function appendFormattedSecurePartIfSet(array $cookieStringParts): array
372
    {
373 20
        if ($this->secure) {
374 8
            $cookieStringParts[] = 'Secure';
375
        }
376
377 20
        return $cookieStringParts;
378
    }
379
380
    /**
381
     * @param string[] $cookieStringParts
382
     *
383
     * @return string[]
384
     */
385 20
    private function appendFormattedHttpOnlyPartIfSet(array $cookieStringParts): array
386
    {
387 20
        if ($this->httpOnly) {
388 10
            $cookieStringParts[] = 'HttpOnly';
389
        }
390
391 20
        return $cookieStringParts;
392
    }
393
394
    /**
395
     * @param string[] $cookieStringParts
396
     *
397
     * @return string[]
398
     */
399 20
    private function appendFormattedSameSitePartIfSet(array $cookieStringParts): array
400
    {
401 20
        if ($this->sameSite === null) {
402 18
            return $cookieStringParts;
403
        }
404
405 3
        $cookieStringParts[] = $this->sameSite->asString();
406
407 3
        return $cookieStringParts;
408
    }
409
}
410