Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use ReflectionParameter;
use ReflectionType;
use ReflectionUnionType;
use Rikudou\MemoizeBundle\Attribute\Memoizable;
use Rikudou\MemoizeBundle\Attribute\Memoize;
use Rikudou\MemoizeBundle\Attribute\NoMemoize;
use Rikudou\MemoizeBundle\Cache\InMemoryCachePool;
Expand All @@ -27,17 +28,46 @@ final class MemoizeProxyCreatorCompilerPass implements CompilerPassInterface
{
private ContainerBuilder $container;

/**
* @var array<string, array{service_id: string, class_name: class-string, memoize_seconds: int, methods: array<array{name: string, memoize_seconds: int}>}>
*/
private array $additionalServicesConfig = [];

public function process(ContainerBuilder $container): void
{
if (!$container->getParameter('rikudou.memoize.enabled')) {
return;
}
$this->container = $container;

assert(is_array($container->getParameter('rikudou.internal.memoize.additional_services')));

/** @var array<int, array{service_id: string, class_name: class-string, memoize_seconds: int, methods: array<array{name: string, memoize_seconds: int}>}> $additionalServices */
$additionalServices = array_map(function (array $service) use ($container): array {
$definition = $container->getDefinition($service['service_id']);
if (!$class = $definition->getClass()) {
throw new RuntimeException("There is no class for service '{$service['service_id']}'");
}
$service['class_name'] = $class;

return $service;
}, $container->getParameter('rikudou.internal.memoize.additional_services'));

$services = [];
foreach ($additionalServices as $additionalService) {
foreach ($additionalService['methods'] as $key => $method) {
$additionalService['methods'][$method['name']] = $method;
unset($additionalService['methods'][$key]);
}
$this->additionalServicesConfig[$additionalService['class_name']] = $additionalService;
$services[] = $additionalService['service_id'];
}

$this->cleanupDirectory();

$cacheServiceName = $container->getParameter('rikudou.memoize.cache_service');
assert(is_string($cacheServiceName));
$services = array_keys($container->findTaggedServiceIds('rikudou.memoize.memoizable_service'));
$services = [...$services, ...array_keys($container->findTaggedServiceIds('rikudou.memoize.memoizable_service'))];

foreach ($services as $service) {
$definition = $container->getDefinition($service);
Expand Down Expand Up @@ -239,6 +269,7 @@ private function shouldMemoize(ReflectionMethod $method): bool
if ($method->getReturnType() !== null && $this->getType($method->getReturnType()) === 'never') {
return false;
}

return (
$this->getAttribute($method, Memoize::class) !== null
|| $this->getAttribute($method->getDeclaringClass(), Memoize::class) !== null
Expand All @@ -257,11 +288,44 @@ private function shouldMemoize(ReflectionMethod $method): bool
private function getAttribute(ReflectionMethod|ReflectionClass $target, string $attribute): ?object
{
$attributes = $target->getAttributes($attribute);
if (!count($attributes)) {
return null;
if (count($attributes)) {
return $attributes[array_key_first($attributes)]->newInstance();
}

// todo make this better
if ($attribute === Memoize::class) {
$class = $target instanceof ReflectionClass ? $target->getName() : $target->getDeclaringClass()->getName();
if (!isset($this->additionalServicesConfig[$class])) {
return null;
}
$config = $this->additionalServicesConfig[$class];

if ($target instanceof ReflectionClass) {
if (count($config['methods'])) {
return null;
}

$seconds = $config['memoize_seconds'];
} else {
if (!isset($config['methods'][$target->getName()])) {
return null;
}

$seconds = $config['methods'][$target->getName()]['memoize_seconds'] ?? $config['memoize_seconds'];
}

// @phpstan-ignore-next-line
return new Memoize($seconds);
} elseif ($attribute === Memoizable::class) {
assert($target instanceof ReflectionClass);

// @phpstan-ignore-next-line
return isset($this->additionalServicesConfig[$target->getName()])
? new Memoizable()
: null;
}

return $attributes[array_key_first($attributes)]->newInstance();
return null;
}

/**
Expand Down
28 changes: 28 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,34 @@ public function getConfigTreeBuilder(): TreeBuilder
->info('The default cache service to use. If default_memoize_seconds is set to -1 this setting is ignored and internal service is used.')
->defaultValue('cache.app')
->end()
->arrayNode('memoize_services')
->info('List additional services that you want to be memoized.')
->arrayPrototype()
->children()
->scalarNode('service_id')
->info('The service ID.')
->end()
->scalarNode('memoize_seconds')
->info('The seconds to memoize the service for. Defaults to null which means the value of default_memoize_seconds.')
->defaultNull()
->end()
->arrayNode('methods')
->info('List of methods to memoize. Leave empty to memoize all of them.')
->arrayPrototype()
->children()
->scalarNode('name')
->info('The method name.')
->end()
->scalarNode('memoize_seconds')
->info('The seconds to memoize the method for. Defaults to null which means the value of memoize_seconds from the parent service.')
->defaultNull()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end();

return $treeBuilder;
Expand Down
23 changes: 23 additions & 0 deletions src/DependencyInjection/RikudouMemoizeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,28 @@ static function (ChildDefinition $definition): void {
$container->setParameter('rikudou.memoize.cache_service', $configs['cache_service']);
$container->setParameter('rikudou.memoize.default_memoize_seconds', $configs['default_memoize_seconds']);
$container->setParameter('rikudou.memoize.enabled', $configs['enabled']);
$container->setParameter(
'rikudou.internal.memoize.additional_services',
$this->filterAdditionalServices($configs['memoize_services'], $configs['default_memoize_seconds']),
);
}

/**
* @param array<int, array{service_id: string|null, memoize_seconds: int|null, methods: array<array{name: string|null, memoize_seconds: int|null}>}> $services
*
* @return array<int, array{service_id: string, memoize_seconds: int, methods: array<array{name: string, memoize_seconds: int}>}>
*/
private function filterAdditionalServices(array $services, int $defaultMemoizeSeconds): array
{
return array_map(function (array $service) use ($defaultMemoizeSeconds): array {
$service['memoize_seconds'] ??= $defaultMemoizeSeconds;
$service['methods'] = array_map(function (array $method) use ($service): array {
$method['memoize_seconds'] ??= $service['memoize_seconds'];

return $method;
}, array_filter($service['methods'], fn (array $method) => $method['name'] !== null));

return $service;
}, array_filter($services, fn (array $service) => $service['service_id'] !== null));
}
}