Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Lazy services - service proxies#7527

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Closed

Conversation

Ocramius
Copy link
Contributor

QA
Bug fix?no
New feature?yes
BC breaks?no
Deprecations?no
Tests pass?yes
Fixed tickets#6140 (supersedes)#5012#6102 (maybe)
LicenseMIT (attached code) - BSD-3-Clause (transitive optional dependency)
Doc PRPlease pester me to death so I do it

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

It introduces anOPTIONAL dependency toProxyManager 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{publicfunction__construct()    {// inject large object graph or do heavy computationsleep(10);    }publicfunctiondoFoo()    {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 foundhere.

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

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 viaeval
  • Generate proxies in the compiled DIC
  • 5.3 compatibility
  • Check all links to ProxyManager to ensure the dependency is completely optional
  • Handle disabledeval case
  • Add class resources to handle changes in definitions

@Crell
Copy link
Contributor

@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. :-)

@Ocramius
Copy link
ContributorAuthor

@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 useZend\Code instead. That would take me a couple of days, but is no problem at all.

@Ocramius
Copy link
ContributorAuthor

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

@lyrixx
Copy link
Member

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

@Ocramius
Copy link
ContributorAuthor

@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 explainedhere.

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

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

@@ -861,8 +864,24 @@ public function findDefinition($id)
* @throws RuntimeException When the service is a synthetic service
* @throws InvalidArgumentException When configure callable is not callable
*/
private function createService(Definition $definition, $id)
public function createService(Definition $definition, $id, $tryProxy = true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

you need to update the phpdoc

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

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

@stof
Copy link
Member

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 ofContainerBuilder::addObjectResource but with a class name rather than an instance (which is easy asaddObjectResource could then be rewritten as$this->addClassResource(get_class($object))

@stof
Copy link
Member

For the ContainerBuilder case, usingeval 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 usingeval when you disable the template cache.

@stof
Copy link
Member

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.

@stof
Copy link
Member

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.

@Ocramius
Copy link
ContributorAuthor

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

@mvrhov
Copy link

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

@Ocramius
Copy link
ContributorAuthor

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

@mvrhov
Copy link

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.

@Ocramius
Copy link
ContributorAuthor

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

@Ocramius
Copy link
ContributorAuthor

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

@jalliot
Copy link
Contributor

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, seeClassUtils) as it allows easily identifying proxies, wherever they come from, without having to test for all possible proxies interfaces out there.

@Ocramius
Copy link
ContributorAuthor

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

@jalliot for CG Lib, I kicked that out and replaced it withZend\Code, which is BSD-3-clause and therefore compatible. That's quite final, since I don't want to get back and rewritethis 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 theutility 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.

use ProxyManager\Factory\LazyLoadingValueHolderFactory;
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
use ProxyManager\Proxy\LazyLoadingInterface;
use ReflectionClass;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

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

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Will revert :)

@lsmith77
Copy link
Contributor

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.

@Ocramius
Copy link
ContributorAuthor

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

*
* @api
*/
public function addClassResource(\ReflectionClass $class)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

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

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Strict typing. This disallows unexisting classes upfront

@Ocramius
Copy link
ContributorAuthor

Still need to fix the 5.3 failures

@@ -11,6 +11,9 @@

namespace Symfony\Component\DependencyInjection\Dumper;

use ProxyManager\Generator\ClassGenerator;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

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

@beberlei
Copy link
Contributor

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

@Ocramius
Copy link
ContributorAuthor

Agreed - fixing later today

@OcramiusOcramius mentioned this pull requestApr 30, 2013
@Ocramius
Copy link
ContributorAuthor

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

@fabpot
Copy link
Member

closing in favor of#7890

@fabpotfabpot closed thisMay 1, 2013
fabpot added a commit that referenced this pull requestMay 6, 2013
This PR was squashed before being merged into the master branch (closes#7890).Discussion----------ProxyManager BridgeAs 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 itThis PR introduces lazy services along the lines ofzendframework/zendframework#4146It 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 exampleFor those who don't know what this is about, here's an example.Assuming you have a service class like following:```phpclass 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 usedThis 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 PRThere'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 anymoreCommits-------78e3710 ProxyManager Bridge
@OcramiusOcramius deleted the feature/lazy-services branchDecember 1, 2021 16:24
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers
No reviews
Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

10 participants
@Ocramius@Crell@lyrixx@stof@mvrhov@jalliot@lsmith77@fabpot@jameshalsall@beberlei

[8]ページ先頭

©2009-2025 Movatter.jp