LazyString.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.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. namespace Symfony\Component\String;
  11. /**
  12. * A string whose value is computed lazily by a callback.
  13. *
  14. * @author Nicolas Grekas <p@tchwork.com>
  15. */
  16. class LazyString implements \Stringable, \JsonSerializable
  17. {
  18. private $value;
  19. /**
  20. * @param callable|array $callback A callable or a [Closure, method] lazy-callable
  21. *
  22. * @return static
  23. */
  24. public static function fromCallable($callback, ...$arguments): self
  25. {
  26. if (!\is_callable($callback) && !(\is_array($callback) && isset($callback[0]) && $callback[0] instanceof \Closure && 2 >= \count($callback))) {
  27. throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a callable or a [Closure, method] lazy-callable, "%s" given.', __METHOD__, get_debug_type($callback)));
  28. }
  29. $lazyString = new static();
  30. $lazyString->value = static function () use (&$callback, &$arguments, &$value): string {
  31. if (null !== $arguments) {
  32. if (!\is_callable($callback)) {
  33. $callback[0] = $callback[0]();
  34. $callback[1] = $callback[1] ?? '__invoke';
  35. }
  36. $value = $callback(...$arguments);
  37. $callback = self::getPrettyName($callback);
  38. $arguments = null;
  39. }
  40. return $value ?? '';
  41. };
  42. return $lazyString;
  43. }
  44. /**
  45. * @param string|int|float|bool|\Stringable $value
  46. *
  47. * @return static
  48. */
  49. public static function fromStringable($value): self
  50. {
  51. if (!self::isStringable($value)) {
  52. throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a scalar or a stringable object, "%s" given.', __METHOD__, get_debug_type($value)));
  53. }
  54. if (\is_object($value)) {
  55. return static::fromCallable([$value, '__toString']);
  56. }
  57. $lazyString = new static();
  58. $lazyString->value = (string) $value;
  59. return $lazyString;
  60. }
  61. /**
  62. * Tells whether the provided value can be cast to string.
  63. */
  64. final public static function isStringable($value): bool
  65. {
  66. return \is_string($value) || $value instanceof self || (\is_object($value) ? method_exists($value, '__toString') : is_scalar($value));
  67. }
  68. /**
  69. * Casts scalars and stringable objects to strings.
  70. *
  71. * @param object|string|int|float|bool $value
  72. *
  73. * @throws \TypeError When the provided value is not stringable
  74. */
  75. final public static function resolve($value): string
  76. {
  77. return $value;
  78. }
  79. /**
  80. * @return string
  81. */
  82. public function __toString()
  83. {
  84. if (\is_string($this->value)) {
  85. return $this->value;
  86. }
  87. try {
  88. return $this->value = ($this->value)();
  89. } catch (\Throwable $e) {
  90. if (\TypeError::class === \get_class($e) && __FILE__ === $e->getFile()) {
  91. $type = explode(', ', $e->getMessage());
  92. $type = substr(array_pop($type), 0, -\strlen(' returned'));
  93. $r = new \ReflectionFunction($this->value);
  94. $callback = $r->getStaticVariables()['callback'];
  95. $e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type));
  96. }
  97. if (\PHP_VERSION_ID < 70400) {
  98. // leverage the ErrorHandler component with graceful fallback when it's not available
  99. return trigger_error($e, E_USER_ERROR);
  100. }
  101. throw $e;
  102. }
  103. }
  104. public function __sleep(): array
  105. {
  106. $this->__toString();
  107. return ['value'];
  108. }
  109. public function jsonSerialize(): string
  110. {
  111. return $this->__toString();
  112. }
  113. private function __construct()
  114. {
  115. }
  116. private static function getPrettyName(callable $callback): string
  117. {
  118. if (\is_string($callback)) {
  119. return $callback;
  120. }
  121. if (\is_array($callback)) {
  122. $class = \is_object($callback[0]) ? get_debug_type($callback[0]) : $callback[0];
  123. $method = $callback[1];
  124. } elseif ($callback instanceof \Closure) {
  125. $r = new \ReflectionFunction($callback);
  126. if (false !== strpos($r->name, '{closure}') || !$class = $r->getClosureScopeClass()) {
  127. return $r->name;
  128. }
  129. $class = $class->name;
  130. $method = $r->name;
  131. } else {
  132. $class = get_debug_type($callback);
  133. $method = '__invoke';
  134. }
  135. return $class.'::'.$method;
  136. }
  137. }