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 594e7ae

Browse filesBrowse 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
Copy full SHA for 594e7ae

File tree

Expand file treeCollapse file tree

4 files changed

+186
-0
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+186
-0
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/CHANGELOG.md
+1Lines 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
-----
+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\DependencyInjection;
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
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+
}
+72Lines 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 <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\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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/composer.json
+1Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.