2525 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation
2626 */
2727
28- use Generator ;
29- use InvalidArgumentException ;
3028use Koded \Http \Interfaces \HttpStatus ;
3129
3230class AcceptHeaderNegotiator
3331{
34- /** @var AcceptHeader[] */
35- private $ supports ;
32+ private string $ supports = '' ;
3633
3734 public function __construct (string $ supportHeader )
3835 {
3936 $ this ->supports = $ supportHeader ;
4037 }
4138
42-
4339 public function match (string $ accepts ): AcceptHeader
40+ {
41+ return $ this ->matches ($ accepts )[0 ];
42+ }
43+
44+ public function matches (string $ accepts ): array
4445 {
4546 /** @var AcceptHeader $support */
4647 foreach ($ this ->parse ($ accepts ) as $ accept ) {
4748 foreach ($ this ->parse ($ this ->supports ) as $ support ) {
48- $ support ->matches ($ accept , $ types );
49+ $ support ->matches ($ accept , $ matches );
4950 }
5051 }
51-
52- usort ($ types , function (AcceptHeader $ a , AcceptHeader $ b ) {
53- return $ b ->weight () <=> $ a ->weight ();
54- });
55-
56- if (empty ($ types )) {
52+ usort ($ matches , fn (AcceptHeader $ a , AcceptHeader $ b ) => $ b ->weight () <=> $ a ->weight ());
53+ if (empty ($ matches )) {
5754 /* Set "q=0", meaning the header is explicitly rejected.
5855 * The consuming clients should handle this according to
5956 * their internal logic. This is much better then throwing
6057 * exceptions which must be handled in every place where
6158 * match() is called. For example, the client may issue a
6259 * 406 status code and be done with it.
6360 */
64- $ types [] = new class ('*;q=0 ' ) extends AcceptHeader {};
61+ $ matches [] = new class ('*;q=0 ' ) extends AcceptHeader {};
6562 }
66-
67- return $ types [0 ];
63+ return $ matches ;
6864 }
6965
7066 /**
7167 * @param string $header
7268 *
73- * @return Generator
69+ * @return \ Generator
7470 */
75- private function parse (string $ header ): Generator
71+ private function parse (string $ header ): \ Generator
7672 {
77- foreach (explode (', ' , $ header ) as $ header ) {
73+ foreach (\ explode (', ' , $ header ) as $ header ) {
7874 yield new class ($ header ) extends AcceptHeader {};
7975 }
8076 }
@@ -83,177 +79,151 @@ private function parse(string $header): Generator
8379
8480abstract class AcceptHeader
8581{
86- private $ header ;
87- private $ separator ;
88- private $ type ;
89- private $ subtype ;
90- private $ quality = 1.0 ;
91- private $ weight = 0.0 ;
92- private $ catchAll = false ;
93- private $ params = [];
82+ private string $ header = '' ;
83+ private string $ separator = ' / ' ;
84+ private string $ type = '' ;
85+ private string $ subtype = ' * ' ;
86+ private float $ quality = 1.0 ;
87+ private float $ weight = 0.0 ;
88+ private bool $ catchAll = false ;
89+ private array $ params = [];
9490
9591 public function __construct (string $ header )
9692 {
9793 $ this ->header = $ header ;
9894
99- $ header = preg_replace ('/[[:space:]]/ ' , '' , $ header );
100- $ bits = explode ('; ' , $ header );
101- $ type = array_shift ($ bits );
102-
103- if (!empty ($ type ) && !preg_match ('~^(\*|[a-z0-9._]+)([/|_-])?(\*|[a-z0-9.\-_+]+)?$~i ' , $ type , $ matches )) {
104- throw new InvalidArgumentException (sprintf ('"%s" is not a valid Access header ' , $ header ),
95+ $ header = \preg_replace ('/[[:space:]]/ ' , '' , $ header );
96+ $ bits = \explode ('; ' , $ header );
97+ $ type = \array_shift ($ bits );
98+ if (!empty ($ type ) && !\preg_match ('~^(\*|[a-z0-9._]+)([/|_\-])?(\*|[a-z0-9.\-_+]+)?$~i ' , $ type , $ matches )) {
99+ throw new \InvalidArgumentException (\sprintf ('"%s" is not a valid Access header ' , $ header ),
105100 HttpStatus::NOT_ACCEPTABLE );
106101 }
107-
108102 $ this ->separator = $ matches [2 ] ?? '/ ' ;
109- [$ type , $ subtype ] = explode ($ this ->separator , $ type , 2 ) + [1 => '* ' ];
110-
103+ [$ type , $ subtype ] = \explode ($ this ->separator , $ type , 2 ) + [1 => '* ' ];
111104 if ('* ' === $ type && '* ' !== $ subtype ) {
112105 // @see https://tools.ietf.org/html/rfc7231#section-5.3.2
113- throw new InvalidArgumentException (sprintf ('"%s" is not a valid Access header ' , $ header ),
106+ throw new \ InvalidArgumentException (\ sprintf ('"%s" is not a valid Access header ' , $ header ),
114107 HttpStatus::NOT_ACCEPTABLE );
115108 }
116-
117109 // @see https://tools.ietf.org/html/rfc7540#section-8.1.2
118- $ this ->type = strtolower ($ type );
119-
120- / * Uses a simple heuristic to check if subtype is part of
121- * some obscure media type like "vnd.api-v1+json".
110+ $ this ->type = \trim ( \ strtolower ($ type) );
111+ /*
112+ * Uses a simple heuristic to check if subtype is part of
113+ * some convoluted media type like "vnd.api-v1+json".
122114 *
123115 * NOTE: It is a waste of time to negotiate on the basis
124116 * of obscure parameters while using a meaningless media
125- * type like "vnd.whatever". But the web world is a big mess
126- * and this module can handle the Dunning-Kruger effect.
117+ * type like "vnd.whatever". The web world is a big mess
118+ * but this module can handle the Dunning-Kruger effect.
127119 */
128- $ this ->subtype = explode ('+ ' , $ subtype )[1 ] ?? $ subtype ;
129- $ this ->catchAll = '* ' === $ this ->type && '* ' === $ this ->subtype ;
130-
131- parse_str (join ('& ' , $ bits ), $ this ->params );
120+ $ this ->subtype = \trim (\explode ('+ ' , $ subtype )[1 ] ?? $ subtype );
121+ $ this ->catchAll = ('* ' === $ this ->type ) && ('* ' === $ this ->subtype );
122+ \parse_str (\join ('& ' , $ bits ), $ this ->params );
132123 $ this ->quality = (float )($ this ->params ['q ' ] ?? 1 );
133124 unset($ this ->params ['q ' ]);
134125 }
135126
136-
137127 public function __toString (): string
138128 {
139129 return $ this ->value ();
140130 }
141131
142-
143132 public function value (): string
144133 {
145134 // The header is explicitly rejected
146135 if (0.0 === $ this ->quality ) {
136+ $ this ->type = $ this ->subtype = '' ;
147137 return '' ;
148138 }
149-
150139 // If language, encoding or charset
151140 if ('* ' === $ this ->subtype ) {
152141 return $ this ->type ;
153142 }
154-
155143 return $ this ->type . $ this ->separator . $ this ->subtype ;
156144 }
157145
158-
159146 public function quality (): float
160147 {
161148 return $ this ->quality ;
162149 }
163150
164-
165151 public function weight (): float
166152 {
167153 return $ this ->weight ;
168154 }
169155
156+ public function is (string $ type ): bool
157+ {
158+ return ($ type === $ this ->subtype ) && ($ this ->subtype !== '* ' );
159+ }
160+
170161 /**
171- * @internal
172- *
173162 * @param AcceptHeader $accept The accept header part
174163 * @param AcceptHeader[] $matches Matched types
175164 *
176- * @return bool TRUE if the accept header part is a match
177- * against the supported (this) header part
178- *
179165 * This method finds the best match for the Accept header,
180- * including all the nonsense that may be passed by the
166+ * including lots of nonsense that may be passed by the
181167 * developers who do not follow RFC standards.
182168 *
169+ * @internal
183170 */
184- public function matches (AcceptHeader $ accept , array &$ matches = null ): bool
171+ public function matches (AcceptHeader $ accept , array &$ matches = null ): void
185172 {
186- $ matches = (array )$ matches ;
187- $ accept = clone $ accept ;
188-
189- $ typeMatch = $ this ->type === $ accept ->type ;
190-
173+ $ matches = (array )$ matches ;
174+ $ accept = clone $ accept ;
175+ $ typeMatch = ($ this ->type === $ accept ->type );
191176 if (1.0 === $ accept ->quality ) {
192177 $ accept ->quality = (float )$ this ->quality ;
193178 }
194-
195179 if ($ accept ->catchAll ) {
196180 $ accept ->type = $ this ->type ;
197181 $ accept ->subtype = $ this ->subtype ;
198182 $ matches [] = $ accept ;
199-
200- return true ;
183+ return ;
201184 }
202-
203185 // Explicitly denied
204186 if (0.0 === $ this ->quality ) {
205187 $ matches [] = clone $ this ;
206-
207- return true ;
188+ return ;
208189 }
209-
210190 // Explicitly denied
211191 if (0.0 === $ accept ->quality ) {
212192 $ matches [] = $ accept ;
213-
214- return true ;
193+ return ;
215194 }
216-
217195 // Explicit type mismatch (w/o asterisk); bail out
218- if (false === $ typeMatch && '* ' !== $ this ->type ) {
219- return false ;
196+ if (( false === $ typeMatch) && ( '* ' !== $ this ->type ) ) {
197+ return ;
220198 }
221-
222199 if ('* ' === $ accept ->subtype ) {
223200 $ accept ->subtype = $ this ->subtype ;
224201 }
225-
226- if ($ accept ->subtype !== $ this ->subtype && '* ' !== $ this ->subtype ) {
227- return false ;
202+ if (($ accept ->subtype !== $ this ->subtype ) && ('* ' !== $ this ->subtype )) {
203+ return ;
228204 }
229-
230205 $ matches [] = $ this ->rank ($ accept );
231-
232- return true ;
233206 }
234207
235208
236209 private function rank (AcceptHeader $ accept ): AcceptHeader
237210 {
238211 // +100 if types are exact match w/o asterisk
239- if ($ this ->type === $ accept ->type && '* ' !== $ accept ->type ) {
212+ if (($ this ->type === $ accept ->type ) &&
213+ ($ this ->subtype === $ accept ->subtype )) {
240214 $ accept ->weight += 100 ;
241215 }
242-
243- $ accept ->weight += $ this ->catchAll ? 0.0 : $ accept ->quality ;
244-
216+ $ accept ->weight += ($ this ->catchAll ? 0.0 : $ accept ->quality );
245217 // +1 for each parameter that matches, except "q"
246218 foreach ($ this ->params as $ k => $ v ) {
247- if (isset ($ accept ->params [$ k ]) && $ accept ->params [$ k ] === $ v ) {
219+ if (isset ($ accept ->params [$ k ]) && ( $ accept ->params [$ k ] === $ v) ) {
248220 $ accept ->weight += 1 ;
249221 } else {
250222 $ accept ->weight -= 1 ;
251223 }
252224 }
253-
254225 // Add "q"
255226 $ accept ->weight += $ accept ->quality ;
256-
257227 return $ accept ;
258228 }
259229}
0 commit comments