Converter.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <?php
  2. /**
  3. * League.Uri (https://uri.thephpleague.com)
  4. *
  5. * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. declare(strict_types=1);
  11. namespace League\Uri\KeyValuePair;
  12. use BackedEnum;
  13. use League\Uri\Exceptions\SyntaxError;
  14. use League\Uri\StringCoercionMode;
  15. use Stringable;
  16. use function array_combine;
  17. use function explode;
  18. use function implode;
  19. use function is_string;
  20. use function preg_match;
  21. use function str_replace;
  22. use const PHP_QUERY_RFC1738;
  23. use const PHP_QUERY_RFC3986;
  24. final class Converter
  25. {
  26. private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/';
  27. /**
  28. * @param non-empty-string $separator the query string separator
  29. * @param array<string> $fromRfc3986 contains all the RFC3986 encoded characters to be converted
  30. * @param array<string> $toEncoding contains all the expected encoded characters
  31. */
  32. private function __construct(
  33. private readonly string $separator,
  34. private readonly array $fromRfc3986 = [],
  35. private readonly array $toEncoding = [],
  36. ) {
  37. if ('' === $this->separator) {
  38. throw new SyntaxError('The separator character must be a non empty string.');
  39. }
  40. }
  41. /**
  42. * @param non-empty-string $separator
  43. */
  44. public static function new(string $separator): self
  45. {
  46. return new self($separator);
  47. }
  48. /**
  49. * @param non-empty-string $separator
  50. */
  51. public static function fromRFC3986(string $separator = '&'): self
  52. {
  53. return self::new($separator);
  54. }
  55. /**
  56. * @param non-empty-string $separator
  57. */
  58. public static function fromRFC1738(string $separator = '&'): self
  59. {
  60. return self::new($separator)
  61. ->withEncodingMap(['%20' => '+']);
  62. }
  63. /**
  64. * @param non-empty-string $separator
  65. *
  66. * @see https://url.spec.whatwg.org/#application/x-www-form-urlencoded
  67. */
  68. public static function fromFormData(string $separator = '&'): self
  69. {
  70. return self::new($separator)
  71. ->withEncodingMap(['%20' => '+', '%2A' => '*']);
  72. }
  73. public static function fromEncodingType(int $encType): self
  74. {
  75. return match ($encType) {
  76. PHP_QUERY_RFC3986 => self::fromRFC3986(),
  77. PHP_QUERY_RFC1738 => self::fromRFC1738(),
  78. default => throw new SyntaxError('Unknown or Unsupported encoding.'),
  79. };
  80. }
  81. /**
  82. * @return non-empty-string
  83. */
  84. public function separator(): string
  85. {
  86. return $this->separator;
  87. }
  88. /**
  89. * @return array<string, string>
  90. */
  91. public function encodingMap(): array
  92. {
  93. return array_combine($this->fromRfc3986, $this->toEncoding);
  94. }
  95. /**
  96. * @return array<non-empty-list<string|null>>
  97. */
  98. public function toPairs(BackedEnum|Stringable|string|int|float|bool|null $value): array
  99. {
  100. $value = StringCoercionMode::Native->coerce($value);
  101. if (null === $value) {
  102. return [];
  103. }
  104. $value = match (1) {
  105. preg_match(self::REGEXP_INVALID_CHARS, $value) => throw new SyntaxError('Invalid query string: `'.$value.'`.'),
  106. default => str_replace($this->toEncoding, $this->fromRfc3986, $value),
  107. };
  108. return array_map(
  109. fn (string $pair): array => explode('=', $pair, 2) + [1 => null],
  110. explode($this->separator, $value)
  111. );
  112. }
  113. /**
  114. * @param iterable<array{0:string|null, 1:BackedEnum|Stringable|string|bool|int|float|null}> $pairs
  115. */
  116. public function toValue(iterable $pairs): ?string
  117. {
  118. $filteredPairs = [];
  119. foreach ($pairs as $pair) {
  120. $filteredPairs[] = match (true) {
  121. !is_string($pair[0]) => throw new SyntaxError('the pair key MUST be a string;, `'.gettype($pair[0]).'` given.'),
  122. null === $pair[1] => StringCoercionMode::Native->coerce($pair[0]),
  123. default => StringCoercionMode::Native->coerce($pair[0]).'='.StringCoercionMode::Native->coerce($pair[1]),
  124. };
  125. }
  126. return match ([]) {
  127. $filteredPairs => null,
  128. default => str_replace($this->fromRfc3986, $this->toEncoding, implode($this->separator, $filteredPairs)),
  129. };
  130. }
  131. /**
  132. * @param non-empty-string $separator
  133. */
  134. public function withSeparator(string $separator): self
  135. {
  136. return match ($this->separator) {
  137. $separator => $this,
  138. default => new self($separator, $this->fromRfc3986, $this->toEncoding),
  139. };
  140. }
  141. /**
  142. * Sets the conversion map.
  143. *
  144. * Each key from the iterable structure represents the RFC3986 encoded characters as string,
  145. * while each value represents the expected output encoded characters
  146. */
  147. public function withEncodingMap(iterable $encodingMap): self
  148. {
  149. $fromRfc3986 = [];
  150. $toEncoding = [];
  151. foreach ($encodingMap as $from => $to) {
  152. [$fromRfc3986[], $toEncoding[]] = match (true) {
  153. !is_string($from) => throw new SyntaxError('The encoding output must be a string; `'.gettype($from).'` given.'),
  154. $to instanceof Stringable,
  155. is_string($to) => [$from, (string) $to],
  156. default => throw new SyntaxError('The encoding output must be a string; `'.gettype($to).'` given.'),
  157. };
  158. }
  159. return match (true) {
  160. $fromRfc3986 !== $this->fromRfc3986,
  161. $toEncoding !== $this->toEncoding => new self($this->separator, $fromRfc3986, $toEncoding),
  162. default => $this,
  163. };
  164. }
  165. }