Skip to content

Commit

Permalink
Merge pull request #951 from dmaicher/issue-899
Browse files Browse the repository at this point in the history
Add ContainerEntityListenerResolver and load entity listeners lazy by default
  • Loading branch information
alcaeus committed Apr 11, 2019
2 parents 5ad6645 + 2926cd4 commit f5c10ec
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 134 deletions.
59 changes: 43 additions & 16 deletions DependencyInjection/Compiler/EntityListenerPass.php
Expand Up @@ -2,8 +2,12 @@

namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;

use Doctrine\Bundle\DoctrineBundle\Mapping\ContainerEntityListenerResolver;
use Doctrine\Bundle\DoctrineBundle\Mapping\EntityListenerServiceResolver;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;

Expand All @@ -19,6 +23,8 @@ public function process(ContainerBuilder $container)
{
$resolvers = $container->findTaggedServiceIds('doctrine.orm.entity_listener');

$lazyServiceReferencesByResolver = [];

foreach ($resolvers as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) {
$name = isset($attributes['entity_manager']) ? $attributes['entity_manager'] : $container->getParameter('doctrine.default_entity_manager');
Expand All @@ -41,35 +47,44 @@ public function process(ContainerBuilder $container)
$this->attachToListener($container, $name, $id, $attributes);
}

if (isset($attributes['lazy']) && $attributes['lazy']) {
$resolverClass = $this->getResolverClass($resolver, $container);
$resolverSupportsLazyListeners = is_a($resolverClass, EntityListenerServiceResolver::class, true);

$lazyByAttribute = isset($attributes['lazy']) && $attributes['lazy'];
if ($lazyByAttribute && ! $resolverSupportsLazyListeners) {
throw new InvalidArgumentException(sprintf(
'Lazy-loaded entity listeners can only be resolved by a resolver implementing %s.',
EntityListenerServiceResolver::class
));
}

if (! isset($attributes['lazy']) && $resolverSupportsLazyListeners || $lazyByAttribute) {
$listener = $container->findDefinition($id);

if ($listener->isAbstract()) {
throw new InvalidArgumentException(sprintf('The service "%s" must not be abstract as this entity listener is lazy-loaded.', $id));
}

$interface = 'Doctrine\\Bundle\\DoctrineBundle\\Mapping\\EntityListenerServiceResolver';
$class = $resolver->getClass();

if (substr($class, 0, 1) === '%') {
// resolve container parameter first
$class = $container->getParameterBag()->resolveValue($resolver->getClass());
}
$resolver->addMethodCall('registerService', [$listener->getClass(), $id]);

if (! is_a($class, $interface, true)) {
throw new InvalidArgumentException(
sprintf('Lazy-loaded entity listeners can only be resolved by a resolver implementing %s.', $interface)
);
// if the resolver uses the default class we will use a service locator for all listeners
if ($resolverClass === ContainerEntityListenerResolver::class) {
if (! isset($lazyServiceReferencesByResolver[$resolverId])) {
$lazyServiceReferencesByResolver[$resolverId] = [];
}
$lazyServiceReferencesByResolver[$resolverId][$id] = new Reference($id);
} else {
$listener->setPublic(true);
}

$listener->setPublic(true);

$resolver->addMethodCall('registerService', [$listener->getClass(), $id]);
} else {
$resolver->addMethodCall('register', [new Reference($id)]);
}
}
}

foreach ($lazyServiceReferencesByResolver as $resolverId => $listenerReferences) {
$container->findDefinition($resolverId)->setArgument(0, ServiceLocatorTagPass::register($container, $listenerReferences));
}
}

private function attachToListener(ContainerBuilder $container, $name, $id, array $attributes)
Expand All @@ -94,4 +109,16 @@ private function attachToListener(ContainerBuilder $container, $name, $id, array

$container->findDefinition($listenerId)->addMethodCall('addEntityListener', $args);
}

private function getResolverClass(Definition $resolver, ContainerBuilder $container) : string
{
$resolverClass = $resolver->getClass();

if (substr($resolverClass, 0, 1) === '%') {
// resolve container parameter first
$resolverClass = $container->getParameterBag()->resolveValue($resolver->getClass());
}

return $resolverClass;
}
}
106 changes: 4 additions & 102 deletions Mapping/ContainerAwareEntityListenerResolver.php
Expand Up @@ -2,107 +2,9 @@

namespace Doctrine\Bundle\DoctrineBundle\Mapping;

use InvalidArgumentException;
use RuntimeException;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ContainerAwareEntityListenerResolver implements EntityListenerServiceResolver
/**
* @deprecated since DoctrineBundle 1.11 and will be removed in 2.0. Use ContainerEntityListenerResolver instead.
*/
class ContainerAwareEntityListenerResolver extends ContainerEntityListenerResolver
{
/** @var ContainerInterface */
private $container;

/** @var object[] Map to store entity listener instances. */
private $instances = [];

/** @var string[] Map to store registered service ids */
private $serviceIds = [];

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

/**
* {@inheritdoc}
*/
public function clear($className = null)
{
if ($className === null) {
$this->instances = [];

return;
}

$className = $this->normalizeClassName($className);

if (! isset($this->instances[$className])) {
return;
}

unset($this->instances[$className]);
}

/**
* {@inheritdoc}
*/
public function register($object)
{
if (! is_object($object)) {
throw new InvalidArgumentException(sprintf('An object was expected, but got "%s".', gettype($object)));
}

$className = $this->normalizeClassName(get_class($object));

$this->instances[$className] = $object;
}

/**
* {@inheritdoc}
*/
public function registerService($className, $serviceId)
{
$this->serviceIds[$this->normalizeClassName($className)] = $serviceId;
}

/**
* {@inheritdoc}
*/
public function resolve($className)
{
$className = $this->normalizeClassName($className);

if (! isset($this->instances[$className])) {
if (isset($this->serviceIds[$className])) {
$this->instances[$className] = $this->resolveService($this->serviceIds[$className]);
} else {
$this->instances[$className] = new $className();
}
}

return $this->instances[$className];
}

/**
* @param string $serviceId
*
* @return object
*/
private function resolveService($serviceId)
{
if (! $this->container->has($serviceId)) {
throw new RuntimeException(sprintf('There is no service named "%s"', $serviceId));
}

return $this->container->get($serviceId);
}

/**
* @param string $className
*
* @return string
*/
private function normalizeClassName($className)
{
return trim($className, '\\');
}
}
103 changes: 103 additions & 0 deletions Mapping/ContainerEntityListenerResolver.php
@@ -0,0 +1,103 @@
<?php

namespace Doctrine\Bundle\DoctrineBundle\Mapping;

use InvalidArgumentException;
use Psr\Container\ContainerInterface;
use RuntimeException;

/**
* @final
*/
class ContainerEntityListenerResolver implements EntityListenerServiceResolver
{
/** @var ContainerInterface */
private $container;

/** @var object[] Map to store entity listener instances. */
private $instances = [];

/** @var string[] Map to store registered service ids */
private $serviceIds = [];

/**
* @param ContainerInterface $container a service locator for listeners
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

/**
* {@inheritdoc}
*/
public function clear($className = null)
{
if ($className === null) {
$this->instances = [];

return;
}

$className = $this->normalizeClassName($className);

unset($this->instances[$className]);
}

/**
* {@inheritdoc}
*/
public function register($object)
{
if (! is_object($object)) {
throw new InvalidArgumentException(sprintf('An object was expected, but got "%s".', gettype($object)));
}

$className = $this->normalizeClassName(get_class($object));

$this->instances[$className] = $object;
}

/**
* {@inheritdoc}
*/
public function registerService($className, $serviceId)
{
$this->serviceIds[$this->normalizeClassName($className)] = $serviceId;
}

/**
* {@inheritdoc}
*/
public function resolve($className)
{
$className = $this->normalizeClassName($className);

if (! isset($this->instances[$className])) {
if (isset($this->serviceIds[$className])) {
$this->instances[$className] = $this->resolveService($this->serviceIds[$className]);
} else {
$this->instances[$className] = new $className();
}
}

return $this->instances[$className];
}

/**
* @return object
*/
private function resolveService(string $serviceId)
{
if (! $this->container->has($serviceId)) {
throw new RuntimeException(sprintf('There is no service named "%s"', $serviceId));
}

return $this->container->get($serviceId);
}

private function normalizeClassName(string $className) : string
{
return trim($className, '\\');
}
}
2 changes: 1 addition & 1 deletion Resources/config/orm.xml
Expand Up @@ -62,7 +62,7 @@
<parameter key="doctrine.orm.quote_strategy.ansi.class">Doctrine\ORM\Mapping\AnsiQuoteStrategy</parameter>

<!-- entity listener resolver -->
<parameter key="doctrine.orm.entity_listener_resolver.class">Doctrine\Bundle\DoctrineBundle\Mapping\ContainerAwareEntityListenerResolver</parameter>
<parameter key="doctrine.orm.entity_listener_resolver.class">Doctrine\Bundle\DoctrineBundle\Mapping\ContainerEntityListenerResolver</parameter>

<!-- second level cache -->
<parameter key="doctrine.orm.second_level_cache.default_cache_factory.class">Doctrine\ORM\Cache\DefaultCacheFactory</parameter>
Expand Down

0 comments on commit f5c10ec

Please sign in to comment.