Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3fa9d12

Browse filesBrowse files
[String] add LazyString to provide generic stringable objects
1 parent 07818f2 commit 3fa9d12
Copy full SHA for 3fa9d12

File tree

6 files changed

+308
-2
lines changed
Filter options

6 files changed

+308
-2
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+8-1Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
111111
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
112112
use Symfony\Component\Stopwatch\Stopwatch;
113+
use Symfony\Component\String\LazyString;
113114
use Symfony\Component\String\Slugger\SluggerInterface;
114115
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
115116
use Symfony\Component\Translation\Translator;
@@ -1374,9 +1375,15 @@ private function registerSecretsConfiguration(array $config, ContainerBuilder $c
13741375
throw new InvalidArgumentException(sprintf('Invalid value "%s" set as "decryption_env_var": only "word" characters are allowed.', $config['decryption_env_var']));
13751376
}
13761377

1377-
$container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%");
1378+
if (class_exists(LazyString::class)) {
1379+
$container->getDefinition('secrets.decryption_key')->replaceArgument(1, $config['decryption_env_var']);
1380+
} else {
1381+
$container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%");
1382+
$container->removeDefinition('secrets.decryption_key');
1383+
}
13781384
} else {
13791385
$container->getDefinition('secrets.vault')->replaceArgument(1, null);
1386+
$container->removeDefinition('secrets.decryption_key');
13801387
}
13811388
}
13821389

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml
+18Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@
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+
<!--
15+
LazyString::fromCallable() is used as a wrapper to lazily read the SYMFONY_DECRYPTION_SECRET var from the env.
16+
By overriding this service and using the same strategy, the decryption key can be fetched lazily from any other service if needed.
17+
-->
18+
<service id="secrets.decryption_key" class="Symfony\Component\String\LazyString">
19+
<factory class="Symfony\Component\String\LazyString" method="fromCallable" />
20+
<argument type="service">
21+
<service class="Closure">
22+
<factory class="Closure" method="fromCallable" />
23+
<argument type="collection">
24+
<argument type="service" id="service_container" />
25+
<argument>getEnv</argument>
26+
</argument>
27+
</service>
28+
</argument>
1129
<argument />
1230
</service>
1331

‎src/Symfony/Component/String/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/String/CHANGELOG.md
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ CHANGELOG
44
5.1.0
55
-----
66

7-
* Added the `AbstractString::reverse()` method.
7+
* added the `AbstractString::reverse()` method.
8+
* added `LazyString` which provides memoizing stringable objects
89

910
5.0.0
1011
-----
+165Lines 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 $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(): string
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+
}
+112Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/String/composer.json
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
"symfony/polyfill-mbstring": "~1.0",
2323
"symfony/translation-contracts": "^1.1|^2"
2424
},
25+
"require-dev": {
26+
"symfony/error-handler": "^4.4|^5.0"
27+
},
2528
"autoload": {
2629
"psr-4": { "Symfony\\Component\\String\\": "" },
2730
"files": [ "Resources/functions.php" ],

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.