Skip to content
This repository

Lazy services - service proxies #7527

Closed
wants to merge 21 commits into from

10 participants

Marco Pivetta Crell Grégoire Pineau Christophe Coevoet Miha Vrhovnik Jordan Alliot Lukas Kahwe Smith Fabien Potencier James Halsall Benjamin Eberlei
Marco Pivetta
Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #6140 (supersedes) #5012 #6102 (maybe)
License MIT (attached code) - BSD-3-Clause (transitive optional dependency)
Doc PR Please pester me to death so I do it

This PR introduces lazy services along the lines of zendframework/zf2#4146

It introduces an OPTIONAL dependency to ProxyManager and transitively to "zendframework/zend-code": "2.*".

Lazy services: why? A comprehensive example

For those who don't know what this is about, here's an example.

Assuming you have a service class like following:

class MySuperSlowClass
{
    public function __construct()
    {
        // inject large object graph or do heavy computation
        sleep(10);
    }

    public function doFoo()
    {
        echo 'Foo!';
    }
}

The DIC will hang for 10 seconds when calling:

$container->get('my_super_slow_class');

With this PR, this can be avoided, and the following call will return a proxy immediately.

$container->getDefinitions('my_super_slow_class')->setLazy(true);
$service = $container->get('my_super_slow_class');

The 10 seconds wait time will be delayed until the object is actually used:

$service->doFoo(); // wait 10 seconds, then 'Foo!'

A more extensive description of the functionality can be found here.

When do we need it?

Lazy services can be used to optimize the dependency graph in cases like:

  • Webservice endpoints
  • Db connections
  • Objects that cause I/O in general
  • Large dependency graphs that are not always used

This could also help in reducing excessive service location usage as I've explained here.

Implementation quirks of this PR

There's a couple of quirks in the implementation:

  • Symfony\Component\DependencyInjection\CompilerBuilder#createService is now public because of the limitations of PHP 5.3
  • Symfony\Component\DependencyInjection\Dumper\PhpDumper now with extra mess!
  • The proxies are dumped at the end of compiled containers, therefore the container class is not PSR compliant anymore

Todos

  • Fix license compatibility
  • Fix: real instantiation of the service swaps the registered instance in the DIC
  • Generate runtime proxies via eval
  • Generate proxies in the compiled DIC
  • 5.3 compatibility
  • Check all links to ProxyManager to ensure the dependency is completely optional
  • Handle disabled eval case
  • Add class resources to handle changes in definitions
Marco Pivetta Ocramius referenced this pull request in zendframework/zf2 March 29, 2013
Merged

Lazy services #4146

Crell
Crell commented March 29, 2013

@jrobeson pointed me here.

The functionality definitely sounds valuable. I know Lukas has been talking about this for a long time. However, the dependency chain here is not fun.

I cannot speak for Fabien, but I'd be wary of adding a dependency on 2 additional libraries to DependencyInjection just for this functionality. Even that aside, the cg-library package is licensed under Apache 2. That's a problem. Specifically, it means that the DI Component cannot run without Apache 2 code, which in turn means that it has the same impact license-wise as being Apache 2. That is, it becomes incompatible with GPLv2 and GPLv2+ code.

While Symfony itself is not GPL licensed, many of the systems that are now depending on it are. In particular, Drupal and phpBB are both GPLv2+ licensed. GPLv2 is incompatible with Apache 2. That would make the DI component, and by extension Symfony fullstack, unusable for both Drupal and phpBB, as well as likely others. (I don't know the license of any of the other Symfony-based systems off hand.) It also makes it unsuable for bespoke systems by shops that use GPLv2 (and there are a lot of them) that want to build off of Symfony2.

So, yeah. Introducing an Apache2 dependency would be a massive license-breaking and ecosystem-crippling change. :-/ jrobeson indicated that there was an alternative library, or that the cg-library could be relicensed if we convince the maintainer to do so. Either way the licensing issue would go away. The question of introducing additional dependencies I leave to fabpot.

Disclaimer: I'm the former Director of Legal Affairs for the Drupal Association, so I've spent a fair bit of time looking into this subject. I'm not just making it up. :-)

Marco Pivetta

@Crell I've been told that today, and didn't reallly expect it (I have no idea whatsoever this involves, I write code :( ). If BSD-3-Clause is compliant, I can convert my library to use Zend\Code instead. That would take me a couple of days, but is no problem at all.

Marco Pivetta Ocramius referenced this pull request in Ocramius/ProxyManager March 30, 2013
Merged

License compatibility #18

Marco Pivetta

@Crell I removed usage of cg-lib in ProxyManager as of Ocramius/ProxyManager#18 - should be ok now

Grégoire Pineau

Do you have any benchmark on a "classic" Symfony2 application ? (without sleep ;) ).

Marco Pivetta

@lyrixx no, I actually don't use Symfony SE. You can actually benchmark it yourself by insulating a portion of your object graph that is not used and marking the root node of that portion as lazy. From what I know, the security component should be quite large.

Another thing this tries to remove is excessive usage of service location for performance optimizations, as I've explained here.

You can also read a detailed description of how lazy loading proxies (as implemented here) work here

OOTB, as it is now, this PR does not introduce any overhead.

src/Symfony/Component/DependencyInjection/ContainerBuilder.php
... ...
@@ -861,8 +864,24 @@ public function findDefinition($id)
861 864
      * @throws RuntimeException When the service is a synthetic service
862 865
      * @throws InvalidArgumentException When configure callable is not callable
863 866
      */
864  
-    private function createService(Definition $definition, $id)
  867
+    public function createService(Definition $definition, $id, $tryProxy = true)
2
Christophe Coevoet Collaborator
stof added a note March 30, 2013

you need to update the phpdoc

Christophe Coevoet Collaborator
stof added a note March 30, 2013

And you should add @internal in the phpdoc to explain that it is only public because of the internal use in a closure, and should not be called by other code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/DependencyInjection/ContainerBuilder.php
((7 lines not shown))
  869
+        if ($tryProxy && ($className = $definition->getClass()) && $definition->isLazy()) {
  870
+            $factory   = new LazyLoadingValueHolderFactory(new Configuration());
  871
+            $container = $this;
  872
+
  873
+            return $factory->createProxy(
  874
+                $className,
  875
+                function (& $wrappedInstance, LazyLoadingInterface $proxy) use ($container, $definition, $id) {
  876
+                    $proxy->setProxyInitializer(null);
  877
+
  878
+                    $wrappedInstance = $container->createService($definition, $id, false);
  879
+
  880
+                    return true;
  881
+                }
  882
+            );
  883
+        }
  884
+
866 885
         if ($definition->isSynthetic()) {
1
Christophe Coevoet Collaborator
stof added a note March 30, 2013

This should be done before creating the lazy proxy IMO.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
((9 lines not shown))
  163
+     * @return string
  164
+     */
  165
+    private function addProxyLoading($id, Definition $definition)
  166
+    {
  167
+        if (!($definition->isLazy() && $definition->getClass())) {
  168
+            return '';
  169
+        }
  170
+
  171
+        $class = $this->dumpValue($definition->getClass());
  172
+
  173
+        if (0 === strpos($class, "'") && !preg_match('/^\'[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) {
  174
+            return '';
  175
+        }
  176
+
  177
+        // @todo this should happen directly through the factory class, but we have to ensure that the proxy
  178
+        // @todo class is generated during the dump process
1
Christophe Coevoet Collaborator
stof added a note March 30, 2013

you should not put @todo on the second line. It makes it quite hard to read (I first thought it was a second TODO)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
((48 lines not shown))
  202
+     * Generates code for the proxy classes to be attached after the container class
  203
+     *
  204
+     * @return string
  205
+     */
  206
+    private function addProxyClasses()
  207
+    {
  208
+        $definitions = $this->container->getDefinitions();
  209
+
  210
+        ksort($definitions);
  211
+
  212
+        $proxyDefinitions = array_filter(
  213
+            $this->container->getDefinitions(),
  214
+            function (Definition $definition) {
  215
+                return $definition->isLazy() && $definition->getClass();
  216
+            }
  217
+        );
2
Christophe Coevoet Collaborator
stof added a note March 30, 2013

Shouldn't you to this before sorting ?

Marco Pivetta
Ocramius added a note March 31, 2013

actually removed the ksort. It is quite useless in this location.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/DependencyInjection/composer.json
... ...
@@ -20,7 +20,8 @@
20 20
     },
21 21
     "require-dev": {
22 22
         "symfony/yaml": "~2.0",
23  
-        "symfony/config": ">=2.2,<2.4-dev"
  23
+        "symfony/config": ">=2.2,<2.4-dev",
  24
+        "ocramius/proxy-manager": "0.3.*"
1
Christophe Coevoet Collaborator
stof added a note March 30, 2013

It should also be suggested (and using a message explaining why would be better than a version number IMO)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Christophe Coevoet
Collaborator
stof commented March 30, 2013

Your current implementation breaks on 1 point: recreating the proxy class when the original class changes. The class has to be added as a container resource for lazy services so that the container is dumped again when the class changes, so that the proxies are dumped again. Otherwise, the proxy might not match the class anymore.

What you would need is the logic of ContainerBuilder::addObjectResource but with a class name rather than an instance (which is easy as addObjectResource could then be rewritten as $this->addClassResource(get_class($object))

Christophe Coevoet
Collaborator
stof commented March 30, 2013

For the ContainerBuilder case, using eval may be better than creating a temporary file on the fly and requiring it. You don't gain anything about caching the generated code (as you regenerate it each time) except potential race conditions (see Doctrine considering to change the generation of proxies when using auto_generate_proxies).
Btw, Twig is already using eval when you disable the template cache.

Christophe Coevoet
Collaborator
stof commented March 30, 2013

Your code has a big issue: it creates a new proxy instance each time the service is retrieved from the container instead of reusing the same instance for container-scoped services. So $container->get('router') !== $container->get('router') when marking it as lazy.

Christophe Coevoet
Collaborator
stof commented March 30, 2013

thus, getting the lazy service, then doing a method call (thus initializing the proxy) and then getting it again would give you the wrapped instance after that, being inconsistent.

Marco Pivetta

@stof very good catch! Couldn't write a for that test because of the conflicting container names. I'll see if I can change the name of a generated container name and write something that covers that.

Miha Vrhovnik

Also note, that the usage of eval may make the symfony2 projects that use lazy loading unusable on some shared hosts.

Marco Pivetta

@mvrhov ouch! Do they turn it off? I can build a switch for that eventually if there's a way of recognizing it.

Miha Vrhovnik

yeah. I worked for one a few years ago and we were disabling it. The problem was that that a large amount of code injections that were happening then abused eval to send a spam or worse. Trying to get unlisted from the sites that list spam sending IPs every few weeks also become troublesome.
There is disable_functions ini setting where all disabled functions a re listed.

Marco Pivetta

@mvrhov I spawned Ocramius/ProxyManager#24 from that. It should not be handled in symfony itself.

Marco Pivetta

@stof can you clarify when I should add a resource? I added addClassResource to the API, but now I'm wondering about what should trigger it. Should I do it in ContainerBuilder#register? Are there other locations where definitions are injected?

Jordan Alliot

The feature by itself would be a great addition but I agree with @Crell that the added dependencies are not so good.
Being able to reuse the CG library (if license compliant of course) would be a little bit better as it is already used by some Symfony2 bundles.

Besides, the generated class names of proxies should follow the naming convention used by both the CG library and Doctrine (with a __CG__ prefix and all, see ClassUtils) as it allows easily identifying proxies, wherever they come from, without having to test for all possible proxies interfaces out there.

Marco Pivetta

To clarify, I modified the PR so that the dependencies are now optional.

@jalliot for CG Lib, I kicked that out and replaced it with Zend\Code, which is BSD-3-clause and therefore compatible. That's quite final, since I don't want to get back and rewrite this backwards without any real benefit.

That's anyway stuff that doesn't affect this PR's code directly. SF2 just benefits from the abstraction layer of ProxyManager and should not really care about how that is achieved except for BC breaks, performance and security issues (and license compatibility, as introduced by @Crell).

For __CG__ I explicitly wanted to use __PM__ (or something different) because ProxyManager allows to hook in its own autoloader with its own rules depending on the naming strategy set in the proxy manager configuration. If that autoloader conflicts with the one in doctrine common, we have a problem. We don't want that, do we? :)

If you want to identify a ProxyManager proxy, you can use the utility within ProxyManager itself. An idea would be to drop the __PM__ constant from there so that you're forced to use the inflector instance and don't rely on a convention anymore.

src/Symfony/Component/DependencyInjection/ContainerBuilder.php
... ...
@@ -11,6 +11,11 @@
11 11
 
12 12
 namespace Symfony\Component\DependencyInjection;
13 13
 
  14
+use ProxyManager\Configuration;
  15
+use ProxyManager\Factory\LazyLoadingValueHolderFactory;
  16
+use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
  17
+use ProxyManager\Proxy\LazyLoadingInterface;
  18
+use ReflectionClass;
2
Christophe Coevoet Collaborator
stof added a note April 01, 2013

Symfony does not add use statements for classes in the global namespace

Marco Pivetta
Ocramius added a note April 02, 2013

Will revert :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Christophe Coevoet stof commented on the diff April 01, 2013
src/Symfony/Component/DependencyInjection/ContainerBuilder.php
((16 lines not shown))
  903
+            && ($className = $definition->getClass())
  904
+            && $definition->isLazy()
  905
+            && class_exists('ProxyManager\\Factory\\LazyLoadingValueHolderFactory')
  906
+        ) {
  907
+            $config = new Configuration();
  908
+
  909
+            $config->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
  910
+
  911
+            $factory   = new LazyLoadingValueHolderFactory($config);
  912
+            $container = $this;
  913
+            $proxy     = $factory->createProxy(
  914
+                $className,
  915
+                function (& $wrappedInstance, LazyLoadingInterface $proxy) use ($container, $definition, $id) {
  916
+                    $proxy->setProxyInitializer(null);
  917
+
  918
+                    $wrappedInstance = $container->createService($definition, $id, false);
2
Christophe Coevoet Collaborator
stof added a note April 01, 2013

Would the call to createService replace the shared instance ?

Marco Pivetta
Ocramius added a note April 02, 2013

No, this has also a test for it, see ContainerBuilderTest:276, where I explicitly check:

$this->assertSame($foo1, $builder->get('foo1'), 'The same proxy is retrieved after initialization'); 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Lukas Kahwe Smith

its an optional dependency but i agree that adding 2 more dependencies can be a concern. doctrine proxy and the cg lib are commonly used already in the Symfony2 ecosystem while proxy manager and zend code are less used.

i didnt check but ideally the code should maybe gracefully ignore the lazy setting in case the dependencies are missing so that no code would break if those optional deps are not installed.

Marco Pivetta

@lsmith77 perfect, I will add the code required to let any lazy markers being ignored in the case where dependencies are not loaded.

Christophe Coevoet stof commented on the diff April 03, 2013
src/Symfony/Component/DependencyInjection/ContainerBuilder.php
((5 lines not shown))
  230
+            $this->addClassResource(new \ReflectionClass($object));
  231
+        }
  232
+
  233
+        return $this;
  234
+    }
  235
+
  236
+    /**
  237
+     * Adds the given class hierarchy as resources.
  238
+     *
  239
+     * @param \ReflectionClass $class
  240
+     *
  241
+     * @return ContainerBuilder The current instance
  242
+     *
  243
+     * @api
  244
+     */
  245
+    public function addClassResource(\ReflectionClass $class)
2
Christophe Coevoet Collaborator
stof added a note April 03, 2013

why not passing a class name and building the ReflectionClass internally ? addObjectResource expects an object, not a ReflectionObject

Marco Pivetta
Ocramius added a note April 03, 2013

Strict typing. This disallows unexisting classes upfront

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Marco Pivetta

Still need to fix the 5.3 failures

Marco Pivetta Ocramius referenced this pull request April 12, 2013
Merged

[2.3] [WIP] Synchronized services... #7007

2 of 2 tasks complete
Marco Pivetta Ocramius referenced this pull request in Ocramius/ProxyManager April 19, 2013
Closed

Alternative approach - feedback? #32

Lukas Kahwe Smith

would love to see support for proxied services in 2.3

Marco Pivetta

@lsmith77 I'll fix the remaining points later tonight

src/Symfony/Component/DependencyInjection/ContainerBuilder.php
... ...
@@ -413,13 +434,14 @@ public function has($id)
413 434
      * Gets a service.
414 435
      *
415 436
      * @param string  $id              The service identifier
416  
-     * @param integer $invalidBehavior The behavior when the service does not exist
  437
+     * @param int     $invalidBehavior The behavior when the service does not exist
1
Fabien Potencier Owner
fabpot added a note April 25, 2013

This change should be reverted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/DependencyInjection/ContainerBuilder.php
((6 lines not shown))
417 438
      *
  439
+     * @throws InvalidArgumentException
  440
+     * @throws InactiveScopeException
  441
+     * @throws LogicException
  442
+     * @throws \Exception
1
Fabien Potencier Owner
fabpot added a note April 25, 2013

you should keep the @throws lines after the @return one like it was before. We also try to document when each exception is thrown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Fabien Potencier
Owner

@Ocramius If we want to include this into 2.3, this should be ready before Tuesday next week. Do you think it will be possible?

Marco Pivetta

@fabpot I was just working on this tonight. You can check it again tomorrow and it should be ok

Marco Pivetta

Not satisfied with Ocramius/symfony@d87086c, but I didn't find a better alternative since I couldn't get a 5.3.3 box to reproduce the phpunit issue

Marco Pivetta

@fabpot if it wasn't clear, this is good to go. Tests fail for an un-related feature

James Halsall

@Ocramius I was going to say that symfony isn't recommended to run on <= 5.3.8 anyway (according to http://symfony.com/doc/current/book/installation.html#installing-a-symfony2-distribution), but the site then contradicts itself here http://symfony.com/doc/current/reference/requirements.html#required saying that 5.3.3 is required. Maybe @fabpot can shed some light on that

Marco Pivetta

@jaitsu87 it's not related with this PR anyway

Benjamin Eberlei beberlei commented on the diff April 30, 2013
src/Symfony/Component/DependencyInjection/ContainerBuilder.php
((9 lines not shown))
877 909
     {
878 910
         if ($definition->isSynthetic()) {
879 911
             throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The DIC does not know how to construct this service.', $id));
880 912
         }
881 913
 
  914
+        if (
1
Benjamin Eberlei Collaborator
beberlei added a note April 30, 2013

Can we move this whole block of code into a new service? This adds a hidden dependency to ZendFramework/ThirdParty code inside the ContainerBuilder that is not replaceable. This violates both Single Responsibilty and Dependency Inversion Principle here, not keeping the code against abstract concepts.

I say introduce a LazyLoad something interface, optionally inject it into the ContainerBuilder and then use the ProxyManager as one possible implementation there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Benjamin Eberlei beberlei commented on the diff April 30, 2013
src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
... ...
@@ -11,6 +11,9 @@
11 11
 
12 12
 namespace Symfony\Component\DependencyInjection\Dumper;
13 13
 
  14
+use ProxyManager\Generator\ClassGenerator;
1
Benjamin Eberlei Collaborator
beberlei added a note April 30, 2013

Again here, can we abstract this against an interface to not violate the DIP? This dependency is optional, but the code makes it "required".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Benjamin Eberlei
Collaborator

To clarify my comments above, the proxy manager is an optional dependency and should therefore be hidden in an adapter, maybe even in its own Bridge, implementing a generic interface from DependencyInjection component

Marco Pivetta

Agreed - fixing later today

added some commits March 30, 2013
Marco Pivetta First implementation of lazy services via proxy manager f503ad8
Marco Pivetta Adding basic logic to generate proxy instantiation into a php dumped …
…container
67aef45
Marco Pivetta Compiling proxies into the generated DIC file f4a19c7
Marco Pivetta Upgrading dependency to ProxyManager 0.3.* d2760f1
Marco Pivetta Suggesting ProxyManager in composer.json, removing useless calls 4a13f82
Marco Pivetta Adding failing test to demonstrate that proxy initialization breaks s…
…hared services
a6a6572
Marco Pivetta Fixing shared service instance 35fdded
Marco Pivetta Sharing services in the container should only happen when proxying fa…
…iled
bec7774
Marco Pivetta Adding tests for proxy sharing within dumped containers 468e92e
Marco Pivetta Fixing shared proxies into the container 4ecd5ad
Marco Pivetta Bumping required version of ProxyManager 11a1da9
Marco Pivetta Docblock for ContainerBuilder#shareService 5870fed
Marco Pivetta Adding `ContainerBuilder#addClassResource` 695e3c5
Marco Pivetta Adding test to check that class resources are registered for lazy ser…
…vices
c5a5af0
Marco Pivetta Fixing tests, registering class resources for lazy services 29899ec
Marco Pivetta Reverting import of global namespace classes b5d0298
Marco Pivetta Lazier checks on the proxy structure (avoiding whitespace-based test …
…failures
cda390b
Marco Pivetta Getters for proxied services are public for 5.3.3 compatibility 1eb4cf7
Marco Pivetta Enforcing soft dependency to ProxyManager b417969
Marco Pivetta Reverting documentation changes, adding exception types description in ` 1e24767
Marco Pivetta Lazier checks on the proxy structure 450635a
Marco Pivetta Ocramius referenced this pull request April 30, 2013
Closed

ProxyManager Bridge #7890

Marco Pivetta

I've opened #7890 with the suggestions of @beberlei

Fabien Potencier
Owner

closing in favor of #7890

Fabien Potencier fabpot closed this April 30, 2013
Fabien Potencier fabpot referenced this pull request from a commit May 06, 2013
Fabien Potencier merged branch Ocramius/feature/proxy-manager-bridge (PR #7890)
This PR was squashed before being merged into the master branch (closes #7890).

Discussion
----------

ProxyManager Bridge

As of @beberlei's suggestion, I re-implemented #7527 as a new bridge to avoid possible hidden dependencies.

Everything is like #7527 except that the new namespace (and possibly package/subtree split) `Symfony\Bridge\ProxyManager` is introduced

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #6140 (supersedes) #5012 #6102 (maybe) #7527 (supersedes)
| License       | MIT (attached code) - BSD-3-Clause (transitive dependency)
| Doc PR        | Please pester me to death so I do it

This PR introduces lazy services along the lines of zendframework/zf2#4146

It introduces an **OPTIONAL** dependency to [ProxyManager](https://github.com/Ocramius/ProxyManager) and transitively to [`"zendframework/zend-code": "2.*"`](https://github.com/zendframework/zf2/tree/master/library/Zend/Code).

## Lazy services: why? A comprehensive example

For those who don't know what this is about, here's an example.

Assuming you have a service class like following:

```php
class MySuperSlowClass
{
    public function __construct()
    {
        // inject large object graph or do heavy computation
        sleep(10);
    }

    public function doFoo()
    {
        echo 'Foo!';
    }
}
```

The DIC will hang for 10 seconds when calling:

```php
$container->get('my_super_slow_class');
```

With this PR, this can be avoided, and the following call will return a proxy immediately.

```php
$container->getDefinitions('my_super_slow_class')->setLazy(true);
$service = $container->get('my_super_slow_class');
```

The 10 seconds wait time will be delayed until the object is actually used:

```php
$service->doFoo(); // wait 10 seconds, then 'Foo!'
```

A more extensive description of the functionality can be found [here](https://github.com/Ocramius/ProxyManager/blob/master/docs/lazy-loading-value-holder.md).

## When do we need it?

Lazy services can be used to optimize the dependency graph in cases like:

 * Webservice endpoints
 * Db connections
 * Objects that cause I/O in general
 * Large dependency graphs that are not always used

This could also help in reducing excessive service location usage as I've explained [here](http://ocramius.github.com/blog/zf2-and-symfony-service-proxies-with-doctrine-proxies/).

## Implementation quirks of this PR

There's a couple of quirks in the implementation:

 * `Symfony\Component\DependencyInjection\CompilerBuilder#createService` is now public because of the limitations of PHP 5.3
 * `Symfony\Component\DependencyInjection\Dumper\PhpDumper` now with extra mess!
 * The proxies are dumped at the end of compiled containers, therefore the container class is not PSR compliant anymore

Commits
-------

78e3710 ProxyManager Bridge
dfd605f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 21 unique commits by 1 author.

Apr 30, 2013
Marco Pivetta First implementation of lazy services via proxy manager f503ad8
Marco Pivetta Adding basic logic to generate proxy instantiation into a php dumped …
…container
67aef45
Marco Pivetta Compiling proxies into the generated DIC file f4a19c7
Marco Pivetta Upgrading dependency to ProxyManager 0.3.* d2760f1
Marco Pivetta Suggesting ProxyManager in composer.json, removing useless calls 4a13f82
Marco Pivetta Adding failing test to demonstrate that proxy initialization breaks s…
…hared services
a6a6572
Marco Pivetta Fixing shared service instance 35fdded
Marco Pivetta Sharing services in the container should only happen when proxying fa…
…iled
bec7774
Marco Pivetta Adding tests for proxy sharing within dumped containers 468e92e
Marco Pivetta Fixing shared proxies into the container 4ecd5ad
Marco Pivetta Bumping required version of ProxyManager 11a1da9
Marco Pivetta Docblock for ContainerBuilder#shareService 5870fed
Marco Pivetta Adding `ContainerBuilder#addClassResource` 695e3c5
Marco Pivetta Adding test to check that class resources are registered for lazy ser…
…vices
c5a5af0
Marco Pivetta Fixing tests, registering class resources for lazy services 29899ec
Marco Pivetta Reverting import of global namespace classes b5d0298
Marco Pivetta Lazier checks on the proxy structure (avoiding whitespace-based test …
…failures
cda390b
Marco Pivetta Getters for proxied services are public for 5.3.3 compatibility 1eb4cf7
Marco Pivetta Enforcing soft dependency to ProxyManager b417969
Marco Pivetta Reverting documentation changes, adding exception types description in ` 1e24767
Marco Pivetta Lazier checks on the proxy structure 450635a
This page is out of date. Refresh to see the latest.

Showing 19 changed files with 709 additions and 29 deletions. Show diff stats Hide diff stats

  1. 3  composer.json
  2. 109  src/Symfony/Component/DependencyInjection/ContainerBuilder.php
  3. 30  src/Symfony/Component/DependencyInjection/Definition.php
  4. 140  src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
  5. 3  src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php
  6. 4  src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
  7. 2  src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
  8. 4  src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
  9. 1  src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
  10. 116  src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
  11. 12  src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php
  12. 37  src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php
  13. 199  src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_service.php
  14. 67  src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_service_structure.txt
  15. 2  src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml
  16. 1  src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml
  17. 1  src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
  18. 1  src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
  19. 6  src/Symfony/Component/DependencyInjection/composer.json
3  composer.json
@@ -66,7 +66,8 @@
66 66
         "doctrine/orm": "~2.2,>=2.2.3",
67 67
         "monolog/monolog": "~1.3",
68 68
         "propel/propel1": "1.6.*",
69  
-        "ircmaxell/password-compat": "1.0.*"
  69
+        "ircmaxell/password-compat": "1.0.*",
  70
+        "ocramius/proxy-manager": ">=0.3.1,<0.4-dev"
70 71
     },
71 72
     "autoload": {
72 73
         "psr-0": { "Symfony\\": "src/" },
109  src/Symfony/Component/DependencyInjection/ContainerBuilder.php
@@ -11,6 +11,10 @@
11 11
 
12 12
 namespace Symfony\Component\DependencyInjection;
13 13
 
  14
+use ProxyManager\Configuration;
  15
+use ProxyManager\Factory\LazyLoadingValueHolderFactory;
  16
+use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
  17
+use ProxyManager\Proxy\LazyLoadingInterface;
14 18
 use Symfony\Component\DependencyInjection\Compiler\Compiler;
15 19
 use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16 20
 use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@@ -223,14 +227,31 @@ public function setResources(array $resources)
223 227
      */
224 228
     public function addObjectResource($object)
225 229
     {
  230
+        if ($this->trackResources) {
  231
+            $this->addClassResource(new \ReflectionClass($object));
  232
+        }
  233
+
  234
+        return $this;
  235
+    }
  236
+
  237
+    /**
  238
+     * Adds the given class hierarchy as resources.
  239
+     *
  240
+     * @param \ReflectionClass $class
  241
+     *
  242
+     * @return ContainerBuilder The current instance
  243
+     *
  244
+     * @api
  245
+     */
  246
+    public function addClassResource(\ReflectionClass $class)
  247
+    {
226 248
         if (!$this->trackResources) {
227 249
             return $this;
228 250
         }
229 251
 
230  
-        $parent = new \ReflectionObject($object);
231 252
         do {
232  
-            $this->addResource(new FileResource($parent->getFileName()));
233  
-        } while ($parent = $parent->getParentClass());
  253
+            $this->addResource(new FileResource($class->getFileName()));
  254
+        } while ($class = $class->getParentClass());
234 255
 
235 256
         return $this;
236 257
     }
@@ -417,8 +438,10 @@ public function has($id)
417 438
      *
418 439
      * @return object The associated service
419 440
      *
420  
-     * @throws InvalidArgumentException if the service is not defined
421  
-     * @throws LogicException if the service has a circular reference to itself
  441
+     * @throws InvalidArgumentException when no definitions are available
  442
+     * @throws InactiveScopeException   when the current scope is not active
  443
+     * @throws LogicException           when a circular dependency is detected
  444
+     * @throws \Exception
422 445
      *
423 446
      * @see Reference
424 447
      *
@@ -584,6 +607,12 @@ public function compile()
584 607
             foreach ($this->compiler->getPassConfig()->getPasses() as $pass) {
585 608
                 $this->addObjectResource($pass);
586 609
             }
  610
+
  611
+            foreach ($this->definitions as $definition) {
  612
+                if ($definition->isLazy() && ($class = $definition->getClass()) && class_exists($class)) {
  613
+                    $this->addClassResource(new \ReflectionClass($class));
  614
+                }
  615
+            }
587 616
         }
588 617
 
589 618
         $this->compiler->compile($this);
@@ -865,6 +894,7 @@ public function findDefinition($id)
865 894
      *
866 895
      * @param Definition $definition A service definition instance
867 896
      * @param string     $id         The service identifier
  897
+     * @param Boolean    $tryProxy   Whether to try proxying the service with a lazy proxy
868 898
      *
869 899
      * @return object The service described by the service definition
870 900
      *
@@ -872,13 +902,43 @@ public function findDefinition($id)
872 902
      * @throws RuntimeException When the factory definition is incomplete
873 903
      * @throws RuntimeException When the service is a synthetic service
874 904
      * @throws InvalidArgumentException When configure callable is not callable
  905
+     *
  906
+     * @internal this method is public because of PHP 5.3 limitations, do not use it explicitly in your code
875 907
      */
876  
-    private function createService(Definition $definition, $id)
  908
+    public function createService(Definition $definition, $id, $tryProxy = true)
877 909
     {
878 910
         if ($definition->isSynthetic()) {
879 911
             throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The DIC does not know how to construct this service.', $id));
880 912
         }
881 913
 
  914
+        if (
  915
+            $tryProxy
  916
+            && ($className = $definition->getClass())
  917
+            && $definition->isLazy()
  918
+            && class_exists('ProxyManager\\Factory\\LazyLoadingValueHolderFactory')
  919
+        ) {
  920
+            $config = new Configuration();
  921
+
  922
+            $config->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
  923
+
  924
+            $factory   = new LazyLoadingValueHolderFactory($config);
  925
+            $container = $this;
  926
+            $proxy     = $factory->createProxy(
  927
+                $className,
  928
+                function (& $wrappedInstance, LazyLoadingInterface $proxy) use ($container, $definition, $id) {
  929
+                    $proxy->setProxyInitializer(null);
  930
+
  931
+                    $wrappedInstance = $container->createService($definition, $id, false);
  932
+
  933
+                    return true;
  934
+                }
  935
+            );
  936
+
  937
+            $this->shareService($definition, $proxy, $id);
  938
+
  939
+            return $proxy;
  940
+        }
  941
+
882 942
         $parameterBag = $this->getParameterBag();
883 943
 
884 944
         if (null !== $definition->getFile()) {
@@ -903,16 +963,9 @@ private function createService(Definition $definition, $id)
903 963
             $service = null === $r->getConstructor() ? $r->newInstance() : $r->newInstanceArgs($arguments);
904 964
         }
905 965
 
906  
-        if (self::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) {
907  
-            if (self::SCOPE_CONTAINER !== $scope && !isset($this->scopedServices[$scope])) {
908  
-                throw new InactiveScopeException($id, $scope);
909  
-            }
910  
-
911  
-            $this->services[$lowerId = strtolower($id)] = $service;
912  
-
913  
-            if (self::SCOPE_CONTAINER !== $scope) {
914  
-                $this->scopedServices[$scope][$lowerId] = $service;
915  
-            }
  966
+        if ($tryProxy || !$definition->isLazy()) {
  967
+            // share only if proxying failed, or if not a proxy
  968
+            $this->shareService($definition, $service, $id);
916 969
         }
917 970
 
918 971
         foreach ($definition->getMethodCalls() as $call) {
@@ -1057,4 +1110,28 @@ private function callMethod($service, $call)
1057 1110
 
1058 1111
         call_user_func_array(array($service, $call[0]), $this->resolveServices($this->getParameterBag()->resolveValue($call[1])));
1059 1112
     }
  1113
+
  1114
+    /**
  1115
+     * Shares a given service in the container
  1116
+     *
  1117
+     * @param Definition $definition
  1118
+     * @param mixed      $service
  1119
+     * @param string     $id
  1120
+     *
  1121
+     * @throws InactiveScopeException
  1122
+     */
  1123
+    private function shareService(Definition $definition, $service, $id)
  1124
+    {
  1125
+        if (self::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) {
  1126
+            if (self::SCOPE_CONTAINER !== $scope && !isset($this->scopedServices[$scope])) {
  1127
+                throw new InactiveScopeException($id, $scope);
  1128
+            }
  1129
+
  1130
+            $this->services[$lowerId = strtolower($id)] = $service;
  1131
+
  1132
+            if (self::SCOPE_CONTAINER !== $scope) {
  1133
+                $this->scopedServices[$scope][$lowerId] = $service;
  1134
+            }
  1135
+        }
  1136
+    }
1060 1137
 }
30  src/Symfony/Component/DependencyInjection/Definition.php
@@ -37,6 +37,7 @@ class Definition
37 37
     private $synthetic;
38 38
     private $abstract;
39 39
     private $synchronized;
  40
+    private $lazy;
40 41
 
41 42
     protected $arguments;
42 43
 
@@ -58,6 +59,7 @@ public function __construct($class = null, array $arguments = array())
58 59
         $this->public = true;
59 60
         $this->synthetic = false;
60 61
         $this->synchronized = false;
  62
+        $this->lazy = false;
61 63
         $this->abstract = false;
62 64
         $this->properties = array();
63 65
     }
@@ -600,6 +602,34 @@ public function isSynchronized()
600 602
     }
601 603
 
602 604
     /**
  605
+     * Sets the lazy flag of this service.
  606
+     *
  607
+     * @param Boolean $lazy
  608
+     *
  609
+     * @return Definition The current instance
  610
+     *
  611
+     * @api
  612
+     */
  613
+    public function setLazy($lazy)
  614
+    {
  615
+        $this->lazy = (Boolean) $lazy;
  616
+
  617
+        return $this;
  618
+    }
  619
+
  620
+    /**
  621
+     * Whether this service is lazy.
  622
+     *
  623
+     * @return Boolean
  624
+     *
  625
+     * @api
  626
+     */
  627
+    public function isLazy()
  628
+    {
  629
+        return $this->lazy;
  630
+    }
  631
+
  632
+    /**
603 633
      * Sets whether this definition is synthetic, that is not constructed by the
604 634
      * container, but dynamically injected.
605 635
      *
140  src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
@@ -11,6 +11,9 @@
11 11
 
12 12
 namespace Symfony\Component\DependencyInjection\Dumper;
13 13
 
  14
+use ProxyManager\Generator\ClassGenerator;
  15
+use ProxyManager\GeneratorStrategy\BaseGeneratorStrategy;
  16
+use ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator;
14 17
 use Symfony\Component\DependencyInjection\Variable;
15 18
 use Symfony\Component\DependencyInjection\Definition;
16 19
 use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -94,7 +97,8 @@ public function dump(array $options = array())
94 97
         $code .=
95 98
             $this->addServices().
96 99
             $this->addDefaultParametersMethod().
97  
-            $this->endClass()
  100
+            $this->endClass().
  101
+            $this->addProxyClasses()
98 102
         ;
99 103
 
100 104
         return $code;
@@ -150,6 +154,88 @@ private function addServiceLocalTempVariables($cId, $definition)
150 154
     }
151 155
 
152 156
     /**
  157
+     * Generates the logic required for proxy lazy loading
  158
+     *
  159
+     * @param string     $id         The service id
  160
+     * @param Definition $definition
  161
+     *
  162
+     * @return string
  163
+     */
  164
+    private function addProxyLoading($id, Definition $definition)
  165
+    {
  166
+        if (!$this->isProxyCandidate($definition)) {
  167
+            return '';
  168
+        }
  169
+
  170
+        $instantiation = 'return';
  171
+
  172
+        if (ContainerInterface::SCOPE_CONTAINER === $definition->getScope()) {
  173
+            $instantiation .= " \$this->services['$id'] =";
  174
+        } elseif (ContainerInterface::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) {
  175
+            $instantiation .= " \$this->services['$id'] = \$this->scopedServices['$scope']['$id'] =";
  176
+        }
  177
+
  178
+        $methodName = 'get' . Container::camelize($id) . 'Service';
  179
+        $proxyClass = str_replace('\\', '', $definition->getClass()) . '_' . spl_object_hash($definition);
  180
+
  181
+        return <<<EOF
  182
+        if (\$lazyLoad) {
  183
+            \$container = \$this;
  184
+
  185
+            $instantiation new $proxyClass(
  186
+                function (& \$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface \$proxy) use (\$container) {
  187
+                    \$proxy->setProxyInitializer(null);
  188
+
  189
+                    \$wrappedInstance = \$container->$methodName(false);
  190
+
  191
+                    return true;
  192
+                }
  193
+            );
  194
+        }
  195
+
  196
+
  197
+EOF;
  198
+    }
  199
+
  200
+    /**
  201
+     * Generates code for the proxy classes to be attached after the container class
  202
+     *
  203
+     * @return string
  204
+     */
  205
+    private function addProxyClasses()
  206
+    {
  207
+        $proxyDefinitions = array();
  208
+
  209
+        foreach ($this->container->getDefinitions() as $definition) {
  210
+            if ($this->isProxyCandidate($definition)) {
  211
+                $proxyDefinitions[] = $definition;
  212
+            }
  213
+        }
  214
+
  215
+        // avoids hard dependency to ProxyManager
  216
+        if (empty($proxyDefinitions)) {
  217
+            return '';
  218
+        }
  219
+
  220
+        $proxyGenerator = new LazyLoadingValueHolderGenerator();
  221
+        $classGenerator = new BaseGeneratorStrategy();
  222
+        $code           = '';
  223
+
  224
+        /* @var $proxyDefinitions Definition[] */
  225
+        foreach ($proxyDefinitions as $definition) {
  226
+            $generatedClass = new ClassGenerator(
  227
+                str_replace('\\', '', $definition->getClass()) . '_' . md5(spl_object_hash($definition))
  228
+            );
  229
+
  230
+            $proxyGenerator->generate(new \ReflectionClass($definition->getClass()), $generatedClass);
  231
+
  232
+            $code .= "\n" . $classGenerator->generate($generatedClass);
  233
+        }
  234
+
  235
+        return $code;
  236
+    }
  237
+
  238
+    /**
153 239
      * Generates the require_once statement for service includes.
154 240
      *
155 241
      * @param string     $id         The service id
@@ -280,12 +366,13 @@ private function addServiceInstance($id, $definition)
280 366
             throw new InvalidArgumentException(sprintf('"%s" is not a valid class name for the "%s" service.', $class, $id));
281 367
         }
282 368
 
283  
-        $simple = $this->isSimpleInstance($id, $definition);
  369
+        $simple           = $this->isSimpleInstance($id, $definition);
  370
+        $isProxyCandidate = $this->isProxyCandidate($definition);
  371
+        $instantiation    = '';
284 372
 
285  
-        $instantiation = '';
286  
-        if (ContainerInterface::SCOPE_CONTAINER === $definition->getScope()) {
  373
+        if (!$isProxyCandidate && ContainerInterface::SCOPE_CONTAINER === $definition->getScope()) {
287 374
             $instantiation = "\$this->services['$id'] = ".($simple ? '' : '$instance');
288  
-        } elseif (ContainerInterface::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) {
  375
+        } elseif (!$isProxyCandidate && ContainerInterface::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) {
289 376
             $instantiation = "\$this->services['$id'] = \$this->scopedServices['$scope']['$id'] = ".($simple ? '' : '$instance');
290 377
         } elseif (!$simple) {
291 378
             $instantiation = '$instance';
@@ -483,18 +570,32 @@ private function addService($id, $definition)
483 570
 EOF;
484 571
         }
485 572
 
486  
-        $code = <<<EOF
  573
+        if ($definition->isLazy()) {
  574
+            $lazyInitialization    = '$lazyLoad = true';
  575
+            $lazyInitializationDoc = "\n     * @param boolean \$lazyLoad whether to try lazy-loading the"
  576
+                . " service with a proxy\n     *";
  577
+        } else {
  578
+            $lazyInitialization    = '';
  579
+            $lazyInitializationDoc = '';
  580
+        }
  581
+
  582
+        // with proxies, for 5.3.3 compatibility, the getter must be public to be accessible to the initializer
  583
+        $isProxyCandidate = $this->isProxyCandidate($definition);
  584
+        $visibility       = $isProxyCandidate ? 'public' : 'protected';
  585
+        $code             = <<<EOF
487 586
 
488 587
     /**
489 588
      * Gets the '$id' service.$doc
490  
-     *
  589
+     *$lazyInitializationDoc
491 590
      * $return
492 591
      */
493  
-    protected function get{$name}Service()
  592
+    {$visibility} function get{$name}Service($lazyInitialization)
494 593
     {
495 594
 
496 595
 EOF;
497 596
 
  597
+        $code .= $isProxyCandidate ? $this->addProxyLoading($id, $definition) : '';
  598
+
498 599
         if (!in_array($scope, array(ContainerInterface::SCOPE_CONTAINER, ContainerInterface::SCOPE_PROTOTYPE))) {
499 600
             $code .= <<<EOF
500 601
         if (!isset(\$this->scopedServices['$scope'])) {
@@ -1179,6 +1280,29 @@ private function getServiceCall($id, Reference $reference = null)
1179 1280
     }
1180 1281
 
1181 1282
     /**
  1283
+     * Tells if the given definitions are to be used for proxying, and if proxying is possible,
  1284
+     * since ProxyManager may not be available
  1285
+     *
  1286
+     * @param Definition $definition
  1287
+     *
  1288
+     * @return bool
  1289
+     */
  1290
+    private function isProxyCandidate(Definition $definition)
  1291
+    {
  1292
+        if (!(
  1293
+            $definition->isLazy()
  1294
+            && $definition->getClass()
  1295
+            && class_exists('ProxyManager\\Factory\\LazyLoadingValueHolderFactory')
  1296
+        )) {
  1297
+            return false;
  1298
+        }
  1299
+
  1300
+        $class = $this->dumpValue($definition->getClass());
  1301
+
  1302
+        return (boolean) preg_match('/^\'[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class);
  1303
+    }
  1304
+
  1305
+    /**
1182 1306
      * Returns the next name to use
1183 1307
      *
1184 1308
      * @return string
3  src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php
@@ -133,6 +133,9 @@ private function addService($definition, $id, \DOMElement $parent)
133 133
         if ($definition->isSynchronized()) {
134 134
             $service->setAttribute('synchronized', 'true');
135 135
         }
  136
+        if ($definition->isLazy()) {
  137
+            $service->setAttribute('lazy', 'true');
  138
+        }
136 139
 
137 140
         foreach ($definition->getTags() as $name => $tags) {
138 141
             foreach ($tags as $attributes) {
4  src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
@@ -106,6 +106,10 @@ private function addService($id, $definition)
106 106
             $code .= sprintf("        factory_class: %s\n", $definition->getFactoryClass());
107 107
         }
108 108
 
  109
+        if ($definition->isLazy()) {
  110
+            $code .= sprintf("        lazy: true\n");
  111
+        }
  112
+
109 113
         if ($definition->getFactoryMethod()) {
110 114
             $code .= sprintf("        factory_method: %s\n", $definition->getFactoryMethod());
111 115
         }
2  src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
@@ -148,7 +148,7 @@ private function parseDefinition($id, $service, $file)
148 148
             $definition = new Definition();
149 149
         }
150 150
 
151  
-        foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'synchronized', 'abstract') as $key) {
  151
+        foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'synchronized', 'lazy', 'abstract') as $key) {
152 152
             if (isset($service[$key])) {
153 153
                 $method = 'set'.str_replace('-', '', $key);
154 154
                 $definition->$method((string) $service->getAttributeAsPhp($key));
4  src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
@@ -157,6 +157,10 @@ private function parseDefinition($id, $service, $file)
157 157
             $definition->setSynchronized($service['synchronized']);
158 158
         }
159 159
 
  160
+        if (isset($service['lazy'])) {
  161
+            $definition->setLazy($service['lazy']);
  162
+        }
  163
+
160 164
         if (isset($service['public'])) {
161 165
             $definition->setPublic($service['public']);
162 166
         }
1  src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
@@ -87,6 +87,7 @@
87 87
     <xsd:attribute name="public" type="boolean" />
88 88
     <xsd:attribute name="synthetic" type="boolean" />
89 89
     <xsd:attribute name="synchronized" type="boolean" />
  90
+    <xsd:attribute name="lazy" type="boolean" />
90 91
     <xsd:attribute name="abstract" type="boolean" />
91 92
     <xsd:attribute name="factory-class" type="xsd:string" />
92 93
     <xsd:attribute name="factory-method" type="xsd:string" />
116  src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
@@ -14,6 +14,7 @@
14 14
 require_once __DIR__.'/Fixtures/includes/classes.php';
15 15
 require_once __DIR__.'/Fixtures/includes/ProjectExtension.php';
16 16
 
  17
+use Symfony\Component\Config\Resource\ResourceInterface;
17 18
 use Symfony\Component\DependencyInjection\Alias;
18 19
 use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
19 20
 use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -266,6 +267,32 @@ public function testCreateService()
266 267
     /**
267 268
      * @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
268 269
      */
  270
+    public function testCreateServiceWithDelegateFactory()
  271
+    {
  272
+        $builder = new ContainerBuilder();
  273
+
  274
+        $builder->register('foo1', 'FooClass')->setFile(__DIR__.'/Fixtures/includes/foo.php');
  275
+        $builder->getDefinition('foo1')->setLazy(true);
  276
+
  277
+        /* @var $foo1 \ProxyManager\Proxy\LazyLoadingInterface|\ProxyManager\Proxy\ValueHolderInterface */
  278
+        $foo1 = $builder->get('foo1');
  279
+
  280
+        $this->assertSame($foo1, $builder->get('foo1'), 'The same proxy is retrieved on multiple subsequent calls');
  281
+        $this->assertInstanceOf('\FooClass', $foo1);
  282
+        $this->assertInstanceOf('\ProxyManager\Proxy\LazyLoadingInterface', $foo1);
  283
+        $this->assertFalse($foo1->isProxyInitialized());
  284
+
  285
+        $foo1->initializeProxy();
  286
+
  287
+        $this->assertSame($foo1, $builder->get('foo1'), 'The same proxy is retrieved after initialization');
  288
+        $this->assertTrue($foo1->isProxyInitialized());
  289
+        $this->assertInstanceOf('\FooClass', $foo1->getWrappedValueHolderValue());
  290
+        $this->assertNotInstanceOf('\ProxyManager\Proxy\LazyLoadingInterface', $foo1->getWrappedValueHolderValue());
  291
+    }
  292
+
  293
+    /**
  294
+     * @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
  295
+     */
269 296
     public function testCreateServiceClass()
270 297
     {
271 298
         $builder = new ContainerBuilder();
@@ -466,6 +493,95 @@ public function testFindDefinition()
466 493
     }
467 494
 
468 495
     /**
  496
+     * @covers Symfony\Component\DependencyInjection\ContainerBuilder::addObjectResource
  497
+     */
  498
+    public function testAddObjectResource()
  499
+    {
  500
+        if (!class_exists('Symfony\Component\Config\Resource\FileResource')) {
  501
+            $this->markTestSkipped('The "Config" component is not available');
  502
+        }
  503
+
  504
+        $container = new ContainerBuilder();
  505
+
  506
+        $container->setResourceTracking(false);
  507
+        $container->addObjectResource(new \BarClass());
  508
+
  509
+        $this->assertEmpty($container->getResources(), 'No resources get registered without resource tracking');
  510
+
  511
+        $container->setResourceTracking(true);
  512
+        $container->addObjectResource(new \BarClass());
  513
+
  514
+        $resources = $container->getResources();
  515
+
  516
+        $this->assertCount(1, $resources, '1 resource was registered');
  517
+
  518
+        /* @var $resource \Symfony\Component\Config\Resource\FileResource */
  519
+        $resource = end($resources);
  520
+
  521
+        $this->assertInstanceOf('Symfony\Component\Config\Resource\FileResource', $resource);
  522
+        $this->assertSame(realpath(__DIR__.'/Fixtures/includes/classes.php'), realpath($resource->getResource()));
  523
+    }
  524
+
  525
+    /**
  526
+     * @covers Symfony\Component\DependencyInjection\ContainerBuilder::addClassResource
  527
+     */
  528
+    public function testAddClassResource()
  529
+    {
  530
+        if (!class_exists('Symfony\Component\Config\Resource\FileResource')) {
  531
+            $this->markTestSkipped('The "Config" component is not available');
  532
+        }
  533
+
  534
+        $container = new ContainerBuilder();
  535
+
  536
+        $container->setResourceTracking(false);
  537
+        $container->addClassResource(new \ReflectionClass('BarClass'));
  538
+
  539
+        $this->assertEmpty($container->getResources(), 'No resources get registered without resource tracking');
  540
+
  541
+        $container->setResourceTracking(true);
  542
+        $container->addClassResource(new \ReflectionClass('BarClass'));
  543
+
  544
+        $resources = $container->getResources();
  545
+
  546
+        $this->assertCount(1, $resources, '1 resource was registered');
  547
+
  548
+        /* @var $resource \Symfony\Component\Config\Resource\FileResource */
  549
+        $resource = end($resources);
  550
+
  551
+        $this->assertInstanceOf('Symfony\Component\Config\Resource\FileResource', $resource);
  552
+        $this->assertSame(realpath(__DIR__.'/Fixtures/includes/classes.php'), realpath($resource->getResource()));
  553
+    }
  554
+
  555
+    /**
  556
+     * @covers Symfony\Component\DependencyInjection\ContainerBuilder::compile
  557
+     */
  558
+    public function testCompilesClassDefinitionsOfLazyServices()
  559
+    {
  560
+        if (!class_exists('Symfony\Component\Config\Resource\FileResource')) {
  561
+            $this->markTestSkipped('The "Config" component is not available');
  562
+        }
  563
+
  564
+        $container = new ContainerBuilder();
  565
+
  566
+        $this->assertEmpty($container->getResources(), 'No resources get registered without resource tracking');
  567
+
  568
+        $container->register('foo', 'BarClass');
  569
+        $container->getDefinition('foo')->setLazy(true);
  570
+
  571
+        $container->compile();
  572
+
  573
+        $classesPath       = realpath(__DIR__.'/Fixtures/includes/classes.php');
  574
+        $matchingResources = array_filter(
  575
+            $container->getResources(),
  576
+            function (ResourceInterface $resource) use ($classesPath) {
  577
+                return $resource instanceof FileResource && $classesPath === realpath($resource->getResource());
  578
+            }
  579
+        );
  580
+
  581
+        $this->assertNotEmpty($matchingResources);
  582
+    }
  583
+
  584
+    /**
469 585
      * @covers Symfony\Component\DependencyInjection\ContainerBuilder::getResources
470 586
      * @covers Symfony\Component\DependencyInjection\ContainerBuilder::addResource
471 587
      */
12  src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php
@@ -165,6 +165,18 @@ public function testSetIsSynchronized()
165 165
     }
166 166
 
167 167
     /**
  168
+     * @covers Symfony\Component\DependencyInjection\Definition::setLazy
  169
+     * @covers Symfony\Component\DependencyInjection\Definition::isLazy
  170
+     */
  171
+    public function testSetIsLazy()
  172
+    {
  173
+        $def = new Definition('stdClass');
  174
+        $this->assertFalse($def->isLazy(), '->isLazy() returns false by default');
  175
+        $this->assertSame($def, $def->setLazy(true), '->setLazy() implements a fluent interface');
  176
+        $this->assertTrue($def->isLazy(), '->isLazy() returns true if the service is lazy.');
  177
+    }
  178
+
  179
+    /**
168 180
      * @covers Symfony\Component\DependencyInjection\Definition::setAbstract
169 181
      * @covers Symfony\Component\DependencyInjection\Definition::isAbstract
170 182
      */
37  src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php
@@ -52,6 +52,43 @@ public function testDumpFrozenContainerWithNoParameter()
52 52
         $this->assertNotRegexp("/function getDefaultParameters\(/", $dumpedString, '->dump() does not add getDefaultParameters() method definition.');
53 53
     }
54 54
 
  55
+    public function testDumpContainerWithProxyService()
  56
+    {
  57
+        $container = new ContainerBuilder();
  58
+
  59
+        $container->register('foo', 'stdClass');
  60
+        $container->getDefinition('foo')->setLazy(true);
  61
+        $container->compile();
  62
+
  63
+        $dumper       = new PhpDumper($container);
  64
+        $dumpedString = $dumper->dump();
  65
+
  66
+        $this->assertStringMatchesFormatFile(self::$fixturesPath.'/php/lazy_service_structure.txt', $dumpedString, '->dump() does generate proxy lazy loading logic.');
  67
+    }
  68
+
  69
+    /**
  70
+     * Verifies that the generated container retrieves the same proxy instance on multiple subsequent requests
  71
+     */
  72
+    public function testDumpContainerWithProxyServiceWillShareProxies()
  73
+    {
  74
+        require_once