I'd like to inject an array of objects that implement a common interface into one of my services. I am using zend servicemanager as the DI container. I have been reading the docs for quite a bit now and it seems to me that AbstractPluginManager is the way to go. I haven't been able to make it work though.
Is there an example using an AbstractPluginManager + Zend Expressive 3 that I can take a look at?
My ultimate goal is to dynamically inject all registered classes that implement a common interface into my service.
Example:
interface I{}
class A implements I{}
class B implements I{}
class C{}
MyService
__construct(array Iimplementations){...}
$service = $container->get('myservice')
$service has Iimplementations
Thanks in advance
The AbstractPluginManager is mostly for validation and filter plugins. You can create classes and while validating, you can pass specific configuration which makes the filter or validator re-usable.
What you are looking for is probably an abstract factory. You register the factory once and it can create a service for you. In your case with a specific set of dependencies.
interface I{}
class A implements I{}
class B implements I{}
class MyAbstractFactory implements AbstractFactoryInterface
{
public function canCreate(ContainerInterface $container, $requestedName)
{
return in_array('I', class_implements($requestedName), true);
}
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new $requestedName(
$container->get(DependencyFoo::class),
$container->get(DependencyBar::class)
);
}
}
// config/autoload/dependencies.global.php
return [
'dependencies' => [
'factories' => [
// ...
],
'abstract_factories' => [
MyAbstractFactory::class,
],
],
];
You can also go crazy and use reflection to detect dependencies if they are different for each class, however that adds a lot of overhead. I think it's easier and more maintainable to create separate factories. And then there is zend-expressive-tooling which is a cli tool that can create factories, handlers and middleware.
/*Getting I concrete implementations via the plugin manager will ensure the implementation of the I interface*/
class IPluginManager extends AbstractPluginManager
{
protected $instanceOf = I::class;
public function getIConcreteImplementations()
{
$concreteImpl = [];
foreach(array_keys($this->factories) as $key)
{
$concreteImpl[] = $this->get($key);
}
return $concreteImpl;
}
}
/*IPluginManagerFactory*/
class TransactionSourcePluginManagerFactory
{
const CONFIG_KEY = 'i-implementations-config-key';
public function __invoke(ContainerInterface $container, $name, array $options = null)
{
$pluginManager = new IPluginManager($container, $options ?: []);
// If this is in a zend-mvc application, the ServiceListener will inject
// merged configuration during bootstrap.
if ($container->has('ServiceListener')) {
return $pluginManager;
}
// If we do not have a config service, nothing more to do
if (! $container->has('config')) {
return $pluginManager;
}
$config = $container->get('config');
// If we do not have validators configuration, nothing more to do
if (! isset($config[self::CONFIG_KEY]) || !
is_array($config[self::CONFIG_KEY])) {
return $pluginManager;
}
// Wire service configuration for validators
(new Config($config[self::CONFIG_KEY]))->configureServiceManager($pluginManager);
return $pluginManager;
}
}
/*In the ConfigProvider of the module or global config*/
class ConfigProvider
{
/**
* Returns the configuration array
*
* To add a bit of a structure, each section is defined in a separate
* method which returns an array with its configuration.
*
*/
public function __invoke() : array
{
return [
'dependencies' => $this->getDependencies(),
'routes' => $this->getRoutes(),
'i-implementations-config-key' => $this->getIConcreteImplementations(),
];
}
public function getIConcreteImplementations() : array
{
return [
'factories' => [
A::class => AFactory::class,
B::class => InvokableFactory::class,
],
];
}
}
/*I can now be sure that I am injecting an array of I implementations into my Service*/
class ServiceFactory
{
public function __invoke(ContainerInterface $container) : Service
{
$pluginManager = $container->get(IPluginManager::class);
$impl = $pluginManager->getIConcreteImplementations();
return new Service($impl);
}
}
Related
In ZF3 you would normally attach your event listener for the MvcEvent's in your module's Module.php like so:
<?php
namespace MyModule;
class Module
{
public function onBootstrap(MvcEvent $event)
{
$eventManager = $event->getApplication()->getEventManager();
$eventManager->attach(MvcEvent::EVENT_DISPATCH, function(MvcEvent $event) {
// Do someting...
});
}
}
Now there are two typical situations where your Module.php can grow big:
Your module has to handle multiple (or even all) MvcEvent's and maybe even treat them in different ways.
Your module has to perform multiple actions on a single MvcEvent.
What I'd like to be able to do is to specify a class name in my module.config.php along with one or multiple MvcEvent names to keep my Module.php nice and clean.
Is there a way to do this in Zend Framework 3?
#Nukeface has a great example but it does not directly answer my specific question.
To answer my own question:
This is possible with the use of listeners. A listener can be configured in the configuration files but it cannot be mapped to an event directly from the configuration alone.
It is possible to check for a specific setting in the configuration and determine what classes to map to what events. Even MvcEvents can be mapped this way.
Here's how to set it up:
1. The listener
We want to listen to multiple MvcEvents with one simple class. Note the class it extends.
namespace Demo\Listener;
class MyListener extends EventClassMapListener
{
public function handleEvent(MvcEvent $event)
{
// Do something
\Zend\Debug\Debug::dump($event->getName());
}
}
2. The abstract listener class
The above class needs a bit more body but that can be provided by the abstract listener class:
namespace Demo\Listener;
abstract class EventClassMapListener implements ListenerAggregateInterface
{
private $configuration;
public function __construct(array $configuration)
{
$this->configuration = $configuration;
}
public function attach(EventManagerInterface $events, $priority = 1)
{
$sharedManager = $events->getSharedManager();
foreach ($this->configuration as $identifier => $settings) {
foreach ($settings as $event => $configPriority) {
$sharedManager->attach($identifier, $event, [$this, 'handleEvent'], $configPriority ?: $priority);
}
}
}
public function detach(EventManagerInterface $events)
{
// Do the opposite of attach
}
abstract public function handleEvent(MvcEvent $event);
}
3. The factory
Now we need a factory that we can reuse for all our classes that need to listen to multiple events:
namespace Demo\Factory\Listener;
class EventClassmapListenerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$globalConfiguration = $container->get('config');
$configuration = [];
if (array_key_exists('event_classmap', $globalConfiguration)
&& array_key_exists($requestedName, $globalConfiguration['event_classmap'])
) {
$configuration = $globalConfiguration['event_classmap'][$requestedName];
}
return new $requestedName($configuration);
}
}
4. Configuration
In your module.config.php:
'service_manager' => [
'factories' => [
Listener\MyListener::class => Factory\Listener\EventClassmapListenerFactory::class,
],
],
'listeners' => [
Listener\MyListener::class,
],
'event_classmap' => [
// Name of the class that needs to listen to events
Listener\MyListener::class => [
// Identifier
\Zend\Mvc\Application::class => [
// List of event names and priorities
MvcEvent::EVENT_BOOTSTRAP => 1,
],
// Another identifier
MyEventEmitterClass::class => [
MyEventEmitterClass::EVENT_ONE,
MyEventEmitterClass::EVENT_TWO,
MyEventEmitterClass::EVENT_THREE,
],
],
],
Conclusion:
Although it might not really refined, I really like this idea. It is now fairly easy to add another listener and make it listen to a list of events from one or more emitters.
My opinion after some research
A listener itself should state what it wants to listen to, to keep things strict. Putting that information in a configuration file might result in a more complicated situation when it is not needed.
You need a few things for Listener classes:
Events
Listeners
Handlers
Factories
Config
Now, 2 & 3 are usually in the same class as you would usually have a Listener class for a specific purpose. Such as "Listen for Rocket launch and steer Rocket to Mars".
As such, you would need to "create" these "events" to listen for somewhere. Such as a DemoEvents class!
namespace Demo\Event;
use Zend\EventManager\Event;
class DemoEvent extends Event
{
const THE_STRING_TO_LISTEN_FOR = 'rocket.ready.for.launch';
const ANOTHER_STRING_TO_LISTEN_FOR = 'rocket.steer.to.mars';
}
Now that we have "events", we need to "listen" for them. For that we need a Listener. Because I'm limiting this to 1 example, the Handler (function(-ality) to be executed when the "event" we're "listening" for is "heard") will be in the same class.
namespace Demo\Listener;
use Demo\Event\DemoEvent;
use Zend\EventManager\Event;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
class DemoListener implements ListenerAggregateInterface
{
/**
* #var array
*/
protected $listeners = [];
/**
* #param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(Demo::class, DemoEvent::THE_STRING_TO_LISTEN_FOR, [$this, 'doSomethingOnTrigger'], -10000);
}
/**
* Apart from triggering specific Listener function and de-registering itself, it does nothing else. Add your own functionality
*
* #param Event $event
*/
public function doSomethingOnTrigger(Event $event)
{
// Gets passed along parameters from the ->trigger() function elsewhere
$params = $event->getParams();
$specificClass = $params[SpecificClass::class];
// Do something useful here
$specificClass->launchRocketIntoOrbit();
// Detach self to prevent running again
$specificClass->getEventManager()->getSharedManager()->clearListeners(get_class($specificClass), $event->getName());
// NOTE: USE THIS TRIGGER METHODOLOGY ELSEWHERE USING THE STRING FROM THE ATTACH() FUNCTION TO TRIGGER THIS FUNCTION
// Trigger events specific for the Entity/class (this "daisy-chains" events, allowing for follow-up functionality)
$specificClass->getEventManager()->trigger(
DemoEvent::ANOTHER_STRING_TO_LISTEN_FOR,
$specificClass ,
[get_class($specificClass) => $specificClass ] // Params getting passed along
);
}
}
Excellent. We now have a events, a listener and a handler. We just need a factory to create this class when needed.
namespace Demo\Factory;
use Demo\Listener;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class DemoListenerFactory implements FactoryInterface
{
/**
* #param ContainerInterface $container
* #param string $requestedName
* #param array|null $options
* #return object|DemoListener
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
// If you're implementation of the Listener has any requirements, load them here and add a constructor in the DemoListener class
return new DemoListener();
}
}
Lastly, we need some config. Obviously we need to register the Listener + Factory combination. Let's do that first.
namespace Demo;
use Demo\Listener\DemoListener;
use Demo\Listener\DemoListenerFactory;
'service_manager' => [
'factories' => [
DemoListener::class => DemoListenerFactory::class,
],
],
Now for a little known bit of config to make sure that the Listener gets registered as a Listener:
'listeners' => [
DemoListener::class
],
Yep, that's it.
Make sure to add both of these bits of config at the first level of config, they're siblings.
I use Zend Expressive framework via default ZE skeleton app with Zend ServiceManager as DIC and Plates as template engine.
Let's say I've got index.phtml template. I want to get some service, which dumps me assets, smth like:
<?= $this->getContainer()->get('my service class')->dumpAssets() ?>
Service is registered via factory and accesible in the app:
<? $container->get('my service class') ?>
How to pass external service instance or its result into template?
It's pretty much bad practice to inject the entire service container into a template (or any other class except a factory). A better approach would be to write an extension to dump the assets.
Extension class:
<?php
namespace App\Container;
use League\Plates\Engine;
use League\Plates\Extension\ExtensionInterface;
use App\Service\AssetsService;
class DumpAssetsExtension implements ExtensionInterface
{
public $assetsService;
/**
* AssetsExtension constructor.
* #param $container
*/
public function __construct(AssetsService $assetsService)
{
$this->assetsService = $assetsService;
}
public function register(Engine $engine)
{
$engine->registerFunction('dumpAssets', [$this, 'dumpAssets']);
}
public function dumpAssets()
{
return $this->assetsService->dumpAssets();
}
}
Factory:
<?php
namespace App\Container;
use Interop\Container\ContainerInterface;
class DumpAssetsFactory
{
public function __invoke(ContainerInterface $container)
{
$assetsService = $container->get(App\Service\AssetsService::class);
return new PlatesExtension($assetsService);
}
}
Configuration:
<?php
return [
// ...
'factories' => [
App\Container\DumpAssetsExtension::class => App\Container\DumpAssetsFactory::class,
]
];
In your template:
<?php
$service = $this->dumpAssets();
?>
I figured out how to access container from template engine via extensions. It's not clear MVC-ly, but...
At first, add plates config into config/autoload/templates.global:
return [
// some othe settings
'plates' => [
'extensions' => [
App\Container\PlatesExtension::class,
],
],
],
At second, create App\Container\PlatesExtension.php with code:
<?php
namespace App\Container;
use League\Plates\Engine;
use League\Plates\Extension\ExtensionInterface;
class PlatesExtension implements ExtensionInterface
{
public $container;
/**
* AssetsExtension constructor.
* #param $container
*/
public function __construct($container)
{
$this->container = $container;
}
public function register(Engine $engine)
{
$engine->registerFunction('container', [$this, 'getContainer']);
}
public function getContainer()
{
return $this->container;
}
}
At third, create factory App\Container\PlatesExtensionFactory.php to inject container into plates extension:
<?php
namespace App\Container;
use Interop\Container\ContainerInterface;
class PlatesExtensionFactory
{
public function __invoke(ContainerInterface $container)
{
return new PlatesExtension($container);
}
}
Next, register plates extension in ServiceManager (config/dependencies.global.php):
return [
// some other settings
'factories' => [
App\Container\PlatesExtension::class => App\Container\PlatesExtensionFactory::class,
]
];
At last, get container and needed service from Plates template:
<?
$service = $this->container()->get('my service class');
?>
I found a few other posts relevant to this issue, however i wasn't able to achieve what i wanted so i decided to delete everything and start over with some help...
This is my work so far, which does the job but the data are provided hard coded in an array and i need to create a database connection to fetch those data.
In my module class i have:
public function getViewHelperConfig()
{
return array(
'factories' => array(
'liveStreaming' => function() {
return new LiveStreaming();
},
),
);
}
This is the code i have in my view helper:
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
class LiveStreaming extends AbstractHelper
{
protected $liveStreamingTable;
public function __invoke()
{
$events = array(
'1' => array('name' => 'Event name',
'sport' => 'Soccer',
'time' => '11:30'),
'2' => array('name' => 'Event name',
'sport' => 'Soccer',
'time' => '17:00'),
);
return $events;
//this is what should be used (or something like that) to get the data from the db...
//return array('events' => $this->getLiveStreamingTable()->fetchAll() );
}
public function getLiveStreamingTable()
{
if (!$this->liveStreamingTable) {
$sm = $this->getServiceLocator();
$this->liveStreamingTable = $sm->get('LiveStreaming\Model\LiveStreamingTable');
}
return $this->liveStreamingTable;
}
}
So, i want to get the $events array from the database. I've created Application\Model\LiveStreaming and Application\Model\LiveStreamingTable (following the instructions of the ZF2 official tutorial) and i need some help proceeding to the next step, which should probably have to do with the service locator.
You seem to be almost there. The only thing missing is the ability to call $this->getServiceLocator(); from within the view helper (as the AbstractHelper doesn't provide this method).
There are two options
Inject the LiveStreamingTable into the view helper directly
inject the ServiceManager itself and create the LiveStreamingTable within the helper
Option 1 Make LiveStreamingTable a dependency of the view helper (type hint in constructor)
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
use LiveStreaming\Model\LiveStreamingTable;
class LiveStreaming extends AbstractHelper
{
protected $liveStreamingTable;
public function __construct(LiveStreamingTable $liveStreamingTable)
{
$this->liveStreamingTable = $liveStreamingTable;
}
public function getLiveStreamingTable()
{
return $this->liveStreamingTable;
}
}
And the factory becomes:
public function getViewHelperConfig()
{
return array(
'factories' => array(
'liveStreaming' => function($sl) {
// Get the shared service manager instance
$sm = $sl->getServiceLocator();
$liveStreamingTable = $sm->get('LiveStreaming\Model\LiveStreamingTable');
// Now inject it into the view helper constructor
return new LiveStreaming($liveStreamingTable);
},
),
);
}
Option 2 - Implement the ServiceLocatorAwareInterface (making it again a dependency of the view helper)
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class LiveStreaming extends AbstractHelper implements ServiceLocatorAwareInterface
{
protected $serviceLocator;
protected $liveStreamingTable;
public function __construct(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
public function setServiceLocator(ServiceLocatorInterface $serviceLocator);
public function getServiceLocator();
public function getLiveStreamingTable()
{
if (null == $this->liveStreamingTable) {
$this->liveStreamingTable = $this->getServiceLocator()->get('LiveStreaming\Model\LiveStreamingTable');
}
return $this->liveStreamingTable;
}
}
Your factory will then look like:
public function getViewHelperConfig()
{
return array(
'factories' => array(
'liveStreaming' => function($sl) {
// Get the shared service manager instance
$sm = $sl->getServiceLocator();
// Now inject it into the view helper constructor
return new LiveStreaming($sm);
},
),
);
}
Personally, I feel that Option 1 makes more sense from a Dependency Injection (DI) point of view - It's clear that the LiveStreamingTable is what is needed to create the view helper.
Edit
Make sure you have the LiveStreaming\Model\LiveStreamingTable service also registered with the service manager (as we request it in the above code when we did $sm->get('LiveStreaming\Model\LiveStreamingTable');)
// Module.php
public function getServiceConfig()
{
return array(
'factories' => array(
'LiveStreaming\Model\LiveStreamingTable' => function($sm) {
// If you have any dependencies for the this instance
// Such as the database adapter etc either create them here
// or request it from the service manager
// for example:
$foo = $sm->get('Some/Other/Registered/Service');
$bar = new /Directly/Created/Instance/Bar();
return new \LiveStreaming\Model\LiveStreamingTable($foo, $bar);
},
),
);
}
I'm trying to make the Zend\ServiceManager use Zend\Di to create my instances, since I have pre-scanned and cached DI definitions already. I realize this might come with a speed penalty but on the other hand, I need to write a lot less meta-code.
The ServiceManager documentation says that
the ServiceManager also provides optional ties to Zend\Di, allowing Di
to act as an initializer or an abstract factory for the manager.
But I don't find any examples of how make the ServiceManager use Zend\Di. I'm not even sure where I should set this up, maybe in Module::getServiceConfig()? Can anyone provide some example code?
The following works for me. In order to make Zend\Di compatible with Zend\ServiceManager, I extended a class MyLib\Di\Di from Zend\Di\Di which implements the AbstractFactoryInterface.
namespace MyLib\Di;
use Zend\ServiceManager\AbstractFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class Di extends \Zend\Di\Di implements AbstractFactoryInterface
{
public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
{
return true;
}
public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
{
return $this->get($requestedName);
}
}
Now, I can use MyLib\Di\Di as a fallback abstract factory for Zend\ServiceManager. Here's an example of how I create my IndexController. The IndexController's dependencies (constructor parameters) are injected automatically.
class Module
{
...
public function getServiceConfig()
{
$this->di = new \MyLib\Di\Di;
$this->configureDi($this->di); // Set up definitions and shared instances
return array(
'abstract_factories' => array($this->di),
);
}
public function getControllerConfig()
{
return array(
'factories' => array(
'Survey\Controller\IndexController' => function() {
return $this->di->get('Survey\Controller\IndexController');
},
),
);
}
}
One option - add to config/module.config.php
'service_manager' => array(
'invokables' => array(
'Application\Service\User' => 'Application\Service\User',
),
),
then class needs to implement Zend\ServiceManager\ServiceManagerAwareInterface
When initiated, serviceManager instance is going to be injected, then you can use something like this in class:
$authService = $this->getServiceManager()->get('Zend\Authentication\AuthenticationService');
second option would be to put it into Module.php
public function getServiceConfig()
assumption: Event\Service\EventService is my personal object that works with Event\Entity\Event entities
This code works in an ActionController:
$eventService = $this->getServiceLocator()->get('Event\Service\EventService');
How can I get $eventService in a Zend\Form\Form in the same way?
You have two options if you have a dependency like this. In your case, a Form depends on a Service. The first option is to inject dependencies:
class Form
{
protected $service;
public function setService(Service $service)
{
$this->service = $service;
}
}
$form = new Form;
$form->setService($service);
In this case, the $form is unaware of the location of $service and generally accepted as a good idea. To make sure you don't need to set up all the dependencies yourself each time you need a Form, you can use the service manager to create a factory.
One way (there are more) to create a factory is to add a getServiceConfiguration() method to your module class and use a closure to instantiate a Form object. This is an example to inject a Service into a Form:
public function getServiceConfiguration()
{
return array(
'factories' => array(
'Event\Form\Event' => function ($sm) {
$service = $sm->get('Event\Service\EventService');
$form = new Form;
$form->setService($service);
return $form;
}
)
);
}
Then you simply get the Form from your service manager. For example, in your controller:
$form = $this->getServiceLocator()->get('Event\Form\Event');
A second option is to pull dependencies. Though it is not recommended for classes like forms, you can inject a service manager so the form can pull dependencies itself:
class Form
{
protected $sm;
public function setServiceManager(ServiceManager $sm)
{
$this->sm = $sm;
}
/**
* This returns the Service you depend on
*
* #return Service
*/
public function getService ()
{
return $this->sm->get('Event\Service\EventService');
}
}
However, this second option couples your code with unnecessary couplings and it makes it very hard to test your code. So please use dependency injection instead of pulling dependencies yourself. There are only a handful of cases where you might want to pull dependencies yourself :)
You can just configure the form with all the options in the module.php. In the following code I:
Name the service as my_form
Associate the new object \MyModule\Form\MyForm with this service
Inject the service 'something1' to the _construct()
Inject the service 'something2' to the setSomething()
Code:
public function getServiceConfiguration()
{
return array(
'factories' => array(
'my_form' => function ($sm) {
$model = new \MyModule\Form\MyForm($sm->get('something1'));
$obj = $sm->get('something2');
$model->setSomething($obj);
return $model;
},
),
);
}
And then in the controller the following line will populate your object with all needed dependencies
$form = $this->getServiceLocator()->get('my_form');
Use the form element manager to get the form in your controller:
$form = $this->getServiceLocator()->get('FormElementManager')->get('Path\To\Your\Form', $args);
Then in your form will become this
<?php
namespace Your\Namespace;
use Zend\Form\Form;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ ServiceLocatorAwareTrait;
class MyForm extends Form implements ServiceLocatorAwareInterface {
use ServiceLocatorAwareTrait;
public function __construct($class_name, $args)
{
/// you cannot get the service locator in construct.
}
public function init()
{
$this->getServiceLocator()->get('Path\To\Your\Service');
}
}