ByteRange   A
last analyzed

Complexity

Total Complexity 42

Size/Duplication

Total Lines 191
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 9
Bugs 0 Features 0
Metric Value
eloc 65
c 9
b 0
f 0
dl 0
loc 191
ccs 72
cts 72
cp 1
rs 9.0399
wmc 42

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 5
B getFirstBytePos() 0 24 7
A isValid() 0 3 2
A isSatisfiable() 0 18 5
A fromString() 0 17 6
B coversFile() 0 17 7
A getLength() 0 3 1
A __toString() 0 3 1
B getLastBytePos() 0 24 8

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\Http\Header\Value\Range;
6
7
use InvalidArgumentException;
8
use Stadly\Http\Utilities\Rfc7233;
9
10
/**
11
 * Class for handling byte ranges.
12
 *
13
 * Specification: https://tools.ietf.org/html/rfc7233#section-2.1
14
 */
15
final class ByteRange
16
{
17
    /**
18
     * @var int|null Position of first byte in range.
19
     */
20
    private $firstByte;
21
22
    /**
23
     * @var int|null Position of last byte in range.
24
     */
25
    private $lastByte;
26
27
    /**
28
     * Constructor.
29
     *
30
     * @param int|null $firstByte Position of first byte in range, null covers from end of file.
31
     * @param int|null $lastByte Position of last byte in range, null covers to end of file.
32
     */
33 11
    public function __construct(?int $firstByte, ?int $lastByte)
34
    {
35
        // Both $firstByte and $lastByte are null.
36 11
        if ($firstByte === null && $lastByte === null) {
37 1
            throw new InvalidArgumentException('Invalid range: ' . $firstByte . '-' . $lastByte);
38
        }
39
40
        // $firstByte or $lastByte are negative.
41 10
        if ($firstByte < 0 || $lastByte < 0) {
42 2
            throw new InvalidArgumentException('Invalid range: ' . $firstByte . '-' . $lastByte);
43
        }
44
45 8
        $this->firstByte = $firstByte;
46 8
        $this->lastByte = $lastByte;
47
    }
48
49
    /**
50
     * Construct range from string.
51
     *
52
     * @param string $range Range string.
53
     * @return self Range generated based on the string.
54
     */
55 15
    public static function fromString(string $range): self
56
    {
57 15
        $regEx = '{^(?:' . Rfc7233::BYTE_RANGE_SPEC_CAPTURE . '|' . Rfc7233::SUFFIX_BYTE_RANGE_SPEC_CAPTURE . ')$}';
58 15
        $plainRange = mb_convert_encoding($range, 'ISO-8859-1', 'UTF-8');
59 15
        if ($plainRange !== $range || preg_match($regEx, $range, $matches) !== 1) {
60 7
            throw new InvalidArgumentException('Invalid range: ' . $range);
61
        }
62
63 8
        $firstByte = $matches['FIRST_BYTE_POS'] === '' ? null : intval($matches['FIRST_BYTE_POS']);
64
65 8
        if ($firstByte === null) {
66 2
            $lastByte = intval($matches['SUFFIX_LENGTH']);
67
        } else {
68 6
            $lastByte = ($matches['LAST_BYTE_POS'] ?? '') === '' ? null : intval($matches['LAST_BYTE_POS']);
69
        }
70
71 8
        return new self($firstByte, $lastByte);
72
    }
73
74
    /**
75
     * @return string String representation of the range.
76
     */
77 4
    public function __toString(): string
78
    {
79 4
        return $this->firstByte . '-' . $this->lastByte;
80
    }
81
82
    /**
83
     * @param int|null $fileSize Size of the file.
84
     * @return bool Whether the range is satisfiable.
85
     */
86 22
    public function isSatisfiable(?int $fileSize): bool
87
    {
88 22
        if (!$this->isValid()) {
89 2
            return false;
90
        }
91
92 20
        if ($fileSize === null) {
93
            // When file size is unknown, both first byte and last byte must be specified.
94 4
            return $this->firstByte !== null && $this->lastByte !== null;
95
        }
96
97
        // When covering from the end, the number of bytes covered must be positive.
98 16
        if ($this->firstByte === null) {
99 4
            return $this->lastByte > 0;
100
        }
101
102
        // First byte must be smaller than file size.
103 12
        return $this->firstByte < $fileSize;
104
    }
105
106
    /**
107
     * @return bool Whether the range is valid.
108
     */
109 5
    public function isValid(): bool
110
    {
111 5
        return $this->lastByte === null || $this->firstByte <= $this->lastByte;
112
    }
113
114
    /**
115
     * @param int|null $fileSize Size of the file.
116
     * @return int Position of the first byte in the range.
117
     */
118 20
    public function getFirstBytePos(?int $fileSize): int
119
    {
120 20
        if ($fileSize === null) {
121
            // When file size is unknown, first byte must be specified.
122 4
            if ($this->firstByte !== null) {
123 4
                return $this->firstByte;
124
            }
125 16
        } elseif ($fileSize > 0) {
126 12
            if ($this->firstByte === null) {
127
                // When covering from the end, the number of bytes covered must be positive.
128 3
                if ($this->lastByte > 0) {
129 3
                    return max(0, $fileSize - $this->lastByte);
130
                }
131
            // First byte must be smaller than file size.
132 9
            } elseif ($this->firstByte < $fileSize) {
133 6
                return $this->firstByte;
134
            }
135
        }
136
137 9
        throw new InvalidArgumentException(sprintf(
138 9
            'Cannot calculate first byte position: %s-%s/%s',
139 9
            $this->firstByte,
140 9
            $this->lastByte,
141 9
            $fileSize ?? '*'
142 9
        ));
143
    }
144
145
    /**
146
     * @param int|null $fileSize Size of the file.
147
     * @return int Position of the last byte in the range.
148
     */
149 20
    public function getLastBytePos(?int $fileSize): int
150
    {
151 20
        if ($fileSize === null) {
152
            // When file size is unknown, both first byte and last byte must be specified.
153 4
            if ($this->firstByte !== null && $this->lastByte !== null) {
154 4
                return $this->lastByte;
155
            }
156 16
        } elseif ($fileSize > 0) {
157 12
            if ($this->firstByte === null) {
158
                // When covering from the end, the number of bytes covered must be positive.
159 3
                if ($this->lastByte > 0) {
160 3
                    return $fileSize - 1;
161
                }
162
            // First byte must be smaller than file size.
163 9
            } elseif ($this->firstByte < $fileSize) {
164 6
                return min($this->lastByte ?? $fileSize - 1, $fileSize - 1);
165
            }
166
        }
167
168 10
        throw new InvalidArgumentException(sprintf(
169 10
            'Cannot calculate last byte position: %s-%s/%s',
170 10
            $this->firstByte,
171 10
            $this->lastByte,
172 10
            $fileSize ?? '*'
173 10
        ));
174
    }
175
176
    /**
177
     * @param int|null $fileSize Size of the file.
178
     * @return int Number of bytes in the range.
179
     */
180 20
    public function getLength(?int $fileSize): int
181
    {
182 20
        return $this->getLastBytePos($fileSize) - $this->getFirstBytePos($fileSize) + 1;
183
    }
184
185
    /**
186
     * @param int|null $fileSize Size of the file.
187
     * @return bool Whether the range covers the entire file.
188
     */
189 13
    public function coversFile(?int $fileSize): bool
190
    {
191 13
        if ($this->lastByte === null) {
192 2
            return $this->firstByte === 0;
193
        }
194
195 11
        if ($fileSize !== null) {
196 8
            if ($this->firstByte === 0 && $fileSize <= $this->lastByte + 1) {
197 3
                return true;
198
            }
199
200 5
            if ($this->firstByte === null && $fileSize <= $this->lastByte) {
201 2
                return true;
202
            }
203
        }
204
205 6
        return false;
206
    }
207
}
208