-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DependencyInjection] [WIP] add a #[Memoize] method attribute #47099
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<?php | ||
|
||
namespace Symfony\Bridge\ProxyManager\AccessInterceptor\Instanciator; | ||
|
||
use ProxyManager\Factory\AccessInterceptorValueHolderFactory as BaseFactory; | ||
|
||
final class AccessInterceptorValueHolderFactory extends BaseFactory | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\DependencyInjection\Attribute; | ||
|
||
#[\Attribute(\Attribute::TARGET_CLASS)] | ||
class Memoizable | ||
{} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\DependencyInjection\Attribute; | ||
|
||
#[\Attribute(\Attribute::TARGET_METHOD)] | ||
class Memoize | ||
{ | ||
public function __construct( | ||
public string $pool, | ||
public ?string $keyGenerator = null, | ||
public ?int $ttl = null, | ||
) | ||
{} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\DependencyInjection\Compiler; | ||
|
||
use Symfony\Component\DependencyInjection\Attribute\Memoizable; | ||
use Symfony\Component\DependencyInjection\Attribute\Memoize; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use Symfony\Component\DependencyInjection\MemoizeProxy\BaseKeyGenerator; | ||
use Symfony\Component\DependencyInjection\MemoizeProxy\MemoizeFactory; | ||
use Symfony\Component\DependencyInjection\Reference; | ||
|
||
class MemoizePass implements CompilerPassInterface | ||
{ | ||
public function process(ContainerBuilder $container): void | ||
{ | ||
$container->register('dependency_injection.memoize_proxy.factory', MemoizeFactory::class) | ||
->setArguments([$container->getParameter('kernel.cache_dir').'/memoize']); | ||
$container->register('dependency_injection.memoize_proxy.key_generator.base', BaseKeyGenerator::class); | ||
|
||
foreach ($container->getDefinitions() as $id => $definition) { | ||
if (!$definition->isAutoconfigured()) { | ||
continue; | ||
} | ||
|
||
// Is MemoizeClass | ||
if (!$class = $container->getReflectionClass($definition->getClass())) { | ||
continue; | ||
} | ||
if (!$class->getAttributes(Memoizable::class, \ReflectionAttribute::IS_INSTANCEOF)) { | ||
continue; | ||
} | ||
|
||
// Get Memoize methods | ||
$methods = []; | ||
foreach ($class->getMethods() as $method) { | ||
if (!$memoize = $method->getAttributes(Memoize::class, \ReflectionAttribute::IS_INSTANCEOF)) { | ||
continue; | ||
} | ||
$memoize = $memoize[0]->newInstance(); | ||
|
||
$methods[$method->getName()] = [ | ||
new Reference($memoize->pool), | ||
new Reference($memoize->keyGenerator ?: 'dependency_injection.memoize_proxy.key_generator.base'), | ||
$memoize->ttl, | ||
]; | ||
} | ||
|
||
if (!$methods) { | ||
continue; | ||
} | ||
|
||
// Create proxy | ||
$proxy = $container->register($id.'.memoized', $definition->getClass()); | ||
$proxy->setDecoratedService($id); | ||
$proxy->setFactory(new Reference('dependency_injection.memoize_proxy.factory')); | ||
$proxy->setArguments([new Reference('.inner'), $methods]); | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\DependencyInjection\MemoizeProxy; | ||
|
||
class BaseKeyGenerator implements KeyGeneratorInterface | ||
{ | ||
/** | ||
* @inheritDoc | ||
*/ | ||
public function __invoke(string $className, string $method, array $arguments): string | ||
{ | ||
return hash('sha256', $className.$method.serialize($arguments)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just remember. You don't know what is inside in $arguments variable. May be anything, for example a closure instance that's impossible to serialize here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I totally agree. The |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\DependencyInjection\MemoizeProxy; | ||
|
||
use Psr\Cache\CacheItemInterface; | ||
use Psr\Cache\CacheItemPoolInterface; | ||
|
||
class Interceptor | ||
{ | ||
private CacheItemInterface $item; | ||
|
||
public function __construct( | ||
private readonly CacheItemPoolInterface $cache, | ||
private readonly KeyGeneratorInterface $keyGenerator, | ||
private readonly ?int $ttl = null | ||
) | ||
{ | ||
} | ||
|
||
public function getPrefixInterceptor($proxy, $instance, $method, $params, &$returnEarly) { | ||
$this->item = $this->cache->getItem(($this->keyGenerator)(\get_class($instance), $method, $params)); | ||
if ($this->item->isHit()) { | ||
$returnEarly = true; | ||
|
||
return $this->item->get(); | ||
} | ||
} | ||
|
||
public function getSuffixInterceptor($proxy, $instance, $method, $params, $returnValue) { | ||
$this->item->expiresAfter($this->ttl); | ||
$this->item->set($returnValue); | ||
$this->cache->save($this->item); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\DependencyInjection\MemoizeProxy; | ||
|
||
interface KeyGeneratorInterface | ||
{ | ||
/** | ||
* Generates a cache key for the given arguments. | ||
*/ | ||
public function __invoke(string $className, string $method, array $arguments): string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\DependencyInjection\MemoizeProxy; | ||
|
||
use ProxyManager\Configuration; | ||
use ProxyManager\FileLocator\FileLocator; | ||
use ProxyManager\GeneratorStrategy\FileWriterGeneratorStrategy; | ||
use ProxyManager\Proxy\AccessInterceptorValueHolderInterface; | ||
use Psr\Cache\CacheItemPoolInterface; | ||
use Symfony\Bridge\ProxyManager\AccessInterceptor\Instanciator\AccessInterceptorValueHolderFactory; | ||
|
||
/** | ||
* Memoize a service by creating a ProxyManager\Proxy\AccessInterceptorValueHolder | ||
*/ | ||
final class MemoizeFactory | ||
{ | ||
readonly private AccessInterceptorValueHolderFactory $factory; | ||
|
||
public function __construct(string $cacheDirectory) | ||
{ | ||
if (!is_dir($cacheDirectory)) { | ||
@mkdir($cacheDirectory, 0777, true); | ||
} | ||
|
||
$config = new Configuration(); | ||
$config->setGeneratorStrategy(new FileWriterGeneratorStrategy(new FileLocator($cacheDirectory))); | ||
$config->setProxiesTargetDir($cacheDirectory); | ||
|
||
$this->factory = new AccessInterceptorValueHolderFactory($config); | ||
} | ||
|
||
/** | ||
* @param object $service Service to memoize | ||
* @param array<array{CacheItemPoolInterface, KeyGeneratorInterface, int}> $methods | ||
*/ | ||
public function __invoke(object $service, array $methods): AccessInterceptorValueHolderInterface | ||
{ | ||
$proxy = $this->factory->createProxy($service); | ||
foreach ($methods as $name => [$cache, $key, $ttl]) { | ||
$interceptor = new Interceptor($cache, $key, $ttl); | ||
$proxy->setMethodPrefixInterceptor($name, $interceptor->getPrefixInterceptor(...)); | ||
$proxy->setMethodSuffixInterceptor($name, $interceptor->getSuffixInterceptor(...)); | ||
} | ||
|
||
return $proxy; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO we can skip this class as attribute, useless double check in compiler pass :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used it to make a first filter instead of retrieving all services' method attributes.
By the way, this attribute could contain default configuration like TTL or cache pool.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good idea. ;)