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 <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+
}
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 <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

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy