StringCoercionMode.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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;
  12. use BackedEnum;
  13. use DateTimeInterface;
  14. use League\Uri\Contracts\UriComponentInterface;
  15. use Stringable;
  16. use TypeError;
  17. use Uri\Rfc3986\Uri as Rfc3986Uri;
  18. use Uri\WhatWg\Url as WhatWgUrl;
  19. use ValueError;
  20. use function array_is_list;
  21. use function array_map;
  22. use function get_debug_type;
  23. use function implode;
  24. use function is_array;
  25. use function is_float;
  26. use function is_infinite;
  27. use function is_nan;
  28. use function is_object;
  29. use function is_resource;
  30. use function is_scalar;
  31. use function json_encode;
  32. use const JSON_PRESERVE_ZERO_FRACTION;
  33. enum StringCoercionMode
  34. {
  35. /**
  36. * PHP conversion mode.
  37. *
  38. * Guarantees that only scalar values, BackedEnum, and null are accepted.
  39. * Any object, Non-backed enums, resource, or recursive structure results in an error.
  40. *
  41. * - null: is not converted and stays the `null` value
  42. * - string: used as-is
  43. * - bool: converted to string “0” (false) or “1” (true)
  44. * - int: converted to numeric string (123 -> “123”)
  45. * - float: converted to decimal string (3.14 -> “3.14”)
  46. * - Backed Enum: converted to their backing value and then stringify see int and string
  47. */
  48. case Native;
  49. /**
  50. * Ecmascript conversion mode.
  51. *
  52. * Guarantees that only scalar values, BackedEnum, and null are accepted.
  53. * Any resource, or recursive structure results in an error.
  54. *
  55. * - null: converted to string “null”
  56. * - string: used as-is
  57. * - bool: converted to string “false” (false) or “true” (true)
  58. * - int: converted to numeric string (123 -> “123”)
  59. * - float: converted to decimal string (3.14 -> “3.14”), "NaN", "-Infinity" or "Infinity"
  60. * - Backed Enum: converted to their backing value and then stringify see int and string
  61. * - Array as list are flatten into a string list using the "," character as separator
  62. * - Associative array, Non-backed enums, any object without stringification semantics is coerced to "[object Object]".
  63. * - DateTimeInterface implementing object are coerce to their string representation using DateTimeInterface::RFC2822 format
  64. */
  65. case Ecmascript;
  66. private const RECURSION_MARKER = "\0__RECURSION_INTERNAL_MARKER_WHATWG__\0";
  67. public function isCoercible(mixed $value): bool
  68. {
  69. return self::Ecmascript === $this
  70. ? !is_resource($value)
  71. : match (true) {
  72. $value instanceof Rfc3986Uri,
  73. $value instanceof WhatWgUrl,
  74. $value instanceof BackedEnum,
  75. $value instanceof Stringable,
  76. is_scalar($value),
  77. null === $value => true,
  78. default => false,
  79. };
  80. }
  81. /**
  82. * @throws TypeError if the type is not supported by the specific case
  83. * @throws ValueError if circular reference is detected
  84. */
  85. public function coerce(mixed $value): ?string
  86. {
  87. return match ($this) {
  88. self::Ecmascript => match (true) {
  89. $value instanceof Rfc3986Uri => $value->toString(),
  90. $value instanceof WhatWgUrl => $value->toAsciiString(),
  91. $value instanceof DateTimeInterface => $value->format(DateTimeInterface::RFC2822),
  92. $value instanceof BackedEnum => (string) $value->value,
  93. $value instanceof Stringable => $value->__toString(),
  94. is_object($value) => '[object Object]',
  95. is_array($value) => match (true) {
  96. self::hasCircularReference($value) => throw new ValueError('Recursive array structure detected; unable to coerce value.'),
  97. array_is_list($value) => implode(',', array_map($this->coerce(...), $value)),
  98. default => '[object Object]',
  99. },
  100. true === $value => 'true',
  101. false === $value => 'false',
  102. null === $value => 'null',
  103. is_float($value) => match (true) {
  104. is_nan($value) => 'NaN',
  105. is_infinite($value) => 0 < $value ? 'Infinity' : '-Infinity',
  106. default => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION),
  107. },
  108. is_scalar($value) => (string) $value,
  109. default => throw new TypeError('Unable to coerce value of type "'.get_debug_type($value).'" with "'.$this->name.'" coercion.'),
  110. },
  111. self::Native => match (true) {
  112. $value instanceof UriComponentInterface => $value->value(),
  113. $value instanceof WhatWgUrl => $value->toAsciiString(),
  114. $value instanceof Rfc3986Uri => $value->toString(),
  115. $value instanceof BackedEnum => (string) $value->value,
  116. $value instanceof Stringable => $value->__toString(),
  117. false === $value => '0',
  118. true === $value => '1',
  119. null === $value => null,
  120. is_scalar($value) => (string) $value,
  121. default => throw new TypeError('Unable to coerce value of type "'.get_debug_type($value).'" with "'.$this->name.'" coercion.'),
  122. },
  123. };
  124. }
  125. /**
  126. * Array recursion detection.
  127. * @see https://stackoverflow.com/questions/9042142/detecting-infinite-array-recursion-in-php
  128. */
  129. private static function hasCircularReference(array &$arr): bool
  130. {
  131. if (isset($arr[self::RECURSION_MARKER])) {
  132. return true;
  133. }
  134. try {
  135. $arr[self::RECURSION_MARKER] = true;
  136. foreach ($arr as $key => &$value) {
  137. if (self::RECURSION_MARKER !== $key && is_array($value) && self::hasCircularReference($value)) {
  138. return true;
  139. }
  140. }
  141. return false;
  142. } finally {
  143. unset($arr[self::RECURSION_MARKER]);
  144. }
  145. }
  146. }