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');
?>
Related
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);
}
}
I've made a global variable in bootstrap of Module.php
public function setCashServiceToView($event) {
$app = $event->getParam('application');
$cashService = $app->getServiceManager()->get('Calculator/Service/CashServiceInterface');
$viewModel = $event->getViewModel();
$viewModel->setVariables(array(
'cashService' => $cashService,
));
}
public function onBootstrap($e) {
$app = $e->getParam('application');
$app->getEventManager()->attach(\Zend\Mvc\MvcEvent::EVENT_RENDER, array($this, 'setCashServiceToView'), 100);
}
I can use it inside of my layout.phtml as
$this->cashService;
But I need this variable to use in my partial script of navigation menu, which I call in layout.phtml:
echo $this->navigation('navigation')
->menu()->setPartial('partial/menu')
->render();
?>
How can I use it inside of my partial/menu.phtml? And may be there is a better way, than to declare it in onBootstrap function?
Thank you for your answers. I decided to make an extended class of \Zend\View\Helper\Navigation\Menu to provide there a property of cashService. However I receive an error:'Zend\View\Helper\Navigation\PluginManager::get was unable to fetch or create an instance for Calculator\Service\CashServiceInterface'.
I need this service to display navigation menu. Seems weird, but that's true. I display some diagram in it, using the data, which I get from the service. So why do I have the error?
I added to module.config.php
'navigation_helpers' => array(
'factories' => array(
'mainMenu' => 'Calculator\View\Helper\Factory\MainMenuFactory'
),
MainMenuFactory:
namespace Calculator\View\Helper\Factory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Calculator\View\Helper\Model\MainMenu;
Class MainMenuFactory implements FactoryInterface {
/**
* Create service
*
* #param ServiceLocatorInterface $serviceLocator
* #return mixed
*/
public function createService(ServiceLocatorInterface $serviceLocator) {
return new MainMenu(
$serviceLocator->get('Calculator\Service\CashServiceInterface')
);
}
P.S: CashServiceInterface is an alias to CashServiceFactory
You could remove the event listener and use a custom view helper to access the service in the view.
namespace Calculator\View\Helper;
use Zend\View\Helper\AbstractHelper;
class CashService extends AbstractHelper
{
protected $cashService;
public function __construct(CashServiceInterface $cashService)
{
$this->cashService = $cashService;
}
public function __invoke()
{
return $this->cashService;
}
}
Create a factory.
namespace Calculator\View\Helper;
class CashServiceFactory
{
public function __invoke($viewPluginManager)
{
$serviceManager = $viewPluginManager->getServiceLocator();
$cashService = $serviceManager->get('Calculator\\Service\\CashServiceInterface');
return new CashService($cashService);
}
}
Register the new helper in moudle.config.php.
'view_helpers' => [
'factories' => [
'CashService' => 'Calculator\View\Helper\CashServiceFactory',
],
],
Then you can use the plugin in all view scripts.
$cashService = $this->cashService();
I have created a View Helper to display latest Adverts from a Database Table. Since I have different Types of Adverts, I would like to be able to pass a variable from inside my View where I call the View Helper to show specific Adverts.
I am sorry that I can not explain it in a better way, but I am still a total beginner in ZF2. I will add the Sourcecode and hopefully this will make it more clear. Please note that I have the Sourcecode from a Book which displayed Pizza's randomly and changed it till it worked to show my adverts. I might still have Code in it which is not actually needed, so please do not wonder... Okay here the code:
1. the view: index.html
<?php foreach ($this->latestAdvert() as $value){ ?>
<li><?php echo $value->getAdvertTitle();?></li>
<?php }?>
2. the view Helper: Advert\View\Helper\LatestAdvert.php
namespace Advert\View\Helper;
use Zend\View\Helper\AbstractHelper;
class LatestAdvert extends AbstractHelper
{
protected $random = null;
public function __construct($random)
{
$this->setLatestAdvert($random);
}
public function setLatestAdvert($random)
{
$this->random = $random;
}
public function getLatestAdvert()
{
return $this->random;
}
public function __invoke()
{
$latestAdverts = $this->getLatestAdvert();
return $latestAdverts;
}
}
3. the Factory: Advert\View\Helper\LatestAdvertFactory.php
namespace Advert\View\Helper;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class LatestAdvertFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$locator = $serviceLocator->getServiceLocator();
$service = $locator->get('Advert\Service');
$random = $service->fetchSingleByRandom();
$helper = new LatestAdvert($random);
return $helper;
}
}
4. the Service: Advert\Service\LatestAdvertService .php
namespace Advert\Service;
use Advert\Entity\Advert as AdvertEntity;
use Doctrine\ORM\EntityManager;
use Zend\ServiceManager\ServiceManager;
use Zend\ServiceManager\ServiceManagerAwareInterface;
use Zend\Debug\Debug;
class LatestAdvertService implements ServiceManagerAwareInterface
{
/**
* Service manager.
* #var Zend\ServiceManager\ServiceManager
*/
private $serviceManager = null;
/**
* Sets service manager.
* #param Zend\ServiceManager\ServiceManager $serviceManager Service manager.
*/
public function setServiceManager(ServiceManager $serviceManager)
{
$this->serviceManager = $serviceManager;
}
/**
* Returns service manager.
* #return type
*/
public function getServiceLocator()
{
return $this->serviceManager;
}
public function fetchSingleByRandom()
{
// Get Doctrine entity manager.
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
$advertType = 'wanted'; // This should be removed
$random = $entityManager->getRepository('Advert\Entity\Advert')
->findAdvertsByDate($advertType);
return $random;
}
}
5. Module: Advert\Module.php
public function getServiceConfig()
{
return array(
'invokables' => array(
'Advert\Service' => 'Advert\Service\LatestAdvertService',
),
);
}
public function getViewHelperConfig()
{
return array(
'factories' => array(
'latestAdvert' => 'Advert\View\Helper\LatestAdvertFactory',
),
);
}
As you can see in #4 I have a Variable called $advertType. I want to set the variable when I call the view Helper in my index.html, f.e. $this->latestAdvert('wanted'), but how can I pass this variable through all my files? I just can not find a solution for it. Does anyone got a tip for me how to achieve it? Thank you very much in advance.
!UPDATE!
As SenseException pointed out below, that injecting a service locator into a service is a bad practice and instead I should either inject repository or entity manager into the service, I have now worked out the first working solution for the entity manager.
For that I have updated 2 Files: module.php and LatestAdvertService.php
#5 module.php
public function getServiceConfig()
{
return array(
'factories' => array(
'Advert\Service' => function ($sl) {
$entityManager = $sl->get('doctrine.entitymanager.orm_default');
$myService = new Service\LatestAdvertService();
$myService->setEntityManager($entityManager);
//or you can set repository
//$repository = $entityManager->getRepository('Advert\Entity\Advert');
//$myService->setRepository($repository);
return $myService;
},
4. the Service: Advert\Service\LatestAdvertService .php
namespace Advert\Service;
use Advert\Entity\Advert as AdvertEntity;
use Doctrine\ORM\EntityManager;
class LatestAdvertService
{
public function setEntityManager(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function setRepository(Repository $repository) {
$this->repository = $repository;
}
public function fetchSingleByAdvertType($advertType)
{
$random = $this->entityManager->getRepository('Advert\Entity\Advert')->findAdvertsByDate($advertType);
// $random = $this->repository->findAdvertsByDate($advertType);
return $random;
}
}
I have tried to inject the repository but get the following error message:
Argument 1 passed to Advert\Service\LatestAdvertService::setRepository() must be an instance of Advert\Service\AdvertRepository, instance of Advert\Repository\AdvertRepository given, called in
I will continue to find a solution for the repository injection and update when successful.
How about this solution:
namespace Advert\View\Helper;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class LatestAdvertFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$locator = $serviceLocator->getServiceLocator();
$service = $locator->get('Advert\Service');
$helper = new LatestAdvert($service);
return $helper;
}
}
And of course the helper class:
namespace Advert\View\Helper;
use Zend\View\Helper\AbstractHelper;
class LatestAdvert extends AbstractHelper
{
protected $service;
public function __construct($service)
{
$this->service = $service;
}
public function __invoke($advertType)
{
$latestAdverts = $this->service->fetchSingleByAdvertType($advertType);
return $latestAdverts;
}
}
And for the service:
public function fetchSingleByAdvertType($advertType)
{
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
$random = $entityManager->getRepository('Advert\Entity\Advert')
->findAdvertsByDate($advertType);
return $random;
}
I tried to keep your code as close to your original as possible but please hear some suggestions about the service locator. It is a bad practice to inject a service locator into a service like you did in LatestAdvertService. Since you only need a repository for your service, just inject that one into it. If you need the entity manager in your service, inject it instead. Your unittests will thank you.
In two words: I need to get access to the service manager (locator) from external class.
Details:
I have next structure in my ZF2 project:
Api.php is the class, I use in SOAP server, which is created in Controller:
class IncomingInterfaceController extends AbstractActionController
{
...
public function indexAction()
{
if (isset($_GET['wsdl']))
$this->handleWSDL();
else
$this->handleSOAP();
return $this->getResponse();
}
private function handleWSDL()
{
$autodiscover = new AutoDiscover();
$autodiscover->setClass('\Application\Api\Api')->setUri($this->getURI());
$autodiscover->handle();
}
In this Api.php class I need to get access to services.
I need something like this in my Api.php class:
public function OnNewDeal($uid)
{
$error_log=$this->getServiceLocator()->get('error_log'); // this doesn't work!
$error_log->write_error('error_text');
}
In Module.php
public function getServiceConfig() {
return array(
'invokables' => array(
'Application\Api\Api' => 'Application\Api\Api'
)
);
}
In Api.php
class Api implements ServiceLocatorAwareInterface{
protected $services;
public function OnNewDeal($uid){
$this->getServiceLocator()->get('error_log')->write_error('SOAP ERROR');
}
public function setServiceLocator(ServiceLocatorInterface $serviceLocator){
$this->services = $serviceLocator;
}
public function getServiceLocator(){
return $this->services;
}
}
In IncomingInterfaceController.php
class IncomingInterfaceController extends AbstractActionController{
...
protected $api;
public function indexAction()
{
if (isset($_GET['wsdl']))
$this->handleWSDL();
else
$this->handleSOAP();
return $this->getResponse();
}
private function handleWSDL()
{
$autodiscover = new AutoDiscover();
$autodiscover->setClass('\Application\Api\Api')->setUri($this->getURI());
$autodiscover->handle();
}
public getApi(){
if(!$api){
$this->api = $this->getServiceLocator()->get('Application\Api\Api');
}
return $this->api;
}
In controller where you do $this->handleSOAP(); use setObject with already created instance instead setClass.
You should pass into Api __construct $this->getServiceLocator() and handle it there.
class IncomingInterfaceController extends AbstractActionController
{
private function handleSOAP()
{
$soap = new Server(null, array('wsdl'=>$this->getWSDLURI()));
$soapClass = new \Application\Api\Api($this->getServiceLocator());
$soap->setObject($soapClass);
$soap->handle();
}
In Api class, handle serviceManager instance and use as you wish:
class Api
{
protected $serviceManager;
public function __construct($serviceManager)
{
$this->serviceManager = $serviceManager;
}
public function OnNewDeal($uid)
{
$this->serviceManager->get('error_log')->write_error('SOAP ERROR');
}
....
}
Perhaps your API could implement ServiceLocatorAwareInterface like:
class Api implements ServiceLocatorAwareInterface
and add
class Api implements ServiceLocatorAwareInterface
{
protected $serviceManager;
}
Then the service manager would be available
UPDATED
module.config.php example
<?php
return array(
'service_manager' => array(
'factories' => array(
'Api' => 'Namespace\Api'
),
'shared' => array(
'Api' => false
)
),
)
?>
Injecting the Service Manager instance to an user defined "service locator aware class" should responsibility of the framework's itself (via initializers, invokables or user defined factories) not a specific controller's handleSOAP() method.
Yes, #SirJ's solution will work too but that's not a good practice. ZF2 provides ready-to-use Traits and Interfaces exactly for requirements like this. Just use them!
Your very own API class should seem like this:
<?php
namespace Application\Api;
use Zend\ServiceManager\ServiceLocatorInterface;
class Api implements ServiceLocatorInterface
{
// Here is the trait. (php >= 5.4)
use \Zend\ServiceManager\ServiceLocatorAwareTrait;
public function OnNewDeal($uid)
{
$this->getServiceLocator()->get('error_log')->write_error('SOAP ERROR');
}
}
And you should add this key to your module.config.php
<?php
return array(
'service_manager' => array(
'invokables' => array(
'api-service' => 'Application\Api\Api',
)
);
Thats all! Now you can:
<?php
...
$soap = new Server(null, array('wsdl'=>$this->getWSDLURI()));
$soapClass = $this->getServiceLocator()->get('api-service');
$soap->setObject($soapClass);
...
I'm currently playing with the ZF2 serviceManager, and i'm trying to figure out why the serviceManager doesn't inject the sm in a class that implements ServiceLocatorAwareInterface.
My main question is am i doing it right or is the "services" key not for services that implement ServiceLocatorAwareInterface but for services that don't need injection?
in Module.php
public function getServiceConfig() {
return array(
'invokables' => array(
'myService1' => 'MyModule\Service\Service'
),
'services' => array(
'myService2' => new MyModule\Service\Service(),
),
);
}
in MyModule\Service\Service.php
namespace MyModule\Service;
use Zend\ServiceManager\ServiceManagerAwareInterface;
use Zend\ServiceManager\ServiceManager;
class Service implements ServiceManagerAwareInterface
{
/**
* #var ServiceManager
*/
protected $serviceManager = NULL;
/**
* Retrieve service manager instance
*
* #return ServiceManager
*/
public function getServiceManager()
{
return $this->serviceManager;
}
/**
* Set service manager instance
*
* #param ServiceManager $serviceManager
*/
public function setServiceManager(ServiceManager $serviceManager)
{
$this->serviceManager = $serviceManager;
}
}
When i call the service in a controller
<?php
namespace MyModule\Controller;
use Zend\Mvc\Controller\AbstractActionController;
class IndexController extends AbstractActionController
{
public function IndexAction() {
$service1 = $this->getServiceLocator()->get('myService1');
$sm1 = $service1->getServiceManager();
//$sm1 becomes a object of Zend\ServiceManager\ServiceManager
//In other words i now can access the SM from within my service.
$service2 = $this->getServiceLocator()->get('myService2');
$sm2 = $service2->getServiceManager();
//$sm2 becomes NULL
//The service isn't aware of the SM and can't access it.
}
}
You should continue to use the "invokables" section if you'd like to utilize the ServiceManagerAwareInterface and the automatic injection of the Service Manager into your service.
Looking through /Zend/ServiceManager/ServiceManager.php, "services" are meant to be registered as already instantiated objects with the ServiceManager. When the service locator looks up in it's local cache of services during retrieval, it assumes that "services" are already fully set up and does not inject the sm or run any initializers.
"invokables", "factories", "abstract_factories" are created on the fly and injects the sm when the "initializers" are run on a newly created service instance (see function create($name)).
Forget about getServiceConfig() and use your module config instead. It's faster and cacheable:
module.config.php:
'service_manager' => array(
'invokables' => array(
'MyModule\Service\Service' => 'MyModule\Service\Service',
),
)
MyModule\Service\Service.php:
<?php
namespace MyModule\Service;
use \Zend\ServiceManager\ServiceLocatorInterface;
class Service
{
public function __invoke(ServiceLocatorInterface $sm)
{
// go bananas here :)
}
public functiom greet()
{
return 'Hello World';
}
}
anywhere inside zend2:
<?php
$service=$serviceManager->get('MyModule\Service\Service');
echo $service->greet();