Skip to content

Commit

Permalink
merged branch Ocramius/feature/proxy-manager-bridge (PR #7890)
Browse files Browse the repository at this point in the history
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/zendframework#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
  • Loading branch information
fabpot committed May 6, 2013
2 parents 4dd9cc6 + b5bfea4 commit 4c43083
Show file tree
Hide file tree
Showing 21 changed files with 584 additions and 27 deletions.
123 changes: 107 additions & 16 deletions ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator;

/**
* ContainerBuilder is a DI container that provides an API to easily describe services.
Expand Down Expand Up @@ -71,6 +73,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface

private $trackResources = true;

/**
* @var InstantiatorInterface|null
*/
private $proxyInstantiator;

/**
* Sets the track resources flag.
*
Expand All @@ -94,6 +101,16 @@ public function isTrackingResources()
return $this->trackResources;
}

/**
* Sets the instantiator to be used when fetching proxies.
*
* @param InstantiatorInterface $proxyInstantiator
*/
public function setProxyInstantiator(InstantiatorInterface $proxyInstantiator)
{
$this->proxyInstantiator = $proxyInstantiator;
}

/**
* Registers an extension.
*
Expand Down Expand Up @@ -222,15 +239,30 @@ public function setResources(array $resources)
* @api
*/
public function addObjectResource($object)
{
if ($this->trackResources) {
$this->addClassResource(new \ReflectionClass($object));
}

return $this;
}

/**
* Adds the given class hierarchy as resources.
*
* @param \ReflectionClass $class
*
* @return ContainerBuilder The current instance
*/
public function addClassResource(\ReflectionClass $class)
{
if (!$this->trackResources) {
return $this;
}

$parent = new \ReflectionObject($object);
do {
$this->addResource(new FileResource($parent->getFileName()));
} while ($parent = $parent->getParentClass());
$this->addResource(new FileResource($class->getFileName()));
} while ($class = $class->getParentClass());

return $this;
}
Expand Down Expand Up @@ -417,8 +449,10 @@ public function has($id)
*
* @return object The associated service
*
* @throws InvalidArgumentException if the service is not defined
* @throws LogicException if the service has a circular reference to itself
* @throws InvalidArgumentException when no definitions are available
* @throws InactiveScopeException when the current scope is not active
* @throws LogicException when a circular dependency is detected
* @throws \Exception
*
* @see Reference
*
Expand Down Expand Up @@ -584,6 +618,12 @@ public function compile()
foreach ($this->compiler->getPassConfig()->getPasses() as $pass) {
$this->addObjectResource($pass);
}

foreach ($this->definitions as $definition) {
if ($definition->isLazy() && ($class = $definition->getClass()) && class_exists($class)) {
$this->addClassResource(new \ReflectionClass($class));
}
}
}

$this->compiler->compile($this);
Expand Down Expand Up @@ -865,20 +905,40 @@ public function findDefinition($id)
*
* @param Definition $definition A service definition instance
* @param string $id The service identifier
* @param Boolean $tryProxy Whether to try proxying the service with a lazy proxy
*
* @return object The service described by the service definition
*
* @throws RuntimeException When the scope is inactive
* @throws RuntimeException When the factory definition is incomplete
* @throws RuntimeException When the service is a synthetic service
* @throws InvalidArgumentException When configure callable is not callable
*
* @internal this method is public because of PHP 5.3 limitations, do not use it explicitly in your code
*/
private function createService(Definition $definition, $id)
public function createService(Definition $definition, $id, $tryProxy = true)
{
if ($definition->isSynthetic()) {
throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The DIC does not know how to construct this service.', $id));
}

if ($tryProxy && $definition->isLazy()) {
$container = $this;

$proxy = $this
->getProxyInstantiator()
->instantiateProxy(
$container,
$definition,
$id, function () use ($definition, $id, $container) {
return $container->createService($definition, $id, false);
}
);
$this->shareService($definition, $proxy, $id);

return $proxy;
}

$parameterBag = $this->getParameterBag();

if (null !== $definition->getFile()) {
Expand All @@ -903,16 +963,9 @@ private function createService(Definition $definition, $id)
$service = null === $r->getConstructor() ? $r->newInstance() : $r->newInstanceArgs($arguments);
}

if (self::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) {
if (self::SCOPE_CONTAINER !== $scope && !isset($this->scopedServices[$scope])) {
throw new InactiveScopeException($id, $scope);
}

$this->services[$lowerId = strtolower($id)] = $service;

if (self::SCOPE_CONTAINER !== $scope) {
$this->scopedServices[$scope][$lowerId] = $service;
}
if ($tryProxy || !$definition->isLazy()) {
// share only if proxying failed, or if not a proxy
$this->shareService($definition, $service, $id);
}

foreach ($definition->getMethodCalls() as $call) {
Expand Down Expand Up @@ -1019,6 +1072,20 @@ public static function getServiceConditionals($value)
return $services;
}

/**
* Retrieves the currently set proxy instantiator or instantiates one.
*
* @return InstantiatorInterface
*/
private function getProxyInstantiator()
{
if (!$this->proxyInstantiator) {
$this->proxyInstantiator = new RealServiceInstantiator();
}

return $this->proxyInstantiator;
}

/**
* Synchronizes a service change.
*
Expand Down Expand Up @@ -1057,4 +1124,28 @@ private function callMethod($service, $call)

call_user_func_array(array($service, $call[0]), $this->resolveServices($this->getParameterBag()->resolveValue($call[1])));
}

/**
* Shares a given service in the container
*
* @param Definition $definition
* @param mixed $service
* @param string $id
*
* @throws InactiveScopeException
*/
private function shareService(Definition $definition, $service, $id)
{
if (self::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) {
if (self::SCOPE_CONTAINER !== $scope && !isset($this->scopedServices[$scope])) {
throw new InactiveScopeException($id, $scope);
}

$this->services[$lowerId = strtolower($id)] = $service;

if (self::SCOPE_CONTAINER !== $scope) {
$this->scopedServices[$scope][$lowerId] = $service;
}
}
}
}
26 changes: 26 additions & 0 deletions Definition.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class Definition
private $synthetic;
private $abstract;
private $synchronized;
private $lazy;

protected $arguments;

Expand All @@ -58,6 +59,7 @@ public function __construct($class = null, array $arguments = array())
$this->public = true;
$this->synthetic = false;
$this->synchronized = false;
$this->lazy = false;
$this->abstract = false;
$this->properties = array();
}
Expand Down Expand Up @@ -599,6 +601,30 @@ public function isSynchronized()
return $this->synchronized;
}

/**
* Sets the lazy flag of this service.
*
* @param Boolean $lazy
*
* @return Definition The current instance
*/
public function setLazy($lazy)
{
$this->lazy = (Boolean) $lazy;

return $this;
}

/**
* Whether this service is lazy.
*
* @return Boolean
*/
public function isLazy()
{
return $this->lazy;
}

/**
* Sets whether this definition is synthetic, that is not constructed by the
* container, but dynamically injected.
Expand Down
Loading

0 comments on commit 4c43083

Please sign in to comment.