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

Skip to content

Commit ea7db68

Browse files
[DI] add LazyString for lazy computation of string values injected into services
1 parent cbc4efc commit ea7db68

File tree

5 files changed

+196
-6
lines changed

5 files changed

+196
-6
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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
}
55+
56+
/**
57+
* @runInSeparateProcess
58+
*/
59+
public function testReturnTypeError()
60+
{
61+
ErrorHandler::register();
62+
63+
$s = LazyString::fromCallable(function () { return []; });
64+
65+
$this->expectException(\TypeError::class);
66+
$this->expectExceptionMessage('Return value of '.__NAMESPACE__.'\{closure}() passed to '.LazyString::class.'::fromCallable() must be of the type string, array returned.');
67+
68+
(string) $s;
69+
}
70+
}

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": {

src/Symfony/Component/EventDispatcher/EventDispatcher.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,16 @@ public function getListenerPriority($eventName, $listener)
111111
return null;
112112
}
113113

114-
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure) {
114+
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
115115
$listener[0] = $listener[0]();
116+
$listener[1] = $listener[1] ?? '__invoke';
116117
}
117118

118119
foreach ($this->listeners[$eventName] as $priority => &$listeners) {
119120
foreach ($listeners as &$v) {
120-
if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure) {
121+
if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) {
121122
$v[0] = $v[0]();
123+
$v[1] = $v[1] ?? '__invoke';
122124
}
123125
if ($v === $listener) {
124126
return $priority;
@@ -165,14 +167,16 @@ public function removeListener($eventName, $listener)
165167
return;
166168
}
167169

168-
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure) {
170+
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
169171
$listener[0] = $listener[0]();
172+
$listener[1] = $listener[1] ?? '__invoke';
170173
}
171174

172175
foreach ($this->listeners[$eventName] as $priority => &$listeners) {
173176
foreach ($listeners as $k => &$v) {
174-
if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure) {
177+
if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) {
175178
$v[0] = $v[0]();
179+
$v[1] = $v[1] ?? '__invoke';
176180
}
177181
if ($v === $listener) {
178182
unset($listeners[$k], $this->sorted[$eventName], $this->optimized[$eventName]);
@@ -271,8 +275,9 @@ private function sortListeners(string $eventName)
271275

272276
foreach ($this->listeners[$eventName] as &$listeners) {
273277
foreach ($listeners as $k => $listener) {
274-
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure) {
278+
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
275279
$listener[0] = $listener[0]();
280+
$listener[1] = $listener[1] ?? '__invoke';
276281
}
277282
$this->sorted[$eventName][] = $listener;
278283
}
@@ -290,10 +295,11 @@ private function optimizeListeners(string $eventName): array
290295
foreach ($this->listeners[$eventName] as &$listeners) {
291296
foreach ($listeners as &$listener) {
292297
$closure = &$this->optimized[$eventName][];
293-
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure) {
298+
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
294299
$closure = static function (...$args) use (&$listener, &$closure) {
295300
if ($listener[0] instanceof \Closure) {
296301
$listener[0] = $listener[0]();
302+
$listener[1] = $listener[1] ?? '__invoke';
297303
}
298304
($closure = \Closure::fromCallable($listener))(...$args);
299305
};

0 commit comments

Comments
 (0)