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

Version 3 should be useable without specific DI container #160

Closed
SvenRtbg opened this issue Aug 6, 2021 · 6 comments
Closed

Version 3 should be useable without specific DI container #160

SvenRtbg opened this issue Aug 6, 2021 · 6 comments
Labels

Comments

@SvenRtbg
Copy link

SvenRtbg commented Aug 6, 2021

Feature Request

This library should be usable standalone when attempting to create instances of caching objects.

Q A
New Feature yes
RFC yes
BC Break no

Summary

The static factories StorageFactory and PatternFactory are deprecated in the latest V2.12, however their use case does not seem to be recreated properly in alternative methods.

The way I think I'm supposed to consume this library in V3 is that I have to use either Mezzio or Laminas MVC because they provide a means to consume the output of the ConfigProvider class - which is very Laminas-specific configuration code that will not run on its own or explain by itself what has to be done in order to create a class that is able to do caching.

My use case: I have multiple applications, which use about any possible DI container, for example PHP-DI, Pimple or Laminas. I cannot rule out homebrew solutions as well, and possibly no DI in very old applications.

These applications have access to quite a number of libraries that access HTTP-based services (SOAP and REST), so caching them is one required feature. And each of these library has a static factory call that anybody can use to get a working instance of the service client. I intentionally did not want to tie these libraries into any DI container, i.e. I am unable to "just use one DI everywhere", so instead I opted for an interface that any DI container is able to access.

Going one step deeper, these client factories have to include the caching layer, and for that they are calling another static factory of mine, that is eventually calling the StorageFactory mentioned above. This acts mostly as a convenience layer.

I would be able to add any dependency injection required for Laminas to build StorageInterface instances in the future, however I have no clue how to do this without being required to effectively grab quite an amount of otherwise unused dependencies unrelated to the task, build one of the mentioned DI containers by injecting the configuration, and then get a working instance. Something that used to be a simple static call with some carefully prepared configuration array.

I do understand removing legacy waste is an issue, and I have no intention of reverting the deprecation, but there is some information gap right now, and maybe functionality gap as well.

Currently I cannot "inject StorageAdapterFactoryInterface" because the implementing class StorageAdapterFactory requires two dependencies, both of them having non-trivial constructor parameters themselves.

The StorageAdapterFactoryFactory also doesn't help because it requires a properly configured container.

@boesing
Copy link
Member

boesing commented Aug 6, 2021

@SvenRtbg You can always create all the instances by yourself using constructor injection.

As per design, v3 will and can never now which storage adapters are in upstream projects without populating them to the AdapterPluginManager. This will work automagically in laminas-mvc and mezzio projects.

Could you please provide more informations on how you are using StorageFactory or PatternFactory as of today so we can find a solution?

Happy to help here so you don't have to require plenty of dependencies you wont use otherwise.

@SvenRtbg
Copy link
Author

SvenRtbg commented Aug 7, 2021

I'll add more details if necessary on Monday, but for the time being:

I am using the ObjectCache pattern in my libraries. The library defines an interface, I have an implementation for it, and then I have another implementation extends ObjectCache that implements all methods and simply does a forwarding to the call() method with method name and parameters. That way I can strictly separate concerns: One class does the real thing, the other only does caching.

This setup requires a bit of configuration, so I add the original class into the cache class to do the real work, and I have to add a storage adapter. I create that storage adapter effectively by the static call to StorageFactory in order to create either a file storage or a memcache storage - that's what my storage factory lib is doing: Pulling in configuration, calling the Laminas factory, returning the storage object.

The library factory is passing it into the caching instance together with additional config to get it into a use case specific state (that's where the real strength in Laminas Cache is: Ability to add about any custom code to influence the caching workflow if necessary - I liked it when it was called Zend Cache ;) ).

So the task I am facing right now: How would I instantiate a StorageAdapterFactory? I was looking at the 2.12.1 code base, btw.

Investigating with the 3.0.x branch:
public function __construct(\Laminas\ServiceManager\PluginManagerInterface $adapters, \Laminas\Cache\Service \StoragePluginFactoryInterface $pluginFactory)

Requires the StoragePluginFactory:
public function __construct(\Laminas\ServiceManager\PluginManagerInterface $plugins)

And both require the ServiceManagers PluginManagerInterface implementation, which is an abstract class, so there is a local implementation here: Laminas\Cache\Storage\PluginManager. No constructor, so am I fine? Unfortunately, not, because the Laminas\ServiceManager\AbstractPluginManager requires some very unspecific configuration arrays.
public function __construct($configInstanceOrParentLocator = null, array $config = [])

And that's where I am stuck. I will be perfectly able to "just" push all these instances into each other until this particular step.

And maybe I am already missing something until here, e.g. the two PluginManagerInterface instances not being the same object. If so, the configuration problem has just doubled (or maybe it exists as a whole from the start, and I'd just have to decide which parts go where).

Anyways: Happy to have your help here to understand things better! :)

@boesing
Copy link
Member

boesing commented Aug 8, 2021

@SvenRtbg As far as I can see, laminas/laminas-servicemanager is a requirement of laminas/laminas-cache and thus, its already part of your application unless you are replacing it via composer.json to get rid of it.


And maybe I am already missing something until here, e.g. the two PluginManagerInterface instances not being the same object.

Exactly, in the laminas-servicemanager world, "plugin managers" are containers which only return instances of ONE specific type. So there is a plugin manager for storage adapters (AdapterPluginManager) and a plugin manager for storage plugins (PluginManager). Both plugin managers return different types of instances, e.g. StorageInterface or PluginInterface.


So one solution for you could be to replace the StorageFactory with a static factory created by yourself. Something like this might work out of the box if you only have Memory and Filesystem adapters.

class YourOwnStorageFactory
{
    /**
     * @var StorageAdapterFactoryInterface|null
     */
    private static $storageAdapterFactory;
    
    public static function factory(array $config): StorageInterface
    {
        $storageFactory = self::storageAdapterFactorySingleton();
        return $storageFactory->createFromArrayConfiguration($config);
    }

    /**
     * Singletons are considered bad, for more about this topic read this article
     * https://www.michaelsafyan.com/tech/design/patterns/singleton
     * But as you don't use dependency injection, thats actually a way to restore `StorageFactory` behavior as it was done
     * in laminas-cache v2.
     */
    private static function storageAdapterFactorySingleton(): StorageAdapterFactoryInterface
    {
        if (self::$storageAdapterFactory) {
            return self::$storageAdapterFactory;
        }
        
        $config = array_merge_recursive(
            (new \Laminas\Cache\ConfigProvider())(),
            // Starting with v3 you will have to uncomment these
            // (new \Laminas\Cache\Storage\Adapter\Memory\ConfigProvider())(),
            // (new \Laminas\Cache\Storage\Adapter\Filesystem\ConfigProvider())(),
        );
        
        $containerConfiguration = $config['dependencies'] ?? [];
        
        $container = new ServiceManager($containerConfiguration);
        return self::$storageAdapterFactory = $container->get(StorageAdapterFactoryInterface::class);
    }
}

The reason why YOU have to create this static factory is, that only YOU know what adapters your project requires and which not. Thats the main idea behind the changes regarding laminas-cache v3. There are plenty of upstream projects out there which do not use redis or memcached or apcu or mongo DB as a storage backend. And to not enforce them to install these (as done since v1 of this component), satellite packages were introduced.


If you only use the ObjectCache pattern, just instantiate it by yourself where necessary. The current PatternFactory does not work with the new requirements of v3 (e.g. require StorageInterface as constructor dependency) and thus, there is no way to have a generic factory around as some patterns require the StorageInterface while others dont.

So wherever you need the ObjectCache or your own implementation of ObjectCache (however you decide to either pick laminas pattern or your own), you just can manually instantiate it with new ObjectCache($storage) rather than PatternFactory::factory(ObjectCache::class, ['storage' => $storage|);.


If you have any other questions, feel free to drop them here. Please keep in mind that if you want to use StorageAdapterFactoryInterface, your configuration needs to be normalized as there were multiple configuration array structures allowed in the StorageFactory::factory.

https://docs.laminas.dev/laminas-cache/storage/adapter/#quick-start

@boesing
Copy link
Member

boesing commented Nov 22, 2021

I am closing here as I think I've provided some examples which enable projects which do use laminas-cache standalone without any dependency injection. Directly instantiating cache adapters is still possible, dependency injection is only suggested if you want to use config-driven adapters. If you do use config-driven adapters, you can write your own adapter factory for that decorator you want to use.
Static factories will no longer be supported due to the reasons mentioned above.

If you have any problems during the migration to laminas-cache v3 (which was released last week), please let us know in a dedicated issue so we can see how to solve specific problems more accurately.

@boesing boesing closed this as completed Nov 22, 2021
@SvenRtbg
Copy link
Author

I'm sorry for not replying earlier. Your suggestions do work nicely, but I wasn't able to look into the 3.0 release. Will come back with any issues if I encounter them.

@SvenRtbg
Copy link
Author

Just coming back reporting that 3.0 (or more precisely 3.1 now) behave exactly as stated above. Thanks again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants