Version 3 should be useable without specific DI container
SvenRtbg opened this issue · 6 comments
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.
@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.
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! :)
@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
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.
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.
Just coming back reporting that 3.0 (or more precisely 3.1 now) behave exactly as stated above. Thanks again.