I'm not sure how to formulate the question, so feel free to edit it.
My current situation is as following:
I have a factory class which instantiates a form class. Dependency Injection (DI) is done via constructor injection. My problem is, that this form element has a Doctrine ObjectMultiCheckbox which requires a findby-method. For this findby-method I need the ID of a certain entity, but I cannot pass the ID through the factory class to the form.
My Question is, how can I deal with this situation? What is the best approach?
Let's say this is my factory class:
class CustomerFormFactory implements FactoryInterface
{
/**
* Create service
*
* #param ServiceLocatorInterface $serviceLocator
* #return Form
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$em = $serviceLocator->get('Doctrine\ORM\EntityManager');
return new CustomerForm($em);
}
}
And I get the form via the service locator like this:
$customerForm = $this->getServiceLocator()->get('CustomerForm');
How can I pass the ID to the service locator? And if the form element requires a certain ID, doesn't it break the purpose of DI and services? Should I go for the "classic" way and instantiate the form element by myself like this:
$customerForm = new CustomerForm(EntityManager $em, int $id);
I'm really not sure what I should do or what is the best way to handle this.
In order to insert options into your form you could use the CreationOptions of the factory class.
So lets start by setting up our configurations for the FormElementManager (a serviceLocator for our Form Elements).
Within your Module.php:
use Zend\ModuleManager\Feature\FormElementProviderInterface;
class Module implements FormElementProviderInterface
{
// your module code
public function getFormElementConfig()
{
return [
'factories' => [
'myForm' => \Module\Form\MyFormFactory::class
]
];
}
}
After we've set up the configruation we should create our Factory, which returns the Form including it's dependencies. We also insert the options which we can re-use within our form class.
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\MutableCreationOptionsTrait;
use Zend\ServiceManager\ServiceLocatorInterface;
class MyFormFactory implements FactoryInterface
{
use MutableCreationOptionsTrait;
/**
* Create service
*
* #param ServiceLocatorInterface $serviceLocator
*
* #return mixed
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
return new MyForm(
$serviceLocator->getServiceLocator()->get('Doctrine\ORM\EntityManager'),
'MyForm',
$this->getCreationOptions()
);
}
}
When using ZF3 it is better to use \Zend\ServiceManager\Factory\FactoryInterface instead of the \Zend\ServiceManager\FactoryInterface as this is the way ZF3 is going with using factories. In the example above I used the ZF2 (v2.7.6 zendframework/zend-servicemanager) version. See the comment on the class Zend\ServiceManager\FactoryInterface::class to replace it with the ZF3 version.
So now when we call ::get('myForm', ['id' => $id]) on the FormElementManager class you will get a MyForm instance and the options of the form will contain the options we've passed along.
So your form might look something similar:
class MyForm extends \Zend\Form\Form
{
public function __construct(
\Doctrine\Common\Persistence\ObjectManager $entityManager,
$name = 'myForm',
$options = []
) {
parent::__construct($name, $options);
$this->setEntityManager($entityManager);
}
public function init () {
/** add form elements **/
$id = $this->getOption('id');
}
}
You can also create the form and set the entityManager, but that is all up to you. You don't need to use constructor injection.
So an exmaple for your controller:
$myForm = $this->getServiceManager()->get('FormElementManager')->get('myForm', ['id' => 1337]);
$options = $myForm->getOptions();
// your options: ['id' => 1337]
You might not have the ServiceManager or Locator within your Controller as you're using ZF2.5+ or ZF3 so you've got to inject the FormElementManager or the Form class into your Controller by factory.
In case you don't have any other dependencies within your form but you want to set the options, you don't need to create a factory for each class. You can re-use the InvokableFactory::class as this will also inject the creationOptions.
Related
I am trying to access service locator object inside my controller but unable to do this.
I tried online help but most of them are following approach for ZF2
Previously Servicelocator access in zf2 was a breeze, I just had to do $this->getServiceLocator();
I have tried creating factory Class for my controller and Created createService method there but it says I have to implement __invoke() method too.
My Objective is to do something like this
public function getPropertyTable()
{
if (!$this->PropertyTable) {
$sm = $this->getServiceLocator();
$this->PropertyTable = $sm->get('Application\Model\PropertyTable');
}
return $this->PropertyTable;
}
Can anyone provide me a complete steps to achieve this?
I have tried to implement almost all Answers related to Servicelocator before asking this question, so please help me before marking this as Duplicate or something, ignore the typos
Thanks everyone to tell me I am doing it the wrong way. Some more research on this topic helped me to get my issue resolved here is what I have done to solve it
Create Factory class for your Controller
You have to create a factory class for your controller which will implement FactoryInterface of zend.
in there you have to call
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new ListController($container->get(PropertyInterface::class));
}
here I am passing PropertyInterface refrence which is implemented by my another table called Property Table in which I have given body to all my inteface functions for model
like searchProperty()
Add factory class in config file module.config.php for your controller
instructing config file to let our factory create the object for our controller
'controllers' => [
'factories' => [
// Update the following line:
Controller\ListController::class => Factory\ListControllerFactory::class,
],
],
Register your Model in config file
You have to add new section for service manager and provide your model classes there.
// Add this section:
'service_manager' => [
'aliases' => [
Model\PropertyInterface::class => Model\PropertyTable::class,
],
'factories' => [
Model\PropertyTable::class => InvokableFactory::class,
],
],
Now only thing left is add functions in your PropertyInterface and Implentation in PropertyTable for them and then call them in your controller
This Complete Steps for implementation Helped me in implementing the new flow.
Thanks to community. You all are best.
As long as the new factory interface in ZF3 is:
interface FactoryInterface
{
/**
* Create an object
*
* #param ContainerInterface $container
* #param string $requestedName
* #param null|array $options
* #return object
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null);
}
you have to implements your factory like this.
For helping you, you can use that link
EDIT :
In practise you should have this :
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$propertyTable = $container->get('Application\Model\PropertyTable');
return new Controller($propertyTable);
}
Another way
/**
* Retrieve service manager instance
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
* #return ContainerInterface
*/
public function getServiceLocator()
{
return $this->getEvent()->getApplication()->getServiceManager();
}
In ZF3 it is not recommended to pass service locator into controller.
You need to get all your dependencies from $container inside factory of the controller and pass they into the controller through the constructor (or setters).
Since version 2.7.0 of zend-mvc the ServiceLocatorAwareInterface is depricated, so are $this->serviceLocator->get() calls inside controllers.
Thats why some days ago I did a huge refactoring of all my modules to inject the needed services/objects through constructors using factories for mostly everything.
Sure, I understand why this is the better/cleaner way to do things, because dependendies are much more visible now. But on the other side:
This leads to a heavy overhead and much more never-used class instances, doesn't it?
Let's look to an example:
Because all my controllers having dependencies, I've created factories for all of them.
CustomerControllerFactory.php
namespace Admin\Factory\Controller;
class CustomerControllerFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $controllerManager) {
$serviceLocator = $controllerManager->getServiceLocator();
$customerService = $serviceLocator->get('Admin\Service\CustomerService');
$restSyncService = $serviceLocator->get('Admin\Service\SyncRestClientService');
return new \Admin\Controller\CustomerController($customerService, $restSyncService);
}
}
CustomerController.php
namespace Admin\Controller;
class CustomerController extends AbstractRestfulController {
public function __construct($customerService, $restSyncService) {
$this->customerService = $customerService;
$this->restSyncService = $restSyncService;
}
}
module.config.php
'controllers' => [
'factories' => [
'Admin\Controller\CustomerController' => 'Admin\Factory\Controller\CustomerControllerFactory',
]
],
'service_manager' => [
'factories' => [
'Admin\Service\SyncRestClientService' => 'Admin\Factory\SyncRestClientServiceFactory',
]
]
SyncRestClientServiceFactory.php
namespace Admin\Factory;
class SyncRestClientServiceFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $serviceLocator) {
$entityManager = $serviceLocator->get('doctrine.entitymanager.orm_default');
$x1 = $serviceLocator->get(...);
$x2 = $serviceLocator->get(...);
$x3 = $serviceLocator->get(...);
// ...
return new \Admin\Service\SyncRestClientService($entityManager, $x1, $x2, $x3, ...);
}
}
The SyncRestService is a complex service class which queries some internal server of our system. It has a lot of dependencies, and is always created if a request comes to the CustomerController. But this sync-service is only used inside the syncAction() of the CustomerController! Before I was using simply $this->serviceLocator->get('Admin\Service\SyncRestClientService') inside the syncAction() so only then it was instantiated.
In general it looks like a lot of instances are created through factories at every request, but the most dependencies are not used. Is this an issue because of my design or it is a normal side-effect behaviour of "doing dependency injection through constructors"?
In my opinion it is a normal effect of dependency injection through constructors.
I think you have now two options (not mutually exclusive) to improve how your application works:
Split your controllers, so that the dependencies are instanciated only when needed. This would certainly give rise to more classes, more factories, and so on, but your code would attain more to the single responsability principle
You could use Lazy Services, so that, even if some services are dependencies of the whole controller, they will be actually instanciated only the first time they are called (so never for the actions where they are not called!)
If you only use your SyncRestClientService inside a controller you should consider changing it from a service to a controller plugin (or make a controller plugin where you inject your SyncRestClientService).
Like that you can still get it inside your controller syncAction method very similar to like you did before. This is exactly the purpose of the ZF2 controller plugins.
First you need to create your controller plugin class (extending Zend\Mvc\Controller\Plugin\AbstractPlugin):
<?php
namespace Application\Controller\Plugin;
use Zend\Mvc\Controller\Plugin\AbstractPlugin;
class SyncPlugin extends AbstractPlugin{
protected $syncRestClientService;
public function __constuct(SyncRestClientService $syncRestClientService){
$this->syncRestClientService = $syncRestClientService
}
public function sync(){
// do your syncing using the service that was injected
}
}
Then a factory to inject your service in the class:
<?php
namespace Application\Controller\Plugin\Factory;
use Application\Controller\Plugin\SyncPlugin;
class SyncPluginFactory implements FactoryInterface
{
/**
* #param ServiceLocatorInterface $serviceController
* #return SyncPlugin
*/
public function createService(ServiceLocatorInterface $serviceController)
{
$serviceManager = $serviceController->getServiceLocator();
$syncRestClientService = $serviceManager>get('Admin\Service\SyncRestClientService');
return new SyncPlugin($syncRestClientService);
}
}
Then you need to register your plugin in your module.config.php:
<?php
return array(
//...
'controller_plugins' => array(
'factories' => array(
'SyncPlugin' => 'Application\Controller\Plugin\Factory\SyncPluginFactory',
)
),
// ...
);
Now you can use it inside your controller action like this:
protected function syncAction(){
$plugin = $this->plugin('SyncPlugin');
//now you can call your sync logic using the plugin
$plugin->sync();
}
Read more on controller plugins here in the documentation
Maybe you only need one dependency to be injected into the controller constructor (the ServiceManager instance). I don't see any cops around...
namespace Admin\Factory\Controller;
class CustomerControllerFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $controllerManager)
{
$serviceLocator = $controllerManager->getServiceLocator();
return new \Admin\Controller\CustomerController($serviceLocator);
}
}
Personally I get the action name in the controller factory to inject services on a per action basis.
Have a look at my sites controller.
namespace Admin\Controller\Service;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Admin\Controller\SitesController;
use Admin\Model\Sites as Models;
class SitesControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$actionName = $serviceLocator->getServiceLocator()->get('Application')->getMvcEvent()->getRouteMatch()->getParam('action');
$controller = new SitesController();
switch ($actionName) {
case 'list':
$controller->setModel($serviceLocator->getServiceLocator()->get(Models\ListSitesModel::class));
break;
case 'view':
$controller->setModel($serviceLocator->getServiceLocator()->get(Models\ViewSiteModel::class));
break;
case 'add':
$controller->setModel($serviceLocator->getServiceLocator()->get(Models\AddSiteModel::class));
break;
case 'edit':
$controller->setModel($serviceLocator->getServiceLocator()->get(Models\EditSiteModel::class));
break;
}
return $controller;
}
}
As you can see I use $serviceLocator->getServiceLocator()->get('Application')->getMvcEvent()->getRouteMatch()->getParam('action'); to get the action name and use a switch statement to inject the dependencies when required.
I don't know if this is the best solution but it works for me.
Hope this helps.
I have extended the PhpRenderer class in my ZF2 application like this:
namespace MyLib\View\Renderer;
class PhpRenderer extends \Zend\View\Renderer\PhpRenderer
{
}
I don't want to add a new rendering strategy, I just extend the PhpRenderer to add some #method phpdoc for my viewhelpers.
How can I replace the standard PhpRenderer with my extended PhpRenderer so it will be used to render my viewscripts?
The php renderer is a service inside the service manager. You can override this service directly or do it via the view manager (which instantiates and configures the renderer).
Override the service
In your module you define an onBootstrap() method. The "old" php renderer is already registered, you have to redefine it.
public function onBootstrap($e)
{
$app = $e->getApplication();
$sm = $app->getServiceManager();
$old = $sm->get('ViewRenderer');
$new = new MyCustomViewRenderer;
$new->setHelperPluginManager($old->getHelperPluginManager());
$new->setResolver($old->getResolver());
$sm->setAllowOverride(true);
$sm->setService('ViewRenderer', $new);
$sm->setAllowOverride(false);
}
Override the view manager
There is an alternative where you can redefine the view manager where the php renderer is instantiated. You have to redefine the view manager's factory for this:
In your application.config.php (note it is the application config, as the module config will not work here!)
service_manager => array(
'factories' => array(
'HttpViewManager' => 'MyModule\Service\HttpViewManagerFactory',
),
);
Then create your MyModule\Service\HttpViewManagerFactory:
use MyModule\View\Http\ViewManager as HttpViewManager;
class HttpViewManagerFactory implements FactoryInterface
{
/**
* Create and return a view manager for the HTTP environment
*
* #param ServiceLocatorInterface $serviceLocator
* #return HttpViewManager
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
return new HttpViewManager();
}
}
And then you can finally update the factory of the php renderer itself:
use Zend\Mvc\View\Http\ViewManager as BaseViewManager;
class ViewManager extends BaseViewManager
{
public function getRenderer()
{
if ($this->renderer) {
return $this->renderer;
}
$this->renderer = new MyCustomViewPhpRenderer;
$this->renderer->setHelperPluginManager($this->getHelperManager());
$this->renderer->setResolver($this->getResolver());
$model = $this->getViewModel();
$modelHelper = $this->renderer->plugin('view_model');
$modelHelper->setRoot($model);
$this->services->setService('ViewRenderer', $this->renderer);
$this->services->setAlias('Zend\View\Renderer\PhpRenderer', 'ViewRenderer');
$this->services->setAlias('Zend\View\Renderer\RendererInterface', 'ViewRenderer');
return $this->renderer;
}
}
Conclusion
The first method instantiates the normal php renderer already, so you instantiate two of them and replace the default with your own.
An alternative is to circumvent the instantiation of the default Zend's php renderer, but you have to do this inside the view manager class. The problem here is you have to redefine the factory for the view manager as well. This sounds as a detour, but it is the only way to get this done.
If all your custom class contains is #method declarations then you don't need to replace the php renderer class. Just make sure to use the #var docblock and your IDE will know what to do:
Document the type for the $this variable in your view files:
<!-- in a view file -->
<?php /* #var $this MyLib\View\Renderer\PhpRenderer */ ?>
<?= $this->myCustomViewHelper() ?>
Document individual variables or properties for view helpers, classes, etc:
class SomeHelper extends AbstractHelper
{
/** #var \MyLib\View\Renderer\PhpRenderer */
protected $view;
public function __invoke()
{
$this->view->myCustomViewHelper();
}
}
Short story:
I need to get the Request service from a class that doesn't inherit from the Controller class (it's a DataTransformer which -obviously- implements the DataTransformerInterface).
Long story:
I have an embedded form that has an email field. If the user enters an email which doesn't exists in my users database table, I want to create a new user with this email.
In order to do that, I need to set its IP, so I followed the embedded forms tutorial and the data transformer recipe, but finally I have no idea where I'm able to inject the Request instance to my DataTransformer constructor or something else.
If I was in a class extending form the Controller one, it would be as simple as:
$this->container->get('request')->getClientIp()
You can do this by "Referencing (Injecting) Services". In your case you want to inject the Request which is a service from a narrower scope.
If you are using transformers, you are probably already using a Custom Form Type, and are instantiating the Data Transformer within your Form Type BuildForm Method, see here for more info.
You want to inject the Request object to the custom Form Type, then it can passed to the Data Transformer as a constructor parameter.
To do this modify the services.yml file with in your bundle, and add a constructor to the Custom Form Type and the Custom Data Transformer like this:
// src/Acme/HelloBundle/Resources/config/services.yml
parameters:
// ...
services:
acme.type.custom_type:
class: Acme\HelloBundle\Form\Type\CustomType
scope: request
arguments: ["#doctrine.odm.mongodb.document_manager", "#request"]
tags:
- { name: form.type, alias: custom_type }
The update the CustomType Class like this:
<?php
// src/Acme/HelloBundle/Form/Type/CustomType.php
namespace Acme\HelloBundle\Form\Type;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
use Acme\HelloBundle\Form\DataTransformer\CustomDataTransformer;
class CustomType extends AbstractType
{
private $request;
private $dm;
public function __construct(DocumentManager $dm, Request $request) {
$this->dm = $dm;
$this->request = $request;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Create a new Data Transformer that will be able to use the Request Object!
$transformer = new CustomDataTransformer($this->dm, $this->request);
$builder->addModelTransformer($transformer);
}
// ...
}
and finally add a constructor to the transformer similar to the one added in the Form Type:
<?php
// src/Acme/HelloBundle/Form/DataTransformer/CustomDataTransformer.php
namespace Acme\HelloBundle\Form\DataTransformer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\DataTransformerInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
class CustomDataTransformer implements DataTransformerInterface
{
private $request;
private $dm;
public function __construct(DocumentManager $dm, Request $request) {
$this->dm = $dm;
$this->request = $request;
}
// ...
}
Notice that along with the Request I have injected the MongoDB DocumentManager, this is to show that multiple objects can be injected.
Ok, that's simple:
In my question I was assuming that the DataTransformer will be "magically" invoked, but it's instanced while building the form, so if it helps to anyone, here it is:
In the DataTransformer class (implementing the DataTransformerInterface):
Define the new class attributes in order to hold the dependency injection:
/**
* #var EntityManager
*/
private $entityManager;
/**
* #var \Symfony\Component\DependencyInjection\Container
*/
private $container;
Define the constructor like:
public function __construct( EntityManager $entityManager, Container $container )
{
$this->entityManager = $entityManager;
$this->container = $container;
}
In your form class (implementing the AbstractType)
Add the following calls to the setDefaultOptions method:
$resolver->setRequired( array( 'em', 'container' ) );
$resolver->setAllowedTypes( array(
'em' => 'Doctrine\Common\Persistence\ObjectManager',
'container' => 'appDevDebugProjectContainer',
) );
In the buildForm method, apply the transformer as defined in the transformer recipe but instance it as:
$entityManager = $options['em'];
$container = $options['container'];
$transformer = new FantasticTransformer( $entityManager, $container );
In your controller, when you're calling to the createForm method, is it possible to inject the EntityManager and the Container instances simply adding them as follows:
$form = $this->createForm( 'your_form', $lookup, array(
'action' => $this->generateUrl( 'your_action_url' ),
'em' => $this->getDoctrine()->getManager(),
'container' => $this->container
) );
Now, you can finally get the client IP from the request service calling to the container defined in the constructor of your DataTransformer class:
$ip = $this->container->get('request')->getClientIp();
Note that we're injecting the container instead of the request instance, it's due to the Symfony scopes.
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');
}
}