8000 [String] add LazyString to provide generic stringable objects · symfony/symfony@4bb19c6 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4bb19c6

Browse files
[String] add LazyString to provide generic stringable objects
1 parent 80b003f commit 4bb19c6

File tree

7 files changed

+307
-3
lines changed

7 files changed

+307
-3
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
113113
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
114114
use Symfony\Component\Stopwatch\Stopwatch;
115+
use Symfony\Component\String\LazyString;
115116
use Symfony\Component\String\Slugger\SluggerInterface;
116117
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
117118
use Symfony\Component\Translation\Translator;
@@ -1390,9 +1391,15 @@ private function registerSecretsConfiguration(array $config, ContainerBuilder $c
13901391
throw new InvalidArgumentException(sprintf('Invalid value "%s" set as "decryption_env_var": only "word" characters are allowed.', $config['decryption_env_var']));
13911392
}
13921393

1393-
$container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%");
1394+
if (class_exists(LazyString::class)) {
1395+
$container->getDefinition('secrets.decryption_key')->replaceArgument(1, $config['decryption_env_var']);
1396+
} else {
1397+
$container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%");
1398+
$container->removeDefinition('secrets.decryption_key');
1399+
}
13941400
} else {
13951401
$container->getDefinition('secrets.vault')->replaceArgument(1, null);
1402+
$container->removeDefinition('secrets.decryption_key');
13961403
}
13971404
}
13981405

src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
<service id="secrets.vault" class="Symfony\Bundle\FrameworkBundle\Secrets\SodiumVault">
99
<tag name="container.env_var_loader" />
1010
<argument />
11+
<argument type="service" id="secrets.decryption_key" on-invalid="ignore" />
12+
</service>
13+
14+
<service id="secrets.decryption_key" parent="getenv">
1115
<argument />
1216
</service>
1317

src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,5 +129,19 @@
129129
<tag name="kernel.locale_aware" />
130130
</service>
131131
<service id="Symfony\Component\String\Slugger\SluggerInterface" alias="slugger" />
132+
133+
<!-- inherit from this service to lazily access env vars -->
134+
<service id="getenv" class="Symfony\Component\String\LazyString" abstract="true">
135+
<factory class="Symfony\Component\String\LazyString" method="fromCallable" />
136+
<argument type="service">
137+
<service class="Closure">
138+
<factory class="Closure" method="fromCallable" />
139+
<argument type="collection">
140+
<argument type="service" id="service_container" />
141+
<argument>getEnv</argument>
142+
</argument>
143+
</service>
144+
</argument>
145+
</service>
132146
</services>
133147
</container>

src/Symfony/Component/String/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ CHANGELOG
44
5.1.0
55
-----
66

7-
* Added the `AbstractString::reverse()` method.
8-
* Made `AbstractString::width()` follow POSIX.1-2001.
7+
* added the `AbstractString::reverse()` method
8+
* made `AbstractString::width()` follow POSIX.1-2001
9+
* added `LazyString` which provides memoizing stringable objects
910

1011
5.0.0
1112
-----
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\String;
13+
14+
/**
15+
* A string whose value is computed lazily by a callback.
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
class LazyString implements \JsonSerializable
20+
{
21+
private $value;
22+
23+
/**
24+
* @param callable|array $callback A callable or a [Closure, method] lazy-callable
25+
*
26+
* @return static
27+
*/
28+
public static function fromCallable($callback, ...$arguments): self
29+
{
30+
if (!\is_callable($callback) && !(\is_array($callback) && isset($callback[0]) && $callback[0] instanceof \Closure && 2 >= \count($callback))) {
31+
throw new \TypeError(sprintf('Argument 1 passed to %s() must be a callable or a [Closure, method] lazy-callable, %s given.', __METHOD__, \gettype($callback)));
32+
}
33+
34+
$lazyString = new static();
35+
$lazyString->value = static function () use (&$callback, &$arguments, &$value): string {
36+
if (null !== $arguments) {
37+
if (!\is_callable($callback)) {
38+
$callback[0] = $callback[0]();
39+
$callback[1] = $callback[1] ?? '__invoke';
40+
}
41+
$value = $callback(...$arguments);
42+
$callback = self::getPrettyName($callback);
43+
$arguments = null;
44+
}
45+
46+
return $value ?? '';
47+
};
48+
49+
return $lazyString;
50+
}
51+
52+
/**
53+
* @param object|string|int|float|bool $value A scalar or an object that implements the __toString() magic method
54+
*
55+
* @return static
56+
*/
57+
public static function fromStringable($value): self
58+
{
59+
if (!self::isStringable($value)) {
60+
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)));
61+
}
62+
63+
if (\is_object($value)) {
64+
return static::fromCallable([$value, '__toString']);
65+
}
66+
67+
$lazyString = new static();
68+
$lazyString->value = (string) $value;
69+
70+
return $lazyString;
71+
}
72+
73+
/**
74+
* Tells whether the provided value can be cast to string.
75+
*/
76+
final public static function isStringable($value): bool
77+
{
78+
return \is_string($value) || $value instanceof self || (\is_object($value) ? \is_callable([$value, '__toString']) : is_scalar($value));
79+
}
80+
81+
/**
82+
* Casts scalars and stringable objects to strings.
83+
*
84+
* @param object|string|int|float|bool $value
85+
*
86+
* @throws \TypeError When the provided value is not stringable
87+
*/
88+
final public static function resolve($value): string
89+
{
90+
return $value;
91+
}
92+
93+
public function __toString()
94+
{
95+
if (\is_string($this->value)) {
96+
return $this->value;
97+
}
98+
99+
try {
100+
return $this->value = ($this->value)();
101+
} catch (\Throwable $e) {
102+
if (\TypeError::class === \get_class($e) && __FILE__ === $e->getFile()) {
103+
$type = explode(', ', $e->getMessage());
104+
$type = substr(array_pop($type), 0, -\strlen(' returned'));
105+
$r = new \ReflectionFunction($this->value);
106+
$callback = $r->getStaticVariables()['callback'];
107+
108+
$e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type));
109+
}
110+
111+
if (\PHP_VERSION_ID < 70400) {
112+
// leverage the ErrorHandler component with graceful fallback when it's not available
113+
return trigger_error($e, E_USER_ERROR);
114+
}
115+
116+
throw $e;
117+
}
118+
}
119+
120+
public function __sleep(): array
121+
{
122+
$this->__toString();
123+
124+
return ['value'];
125+
}
126+
127+
public function jsonSerialize(): string
128+
{
129+
return $this->__toString();
130+
}
131+
132+
private function __construct()
133+
{
134+
}
135+
136+
private static function getPrettyName(callable $callback): string
137+
{
138+
if (\is_string($callback)) {
139+
return $callback;
140+
}
141+
142+
if (\is_array($callback)) {
143+
$class = \is_object($callback[0]) ? \get_class($callback[0]) : $callback[0];
144+
$method = $callback[1];
145+
} elseif ($callback instanceof \Closure) {
146+
$r = new \ReflectionFunction($callback);
147+
148+
if (false !== strpos($r->name, '{closure}') || !$class = $r->getClosureScopeClass()) {
149+
return $r->name;
150+
}
151+
152+
$class = $class->name;
153+
$method = $r->name;
154+
} else {
155+
$class = \get_class($callback);
156+
$method = '__invoke';
157+
}
158+
159+
if (isset($class[15]) && "\0" === $class[15] && 0 === strpos($class, "class@anonymous\x00")) {
160+
$class = (get_parent_class($class) ?: key(class_implements($class))).'@anonymous';
161+
}
162+
163+
return $class.'::'.$method;
164+
}
165+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\String\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\ErrorHandler\ErrorHandler;
16+
use Symfony\Component\String\LazyString;
17+
18+
class LazyStringTest extends TestCase
19+
{
20+
public function testLazyString()
21+
{
22+
$count = 0;
23+
$s = LazyString::fromCallable(function () use (&$count) {
24+
return ++$count;
25+
});
26+
27+
$this->assertSame(0, $count);
28+
$this->assertSame('1', (string) $s);
29+
$this->assertSame(1, $count);
30+
}
31+
32+
public function testLazyCallable()
33+
{
34+
$count = 0;
35+
$s = LazyString::fromCallable([function () use (&$count) {
36+
return new class($count) {
37+
private $count;
38+
39+
public function __construct(int &$count)
40+
{
41+
$this->count = &$count;
42+
}
43+
44+
public function __invoke()
45+
{
46+
return ++$this->count;
47+
}
48+
};
49+
}]);
50+
51+
$this->assertSame(0, $count);
52+
$this->assertSame('1', (string) $s);
53+
$this->assertSame(1, $count);
54+
$this->assertSame('1', (string) $s); // ensure the value is memoized
55+
$this->assertSame(1, $count);
56+
}
57+
58+
/**
59+
* @runInSeparateProcess
60+
*/
61+
public function testReturnTypeError()
62+
{
63+
ErrorHandler::register();
64+
65+
$s = LazyString::fromCallable(function () { return []; });
66+
67+
$this->expectException(\TypeError::class);
68+
$this->expectExceptionMessage('Return value of '.__NAMESPACE__.'\{closure}() passed to '.LazyString::class.'::fromCallable() must be of the type string, array returned.');
69+
70+
(string) $s;
71+
}
72+
73+
public function testFromStringable()
74+
{
75+
$this->assertInstanceOf(LazyString::class, LazyString::fromStringable('abc'));
76+
$this->assertSame('abc', (string) LazyString::fromStringable('abc'));
77+
$this->assertSame('1', (string) LazyString::fromStringable(true));
78+
$this->assertSame('', (string) LazyString::fromStringable(false));
79+
$this->assertSame('123', (string) LazyString::fromStringable(123));
80+
$this->assertSame('123.456', (string) LazyString::fromStringable(123.456));
81+
$this->assertStringContainsString('hello', (string) LazyString::fromStringable(new \Exception('hello')));
82+
}
83+
84+
public function testResolve()
85+
{
86+
$this->assertSame('abc', LazyString::resolve('abc'));
87+
$this->assertSame('1', LazyString::resolve(true));
88+
$this->assertSame('', LazyString::resolve(false));
89+
$this->assertSame('123', LazyString::resolve(123));
90+
$this->assertSame('123.456', LazyString::resolve(123.456));
91+
$this->assertStringContainsString('hello', LazyString::resolve(new \Exception('hello')));
92+
}
93+
94+
public function testIsStringable()
95+
{
96+
$this->assertTrue(LazyString::isStringable('abc'));
97+
$this->assertTrue(LazyString::isStringable(true));
98+
$this->assertTrue(LazyString::isStringable(false));
99+
$this->assertTrue(LazyString::isStringable(123));
100+
$this->assertTrue(LazyString::isStringable(123.456));
101+
$this->assertTrue(LazyString::isStringable(new \Exception('hello')));
102+
}
103+
104+
public function testIsNotStringable()
105+
{
106+
$this->assertFalse(LazyString::isStringable(null));
107+
$this->assertFalse(LazyString::isStringable([]));
108+
$this->assertFalse(LazyString::isStringable(STDIN));
109+
$this->assertFalse(LazyString::isStringable(new \StdClass()));
110+
$this->assertFalse(LazyString::isStringable(@eval('return new class() {private function __toString() {}};')));
111+
}
112+
}

src/Symfony/Component/String/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"symfony/translation-contracts": "^1.1|^2"
2424
},
2525
"require-dev": {
26+
"symfony/error-handler": "^4.4|^5.0",
2627
"symfony/http-client": "^4.4|^5.0",
2728
"symfony/var-exporter": "^4.4|^5.0"
2829
},

0 commit comments

Comments
 (0)
0