Skip to content

[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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, good idea. ;)

{}
14 changes: 14 additions & 0 deletions src/Symfony/Component/DependencyInjection/Attribute/Memoize.php
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,
)
{}
}
60 changes: 60 additions & 0 deletions src/Symfony/Component/DependencyInjection/Compiler/MemoizePass.php
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
Expand Up @@ -47,6 +47,7 @@ public function __construct()
new AttributeAutoconfigurationPass(),
new ResolveInstanceofConditionalsPass(),
new RegisterEnvVarProcessorsPass(),
new MemoizePass(),
],
-1000 => [new ExtensionCompilerPass()],
];
Expand Down
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));
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I totally agree. The KeyGenerator is configurable to allow user to handle this kind of specific cases.

}
}
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;
}
}