1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Http; |
4: | |
5: | use Psr\Http\Message\RequestInterface; |
6: | use Psr\Http\Message\UriInterface as PsrUriInterface; |
7: | use Salient\Contract\Core\DateFormatterInterface; |
8: | use Salient\Contract\Core\MimeType; |
9: | use Salient\Contract\Http\FormDataFlag; |
10: | use Salient\Contract\Http\HttpRequestMethod; |
11: | use Salient\Utility\AbstractUtility; |
12: | use Salient\Utility\Date; |
13: | use Salient\Utility\Package; |
14: | use Salient\Utility\Reflect; |
15: | use Salient\Utility\Regex; |
16: | use Salient\Utility\Str; |
17: | use DateTimeImmutable; |
18: | use DateTimeInterface; |
19: | use DateTimeZone; |
20: | use Generator; |
21: | use InvalidArgumentException; |
22: | use Stringable; |
23: | |
24: | |
25: | |
26: | |
27: | final class HttpUtil extends AbstractUtility |
28: | { |
29: | |
30: | |
31: | |
32: | |
33: | |
34: | private const SUFFIX_TYPE = [ |
35: | 'gzip' => MimeType::GZIP, |
36: | 'json' => MimeType::JSON, |
37: | 'jwt' => MimeType::JWT, |
38: | 'xml' => MimeType::XML, |
39: | 'yaml' => MimeType::YAML, |
40: | 'zip' => MimeType::ZIP, |
41: | ]; |
42: | |
43: | |
44: | |
45: | |
46: | private const ALIAS_TYPE = [ |
47: | 'text/xml' => MimeType::XML, |
48: | ]; |
49: | |
50: | |
51: | |
52: | |
53: | |
54: | |
55: | public static function isRequestMethod(string $method): bool |
56: | { |
57: | return Reflect::hasConstantWithValue(HttpRequestMethod::class, $method); |
58: | } |
59: | |
60: | |
61: | |
62: | |
63: | |
64: | |
65: | |
66: | |
67: | public static function mediaTypeIs(string $type, string $mimeType): bool |
68: | { |
69: | |
70: | [$type] = explode(';', $type); |
71: | $type = Str::lower(rtrim($type)); |
72: | |
73: | if ((self::ALIAS_TYPE[$type] ?? $type) === $mimeType) { |
74: | return true; |
75: | } |
76: | |
77: | |
78: | $pos = strrpos($type, '+'); |
79: | if ($pos === false) { |
80: | return false; |
81: | } |
82: | |
83: | $suffix = substr($type, $pos + 1); |
84: | $type = substr($type, 0, $pos); |
85: | |
86: | return (self::ALIAS_TYPE[$type] ?? $type) === $mimeType |
87: | || (self::SUFFIX_TYPE[$suffix] ?? null) === $mimeType; |
88: | } |
89: | |
90: | |
91: | |
92: | |
93: | public static function getDate(?DateTimeInterface $date = null): string |
94: | { |
95: | return ($date ? Date::immutable($date) : new DateTimeImmutable()) |
96: | ->setTimezone(new DateTimeZone('UTC')) |
97: | ->format(DateTimeInterface::RFC7231); |
98: | } |
99: | |
100: | |
101: | |
102: | |
103: | |
104: | |
105: | public static function getParameters( |
106: | string $value, |
107: | bool $firstIsParameter = false, |
108: | bool $unquote = true, |
109: | bool $strict = false |
110: | ): array { |
111: | foreach (Str::splitDelimited(';', $value, false) as $i => $param) { |
112: | if ($i === 0 && !$firstIsParameter) { |
113: | $params[] = $unquote |
114: | ? self::unquoteString($param) |
115: | : $param; |
116: | continue; |
117: | } |
118: | if (Regex::match('/^(' . Regex::HTTP_TOKEN . ')(?:\h*+=\h*+(.*))?$/D', $param, $matches)) { |
119: | $param = $matches[2] ?? ''; |
120: | $params[Str::lower($matches[1])] = $unquote |
121: | ? self::unquoteString($param) |
122: | : $param; |
123: | continue; |
124: | } |
125: | if ($strict) { |
126: | throw new InvalidArgumentException(sprintf('Invalid parameter: %s', $param)); |
127: | } |
128: | } |
129: | return $params ?? []; |
130: | } |
131: | |
132: | |
133: | |
134: | |
135: | |
136: | |
137: | public static function mergeParameters(array $parameters): string |
138: | { |
139: | if (!$parameters) { |
140: | return ''; |
141: | } |
142: | |
143: | foreach ($parameters as $key => $param) { |
144: | $value = is_int($key) ? [] : [$key]; |
145: | if ($param !== '') { |
146: | $value[] = self::maybeQuoteString($param); |
147: | } |
148: | $merged[] = $last = implode('=', $value); |
149: | } |
150: | $merged = implode('; ', $merged); |
151: | return $last === '' && $merged !== '' |
152: | ? substr($merged, 0, -1) |
153: | : $merged; |
154: | } |
155: | |
156: | |
157: | |
158: | |
159: | |
160: | public static function getProduct(): string |
161: | { |
162: | return sprintf( |
163: | '%s/%s php/%s', |
164: | str_replace('/', '~', Package::name()), |
165: | Package::version(true, true), |
166: | \PHP_VERSION, |
167: | ); |
168: | } |
169: | |
170: | |
171: | |
172: | |
173: | |
174: | public static function maybeQuoteString(string $string): string |
175: | { |
176: | return Regex::match('/^' . Regex::HTTP_TOKEN . '$/D', $string) |
177: | ? $string |
178: | : '"' . self::escapeQuotedString($string) . '"'; |
179: | } |
180: | |
181: | |
182: | |
183: | |
184: | |
185: | public static function escapeQuotedString(string $string): string |
186: | { |
187: | return str_replace(['\\', '"'], ['\\\\', '\"'], $string); |
188: | } |
189: | |
190: | |
191: | |
192: | |
193: | public static function unquoteString(string $string): string |
194: | { |
195: | $string = Regex::replace('/^"(.*)"$/D', '$1', $string, -1, $count); |
196: | return $count |
197: | ? Regex::replace('/\\\\(.)/', '$1', $string) |
198: | : $string; |
199: | } |
200: | |
201: | |
202: | |
203: | |
204: | |
205: | |
206: | |
207: | |
208: | |
209: | |
210: | |
211: | public static function mergeQuery( |
212: | $value, |
213: | array $data, |
214: | int $flags = FormDataFlag::PRESERVE_NUMERIC_KEYS | FormDataFlag::PRESERVE_STRING_KEYS, |
215: | ?DateFormatterInterface $dateFormatter = null |
216: | ) { |
217: | $uri = $value instanceof RequestInterface |
218: | ? $value->getUri() |
219: | : ($value instanceof PsrUriInterface |
220: | ? $value |
221: | : new Uri((string) $value)); |
222: | |
223: | parse_str($uri->getQuery(), $query); |
224: | $query = (new FormData(array_replace_recursive($query, $data))) |
225: | ->getQuery($flags, $dateFormatter); |
226: | if ($value instanceof RequestInterface) { |
227: | return $value->withUri($uri->withQuery($query)); |
228: | } |
229: | return $uri->withQuery($query); |
230: | } |
231: | |
232: | |
233: | |
234: | |
235: | |
236: | |
237: | |
238: | |
239: | |
240: | |
241: | |
242: | public static function replaceQuery( |
243: | $value, |
244: | array $data, |
245: | int $flags = FormDataFlag::PRESERVE_NUMERIC_KEYS | FormDataFlag::PRESERVE_STRING_KEYS, |
246: | ?DateFormatterInterface $dateFormatter = null |
247: | ) { |
248: | $query = (new FormData($data))->getQuery($flags, $dateFormatter); |
249: | if ($value instanceof RequestInterface) { |
250: | return $value->withUri($value->getUri()->withQuery($query)); |
251: | } |
252: | if (!$value instanceof PsrUriInterface) { |
253: | $value = new Uri((string) $value); |
254: | } |
255: | return $value->withQuery($query); |
256: | } |
257: | |
258: | |
259: | |
260: | |
261: | |
262: | |
263: | |
264: | public static function getNameValueGenerator(iterable $items): Generator |
265: | { |
266: | foreach ($items as $item) { |
267: | yield $item['name'] => $item['value']; |
268: | } |
269: | } |
270: | } |
271: | |