8000 feature #27697 [ProxyManagerBridge][DI] allow proxifying interfaces w… · symfony/symfony@a5709ee · GitHub
[go: up one dir, main page]

Skip to content

Commit a5709ee

Browse files
committed
feature #27697 [ProxyManagerBridge][DI] allow proxifying interfaces with "lazy: Some\ProxifiedInterface" (nicolas-grekas)
This PR was merged into the 4.2-dev branch. Discussion ---------- [ProxyManagerBridge][DI] allow proxifying interfaces with "lazy: Some\ProxifiedInterface" | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #20656 | License | MIT | Doc PR | - By adding `<tag name="proxy" interface="Some\ProxifiedInterface" />` to your service definitions, this PR allows generating interface-based proxies. This would allow two things: - generating lazy proxies for final classes - wrapping a service into such proxy to forbid using the methods that are on a specific implementation but not on some interface - the proxy acting as a safe guard. The generated proxies are always lazy, so that lazy=true/false makes no difference. As a shortcut, you can also use `lazy: Some\ProxifiedInterface` to do the same (yaml example this time.) Commits ------- 1d9f1d1 [ProxyManagerBridge][DI] allow proxifying interfaces with "lazy: Some\ProxifiedInterface"
2 parents f20eaf2 + 1d9f1d1 commit a5709ee

24 files changed

+484
-71
lines changed

.php_cs.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ return PhpCsFixer\Config::create()
1919
->in(__DIR__.'/src')
2020
->append(array(__FILE__))
2121
->exclude(array(
22+
'Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/Fixtures',
2223
// directories containing files with content that is autogenerated by `var_export`, which breaks CS in output code
2324
'Symfony/Component/Cache/Tests/Marshaller/Fixtures',
2425
'Symfony/Component/DependencyInjection/Tests/Fixtures',

src/Symfony/Bridge/ProxyManager/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
4.2.0
5+
-----
6+
7+
* allowed creating lazy-proxies from interfaces
8+
49
3.3.0
510
-----
611

src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/LazyLoadingValueHolderFactoryV2.php renamed to src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/LazyLoadingValueHolderFactory.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@
1818
/**
1919
* @internal
2020
*/
21-
class LazyLoadingValueHolderFactoryV2 extends BaseFactory
21+
class LazyLoadingValueHolderFactory extends BaseFactory
2222
{
2323
private $generator;
2424

2525
/**
2626
* {@inheritdoc}
2727
*/
28-
protected function getGenerator(): ProxyGeneratorInterface
28+
public function getGenerator(): ProxyGeneratorInterface
2929
{
3030
return $this->generator ?: $this->generator = new LazyLoadingValueHolderGenerator();
3131
}

src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/LazyLoadingValueHolderFactoryV1.php

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Symfony\Bridge\ProxyManager\LazyProxy\Instantiator;
1313

1414
use ProxyManager\Configuration;
15-
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
1615
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
1716
use ProxyManager\Proxy\LazyLoadingInterface;
1817
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -26,21 +25,14 @@
2625
*/
2726
class RuntimeInstantiator implements InstantiatorInterface
2827
{
29-
/**
30-
* @var LazyLoadingValueHolderFactory
31-
*/
3228
private $factory;
3329

3430
public function __construct()
3531
{
3632
$config = new Configuration();
3733
$config->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
3834

39-
if (method_exists('ProxyManager\Version', 'getVersion')) {
40-
$this->factory = new LazyLoadingValueHolderFactoryV2($config);
41-
} else {
42-
$this->factory = new LazyLoadingValueHolderFactoryV1($config);
43-
}
35+
$this->factory = new LazyLoadingValueHolderFactory($config);
4436
}
4537

4638
/**
@@ -49,9 +41,9 @@ public function __construct()
4941
public function instantiateProxy(ContainerInterface $container, Definition $definition, $id, $realInstantiator)
5042
{
5143
return $this->factory->createProxy(
52-
$definition->getClass(),
44+
$this->factory->getGenerator()->getProxifiedClass($definition),
5345
function (&$wrappedInstance, LazyLoadingInterface $proxy) use ($realInstantiator) {
54-
$wrappedInstance = call_user_func($realInstantiator);
46+
$wrappedInstance = \call_user_func($realInstantiator);
5547

5648
$proxy->setProxyInitializer(null);
5749

src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/LazyLoadingValueHolderGenerator.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,74 @@
1212
namespace Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper;
1313

1414
use ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator as BaseGenerator;
15+
use Symfony\Component\DependencyInjection\Definition;
1516
use Zend\Code\Generator\ClassGenerator;
1617

1718
/**
1819
* @internal
1920
*/
2021
class LazyLoadingValueHolderGenerator extends BaseGenerator
2122
{
23+
private $fluentSafe = false;
24+
25+
public function setFluentSafe(bool $fluentSafe)
26+
{
27+
$this->fluentSafe = $fluentSafe;
28+
}
29+
2230
/**
2331
* {@inheritdoc}
2432
*/
2533
public function generate(\ReflectionClass $originalClass, ClassGenerator $classGenerator)
2634
{
2735
parent::generate($originalClass, $classGenerator);
2836

37+
foreach ($classGenerator->getMethods() as $method) {
38+
$body = preg_replace(
39+
'/(\$this->initializer[0-9a-f]++) && \1->__invoke\(\$this->(valueHolder[0-9a-f]++), (.*?), \1\);/',
40+
'$1 && ($1->__invoke(\$$2, $3, $1) || 1) && $this->$2 = \$$2;',
41+
$method->getBody()
42+
);
43+
$body = str_replace('(new \ReflectionClass(get_class()))', '$reflection', $body);
44+
45+
if ($originalClass->isInterface()) {
46+
$body = str_replace('get_parent_class($this)', var_export($originalClass->name, true), $body);
47+
$body = preg_replace_callback('/\n\n\$realInstanceReflection = [^{]++\{([^}]++)\}\n\n.*/s', function ($m) {
48+
$r = '';
49+
foreach (explode("\n", $m[1]) as $line) {
50+
$r .= "\n".substr($line, 4);
51+
if (0 === strpos($line, ' return ')) {
52+
break;
53+
}
54+
}
55+
56+
return $r;
57+
}, $body);
58+
}
59+
60+
if ($this->fluentSafe) {
61+
$indent = $method->getIndentation();
62+
$method->setIndentation('');
63+
$code = $method->generate();
64+
if (null !== $docBlock = $method->getDocBlock()) {
65+
$code = substr($code, \strlen($docBlock->generate()));
66+
}
67+
$refAmp = (strpos($code, '&') ?: \PHP_INT_MAX) <= strpos($code, '(') ? '&' : '';
68+
$body = preg_replace(
69+
'/\nreturn (\$this->valueHolder[0-9a-f]++)(->[^;]++);$/',
70+
"\nif ($1 === \$returnValue = {$refAmp}$1$2) {\n \$returnValue = \$this;\n}\n\nreturn \$returnValue;",
71+
$body
72+
);
73+
$method->setIndentation($indent);
74+
}
75+
76+
if (0 === strpos($originalClass->getFilename(), __FILE__)) {
77+
$body = str_replace(var_export($originalClass->name, true), '__CLASS__', $body);
78+
}
79+
80+
$method->setBody($body);
81+
}
82+
2983
if ($classGenerator->hasMethod('__destruct')) {
3084
$destructor = $classGenerator->getMethod('__destruct');
3185
$body = $destructor->getBody();
@@ -37,5 +91,53 @@ public function generate(\ReflectionClass $originalClass, ClassGenerator $classG
3791

3892
$destructor->setBody($newBody);
3993
}
94+
95+
if (0 === strpos($originalClass->getFilename(), __FILE__)) {
96+
$interfaces = $classGenerator->getImplementedInterfaces();
97+
array_pop($interfaces);
98+
$classGenerator->setImplementedInterfaces(array_merge($interfaces, $originalClass->getInterfaceNames()));
99+
}
100+
}
101+
102+
public function getProxifiedClass(Definition $definition): ?string
103+
{
104+
if (!$definition->hasTag('proxy')) {
105+
return \class_exists($class = $definition->getClass()) || \interface_exists($class) ? $class : null;
106+
}
107+
if (!$definition->isLazy()) {
108+
throw new \InvalidArgumentException(sprintf('Invalid definition for service of class "%s": setting the "proxy" tag on a service requires it to be "lazy".', $definition->getClass()));
109+
}
110+
$tags = $definition->getTag('proxy');
111+
if (!isset($tags[0]['interface'])) {
112+
throw new \InvalidArgumentException(sprintf('Invalid definition for service of class "%s": the "interface" attribute is missing on the "proxy" tag.', $definition->getClass()));
113+
}
114+
if (1 === \count($tags)) {
115+
return \class_exists($tags[0]['interface']) || \interface_exists($tags[0]['interface']) ? $tags[0]['interface'] : null;
116+
}
117+
118+
$proxyInterface = 'LazyProxy';
119+
$interfaces = '';
120+
foreach ($tags as $tag) {
121+
if (!isset($tag['interface'])) {
122+
throw new \InvalidArgumentException(sprintf('Invalid definition for service of class "%s": the "interface" attribute is missing on a "proxy" tag.', $definition->getClass()));
123+
}
124+
if (!\interface_exists($tag['interface'])) {
125+
throw new \InvalidArgumentException(sprintf('Invalid definition for service of class "%s": several "proxy" tags found but "%s" is not an interface.', $definition->getClass(), $tag['interface']));
126+
}
127+
128+
$proxyInterface .= '\\'.$tag['interface'];
129+
$interfaces .= ', \\'.$tag['interface'];
130+
}
131+
132+
if (!\interface_exists($proxyInterface)) {
133+
$i = strrpos($proxyInterface, '\\');
134+
$namespace = substr($proxyInterface, 0, $i);
135+
$interface = substr($proxyInterface, 1 + $i);
136+
$interfaces = substr($interfaces, 2);
137+
138+
eval("namespace {$namespace}; interface {$interface} extends {$interfaces} {}");
139+
}
140+
141+
return $proxyInterface;
40142
}
41143
}

src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php

Lines changed: 11 additions & 16 deletions
10000
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
use ProxyManager\Generator\ClassGenerator;
1515
use ProxyManager\GeneratorStrategy\BaseGeneratorStrategy;
1616
use ProxyManager\Version;
17-
use Symfony\Component\DependencyInjection\ContainerBuilder;
1817
use Symfony\Component\DependencyInjection\Definition;
1918
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface;
2019

@@ -43,7 +42,7 @@ public function __construct(string $salt = '')
4342
*/
4443
public function isProxyCandidate(Definition $definition)
4544
{
46-
return $definition->isLazy() && ($class = $definition->getClass()) && class_exists($class);
45+
return ($definition->isLazy() || $definition->hasTag('proxy')) && $this->proxyGenerator->getProxifiedClass($definition);
4746
}
4847

4948
/**
@@ -63,14 +62,10 @@ public function getProxyFactoryCode(Definition $definition, $id, $factoryCode =
6362

6463
$proxyClass = $this->getProxyClassName($definition);
6564

66-
$hasStaticConstructor = $this->generateProxyClass($definition)->hasMethod('staticProxyConstructor');
67-
68-
$constructorCall = sprintf($hasStaticConstructor ? '%s::staticProxyConstructor' : 'new %s', '\\'.$proxyClass);
69-
7065
return <<<EOF
7166
if (\$lazyLoad) {
7267
$instantiation \$this->createProxy('$proxyClass', function () {
73-
return $constructorCall(function (&\$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface \$proxy) {
68+
return \\$proxyClass::staticProxyConstructor(function (&\$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface \$proxy) {
7469
\$wrappedInstance = $factoryCode;
7570
7671
\$proxy->setProxyInitializer(null);
@@ -91,12 +86,6 @@ public function getProxyCode(Definition $definition)
9186
{
9287
$code = $this->classGenerator->generate($this->generateProxyClass($definition));
9388

94-
$code = preg_replace(
95-
'/(\$this->initializer[0-9a-f]++) && \1->__invoke\(\$this->(valueHolder[0-9a-f]++), (.*?), \1\);/',
96-
'$1 && ($1->__invoke(\$$2, $3, $1) || 1) && $this->$2 = \$$2;',
97-
$code
98-
);
99-
10089
if (version_compare(self::getProxyManagerVersion(), '2.2', '<')) {
10190
$code = preg_replace(
10291
'/((?:\$(?:this|initializer|instance)->)?(?:publicProperties|initializer|valueHolder))[0-9a-f]++/',
@@ -122,20 +111,26 @@ private static function getProxyManagerVersion(): string
122111
*/
123112
private function getProxyClassName(Definition $definition): string
124113
{
125-
return preg_replace('/^.*\\\\/', '', $definition->getClass()).'_'.$this->getIdentifierSuffix($definition);
114+
$class = $this->proxyGenerator->getProxifiedClass($definition);
115+
116+
return preg_replace('/^.*\\\\/', '', $class).'_'.$this->getIdentifierSuffix($definition);
126117
}
127118

128119
private function generateProxyClass(Definition $definition): ClassGenerator
129120
{
130121
$generatedClass = new ClassGenerator($this->getProxyClassName($definition));
122+
$class = $this->proxyGenerator->getProxifiedClass($definition);
131123

132-
$this->proxyGenerator->generate(new \ReflectionClass($definition->getClass()), $generatedClass);
124+
$this->proxyGenerator->setFluentSafe($definition->hasTag('proxy'));
125+
$this->proxyGenerator->generate(new \ReflectionClass($class), $generatedClass);
133126

134127
return $generatedClass;
135128
}
136129

137130
private function getIdentifierSuffix(Definition $definition): string
138131
{
139-
return substr(hash('sha256', $definition->getClass().$this->salt), -7);
132+
$class = $this->proxyGenerator->getProxifiedClass($definition);
133+
134+
return substr(hash('sha256', $class.$this->salt), -7);
140135
}
141136
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
return new class
4+
{
5+
public $proxyClass;
6+
private $privates = array();
7+
8+
public function getFooService($lazyLoad = true)
9+
{
10+
if ($lazyLoad) {
11+
return $this->privates['foo'] = $this->createProxy('SunnyInterface_1eff735', function () {
12+
return \SunnyInterface_1eff735::staticProxyConstructor(function (&$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface $proxy) {
13+
$wrappedInstance = $this->getFooService(false);
14+
15+
$proxy->setProxyInitializer(null);
16+
17+
return true;
18+
});
19+
});
20+
}
21+
22+
return new Symfony\Bridge\ProxyManager\Tests\LazyProxy\PhpDumper\DummyClass();
23+
}
24+
25+
protected function createProxy($class, \Closure $factory)
26+
{
27+
$this->proxyClass = $class;
28+
29+
return $factory();
30+
}
31+
};

0 commit comments

Comments
 (0)
0