LazyString.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  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 \JsonSerializable
  17. {
  18. /**
  19. * @var \Closure|string
  20. */
  21. private $value;
  22. /**
  23. * @param callable|array $callback A callable or a [Closure, method] lazy-callable
  24. * @param mixed ...$arguments
  25. * @return $this
  26. */
  27. public static function fromCallable($callback, ...$arguments)
  28. {
  29. if (\is_array($callback) && !\is_callable($callback) && !(($callback[0] ?? null) instanceof \Closure || 2 < \count($callback))) {
  30. throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a callable or a [Closure, method] lazy-callable, "%s" given.', __METHOD__, '['.implode(', ', array_map('get_debug_type', $callback)).']'));
  31. }
  32. $lazyString = new static();
  33. $lazyString->value = static function () use (&$callback, &$arguments): string {
  34. static $value;
  35. if (null !== $arguments) {
  36. if (!\is_callable($callback)) {
  37. $callback[0] = $callback[0]();
  38. $callback[1] = $callback[1] ?? '__invoke';
  39. }
  40. $value = $callback(...$arguments);
  41. $callback = self::getPrettyName($callback);
  42. $arguments = null;
  43. }
  44. return $value ?? '';
  45. };
  46. return $lazyString;
  47. }
  48. /**
  49. * @param string|int|float|bool|\Stringable $value
  50. * @return $this
  51. */
  52. public static function fromStringable($value)
  53. {
  54. if (\is_object($value)) {
  55. return static::fromCallable(\Closure::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. * @param mixed $value
  64. */
  65. final public static function isStringable($value)
  66. {
  67. return \is_string($value) || $value instanceof \Stringable || \is_scalar($value);
  68. }
  69. /**
  70. * Casts scalars and stringable objects to strings.
  71. *
  72. * @throws \TypeError When the provided value is not stringable
  73. * @param \Stringable|string|int|float|bool $value
  74. */
  75. final public static function resolve($value)
  76. {
  77. return $value;
  78. }
  79. public function __toString()
  80. {
  81. if (\is_string($this->value)) {
  82. return $this->value;
  83. }
  84. try {
  85. return $this->value = ($this->value)();
  86. } catch (\Throwable $e) {
  87. if (\TypeError::class === get_class($e) && __FILE__ === $e->getFile()) {
  88. $type = explode(', ', $e->getMessage());
  89. $type = substr(array_pop($type), 0, -\strlen(' returned'));
  90. $r = new \ReflectionFunction($this->value);
  91. $callback = $r->getStaticVariables()['callback'];
  92. $e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type));
  93. }
  94. throw $e;
  95. }
  96. }
  97. public function __sleep()
  98. {
  99. $this->__toString();
  100. return ['value'];
  101. }
  102. public function jsonSerialize()
  103. {
  104. return $this->__toString();
  105. }
  106. private function __construct()
  107. {
  108. }
  109. /**
  110. * @param callable $callback
  111. */
  112. private static function getPrettyName($callback)
  113. {
  114. if (\is_string($callback)) {
  115. return $callback;
  116. }
  117. if (\is_array($callback)) {
  118. $class = \is_object($callback[0]) ? get_debug_type($callback[0]) : $callback[0];
  119. $method = $callback[1];
  120. } elseif ($callback instanceof \Closure) {
  121. $r = new \ReflectionFunction($callback);
  122. if (strpos($r->name, '{closure}') !== false || !$class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) {
  123. return $r->name;
  124. }
  125. $class = $class->name;
  126. $method = $r->name;
  127. } else {
  128. $class = get_debug_type($callback);
  129. $method = '__invoke';
  130. }
  131. return $class.'::'.$method;
  132. }
  133. }