LazyString.php 4.2 KB

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