[String] add LazyString to provide generic stringable objects
This commit is contained in:
parent
80b003f888
commit
4bb19c62e2
@ -112,6 +112,7 @@ use Symfony\Component\Serializer\Encoder\EncoderInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
use Symfony\Component\String\LazyString;
|
||||
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
|
||||
use Symfony\Component\Translation\Translator;
|
||||
@ -1390,9 +1391,15 @@ class FrameworkExtension extends Extension
|
||||
throw new InvalidArgumentException(sprintf('Invalid value "%s" set as "decryption_env_var": only "word" characters are allowed.', $config['decryption_env_var']));
|
||||
}
|
||||
|
||||
$container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%");
|
||||
if (class_exists(LazyString::class)) {
|
||||
$container->getDefinition('secrets.decryption_key')->replaceArgument(1, $config['decryption_env_var']);
|
||||
} else {
|
||||
$container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%");
|
||||
$container->removeDefinition('secrets.decryption_key');
|
||||
}
|
||||
} else {
|
||||
$container->getDefinition('secrets.vault')->replaceArgument(1, null);
|
||||
$container->removeDefinition('secrets.decryption_key');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,10 @@
|
||||
<service id="secrets.vault" class="Symfony\Bundle\FrameworkBundle\Secrets\SodiumVault">
|
||||
<tag name="container.env_var_loader" />
|
||||
<argument />
|
||||
<argument type="service" id="secrets.decryption_key" on-invalid="ignore" />
|
||||
</service>
|
||||
|
||||
<service id="secrets.decryption_key" parent="getenv">
|
||||
<argument />
|
||||
</service>
|
||||
|
||||
|
@ -129,5 +129,19 @@
|
||||
<tag name="kernel.locale_aware" />
|
||||
</service>
|
||||
<service id="Symfony\Component\String\Slugger\SluggerInterface" alias="slugger" />
|
||||
|
||||
<!-- inherit from this service to lazily access env vars -->
|
||||
<service id="getenv" class="Symfony\Component\String\LazyString" abstract="true">
|
||||
<factory class="Symfony\Component\String\LazyString" method="fromCallable" />
|
||||
<argument type="service">
|
||||
<service class="Closure">
|
||||
<factory class="Closure" method="fromCallable" />
|
||||
<argument type="collection">
|
||||
<argument type="service" id="service_container" />
|
||||
<argument>getEnv</argument>
|
||||
</argument>
|
||||
</service>
|
||||
</argument>
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
@ -4,8 +4,9 @@ CHANGELOG
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* Added the `AbstractString::reverse()` method.
|
||||
* Made `AbstractString::width()` follow POSIX.1-2001.
|
||||
* added the `AbstractString::reverse()` method
|
||||
* made `AbstractString::width()` follow POSIX.1-2001
|
||||
* added `LazyString` which provides memoizing stringable objects
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
165
src/Symfony/Component/String/LazyString.php
Normal file
165
src/Symfony/Component/String/LazyString.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\String;
|
||||
|
||||
/**
|
||||
* A string whose value is computed lazily by a callback.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class LazyString implements \JsonSerializable
|
||||
{
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @param callable|array $callback A callable or a [Closure, method] lazy-callable
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function fromCallable($callback, ...$arguments): self
|
||||
{
|
||||
if (!\is_callable($callback) && !(\is_array($callback) && isset($callback[0]) && $callback[0] instanceof \Closure && 2 >= \count($callback))) {
|
||||
throw new \TypeError(sprintf('Argument 1 passed to %s() must be a callable or a [Closure, method] lazy-callable, %s given.', __METHOD__, \gettype($callback)));
|
||||
}
|
||||
|
||||
$lazyString = new static();
|
||||
$lazyString->value = static function () use (&$callback, &$arguments, &$value): string {
|
||||
if (null !== $arguments) {
|
||||
if (!\is_callable($callback)) {
|
||||
$callback[0] = $callback[0]();
|
||||
$callback[1] = $callback[1] ?? '__invoke';
|
||||
}
|
||||
$value = $callback(...$arguments);
|
||||
$callback = self::getPrettyName($callback);
|
||||
$arguments = null;
|
||||
}
|
||||
|
||||
return $value ?? '';
|
||||
};
|
||||
|
||||
return $lazyString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object|string|int|float|bool $value A scalar or an object that implements the __toString() magic method
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function fromStringable($value): self
|
||||
{
|
||||
if (!self::isStringable($value)) {
|
||||
throw new \TypeError(sprintf('Argument 1 passed to %s() must be a scalar or an object that implements the __toString() magic method, %s given.', __METHOD__, \is_object($value) ? \get_class($value) : \gettype($value)));
|
||||
}
|
||||
|
||||
if (\is_object($value)) {
|
||||
return static::fromCallable([$value, '__toString']);
|
||||
}
|
||||
|
||||
$lazyString = new static();
|
||||
$lazyString->value = (string) $value;
|
||||
|
||||
return $lazyString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the provided value can be cast to string.
|
||||
*/
|
||||
final public static function isStringable($value): bool
|
||||
{
|
||||
return \is_string($value) || $value instanceof self || (\is_object($value) ? \is_callable([$value, '__toString']) : is_scalar($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts scalars and stringable objects to strings.
|
||||
*
|
||||
* @param object|string|int|float|bool $value
|
||||
*
|
||||
* @throws \TypeError When the provided value is not stringable
|
||||
*/
|
||||
final public static function resolve($value): string
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
if (\is_string($this->value)) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->value = ($this->value)();
|
||||
} catch (\Throwable $e) {
|
||||
if (\TypeError::class === \get_class($e) && __FILE__ === $e->getFile()) {
|
||||
$type = explode(', ', $e->getMessage());
|
||||
$type = substr(array_pop($type), 0, -\strlen(' returned'));
|
||||
$r = new \ReflectionFunction($this->value);
|
||||
$callback = $r->getStaticVariables()['callback'];
|
||||
|
||||
$e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type));
|
||||
}
|
||||
|
||||
if (\PHP_VERSION_ID < 70400) {
|
||||
// leverage the ErrorHandler component with graceful fallback when it's not available
|
||||
return trigger_error($e, E_USER_ERROR);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function __sleep(): array
|
||||
{
|
||||
$this->__toString();
|
||||
|
||||
return ['value'];
|
||||
}
|
||||
|
||||
public function jsonSerialize(): string
|
||||
{
|
||||
return $this->__toString();
|
||||
}
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
private static function getPrettyName(callable $callback): string
|
||||
{
|
||||
if (\is_string($callback)) {
|
||||
return $callback;
|
||||
}
|
||||
|
||||
if (\is_array($callback)) {
|
||||
$class = \is_object($callback[0]) ? \get_class($callback[0]) : $callback[0];
|
||||
$method = $callback[1];
|
||||
} elseif ($callback instanceof \Closure) {
|
||||
$r = new \ReflectionFunction($callback);
|
||||
|
||||
if (false !== strpos($r->name, '{closure}') || !$class = $r->getClosureScopeClass()) {
|
||||
return $r->name;
|
||||
}
|
||||
|
||||
$class = $class->name;
|
||||
$method = $r->name;
|
||||
} else {
|
||||
$class = \get_class($callback);
|
||||
$method = '__invoke';
|
||||
}
|
||||
|
||||
if (isset($class[15]) && "\0" === $class[15] && 0 === strpos($class, "class@anonymous\x00")) {
|
||||
$class = (get_parent_class($class) ?: key(class_implements($class))).'@anonymous';
|
||||
}
|
||||
|
||||
return $class.'::'.$method;
|
||||
}
|
||||
}
|
112
src/Symfony/Component/String/Tests/LazyStringTest.php
Normal file
112
src/Symfony/Component/String/Tests/LazyStringTest.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\String\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\ErrorHandler\ErrorHandler;
|
||||
use Symfony\Component\String\LazyString;
|
||||
|
||||
class LazyStringTest extends TestCase
|
||||
{
|
||||
public function testLazyString()
|
||||
{
|
||||
$count = 0;
|
||||
$s = LazyString::fromCallable(function () use (&$count) {
|
||||
return ++$count;
|
||||
});
|
||||
|
||||
$this->assertSame(0, $count);
|
||||
$this->assertSame('1', (string) $s);
|
||||
$this->assertSame(1, $count);
|
||||
}
|
||||
|
||||
public function testLazyCallable()
|
||||
{
|
||||
$count = 0;
|
||||
$s = LazyString::fromCallable([function () use (&$count) {
|
||||
return new class($count) {
|
||||
private $count;
|
||||
|
||||
public function __construct(int &$count)
|
||||
{
|
||||
$this->count = &$count;
|
||||
}
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
return ++$this->count;
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
$this->assertSame(0, $count);
|
||||
$this->assertSame('1', (string) $s);
|
||||
$this->assertSame(1, $count);
|
||||
$this->assertSame('1', (string) $s); // ensure the value is memoized
|
||||
$this->assertSame(1, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function testReturnTypeError()
|
||||
{
|
||||
ErrorHandler::register();
|
||||
|
||||
$s = LazyString::fromCallable(function () { return []; });
|
||||
|
||||
$this->expectException(\TypeError::class);
|
||||
$this->expectExceptionMessage('Return value of '.__NAMESPACE__.'\{closure}() passed to '.LazyString::class.'::fromCallable() must be of the type string, array returned.');
|
||||
|
||||
(string) $s;
|
||||
}
|
||||
|
||||
public function testFromStringable()
|
||||
{
|
||||
$this->assertInstanceOf(LazyString::class, LazyString::fromStringable('abc'));
|
||||
$this->assertSame('abc', (string) LazyString::fromStringable('abc'));
|
||||
$this->assertSame('1', (string) LazyString::fromStringable(true));
|
||||
$this->assertSame('', (string) LazyString::fromStringable(false));
|
||||
$this->assertSame('123', (string) LazyString::fromStringable(123));
|
||||
$this->assertSame('123.456', (string) LazyString::fromStringable(123.456));
|
||||
$this->assertStringContainsString('hello', (string) LazyString::fromStringable(new \Exception('hello')));
|
||||
}
|
||||
|
||||
public function testResolve()
|
||||
{
|
||||
$this->assertSame('abc', LazyString::resolve('abc'));
|
||||
$this->assertSame('1', LazyString::resolve(true));
|
||||
$this->assertSame('', LazyString::resolve(false));
|
||||
$this->assertSame('123', LazyString::resolve(123));
|
||||
$this->assertSame('123.456', LazyString::resolve(123.456));
|
||||
$this->assertStringContainsString('hello', LazyString::resolve(new \Exception('hello')));
|
||||
}
|
||||
|
||||
public function testIsStringable()
|
||||
{
|
||||
$this->assertTrue(LazyString::isStringable('abc'));
|
||||
$this->assertTrue(LazyString::isStringable(true));
|
||||
$this->assertTrue(LazyString::isStringable(false));
|
||||
$this->assertTrue(LazyString::isStringable(123));
|
||||
$this->assertTrue(LazyString::isStringable(123.456));
|
||||
$this->assertTrue(LazyString::isStringable(new \Exception('hello')));
|
||||
}
|
||||
|
||||
public function testIsNotStringable()
|
||||
{
|
||||
$this->assertFalse(LazyString::isStringable(null));
|
||||
$this->assertFalse(LazyString::isStringable([]));
|
||||
$this->assertFalse(LazyString::isStringable(STDIN));
|
||||
$this->assertFalse(LazyString::isStringable(new \StdClass()));
|
||||
$this->assertFalse(LazyString::isStringable(@eval('return new class() {private function __toString() {}};')));
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@
|
||||
"symfony/translation-contracts": "^1.1|^2"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/error-handler": "^4.4|^5.0",
|
||||
"symfony/http-client": "^4.4|^5.0",
|
||||
"symfony/var-exporter": "^4.4|^5.0"
|
||||
},
|
||||
|
Reference in New Issue
Block a user