Lazy Load Services And Do NOT Inject the Container Into them: Towards Symfony2

@Ocramius and I talked about doing the problem of lazy load services the main focus of the recent PHP Peru Hacking Day V meeting. This blog records the output.

The problem we are about to describe will follow several examples from worst to best. The following script measures performance for the 5 stages that we took in our development.

<?php
 
use PHPPeru\Examples;
 
require __DIR__.'/vendor/autoload.php';
 
$examples = new Examples();
 
$exampleNames = array(
    'nastyExample',
    'goodExample',
    'goodAndFastExample',
    'goodAndFastAndAutomaticExample',
    'pimpleRefactorExample'
);
 
timeExample($exampleNames, $examples);
 
function timeExample($methodNames, $examples) {
    foreach ($methodNames as $methodName) {
        $start = microtime(true);
        call_user_func(array($examples, $methodName));
        echo $methodName . '........ ' . (microtime(true) - $start) . "\n";
    }
}

Nasty example is basically using a `lazy controller` which basically injects the whole container:

<?php
 
namespace PHPPeru;
 
use Pimple;
 
class LazyController
{
    protected $container;
 
    public function __construct(Pimple $pimple)
    {
        $this->container = $pimple;
    }
 
    public function lightAction()
    {
        return 'hello world!';
    }
 
    public function heavyAction()
    {
        /** @var $heavyObject HeavyObject */
        $heavyObject = $this->container['heavy_object'];
 
        return $heavyObject->iSayHello();
    }
}

The heavy object, whose creation is purposely forced heavy as shown in the constructor, is as follows:

<?php
 
namespace PHPPeru;
 
class HeavyObject
{
    protected $items;
 
    public function __construct()
    {
        $this->items = array();
 
        foreach(range(1,10000000) as $value) {
            $this->items[] = $value;
        }
    }
 
    public function iSayHello()
    {
        return array_rand($this->items) . ': hello!';
    }
}

The example class from which we call the lazy controller builds the services as follows:

<?php
 
namespace PHPPeru;
 
use Doctrine\Common\Proxy\ProxyGenerator;
use Pimple;
 
class Examples
{
    protected $c;
 
    public function __construct()
    {
        $this->c = new Pimple();
 
        $this->c['heavy_object'] = function($c) {
            return new HeavyObject();
        };
 
        $this->c['ioc_controller'] = function ($c) {
            return new IocController($c['heavy_object']);
        };
 
        $this->c['lazy_controller'] = function ($c) {
            return new LazyController($c);
        };
    }
 
    public function nastyExample()
    {
        /** @var $controller LazyController */
        $controller = $this->c['lazy_controller'];
 
        $controller->lightAction();
 
        /** @var $controller LazyController */
        $controller = $this->c['lazy_controller'];
 
        $controller->heavyAction();
    }

As you can see we exercise a light action and a heavy action from our controller. The heavy action actually is a hard code of the service HeavyObject using the container. This service is registered with the container with ‘heavy_object’ service id. Injecting the container from the get go kills decoupling in our controller library and makes the container build a HeavyObject from the outset even though it is not used in the light action.

The good example, following one, improves the previous problem by injecting only what our controller library will use, that is the heavy object service. When we call our good example performance worsens even though we are moving in the right direction. The container building takes longer to prepare the heavy object to be injected in the controller library so light action takes as much as the heavy action thereby duplicating this delay.

    public function goodExample()
    {
        /** @var $controller IocController */
        $controller = $this->c['ioc_controller'];
 
        $controller->lightAction();
 
        /** @var $controller IocController */
        $controller = $this->c['ioc_controller'];
 
        $controller->heavyAction();
    }

The controller used with the specific dependency passed:

<?php
 
namespace PHPPeru;
 
class IocController
{
    protected $heavyObject;
 
    public function __construct(HeavyObject $heavyObject)
    {
        $this->heavyObject = $heavyObject;
    }
 
    public function lightAction()
    {
        return 'hello world!';
    }
 
    public function heavyAction()
    {
        return $this->heavyObject->iSayHello();
    }
}

The good and fast example, next, improves performance by relying on a hand written proxy class of the heavy object service. Thus we hand only a proxy object to the previous improved controller lib, this is IocController (for inversion of control).

    public function goodAndFastExample()
    {
        $this->c['heavy_object_proxy'] = function($c) {
            return new HeavyObjectProxy($c, 'heavy_object');
        };
 
        $this->c['ioc_controller'] = function ($c) {
            return new IocController($c['heavy_object_proxy']);
        };
 
        /** @var $controller IocController */
        $controller = $this->c['ioc_controller'];
 
        $controller->lightAction();
 
        /** @var $controller IocController */
        $controller = $this->c['ioc_controller'];
 
        $controller->heavyAction();
    }

The heavy object proxy class uses a wrapped-object proxy approach:

<?php
 
namespace PHPPeru;
 
class HeavyObjectProxy extends HeavyObject
{
    protected $wrappedHeavyObject;
 
    private $__container__;
    private $__serviceId__;
 
    public function __construct($container, $serviceId)
    {
        $this->__container__ = $container;
        $this->__serviceId__ = $serviceId;
    }
 
    public function iSayHello()
    {
        $this->__load();
 
        return $this->wrappedHeavyObject->iSayHello();
    }
 
    public function __load()
    {
        if (!$this->wrappedHeavyObject) {
            $this->wrappedHeavyObject = $this->__container__[$this->__serviceId__];
        }
    }
}

This approach improves performance dramatically however heavy object service proxy is manual. Now we turn to automatize the proxy creation process using a proxy generator. A proxy generator is a device configured and used within a proxy factory to create proxy objects. This will be possible because of @Ocramius‘es latest contribution to doctrine common repository in which he decouples the specific implementations of the generator and preset us with a decoupled generator. Doctrine uses this generator for the persistence layer, however this generator (due to be on air on Doctrine 2.4) is versatile enough to be customized for our service proxy generation task.

So we implement our service proxy factory to use the new generator as follows:

<?php
 
namespace PHPPeru;
 
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Common\Proxy\Proxy;
use Doctrine\Common\Proxy\ProxyGenerator;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
 
/**
 * This factory is used to create proxy objects for services at runtime.
 */
class ServiceProxyFactory
{
    /**
     * @var ProxyGenerator the proxy generator responsible for creating the proxy classes/files.
     */
    private $proxyGenerator;
 
    /**
     * @var string
     */
    private $proxyNamespace;
 
    /**
     * @var string
     */
    private $proxyDir;
 
    /**
     * Initializes a new instance of the <tt>ProxyFactory</tt>.
     *
     * @param string $proxyDir        The directory to use for the proxy classes. It must exist.
     * @param string $proxyNamespace  The namespace to use for the proxy classes.
     */
    public function __construct($proxyDir, $proxyNamespace)
    {
        $this->proxyDir = $proxyDir;
        $this->proxyNamespace = $proxyNamespace;
    }
 
    /**
     * Gets a reference proxy instance for the service of the given type and identified by
     * the given identifier.
     *
     * @param  string $className
     * @param  mixed  $identifier
     *
     * @return object
     */
    public function getProxy($className, $identifier, $container)
    {
        $fqn = ClassUtils::generateProxyClassName($className, $this->proxyNamespace);
 
        if ( ! class_exists($fqn, false)) {
            $classMetadata = new ServiceClassMetadata($className, $identifier);
            $generator = $this->getProxyGenerator($classMetadata);
            $fileName = $generator->getProxyFileName($className);
            $generator->generateProxyClass($classMetadata);
 
            require $fileName;
        }
 
        $initializer = function (Proxy $proxy) use ($container, $identifier) {
            $proxy->__setInitializer(function () {});
            $proxy->__isInitialized__ = true;
            $proxy->__wrappedObject__ = $container[reset($identifier)];
        };
 
        $cloner = function (Proxy $proxy) {
            if ($proxy->__isInitialized()) {
                return;
            }
 
            $proxy->__setInitialized(true);
            $proxy->__setInitializer(function (){});
 
            return;
        };
 
        return new $fqn($initializer, $cloner, $identifier);
    }
 
    /**
     * @param ProxyGenerator $proxyGenerator
     */
    public function setProxyGenerator(ProxyGenerator $proxyGenerator)
    {
        $this->proxyGenerator = $proxyGenerator;
    }
 
    /**
     * @return ProxyGenerator
     */
    public function getProxyGenerator($classMetadata)
    {
        if (null === $this->proxyGenerator) {
            $methods = $this->generateMethods($classMetadata);
            $this->proxyGenerator = new ProxyGenerator($this->proxyDir, $this->proxyNamespace);
            $this->proxyGenerator->setProxyClassTemplate($this->getWrappedTemplate());
            $this->proxyGenerator->setPlaceholder('<methods>', $methods);
        }
 
        return $this->proxyGenerator;
    }
 
    public function getWrappedTemplate()
    {
        return <<<EOT
<?php
 
namespace <namespace>;
 
require_once __DIR__ . '/../vendor/autoload.php';
 
use Ladybug\Loader;
 
Loader::loadHelpers();
 
class <proxyClassName> extends \<className> implements \<baseProxyInterface>
{
    public \$__wrappedObject__;
    public \$__initializer__;
    public \$__cloner__;
    public \$__isInitialized__ = false;
 
    public static \$lazyPublicPropertiesDefaultValues = array(<lazyLoadedPublicPropertiesDefaultValues>);
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __isInitialized()
    {
        return \$this->__isInitialized__;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __setInitialized(\$initialized)
    {
        \$this->__isInitialized__ = \$initialized;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __setInitializer(\$initializer)
    {
        \$this->__initializer__ = \$initializer;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __getInitializer()
    {
        return \$this->__initializer__;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __setCloner(\$cloner)
    {
        \$this->__cloner__ = \$cloner;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __getCloner()
    {
        return \$this->__cloner__;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __getLazyLoadedPublicProperties()
    {
        return self::\$lazyPublicPropertiesDefaultValues;
    }
 
    public function __construct(\$initializer)
    {
       \$this->__initializer__ = \$initializer;
    }
 
    public function __load()
    {
        if (!\$this->__wrappedObject__) {
            \$this->__wrappedObject__ = \$this->__container__[\$this->__serviceId__];
        }
    }
    <methods>
}
EOT;
 
    }
 
    /**
     * Generates decorated methods by picking those available in the parent class
     *
     * @param  ClassMetadata $class
     *
     * @return string
     */
    public function generateMethods(ClassMetadata $class)
    {
        $methods = '';
        $methodNames = array();
        $reflectionMethods = $class->getReflectionClass()->getMethods(\ReflectionMethod::IS_PUBLIC);
        $skippedMethods = array(
            '__sleep'   => true,
            '__clone'   => true,
            '__wakeup'  => true,
            '__get'     => true,
            '__set'     => true,
        );
 
        foreach ($reflectionMethods as $method) {
            $name = $method->getName();
 
            if (
                $method->isConstructor()
                || isset($skippedMethods[strtolower($name)])
                || isset($methodNames[$name])
                || $method->isFinal()
                || $method->isStatic()
                || ! $method->isPublic()
            ) {
                continue;
            }
 
            $methodNames[$name] = true;
            $methods .= "\n" . '    public function ';
 
            if ($method->returnsReference()) {
                $methods .= '&';
            }
 
            $methods .= $name . '(';
            $firstParam = true;
            $parameterString = $argumentString = '';
            $parameters = array();
 
            foreach ($method->getParameters() as $param) {
                if ($firstParam) {
                    $firstParam = false;
                } else {
                    $parameterString .= ', ';
                    $argumentString  .= ', ';
                }
 
                $paramClass = $param->getClass();
 
                // We need to pick the type hint class too
                if (null !== $paramClass) {
                    $parameterString .= '\\' . $paramClass->getName() . ' ';
                } elseif ($param->isArray()) {
                    $parameterString .= 'array ';
                }
 
                if ($param->isPassedByReference()) {
                    $parameterString .= '&';
                }
 
                $parameters[] = '$' . $param->getName();
                $parameterString .= '$' . $param->getName();
                $argumentString  .= '$' . $param->getName();
 
                if ($param->isDefaultValueAvailable()) {
                    $parameterString .= ' = ' . var_export($param->getDefaultValue(), true);
                }
            }
 
            $methods .= $parameterString . ')';
            $methods .= "\n" . '    {' . "\n";
            //$methods .= 'ladybug_dump( $this);' . "\n";
            //ladybug_dump(this)
            $methods .= '        call_user_func($this->__initializer__, $this);' . "\n" ;
            $methods .= '        return $this->__wrappedObject__->' . $name . '(' . $argumentString . ');';
            $methods .= "\n" . '    }';
        }
 
        return $methods;
    }
 
}

Basically we have configured and used the proxy generator to generate our proxy inside a specified cache folder like:

<?php
 
namespace PHPPeru\__CG__\PHPPeru;
 
require_once __DIR__ . '/../vendor/autoload.php';
 
use Ladybug\Loader;
 
Loader::loadHelpers();
 
class HeavyObject extends \PHPPeru\HeavyObject implements \Doctrine\Common\Proxy\Proxy
{
    public $__wrappedObject__;
    public $__initializer__;
    public $__cloner__;
    public $__isInitialized__ = false;
 
    public static $lazyPublicPropertiesDefaultValues = array();
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __isInitialized()
    {
        return $this->__isInitialized__;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __setInitialized($initialized)
    {
        $this->__isInitialized__ = $initialized;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __setInitializer($initializer)
    {
        $this->__initializer__ = $initializer;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __getInitializer()
    {
        return $this->__initializer__;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __setCloner($cloner)
    {
        $this->__cloner__ = $cloner;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __getCloner()
    {
        return $this->__cloner__;
    }
 
    /**
     * {@inheritDoc}
     * @private
     */
    public function __getLazyLoadedPublicProperties()
    {
        return self::$lazyPublicPropertiesDefaultValues;
    }
 
    public function __construct($initializer)
    {
       $this->__initializer__ = $initializer;
    }
 
    public function __load()
    {
        if (!$this->__wrappedObject__) {
            $this->__wrappedObject__ = $this->__container__[$this->__serviceId__];
        }
    }
 
    public function iSayHello()
    {
        call_user_func($this->__initializer__, $this);
        return $this->__wrappedObject__->iSayHello();
    }
}

The example method uses as below the ServiceProxyFactory registered with the container and passes the proxy created with the factory to the IocController:

    public function goodAndFastAndAutomaticExample()
    {
        $this->c['cache_dir'] = __DIR__.'/../../cache';
        $this->c['phpperu_namespace'] = 'PHPPeru';
 
        $this->c['proxy_factory'] = function($c) {
            return new ServiceProxyFactory($c['cache_dir'], $c['phpperu_namespace']);
        };
 
        $this->c['heavy_object_proxy'] = function($c) {
            return $c['proxy_factory']->getProxy("PHPPeru\\HeavyObject", array("heavy_object"), $c);
        };
 
        $this->c['ioc_controller'] = function ($c) {
            return new IocController($c['heavy_object_proxy']);
        };
 
        /** @var $controller IocController */
        $controller = $this->c['ioc_controller'];
 
        $controller->lightAction();
 
        /** @var $controller IocController */
        $controller = $this->c['ioc_controller'];
 
        $controller->heavyAction();
    }

The last improvement is basically on the Container side and is a refactor that provides a service provider-like method for our Container:

public function pimpleRefactorExample()
    {
        $c = $this->c;
 
        $this->buildService('heavy_object', function($c) { return new HeavyObject(); }, true, $c);
 
        $this->c['ioc_controller'] = function ($c) {
            return new IocController($c['heavy_object']);
        };
 
        /** @var $controller IocController */
        $controller = $this->c['ioc_controller'];
 
        $controller->lightAction();
 
        /** @var $controller IocController */
        $controller = $this->c['ioc_controller'];
 
        $controller->heavyAction();
    }
 
    public function buildService($serviceId, $originalClosure, $proxied = false, $c)
    {
        if ($proxied == true) {
            $c[$serviceId] = $c->share(function ($c) use ($originalClosure, $serviceId) {
                $this->c['cache_dir'] = __DIR__.'/../../cache';
                $this->c['phpperu_namespace'] = 'PHPPeru';
                //$fqcn = get_class(call_user_func($originalClosure, null));
                $c[$serviceId . '_pimple_safe_object'] = $originalClosure;
                $factory = new ServiceProxyFactory($c['cache_dir'], $c['phpperu_namespace']);
                return $factory->getProxy("PHPPeru\\HeavyObject", array($serviceId . '_pimple_safe_object'), $c);
            });
        } else {
            $c[$serviceId] = $c->share($originalClosure);
        }
    }

Here the process has been totally hidden from userland and the proxy generation is done for you.

This blog post was inspired by the discussion in https://github.com/symfony/symfony/issues/5012 .

I would like to thank you and acknowledge first and foremost the Lord Jesus Christ for this totally undeserved opportunity to work on this and enjoy it so much.

Thanks to @Ocramius for all the insight and help provided all throughout the development of this work. He is one of the top Doctrine contributors and also very active in the Symfony2 and ZendFramework2 communities. He also is very well known in Peru where he has accompanied already several times during our Hacking Days at PHPPeru community.

Thanks to the participants of the Hacking Day that learned more and helped to make this a reality.

The performance results are:

~ php test.php 
nastyExample........ 20.5599629879
goodExample........ 37.61582493782
goodAndFastExample........ 16.416062116623
goodAndFastAndAutomaticExample........ 15.935558080673
pimpleRefactorExample........ 12.27201294899

As you can see they look pretty good, you now may see the repo at https://github.com/cordoval/service-proxies.

If you would like to show me your support to continue writing great blog post please drop by the donate page. I hope to hear your constructive feedback on this and thanks for your attention.

5 thoughts on “Lazy Load Services And Do NOT Inject the Container Into them: Towards Symfony2

  1. Pingback: Symfony2 Get Your Parameter’s On Board Too Like A Boss :) Or Maybe A Newbie :’( ? | Craft It Online!

  2. An interesting discussion is definitely worth comment.

    There’s no doubt that that you ought to publish more on this
    subject matter, it might not be a taboo matter but generally people do
    not discuss such subjects. To the next! Best wishes!!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>