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

1d9f1d1b70 [ProxyManagerBridge][DI] allow proxifying interfaces with "lazy: Some\ProxifiedInterface"
This commit is contained in:
Fabien Potencier 2018-07-09 16:47:21 +02:00
commit a5709ee9ba
24 changed files with 484 additions and 71 deletions

View File

@ -19,6 +19,7 @@ return PhpCsFixer\Config::create()
->in(__DIR__.'/src') ->in(__DIR__.'/src')
->append(array(__FILE__)) ->append(array(__FILE__))
->exclude(array( ->exclude(array(
'Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/Fixtures',
// directories containing files with content that is autogenerated by `var_export`, which breaks CS in output code // directories containing files with content that is autogenerated by `var_export`, which breaks CS in output code
'Symfony/Component/Cache/Tests/Marshaller/Fixtures', 'Symfony/Component/Cache/Tests/Marshaller/Fixtures',
'Symfony/Component/DependencyInjection/Tests/Fixtures', 'Symfony/Component/DependencyInjection/Tests/Fixtures',

View File

@ -1,6 +1,11 @@
CHANGELOG CHANGELOG
========= =========
4.2.0
-----
* allowed creating lazy-proxies from interfaces
3.3.0 3.3.0
----- -----

View File

@ -18,14 +18,14 @@ use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\LazyLoadingValueHolderGenera
/** /**
* @internal * @internal
*/ */
class LazyLoadingValueHolderFactoryV2 extends BaseFactory class LazyLoadingValueHolderFactory extends BaseFactory
{ {
private $generator; private $generator;
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected function getGenerator(): ProxyGeneratorInterface public function getGenerator(): ProxyGeneratorInterface
{ {
return $this->generator ?: $this->generator = new LazyLoadingValueHolderGenerator(); return $this->generator ?: $this->generator = new LazyLoadingValueHolderGenerator();
} }

View File

@ -1,31 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\ProxyManager\LazyProxy\Instantiator;
use ProxyManager\Factory\LazyLoadingValueHolderFactory as BaseFactory;
use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\LazyLoadingValueHolderGenerator;
/**
* @internal
*/
class LazyLoadingValueHolderFactoryV1 extends BaseFactory
{
private $generatorV1;
/**
* {@inheritdoc}
*/
protected function getGenerator()
{
return $this->generatorV1 ?: $this->generatorV1 = new LazyLoadingValueHolderGenerator();
}
}

View File

@ -12,7 +12,6 @@
namespace Symfony\Bridge\ProxyManager\LazyProxy\Instantiator; namespace Symfony\Bridge\ProxyManager\LazyProxy\Instantiator;
use ProxyManager\Configuration; use ProxyManager\Configuration;
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy; use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
use ProxyManager\Proxy\LazyLoadingInterface; use ProxyManager\Proxy\LazyLoadingInterface;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
@ -26,9 +25,6 @@ use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInt
*/ */
class RuntimeInstantiator implements InstantiatorInterface class RuntimeInstantiator implements InstantiatorInterface
{ {
/**
* @var LazyLoadingValueHolderFactory
*/
private $factory; private $factory;
public function __construct() public function __construct()
@ -36,11 +32,7 @@ class RuntimeInstantiator implements InstantiatorInterface
$config = new Configuration(); $config = new Configuration();
$config->setGeneratorStrategy(new EvaluatingGeneratorStrategy()); $config->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
if (method_exists('ProxyManager\Version', 'getVersion')) { $this->factory = new LazyLoadingValueHolderFactory($config);
$this->factory = new LazyLoadingValueHolderFactoryV2($config);
} else {
$this->factory = new LazyLoadingValueHolderFactoryV1($config);
}
} }
/** /**
@ -49,9 +41,9 @@ class RuntimeInstantiator implements InstantiatorInterface
public function instantiateProxy(ContainerInterface $container, Definition $definition, $id, $realInstantiator) public function instantiateProxy(ContainerInterface $container, Definition $definition, $id, $realInstantiator)
{ {
return $this->factory->createProxy( return $this->factory->createProxy(
$definition->getClass(), $this->factory->getGenerator()->getProxifiedClass($definition),
function (&$wrappedInstance, LazyLoadingInterface $proxy) use ($realInstantiator) { function (&$wrappedInstance, LazyLoadingInterface $proxy) use ($realInstantiator) {
$wrappedInstance = call_user_func($realInstantiator); $wrappedInstance = \call_user_func($realInstantiator);
$proxy->setProxyInitializer(null); $proxy->setProxyInitializer(null);

View File

@ -12,6 +12,7 @@
namespace Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper; namespace Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper;
use ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator as BaseGenerator; use ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator as BaseGenerator;
use Symfony\Component\DependencyInjection\Definition;
use Zend\Code\Generator\ClassGenerator; use Zend\Code\Generator\ClassGenerator;
/** /**
@ -19,6 +20,13 @@ use Zend\Code\Generator\ClassGenerator;
*/ */
class LazyLoadingValueHolderGenerator extends BaseGenerator class LazyLoadingValueHolderGenerator extends BaseGenerator
{ {
private $fluentSafe = false;
public function setFluentSafe(bool $fluentSafe)
{
$this->fluentSafe = $fluentSafe;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -26,6 +34,52 @@ class LazyLoadingValueHolderGenerator extends BaseGenerator
{ {
parent::generate($originalClass, $classGenerator); parent::generate($originalClass, $classGenerator);
foreach ($classGenerator->getMethods() as $method) {
$body = preg_replace(
'/(\$this->initializer[0-9a-f]++) && \1->__invoke\(\$this->(valueHolder[0-9a-f]++), (.*?), \1\);/',
'$1 && ($1->__invoke(\$$2, $3, $1) || 1) && $this->$2 = \$$2;',
$method->getBody()
);
$body = str_replace('(new \ReflectionClass(get_class()))', '$reflection', $body);
if ($originalClass->isInterface()) {
$body = str_replace('get_parent_class($this)', var_export($originalClass->name, true), $body);
$body = preg_replace_callback('/\n\n\$realInstanceReflection = [^{]++\{([^}]++)\}\n\n.*/s', function ($m) {
$r = '';
foreach (explode("\n", $m[1]) as $line) {
$r .= "\n".substr($line, 4);
if (0 === strpos($line, ' return ')) {
break;
}
}
return $r;
}, $body);
}
if ($this->fluentSafe) {
$indent = $method->getIndentation();
$method->setIndentation('');
$code = $method->generate();
if (null !== $docBlock = $method->getDocBlock()) {
$code = substr($code, \strlen($docBlock->generate()));
}
$refAmp = (strpos($code, '&') ?: \PHP_INT_MAX) <= strpos($code, '(') ? '&' : '';
$body = preg_replace(
'/\nreturn (\$this->valueHolder[0-9a-f]++)(->[^;]++);$/',
"\nif ($1 === \$returnValue = {$refAmp}$1$2) {\n \$returnValue = \$this;\n}\n\nreturn \$returnValue;",
$body
);
$method->setIndentation($indent);
}
if (0 === strpos($originalClass->getFilename(), __FILE__)) {
$body = str_replace(var_export($originalClass->name, true), '__CLASS__', $body);
}
$method->setBody($body);
}
if ($classGenerator->hasMethod('__destruct')) { if ($classGenerator->hasMethod('__destruct')) {
$destructor = $classGenerator->getMethod('__destruct'); $destructor = $classGenerator->getMethod('__destruct');
$body = $destructor->getBody(); $body = $destructor->getBody();
@ -37,5 +91,53 @@ class LazyLoadingValueHolderGenerator extends BaseGenerator
$destructor->setBody($newBody); $destructor->setBody($newBody);
} }
if (0 === strpos($originalClass->getFilename(), __FILE__)) {
$interfaces = $classGenerator->getImplementedInterfaces();
array_pop($interfaces);
$classGenerator->setImplementedInterfaces(array_merge($interfaces, $originalClass->getInterfaceNames()));
}
}
public function getProxifiedClass(Definition $definition): ?string
{
if (!$definition->hasTag('proxy')) {
return \class_exists($class = $definition->getClass()) || \interface_exists($class) ? $class : null;
}
if (!$definition->isLazy()) {
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()));
}
$tags = $definition->getTag('proxy');
if (!isset($tags[0]['interface'])) {
throw new \InvalidArgumentException(sprintf('Invalid definition for service of class "%s": the "interface" attribute is missing on the "proxy" tag.', $definition->getClass()));
}
if (1 === \count($tags)) {
return \class_exists($tags[0]['interface']) || \interface_exists($tags[0]['interface']) ? $tags[0]['interface'] : null;
}
$proxyInterface = 'LazyProxy';
$interfaces = '';
foreach ($tags as $tag) {
if (!isset($tag['interface'])) {
throw new \InvalidArgumentException(sprintf('Invalid definition for service of class "%s": the "interface" attribute is missing on a "proxy" tag.', $definition->getClass()));
}
if (!\interface_exists($tag['interface'])) {
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']));
}
$proxyInterface .= '\\'.$tag['interface'];
$interfaces .= ', \\'.$tag['interface'];
}
if (!\interface_exists($proxyInterface)) {
$i = strrpos($proxyInterface, '\\');
$namespace = substr($proxyInterface, 0, $i);
$interface = substr($proxyInterface, 1 + $i);
$interfaces = substr($interfaces, 2);
eval("namespace {$namespace}; interface {$interface} extends {$interfaces} {}");
}
return $proxyInterface;
} }
} }

View File

@ -14,7 +14,6 @@ namespace Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper;
use ProxyManager\Generator\ClassGenerator; use ProxyManager\Generator\ClassGenerator;
use ProxyManager\GeneratorStrategy\BaseGeneratorStrategy; use ProxyManager\GeneratorStrategy\BaseGeneratorStrategy;
use ProxyManager\Version; use ProxyManager\Version;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface; use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface;
@ -43,7 +42,7 @@ class ProxyDumper implements DumperInterface
*/ */
public function isProxyCandidate(Definition $definition) public function isProxyCandidate(Definition $definition)
{ {
return $definition->isLazy() && ($class = $definition->getClass()) && class_exists($class); return ($definition->isLazy() || $definition->hasTag('proxy')) && $this->proxyGenerator->getProxifiedClass($definition);
} }
/** /**
@ -63,14 +62,10 @@ class ProxyDumper implements DumperInterface
$proxyClass = $this->getProxyClassName($definition); $proxyClass = $this->getProxyClassName($definition);
$hasStaticConstructor = $this->generateProxyClass($definition)->hasMethod('staticProxyConstructor');
$constructorCall = sprintf($hasStaticConstructor ? '%s::staticProxyConstructor' : 'new %s', '\\'.$proxyClass);
return <<<EOF return <<<EOF
if (\$lazyLoad) { if (\$lazyLoad) {
$instantiation \$this->createProxy('$proxyClass', function () { $instantiation \$this->createProxy('$proxyClass', function () {
return $constructorCall(function (&\$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface \$proxy) { return \\$proxyClass::staticProxyConstructor(function (&\$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface \$proxy) {
\$wrappedInstance = $factoryCode; \$wrappedInstance = $factoryCode;
\$proxy->setProxyInitializer(null); \$proxy->setProxyInitializer(null);
@ -91,12 +86,6 @@ EOF;
{ {
$code = $this->classGenerator->generate($this->generateProxyClass($definition)); $code = $this->classGenerator->generate($this->generateProxyClass($definition));
$code = preg_replace(
'/(\$this->initializer[0-9a-f]++) && \1->__invoke\(\$this->(valueHolder[0-9a-f]++), (.*?), \1\);/',
'$1 && ($1->__invoke(\$$2, $3, $1) || 1) && $this->$2 = \$$2;',
$code
);
if (version_compare(self::getProxyManagerVersion(), '2.2', '<')) { if (version_compare(self::getProxyManagerVersion(), '2.2', '<')) {
$code = preg_replace( $code = preg_replace(
'/((?:\$(?:this|initializer|instance)->)?(?:publicProperties|initializer|valueHolder))[0-9a-f]++/', '/((?:\$(?:this|initializer|instance)->)?(?:publicProperties|initializer|valueHolder))[0-9a-f]++/',
@ -122,20 +111,26 @@ EOF;
*/ */
private function getProxyClassName(Definition $definition): string private function getProxyClassName(Definition $definition): string
{ {
return preg_replace('/^.*\\\\/', '', $definition->getClass()).'_'.$this->getIdentifierSuffix($definition); $class = $this->proxyGenerator->getProxifiedClass($definition);
return preg_replace('/^.*\\\\/', '', $class).'_'.$this->getIdentifierSuffix($definition);
} }
private function generateProxyClass(Definition $definition): ClassGenerator private function generateProxyClass(Definition $definition): ClassGenerator
{ {
$generatedClass = new ClassGenerator($this->getProxyClassName($definition)); $generatedClass = new ClassGenerator($this->getProxyClassName($definition));
$class = $this->proxyGenerator->getProxifiedClass($definition);
$this->proxyGenerator->generate(new \ReflectionClass($definition->getClass()), $generatedClass); $this->proxyGenerator->setFluentSafe($definition->hasTag('proxy'));
$this->proxyGenerator->generate(new \ReflectionClass($class), $generatedClass);
return $generatedClass; return $generatedClass;
} }
private function getIdentifierSuffix(Definition $definition): string private function getIdentifierSuffix(Definition $definition): string
{ {
return substr(hash('sha256', $definition->getClass().$this->salt), -7); $class = $this->proxyGenerator->getProxifiedClass($definition);
return substr(hash('sha256', $class.$this->salt), -7);
} }
} }

View File

@ -0,0 +1,31 @@
<?php
return new class
{
public $proxyClass;
private $privates = array();
public function getFooService($lazyLoad = true)
{
if ($lazyLoad) {
return $this->privates['foo'] = $this->createProxy('SunnyInterface_1eff735', function () {
return \SunnyInterface_1eff735::staticProxyConstructor(function (&$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface $proxy) {
$wrappedInstance = $this->getFooService(false);
$proxy->setProxyInitializer(null);
return true;
});
});
}
return new Symfony\Bridge\ProxyManager\Tests\LazyProxy\PhpDumper\DummyClass();
}
protected function createProxy($class, \Closure $factory)
{
$this->proxyClass = $class;
return $factory();
}
};

View File

@ -0,0 +1,165 @@
<?php
class SunnyInterface_1eff735 implements \ProxyManager\Proxy\VirtualProxyInterface, \Symfony\Bridge\ProxyManager\Tests\LazyProxy\PhpDumper\DummyInterface, \Symfony\Bridge\ProxyManager\Tests\LazyProxy\PhpDumper\SunnyInterface
{
private $valueHolder1eff735 = null;
private $initializer1eff735 = null;
private static $publicProperties1eff735 = [
];
public function dummy()
{
$this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, 'dummy', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735;
if ($this->valueHolder1eff735 === $returnValue = $this->valueHolder1eff735->dummy()) {
$returnValue = $this;
}
return $returnValue;
}
public function & dummyRef()
{
$this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, 'dummyRef', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735;
if ($this->valueHolder1eff735 === $returnValue = &$this->valueHolder1eff735->dummyRef()) {
$returnValue = $this;
}
return $returnValue;
}
public function sunny()
{
$this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, 'sunny', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735;
if ($this->valueHolder1eff735 === $returnValue = $this->valueHolder1eff735->sunny()) {
$returnValue = $this;
}
return $returnValue;
}
public static function staticProxyConstructor($initializer)
{
static $reflection;
$reflection = $reflection ?: $reflection = new \ReflectionClass(__CLASS__);
$instance = $reflection->newInstanceWithoutConstructor();
$instance->initializer1eff735 = $initializer;
return $instance;
}
public function __construct()
{
static $reflection;
if (! $this->valueHolder1eff735) {
$reflection = $reflection ?: new \ReflectionClass(__CLASS__);
$this->valueHolder1eff735 = $reflection->newInstanceWithoutConstructor();
}
}
public function & __get($name)
{
$this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__get', ['name' => $name], $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735;
if (isset(self::$publicProperties1eff735[$name])) {
return $this->valueHolder1eff735->$name;
}
$targetObject = $this->valueHolder1eff735;
$backtrace = debug_backtrace(false);
trigger_error(
sprintf(
'Undefined property: %s::$%s in %s on line %s',
__CLASS__,
$name,
$backtrace[0]['file'],
$backtrace[0]['line']
),
\E_USER_NOTICE
);
return $targetObject->$name;
}
public function __set($name, $value)
{
$this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__set', array('name' => $name, 'value' => $value), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735;
$targetObject = $this->valueHolder1eff735;
return $targetObject->$name = $value;
}
public function __isset($name)
{
$this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__isset', array('name' => $name), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735;
$targetObject = $this->valueHolder1eff735;
return isset($targetObject->$name);
}
public function __unset($name)
{
$this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__unset', array('name' => $name), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735;
$targetObject = $this->valueHolder1eff735;
unset($targetObject->$name);
return;
}
public function __clone()
{
$this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__clone', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735;
$this->valueHolder1eff735 = clone $this->valueHolder1eff735;
}
public function __sleep()
{
$this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__sleep', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735;
return array('valueHolder1eff735');
}
public function __wakeup()
{
}
public function setProxyInitializer(\Closure $initializer = null)
{
$this->initializer1eff735 = $initializer;
}
public function getProxyInitializer()
{
return $this->initializer1eff735;
}
public function initializeProxy() : bool
{
return $this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, 'initializeProxy', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735;
}
public function isProxyInitialized() : bool
{
return null !== $this->valueHolder1eff735;
}
public function getWrappedValueHolderValue()
{
return $this->valueHolder1eff735;
}
}

View File

@ -120,6 +120,61 @@ class ProxyDumperTest extends TestCase
$this->dumper->getProxyFactoryCode($definition, 'foo'); $this->dumper->getProxyFactoryCode($definition, 'foo');
} }
public function testGetProxyFactoryCodeForInterface()
{
$class = DummyClass::class;
$definition = new Definition($class);
$definition->setLazy(true);
$definition->addTag('proxy', array('interface' => DummyInterface::class));
$definition->addTag('proxy', array('interface' => SunnyInterface::class));
$implem = "<?php\n\n".$this->dumper->getProxyCode($definition);
$factory = $this->dumper->getProxyFactoryCode($definition, 'foo', '$this->getFooService(false)');
$factory = <<<EOPHP
<?php
return new class
{
public \$proxyClass;
private \$privates = array();
public function getFooService(\$lazyLoad = true)
{
{$factory} return new {$class}();
}
protected function createProxy(\$class, \Closure \$factory)
{
\$this->proxyClass = \$class;
return \$factory();
}
};
EOPHP;
$implem = preg_replace('#\n /\*\*.*?\*/#s', '', $implem);
$implem = str_replace('getWrappedValueHolderValue() : ?object', 'getWrappedValueHolderValue()', $implem);
$implem = str_replace("array(\n \n );", "[\n \n ];", $implem);
$this->assertStringEqualsFile(__DIR__.'/Fixtures/proxy-implem.php', $implem);
$this->assertStringEqualsFile(__DIR__.'/Fixtures/proxy-factory.php', $factory);
require_once __DIR__.'/Fixtures/proxy-implem.php';
$factory = require __DIR__.'/Fixtures/proxy-factory.php';
$foo = $factory->getFooService();
$this->assertInstanceof($factory->proxyClass, $foo);
$this->assertInstanceof(DummyInterface::class, $foo);
$this->assertInstanceof(SunnyInterface::class, $foo);
$this->assertNotInstanceof(DummyClass::class, $foo);
$this->assertSame($foo, $foo->dummy());
$foo->dynamicProp = 123;
$this->assertSame(123, @$foo->dynamicProp);
}
/** /**
* @return array * @return array
*/ */
@ -142,3 +197,34 @@ class ProxyDumperTest extends TestCase
return $definitions; return $definitions;
} }
} }
final class DummyClass implements DummyInterface, SunnyInterface
{
public function dummy()
{
return $this;
}
public function sunny()
{
}
public function &dummyRef()
{
return $this->ref;
}
}
interface DummyInterface
{
public function dummy();
public function &dummyRef();
}
interface SunnyInterface
{
public function dummy();
public function sunny();
}

View File

@ -18,7 +18,7 @@
"require": { "require": {
"php": "^7.1.3", "php": "^7.1.3",
"symfony/dependency-injection": "~3.4|~4.0", "symfony/dependency-injection": "~3.4|~4.0",
"ocramius/proxy-manager": "~0.4|~1.0|~2.0" "ocramius/proxy-manager": "~2.1"
}, },
"require-dev": { "require-dev": {
"symfony/config": "~3.4|~4.0" "symfony/config": "~3.4|~4.0"

View File

@ -44,6 +44,7 @@ class UnusedTagsPass implements CompilerPassInterface
'messenger.receiver', 'messenger.receiver',
'messenger.message_handler', 'messenger.message_handler',
'monolog.logger', 'monolog.logger',
'proxy',
'routing.expression_language_provider', 'routing.expression_language_provider',
'routing.loader', 'routing.loader',
'security.expression_language_provider', 'security.expression_language_provider',

View File

@ -16,11 +16,16 @@ trait LazyTrait
/** /**
* Sets the lazy flag of this service. * Sets the lazy flag of this service.
* *
* @param bool|string A FQCN to derivate the lazy proxy from or `true` to make it extend from the definition's class
*
* @return $this * @return $this
*/ */
final public function lazy(bool $lazy = true) final public function lazy($lazy = true)
{ {
$this->definition->setLazy($lazy); $this->definition->setLazy((bool) $lazy);
if (\is_string($lazy)) {
$this->definition->addTag('proxy', array('interface' => $lazy));
}
return $this; return $this;
} }

View File

@ -265,10 +265,17 @@ class XmlFileLoader extends FileLoader
$definition->setChanges(array()); $definition->setChanges(array());
} }
foreach (array('class', 'public', 'shared', 'synthetic', 'lazy', 'abstract') as $key) { foreach (array('class', 'public', 'shared', 'synthetic', 'abstract') as $key) {
if ($value = $service->getAttribute($key)) { if ($value = $service->getAttribute($key)) {
$method = 'set'.$key; $method = 'set'.$key;
$definition->$method(XmlUtils::phpize($value)); $definition->$method($value = XmlUtils::phpize($value));
}
}
if ($value = $service->getAttribute('lazy')) {
$definition->setLazy((bool) $value = XmlUtils::phpize($value));
if (\is_string($value)) {
$definition->addTag('proxy', array('interface' => $value));
} }
} }

View File

@ -407,7 +407,10 @@ class YamlFileLoader extends FileLoader
} }
if (isset($service['lazy'])) { if (isset($service['lazy'])) {
$definition->setLazy($service['lazy']); $definition->setLazy((bool) $service['lazy']);
if (\is_string($service['lazy'])) {
$definition->addTag('proxy', array('interface' => $service['lazy']));
}
} }
if (isset($service['public'])) { if (isset($service['public'])) {

View File

@ -124,7 +124,7 @@
<xsd:attribute name="shared" type="boolean" /> <xsd:attribute name="shared" type="boolean" />
<xsd:attribute name="public" type="boolean" /> <xsd:attribute name="public" type="boolean" />
<xsd:attribute name="synthetic" type="boolean" /> <xsd:attribute name="synthetic" type="boolean" />
<xsd:attribute name="lazy" type="boolean" /> <xsd:attribute name="lazy" type="xsd:string" />
<xsd:attribute name="abstract" type="boolean" /> <xsd:attribute name="abstract" type="boolean" />
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="parent" type="xsd:string" /> <xsd:attribute name="parent" type="xsd:string" />
@ -145,7 +145,7 @@
<xsd:attribute name="id" type="xsd:string" use="required" /> <xsd:attribute name="id" type="xsd:string" use="required" />
<xsd:attribute name="shared" type="boolean" /> <xsd:attribute name="shared" type="boolean" />
<xsd:attribute name="public" type="boolean" /> <xsd:attribute name="public" type="boolean" />
<xsd:attribute name="lazy" type="boolean" /> <xsd:attribute name="lazy" type="xsd:string" />
<xsd:attribute name="autowire" type="boolean" /> <xsd:attribute name="autowire" type="boolean" />
<xsd:attribute name="autoconfigure" type="boolean" /> <xsd:attribute name="autoconfigure" type="boolean" />
</xsd:complexType> </xsd:complexType>
@ -167,7 +167,7 @@
<xsd:attribute name="exclude" type="xsd:string" /> <xsd:attribute name="exclude" type="xsd:string" />
<xsd:attribute name="shared" type="boolean" /> <xsd:attribute name="shared" type="boolean" />
<xsd:attribute name="public" type="boolean" /> <xsd:attribute name="public" type="boolean" />
<xsd:attribute name="lazy" type="boolean" /> <xsd:attribute name="lazy" type="xsd:string" />
<xsd:attribute name="abstract" type="boolean" /> <xsd:attribute name="abstract" type="boolean" />
<xsd:attribute name="parent" type="xsd:string" /> <xsd:attribute name="parent" type="xsd:string" />
<xsd:attribute name="autowire" type="boolean" /> <xsd:attribute name="autowire" type="boolean" />

View File

@ -0,0 +1,12 @@
services:
service_container:
class: Symfony\Component\DependencyInjection\ContainerInterface
public: true
synthetic: true
foo:
class: stdClass
public: true
tags:
- { name: proxy, interface: SomeInterface }
lazy: true

View File

@ -0,0 +1,8 @@
<?php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return function (ContainerConfigurator $c) {
$di = $c->services();
$di->set('foo', 'stdClass')->lazy('SomeInterface');
};

View File

@ -10,7 +10,7 @@ services:
tags: tags:
- { name: foo } - { name: foo }
- { name: baz } - { name: baz }
deprecated: "%service_id%" deprecated: '%service_id%'
arguments: [1] arguments: [1]
factory: f factory: f
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar:
@ -19,7 +19,7 @@ services:
tags: tags:
- { name: foo } - { name: foo }
- { name: baz } - { name: baz }
deprecated: "%service_id%" deprecated: '%service_id%'
lazy: true lazy: true
arguments: [1] arguments: [1]
factory: f factory: f

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="foo" lazy="SomeInterface" />
</services>
</container>

View File

@ -0,0 +1,3 @@
services:
foo:
lazy: SomeInterface

View File

@ -72,9 +72,11 @@ class PhpFileLoaderTest extends TestCase
yield array('defaults'); yield array('defaults');
yield array('instanceof'); yield array('instanceof');
yield array('prototype'); yield array('prototype');
yield array('prototype_array');
yield array('child'); yield array('child');
yield array('php7'); yield array('php7');
yield array('anonymous'); yield array('anonymous');
yield array('lazy_fqcn');
} }
/** /**

View File

@ -816,4 +816,14 @@ class XmlFileLoaderTest extends TestCase
'$factory' => 'factory', '$factory' => 'factory',
), array_map(function (BoundArgument $v) { return $v->getValues()[0]; }, $definition->getBindings())); ), array_map(function (BoundArgument $v) { return $v->getValues()[0]; }, $definition->getBindings()));
} }
public function testFqcnLazyProxy()
{
$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
$loader->load('services_lazy_fqcn.xml');
$definition = $container->getDefinition('foo');
$this->assertSame(array(array('interface' => 'SomeInterface')), $definition->getTag('proxy'));
}
} }

View File

@ -739,4 +739,14 @@ class YamlFileLoaderTest extends TestCase
'$factory' => 'factory', '$factory' => 'factory',
), array_map(function (BoundArgument $v) { return $v->getValues()[0]; }, $definition->getBindings())); ), array_map(function (BoundArgument $v) { return $v->getValues()[0]; }, $definition->getBindings()));
} }
public function testFqcnLazyProxy()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('services_lazy_fqcn.yml');
$definition = $container->getDefinition('foo');
$this->assertSame(array(array('interface' => 'SomeInterface')), $definition->getTag('proxy'));
}
} }