Skip to content
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

Make it possible to decorate cache adapters with PSR6/PSR16 decorators through configuration #1

Closed
weierophinney opened this issue Dec 31, 2019 · 2 comments
Labels
Feature Request Won't Fix This will not be worked on

Comments

@weierophinney
Copy link
Member

@weierophinney

Good morning,

The configuration provider currently provides the \Zend\Cache\Service\StorageCacheAbstractServiceFactory::class abstract factory which make it possible to map pseudo services. For instance:

// Dependencies
...
'factories' => [
	\iMPSCP\ApplicationCache::class => \Zend\Cache\Service\StorageCacheAbstractServiceFactory::class
]
...

// Config
...
 'caches' => [
	\iMSCP\ApplicationCache::class => [
		'adapter' => [
			'name'    => \Zend\Cache\Storage\Adapter\Apcu::class,
			'options' => [
				'namespace' => \iMSCP\ApplicationCache::class,
			]
		],
		'plugins' => [
			\Zend\Cache\Storage\Plugin\ExceptionHandler::class => [
				'throw_exceptions' => false,
			],
			\Zend\Cache\Storage\Plugin\IgnoreUserAbort::class => [
				'exit_on_abort' => false
			]
		]
	]
]
...

However, currently, it seem that there is no way to decorate the adapters automatically with a PSR6 or PSR16 implementation. Could it be possible to add a decorator option to the adapters factory and if so, automatically return the decorated adapter? For instance:

// Config
...
 'caches' => [
	\iMSCP\ApplicationCache::class => [
		'adapter' => [
			'name'    => \Zend\Cache\Storage\Adapter\Apcu::class,
			'options' => [
				'namespace' => \iMSCP\ApplicationCache::class,
			]
		],
		'plugins' => [
			\Zend\Cache\Storage\Plugin\ExceptionHandler::class => [
				'throw_exceptions' => false,
			],
			\Zend\Cache\Storage\Plugin\IgnoreUserAbort::class => [
				'exit_on_abort' => false
			]
		],
		decorator' => \Zend\Cache\Psr\SimpleCache\SimpleCacheDecorator::class
	]
]
...

For now, I make use of a delegator but...

<?php

declare(strict_types=1);

namespace iMSCP\Foundation\Container;

use Psr\Container\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
use Zend\Cache\Psr\SimpleCache\SimpleCacheDecorator;
use Zend\Cache\Storage\StorageInterface;

class ApplicationCacheDelegatorFactory
{
    /**
     * Decorate a cache adapter with a PSR16 implementation
     *
     * @param ContainerInterface $container
     * @param string $serviceName
     * @param callable $callback
     * @return CacheInterface
     */
    public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): CacheInterface
    {
        try {
            /** @var StorageInterface $storage */
            $storage = $callback();
            
            if($container->get('config')['debug'] ?? false) {
                $storage->setOptions([
                    'readable'  => false,
                    'writable'  => false,
                ]);
            }
            
            // PSR-16 implementation
            $storage = new SimpleCacheDecorator($storage);
        } catch (\Throwable $e) {
            // PSR-16 implementation (fallback)
            $storage = $this->fallbackPsr16Impl();
        }

        return $storage;
    }

    protected function fallbackPsr16Impl()
    {
        return (new class implements CacheInterface
        {
            public function get($key, $default = NULL)
            {
                return false;
            }

            public function set($key, $value, $ttl = NULL)
            {
                return false;
            }

            public function delete($key)
            {
                return false;
            }

            public function clear()
            {
                return false;
            }

            public function getMultiple($keys, $default = NULL)
            {
                return [];
            }

            public function setMultiple($values, $ttl = NULL)
            {
                return false;
            }
            
            public function deleteMultiple($keys)
            {
                return false;
            }

            public function has($key)
            {
                return false;
            }
        });
    }
}

Originally posted by @nuxwin at zendframework/zend-cache#179

@boesing
Copy link
Member

boesing commented Jul 27, 2021

I've created something similar for a PSR-11 LoggerFactory in a project I am working with:

final class LoggerFactory
{
    public const LOGGER_CONFIGURATIONS = 'loggers';

    /**
     * @psalm-var class-string<LoggerInterface>
     */
    private $configurationIdentifier;

    /**
     * @psalm-param class-string<LoggerInterface> $configurationIdentifier References the configuration identifier
     *                                                                     within the {@see LoggerFactory::LOGGER_CONFIGURATIONS}
     *                                                                     configuration map.
     */
    public function __construct(string $configurationIdentifier)
    {
        $this->configurationIdentifier = $configurationIdentifier;
    }

    public function __invoke(ContainerInterface $container): LoggerInterface
    {
        $config = $this->detectConfiguration($container, $this->configurationIdentifier);

        return $container->get(LoggerFactoryInterface::class)->createLogger(
            $this->configurationIdentifier,
            $container,
            $config
        );
    }

    /**
     * @return non-empty-array<string,mixed>
     */
    private function detectConfiguration(ContainerInterface $container, string $configurationIdentifier): array
    {
        $config = $container->get('config');
        Assert::isArrayAccessible($config);

        $loggerConfigurations = $config[self::LOGGER_CONFIGURATIONS] ?? [];
        Assert::isMap($loggerConfigurations);

        $loggerConfigurationForIdentifier = $loggerConfigurations[$configurationIdentifier] ?? null;

        if ($loggerConfigurationForIdentifier === null) {
            throw new RuntimeException(sprintf(
                'Cannot locate configuration for logger identifier %s',
                $configurationIdentifier
            ));
        }

        Assert::isNonEmptyMap($loggerConfigurationForIdentifier);

        return $loggerConfigurationForIdentifier;
    }
}

The project configurations looks like this:

<?php
return [
    'loggers' => [
        ApplicationLoggerInterface::class => [
            // logger config
        ],
    ],
    'dependencies' => [
        'factories' => [ApplicationLoggerInterface::class => new LoggerFactory(ApplicationLoggerInterface::class)],
    ],
];

ApplicationLoggerInterface

interface ApplicationLoggerInterface extends \Psr\Log\LoggerInterface
{}

Something like this could be achieved with both PSR-6 and PSR-16 interfaces.
The problem is, that it is usually not possible to type-hint against that interface and thus using that interface for auto-wiring wont work unless we're hacking compatibility as I tried with our logging logic:

final class LoggerFactory implements LoggerFactoryInterface
{
    private const WRAPPED_LOGGER_TEMPLATE = <<<'EOC'
        return new class(%s) implements %s {
            use \Psr\Log\LoggerTrait;

            /**
             * @var \Psr\Log\LoggerInterface
             */
            private $logger;

            public function __construct(\Psr\Log\LoggerInterface $logger)
            {
                $this->logger = $logger;
            }

            public function log($level, $message, array $context = [])
            {
                $this->logger->log($level, $message, $context);
            }
        };

        EOC;

    
    public function createLogger(string $name, ContainerInterface $container, array $config): LoggerInterface
    {
        
        $this->assertInterfaceDoesNotImplementAnyOtherMethodThanPsrLoggerInterface($name);

        /** @psalm-suppress MixedAssignment */
        $wrappedLogger = eval(sprintf(self::WRAPPED_LOGGER_TEMPLATE, '$logger', $name));
        assert($wrappedLogger instanceof LoggerInterface);

        return $wrappedLogger;
    }

    /**
     * @psalm-param class-string $name
     * @psalm-assert class-string<LoggerInterface> $name
     */
    private function assertInterfaceDoesNotImplementAnyOtherMethodThanPsrLoggerInterface(string $name): void
    {
        $reflection = new ReflectionClass($name);
        if (!$reflection->isInterface()) {
            throw InvalidLoggerConfigurationException::fromInvalidLoggerIdentifier($name);
        }

        if (!$reflection->implementsInterface(LoggerInterface::class)) {
            throw InvalidLoggerConfigurationException::fromInvalidLoggerIdentifier($name);
        }

        $methodNames = $this->extractPublicMethodsFromReflection($reflection);

        if ($methodNames !== $this->getLoggerInterfaceMethods()) {
            throw InvalidLoggerConfigurationException::fromInvalidLoggerIdentifier($name);
        }
    }
    
    private function extractPublicMethodsFromReflection(ReflectionClass $class): array
    {
        // omitted for readability
        return [];
    }
    
    private function getLoggerInterfaceMethods(): array
    {
        // omitted for readability
        return [];
    }
}

The main issue here is, that one has to verify that the interface which extends the LoggerInterface does not implement own methods as that wont work with the decorating logic.
However, this could be a way to achieve this feature request.

Let me know what you think, I'm open for feedback.

@boesing
Copy link
Member

boesing commented Aug 26, 2023

The midterm goal is to actually implement PSR interfaces directly via StorageInterface and thus, I'd say until some1 is working on this, I'm closing here.

@boesing boesing closed this as completed Aug 26, 2023
@boesing boesing added the Won't Fix This will not be worked on label Aug 26, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature Request Won't Fix This will not be worked on
Projects
None yet
Development

No branches or pull requests

2 participants