laminas / laminas-cache

Caching implementation with a variety of storage options, as well as codified caching strategies for callbacks, classes, and output
https://docs.laminas.dev/laminas-cache/
BSD 3-Clause "New" or "Revised" License
98 stars 51 forks source link

Documentation: Add integration examples for `mezzio` and `laminas-mvc` #168

Open boesing opened 2 years ago

boesing commented 2 years ago

Documentation Improvements

Summary

We do actually lacking examples on how to integrate this library into laminas-mvc or mezzio. One of our users found an old blog post written by @samsonasik in 2013 (pointed me on that via Slack).

Luckily, I've chose that configuration style (just the plugins config is not compatible with v3.0) and thus that helped the user. I would prefer having these examples as part of our documentation.

TL;DR

Add examples on how to implement caches configuration for the StorageCacheAbstractServiceFactory and cache configuration for the StorageCacheFactory.

froschdesign commented 2 years ago

@boesing I would like to add the description for the integration in laminas-mvc, with a quick and convenient way. What is the best option to use multiple storage adapters with ReflectionBasedAbstractFactory of laminas-servicemanager?

boesing commented 2 years ago

I have no idea. I do not use ReflectionBasedAbstractFactory at all in my applications and thus, do not have that problem.

We are fetching adapters via caches config entry.

The key for the caches entry is stored in a constant like:

final class Caches
{
     public const KEY_VALUE_CACHE = 'key-value-cache';
}

Then we do use something like:

use Psr\SimpleCache\CacheInterface;

interface KeyValueCacheInterface extends CacheInterface
{}

In combination with a specific factory:

final class KeyValueCacheFactory
{
    public function __invoke(ContainerInterface $container): 
    {
         // returns the storage interface based on the `caches` configuration
         $storage = $container->get(Caches::KEY_VALUE_CACHE);

         return new SimpleCacheDecorator($storage);
    }
}

One could provide an abstract decorator to actually implement the real interface by using something like the following instead of directly returning the simple cache decorator:

return new class(new SimpleCacheDecorator($storage)) extends AbstractSimpleCacheDecorator implements KeyValueCacheInterface 
{
}

But as I said, we do not use ReflectionBasedAbstractFactory so thats only an idea.

froschdesign commented 2 years ago

I have no idea. I do not use ReflectionBasedAbstractFactory at all in my applications and thus, do not have that problem.

It's quite simple, the name for the cache must be an existing class or interface. Example:

return [
    'caches' => [
        Laminas\Cache\Storage\StorageInterface::class => [
            'adapter' => Laminas\Cache\Storage\Adapter\Filesystem::class,
            'options' => [
                'cache_dir' => __DIR__ . '/../../data/cache',
            ],
        ],
    ],
    // …
];

This will work with the ReflectionBasedAbstractFactory:

namespace Application\Controller;

use Laminas\Cache\Storage\StorageInterface;
use Laminas\Mvc\Controller\AbstractActionController;

final class IndexController extends AbstractActionController
{
    private StorageInterface $cache;

    public function __construct(StorageInterface $cache)
    {
        $this->cache = $cache;
    }

    // …
}
return [
    'controllers'  => [
        'factories' => [
            Application\Controller\IndexController::class => Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory::class,
        ],
    ],
    // …
];

But for multiple storage adapters, a different name is needed and the name of the generic storage interface no longer works. And using the concrete class name of a storage adapter binds it to the type of storage which is also not desired.

Then we do use something like:

use Psr\SimpleCache\CacheInterface;

interface KeyValueCacheInterface extends CacheInterface
{}

The idea of creating a separate interface is good, but on the other hand it is another step. :thinking:

froschdesign commented 2 years ago

Another option is ConfigAbstractFactory of laminas-servicemanager, which needs some more configuration but allows any string as a name for the dependency. Example:

return [
    'caches' => [
        'default-cache' => [
            'adapter' => Laminas\Cache\Storage\Adapter\Filesystem::class,
            'options' => [
                'cache_dir' => __DIR__ . '/../../data/cache',
            ],
        ],
    ],
    // …
];
return [
    'controllers' => [
        'factories' => [
            Application\Controller\IndexController::class => Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory::class,
        ],
    ],
    Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory::class => [
        Application\Controller\IndexController::class => [
            'default-cache',
        ],
    ],
    // …
];
froschdesign commented 1 year ago

@boesing How could the Mezzio integration look like if the abstract factory (StorageCacheAbstractServiceFactory) can not be used with Pimple or Aura.DI? I am looking again for a simple solution that is user-friendly without having to create classes or interfaces.

Xerkus commented 1 year ago

How could the Mezzio integration look like if the abstract factory (StorageCacheAbstractServiceFactory) can not be used with Pimple or Aura.DI?

If you refer to one of those only allowing objects as services, most of our factories need an array and some are outright asserting config is array. Those factories are not usable with aura.di

boesing commented 1 year ago

How could the Mezzio integration look like if the abstract factory (StorageCacheAbstractServiceFactory) can not be used with Pimple or Aura.DI?

I have no clue, havent ever worked with these containers. Wasn't even aware that these do not support abstract_factories or initializers. I'd rather implement proper support for these than suggesting people to use stuff in the other way. Most if not any component provided by laminas is somehow built to be used with the servicemanager. It will even get installed when requiring this component.

I am not sure if I feel confident when we have projects out there consuming components which are meant to be consumed via the service-manager as the ConfigProvider does provide service-manager specific config and then it is not consumed via the service-manager but via an implementation which does only support a part of what servicemanager supports.

We could probably decorate these containers and provide abstract_factories support in the appropriate components?

https://github.com/laminas/laminas-auradi-config https://github.com/laminas/laminas-pimple-config

Something like

use Psr\Container\ContainerInterface;

final class AbstractFactoriesContainerDecorator implements ContainerInterface
{
     public function __construct(private readonly ContainerInterface $container, private readonly array $abstractFactories)
     {}
     public function get(string $id): mixed
     {
            try {
                return $this->container->get($id);
            } catch (ServiceNotFoundException $exception) {
                 foreach ($this->abstractFactories as $factory) {
                       if ($factory->canCreate($id)) { return $factory($this, $id); );
                 } 
                 throw $exception;
            }
     }

     public function has(string $id): bool
     {
          if ($this->container->has($id)) { return true; }
          foreach ($this->abstractFactories as $factory) { 
               if ($factory->canCreate($id)) { return true; }
          }
         return false;
      }
}

Please keep in mind that ReflectionBasedAbstractFactory is not able to retrieve services from the container which are not identified as string(config) or class-string. So these projects you are mentioning above will most likely already have an class-string/interface alias for their code. I personally do not use the ConfigAbstractFactory but yes, that could be a solution as well.

froschdesign commented 1 year ago

I have no clue, havent ever worked with these containers. Wasn't even aware that these do not support abstract_factories or initializers.

I have never used them either and was surprised that there is no full support.

We could probably decorate these containers and provide abstract_factories support in the appropriate components?

I think this would be useful and necessary.

froschdesign commented 9 months ago

@boesing For Mezzio the ConfigAbstractFactory can also be used:

return [
    Laminas\Cache\Service\StorageCacheAbstractServiceFactory::CACHES_CONFIGURATION_KEY => [
        'default-cache' => [
            'adapter' => Laminas\Cache\Storage\Adapter\Filesystem::class,
            'options' => [
                'cache_dir' => __DIR__ . '/../../data/cache',
            ],
        ],
    ],
];
namespace App;

use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Mezzio\Template\TemplateRendererInterface;

final class ConfigProvider
{
    public function __invoke(): array
    {
        return [
            'dependencies'               => $this->getDependencies(),
            'templates'                  => $this->getTemplates(),
            ConfigAbstractFactory::class => $this->getConfigurationMap(),
        ];
    }

    public function getDependencies(): array
    {
        return [
            'factories'  => [
                Handler\ExampleHandler::class  => ConfigAbstractFactory::class,
            ],
        ];
    }

    public function getConfigurationMap(): array
    {
        return [
            Handler\ExampleHandler::class => [
                'default-cache',
                TemplateRendererInterface::class,
            ],
        ];
    }

    // …
}
namespace App\Handler;

use Laminas\Cache\Storage\StorageInterface;
use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class ExampleHandler implements RequestHandlerInterface
{
    public function __construct(
        private readonly StorageInterface $cache,
        private readonly TemplateRendererInterface $templateRenderer
    ) {
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        if (! $this->cache->hasItem('example')) {
            $this->cache->addItem('example', 'value');
        }

        $example = $this->cache->getItem('example'); // value;

        return new HtmlResponse(
            $this->templateRenderer->render('app::example')
        );
    }
}

What do you think?

boesing commented 9 months ago

Sure, that could be a way and thus could be mentioned for sure 👍🏻

works for me.