Passed
Push — master ( 9013bb...125cf1 )
by Kirill
03:56
created

AcceptHeader::addItem()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 11
c 1
b 0
f 0
dl 0
loc 18
rs 8.8333
cc 7
nc 6
nop 1
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Pavel Z
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Http\Header;
13
14
use Spiral\Http\Exception\AcceptHeaderException;
15
16
/**
17
 * Can be used for parsing and sorting "Accept*" header items by preferable by the HTTP client.
18
 *
19
 * Supported headers:
20
 *   Accept
21
 *   Accept-Encoding
22
 *   Accept-Charset
23
 *   Accept-Language
24
 */
25
final class AcceptHeader
26
{
27
    /** @var array|AcceptHeaderItem[] */
28
    private $items = [];
29
30
    /** @var bool */
31
    private $sorted = false;
32
33
    /**
34
     * AcceptHeader constructor.
35
     * @param AcceptHeaderItem[]|string[] $items
36
     */
37
    public function __construct(array $items = [])
38
    {
39
        foreach ($items as $item) {
40
            $this->addItem($item);
41
        }
42
    }
43
44
    /**
45
     * @return string
46
     */
47
    public function __toString(): string
48
    {
49
        return implode(', ', $this->getAll());
50
    }
51
52
    /**
53
     * @param string $raw
54
     * @return AcceptHeader
55
     */
56
    public static function fromString(string $raw): self
57
    {
58
        $header = new static();
59
60
        $parts = explode(',', $raw);
61
        foreach ($parts as $part) {
62
            $part = trim($part);
63
            if ($part !== '') {
64
                $header->addItem($part);
65
            }
66
        }
67
68
        return $header;
69
    }
70
71
    /**
72
     * @param AcceptHeaderItem|string $item
73
     * @return $this
74
     */
75
    public function add($item): self
76
    {
77
        $header = clone $this;
78
        $header->addItem($item);
79
80
        return $header;
81
    }
82
83
    /**
84
     * @param string $value
85
     * @return bool
86
     */
87
    public function has(string $value): bool
88
    {
89
        return isset($this->items[strtolower(trim($value))]);
90
    }
91
92
    /**
93
     * @param string $value
94
     * @return AcceptHeaderItem|null
95
     */
96
    public function get(string $value): ?AcceptHeaderItem
97
    {
98
        return $this->items[strtolower(trim($value))] ?? null;
99
    }
100
101
    /**
102
     * @return AcceptHeaderItem[]
103
     */
104
    public function getAll(): array
105
    {
106
        if (!$this->sorted) {
107
            /**
108
             * Sort item in descending order.
109
             */
110
            uasort($this->items, static function (AcceptHeaderItem $a, AcceptHeaderItem $b) {
111
                return self::compare($a, $b) * -1;
112
            });
113
114
            $this->sorted = true;
115
        }
116
117
        return array_values($this->items);
118
    }
119
120
    /**
121
     * Add new item to list.
122
     *
123
     * @param AcceptHeaderItem|string $item
124
     */
125
    private function addItem($item): void
126
    {
127
        if (is_scalar($item)) {
128
            $item = AcceptHeaderItem::fromString((string)$item);
129
        }
130
131
        if (!$item instanceof AcceptHeaderItem) {
0 ignored issues
show
introduced by
$item is always a sub-type of Spiral\Http\Header\AcceptHeaderItem.
Loading history...
132
            throw new AcceptHeaderException(sprintf(
133
                'Accept Header item expected to be an instance of `%s` or a string, got `%s`',
134
                AcceptHeaderItem::class,
135
                is_object($item) ? get_class($item) : gettype($item)
136
            ));
137
        }
138
139
        $value = strtolower($item->getValue());
140
        if ($value !== '' && (!$this->has($value) || self::compare($item, $this->get($value)) === 1)) {
0 ignored issues
show
Bug introduced by
It seems like $this->get($value) can also be of type null; however, parameter $b of Spiral\Http\Header\AcceptHeader::compare() does only seem to accept Spiral\Http\Header\AcceptHeaderItem, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

140
        if ($value !== '' && (!$this->has($value) || self::compare($item, /** @scrutinizer ignore-type */ $this->get($value)) === 1)) {
Loading history...
141
            $this->sorted = false;
142
            $this->items[$value] = $item;
143
        }
144
    }
145
146
    /**
147
     * Compare to header items, witch one is preferable.
148
     * Return 1 if first value preferable or -1 if second, 0 in case of same weight.
149
     *
150
     * @param AcceptHeaderItem|string $a
151
     * @param AcceptHeaderItem|string $b
152
     * @return int
153
     */
154
    private static function compare(AcceptHeaderItem $a, AcceptHeaderItem $b): int
155
    {
156
        if ($a->getQuality() === $b->getQuality()) {
157
            // If quality are same value with more params has more weight.
158
            if (count($a->getParams()) === count($b->getParams())) {
159
                // If quality and params then check for specific type or subtype.
160
                // Means */* or * has less weight.
161
                return static::compareValue($a->getValue(), $b->getValue());
162
            }
163
164
            return count($a->getParams()) <=> count($b->getParams());
165
        }
166
167
        return $a->getQuality() <=> $b->getQuality();
168
    }
169
170
    /**
171
     * Compare to header item values. More specific types ( with no "*" ) has more value.
172
     * Return 1 if first value preferable or -1 if second, 0 in case of same weight.
173
     *
174
     * @param string $a
175
     * @param string $b
176
     * @return int
177
     */
178
    private static function compareValue(string $a, string $b): int
179
    {
180
        // Check "Accept" headers values with it is type and subtype.
181
        if (strpos($a, '/') !== false && strpos($b, '/') !== false) {
182
            [$typeA, $subtypeA] = explode('/', $a, 2);
183
            [$typeB, $subtypeB] = explode('/', $b, 2);
184
185
            if ($typeA === $typeB) {
186
                return static::compareAtomic($subtypeA, $subtypeB);
187
            }
188
189
            return static::compareAtomic($typeA, $typeB);
190
        }
191
192
        return static::compareAtomic($a, $b);
193
    }
194
195
    /**
196
     * @param string $a
197
     * @param string $b
198
     * @return int
199
     */
200
    private static function compareAtomic(string $a, string $b): int
201
    {
202
        if (mb_strpos($a, '*/') === 0) {
203
            $a = '*';
204
        }
205
206
        if (mb_strpos($b, '*/') === 0) {
207
            $b = '*';
208
        }
209
210
        if (strtolower($a) === strtolower($b)) {
211
            return 0;
212
        }
213
214
        if ($a === '*') {
215
            return -1;
216
        }
217
218
        if ($b === '*') {
219
            return 1;
220
        }
221
222
        return 0;
223
    }
224
}
225