Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 594e7ae

Browse files
committed
feature #34013 [DI] add LazyString for lazy computation of string values injected into services (nicolas-grekas)
This PR was merged into the 4.4 branch. Discussion ---------- [DI] add `LazyString` for lazy computation of string values injected into services | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - This is an idea I should have had years ago :) By wrapping any callable into a `LazyString`, we allow resolving the corresponding string value lazily (eg because the value comes from a remote server). The tricky parts are memoization and error handling, which are both dealt with in the class. This is currently part of #33997 Commits ------- ccb0365 [DI] add `LazyString` for lazy computation of string values injected into services
2 parents f771faf + ccb0365 commit 594e7ae

File tree

4 files changed

+186
-0
lines changed

4 files changed

+186
-0
lines changed

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
* made singly-implemented interfaces detection be scoped by file
1414
* added ability to define a static priority method for tagged service
1515
* added support for improved syntax to define method calls in Yaml
16+
* added `LazyString` for lazy computation of string values injected into services
1617

1718
4.3.0
1819
-----
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 <[email protected]>
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\DependencyInjection;
13+
14+
/**
15+
* A string whose value is computed lazily by a callback.
16+
*
17+
* @author Nicolas Grekas <[email protected]>
18+
*/
19+
class LazyString
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+
public function __toString()
53+
{
54+
if (\is_string($this->value)) {
55+
return $this->value;
56+
}
57+
58+
try {
59+
return $this->value = ($this->value)();
60+
} catch (\Throwable $e) {
61+
if (\TypeError::class === \get_class($e) && __FILE__ === $e->getFile()) {
62+
$type = explode(', ', $e->getMessage());
63+
$type = substr(array_pop($type), 0, -\strlen(' returned'));
64+
$r = new \ReflectionFunction($this->value);
65+
$callback = $r->getStaticVariables()['callback'];
66+
67+
$e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type));
68+
}
69+
70+
if (\PHP_VERSION_ID < 70400) {
71+
// leverage the ErrorHandler component with graceful fallback when it's not available
72+
return trigger_error($e, E_USER_ERROR);
73+
}
74+
75+
throw $e;
76+
}
77+
}
78+
79+
private function __construct()
80+
{
81+
}
82+
83+
private static function getPrettyName(callable $callback): string
84+
{
85+
if (\is_string($callback)) {
86+
return $callback;
87+
}
88+
89+
if (\is_array($callback)) {
90+
$class = \is_object($callback[0]) ? \get_class($callback[0]) : $callback[0];
91+
$method = $callback[1];
92+
} elseif ($callback instanceof \Closure) {
93+
$r = new \ReflectionFunction($callback);
94+
95+
if (false !== strpos($r->name, '{closure}') || !$class = $r->getClosureScopeClass()) {
96+
return $r->name;
97+
}
98+
99+
$class = $class->name;
100+
$method = $r->name;
101+
} else {
102+
$class = \get_class($callback);
103+
$method = '__invoke';
104+
}
105+
106+
if (isset($class[15]) && "\0" === $class[15] && 0 === strpos($class, "class@anonymous\x00")) {
107+
$class = get_parent_class($class).'@anonymous';
108+
}
109+
110+
return $class.'::'.$method;
111+
}
112+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\DependencyInjection\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\LazyString;
16+
use Symfony\Component\ErrorHandler\ErrorHandler;
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+
}

src/Symfony/Component/DependencyInjection/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"require-dev": {
2424
"symfony/yaml": "^3.4|^4.0|^5.0",
2525
"symfony/config": "^4.3|^5.0",
26+
"symfony/error-handler": "^4.4|^5.0",
2627
"symfony/expression-language": "^3.4|^4.0|^5.0"
2728
},
2829
"suggest": {

0 commit comments

Comments
 (0)