LazyString.php 4.4 KB

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