StringCoercionMode.php 5.8 KB

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