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');
}
}
Related
I'd like to be able to alter a form using the Symfony Event Dispatcher. The Symfony documentation talks about dynamic form modification, however, in all examples the event listener or subscriber is created in the form class. I'd like to keep this logic decoupled from my form class.
How can I modify a Symfony form without having to specify which event listeners are going to be called in the form class?
Probably what you need is a form type extension, it allow you to modify any existing form types across the entire system. There, you can add event listeners/subscribers or what you want to any specific or generic form type.
However, this task tends to get tedious if it's a very frequent case. So doing something like this can provide you with a perfect fit:
class FoobarFormSubscriber implements EventSubscriberInterface, FormEventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(FormEvents::PRE_SET_DATA => 'preSetData');
}
public static function getFormClass()
{
return FoobarType::class;
}
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
$form->add('custom', null, array('mapped' => false));
}
}
But obviously this isn't a feature implemented by Symfony. Here I leave you a recipe to achieve it:
First, create a new form type extension to add the subscriber to the form builder according to configuration:
class FormEventTypeExtension extends AbstractTypeExtension
{
private $subscribers;
public function __construct(array $subscribers = array())
{
$this->subscribers = $subscribers;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$formClass = get_class($builder->getType()->getInnerType());
if (isset($this->subscribers[$formClass])) {
foreach ($this->subscribers[$formClass] as $subscriber) {
$builder->addEventSubscriber($subscriber);
}
}
}
public function getExtendedType()
{
return FormType::class;
}
}
Create a new interface to configure the form class to listen to:
interface FormEventSubscriberInterface
{
public static function getFormClass();
}
Finally, into a new compiler pass, injects to the extension service all registered kernel.event_subscriber that implement the previous interface:
public function process(ContainerBuilder $container)
{
$subscribers = array();
foreach ($container->findTaggedServiceIds('kernel.event_subscriber') as $serviceId => $tags) {
$subscriberClass = $container->getDefinition($serviceId)->getClass();
if (is_subclass_of($subscriberClass, FormEventSubscriberInterface::class, true)) {
$subscribers[$subscriberClass::getFormClass()][] = new Reference($serviceId);
}
}
$extensionDef = $container->getDefinition(FormEventTypeExtension::class);
$extensionDef->setArgument(0, $subscribers);
}
Then, your custom subscribers are decoupled and ready to work as is, just make sure to implement both interfaces (EventSubscriberInterface, FormEventSubscriberInterface) and register the event subscriber as service.
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.
I need to programmatically change the behaviour of a form based on some options. Let's say, for example, I'm displaying a form with some user's info.
I need to display a checkbox, "send mail", if and only if a user has not received an activation mail yet. Previously, with ZF1, i used to do something like
$form = new MyForm(array("displaySendMail" => true))
which, in turn, was received as an option, and which allow'd to do
class MyForm extends Zend_Form {
protected $displaySendMail;
[...]
public function setDisplaySendMail($displaySendMail)
{
$this->displaySendMail = $displaySendMail;
}
public function init() {
[....]
if($this->displaySendMail)
{
$displaySendMail new Zend_Form_Element_Checkbox("sendmail");
$displaySendMail
->setRequired(true)
->setLabel("Send Activation Mail");
}
}
How could this be accomplished using Zend Framework 2? All the stuff I found is about managing dependencies (classes), and nothing about this scenario, except this SO question: ZF2 How to pass a variable to a form
which, in the end, falls back on passing a dependency. Maybe what's on the last comment, by Jean Paul Rumeau could provide a solution, but I wasn't able to get it work.
Thx
A.
#AlexP, thanks for your support. I already use the FormElementManager, so it should be straightforward. If I understand correctly, I should just retrieve these option in my SomeForm constructor, shouldn't I?
[in Module.php]
'Application\SomeForm' => function($sm)
{
$form = new SomeForm();
$form->setServiceManager($sm);
return $form;
},
while in SomeForm.php
class SomeForm extends Form implements ServiceManagerAwareInterface
{
protected $sm;
public function __construct($name, $options) {
[here i have options?]
parent::__construct($name, $options);
}
}
I tryed this, but was not working, I'll give it a second try and double check everything.
With the plugin managers (classes extending Zend\ServiceManager\AbstractPluginManager) you are able to provide 'creation options' array as the second parameter.
$formElementManager = $serviceManager->get('FormElementManager');
$form = $formElementManager->get('SomeForm', array('foo' => 'bar'));
What is important is how you have registered the service with the manager. 'invokable' services will have the options array passed into the requested service's constructor, however 'factories' (which have to be a string of the factory class name) will get the options in it's constructor.
Edit
You have registered your service with an anonymous function which mean this will not work for you. Instead use a factory class.
// Module.php
public function getFormElementConfig()
{
return array(
'factories' => array(
'Application\SomeForm' => 'Application\SomeFormFactory',
),
);
}
An then it's the factory that will get the options injected into it's constructor (which if you think about it makes sense).
namespace Application;
use Application\SomeForm;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
class SomeFormFactory implements FactoryInterface
{
protected $options = array();
public function __construct(array $options = array())
{
$this->options = $options;
}
public function createService(ServiceLocatorInterface $serviceLocator)
{
return new SomeForm('some_form', $this->options);
}
}
Alternatively, you can inject directly into the service you are requesting (SomeForm) by registering it as an 'invokeable' service; obviously this will depend on what dependencies the service requires.
I need to write a view helper that gets a service and do something with it. I successfully implemented the view helper to have access to the service locator. The problem is that the service I want to get is not being found through the service locator when the __invoke method is called.
The view helper code:
<?php
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper,
Zend\ServiceManager\ServiceLocatorAwareInterface,
Application\Model;
class LoggedCustomer extends AbstractHelper implements ServiceLocatorAwareInterface
{
use \Zend\ServiceManager\ServiceLocatorAwareTrait;
public function __invoke()
{
$model = new Model\Customer($this->getServiceLocator());
return $model->getCurrent();
}
}
A snippet of the model code:
namespace Application\Model;
use Application\Entity,
Andreatta\Model\Base as Base;
class Customer extends Base
{
/**
*
* #return Zend\Authentication\AuthenticationService
*/
public function getAuthService()
{
$serviceLocator = $this->getServiceLocator();
return $serviceLocator->get('Application\Auth');
}
/**
*
* #return Zend\Authentication\Adapter\AdapterInterface
*/
protected function getAuthAdapter()
{
return $this->getAuthService()->getAdapter();
}
public function getCurrent()
{
$authService = $this->getAuthService();
if ($authService->hasIdentity())
return $authService->getIdentity();
return null;
}
The snippet from module.config.php:
'service_manager' => array
(
'factories' => array
(
'Application\Auth' => function($sm)
{
$authService = $sm->get('doctrine.authenticationservice.application');
$authService->setStorage( new \Zend\Authentication\Storage\Session('Application\Auth'));
return $authService;
},
),
),
'view_helpers' => array
(
'invokables' => array
(
'loggedCustomer' => 'Application\View\Helper\LoggedCustomer',
),
),
When calling the view helper from any view I get the following:
Zend\View\HelperPluginManager::get was unable to fetch or create an instance for Application\Auth
The weird is that the application is functioning correctly (i.e. this service is being normally used by other parts of the application).
EDIT:
I did some research and I think the only services that I can access through the service manager inside the view helper are the ones registered inside the 'view_manager' section of module.config.php. Does anyone have an idea of how to access the other services?
$this->getServiceLocator() in view helper can only get u other view helpers you need to use $this->getServiceLocator()->getServiceLocator() to get the application services
#rafaame: I find a simple way to access service locator in view Helper
We just use:
$this->getView()->getHelperPluginManager()->getServiceLocator();
to get a service locator
A sample view Helper:
namespace Tmcore\View\Helper;
use Zend\View\Helper\AbstractHelper;
class Resource extends AbstractHelper
{
public function adminResource()
{
$sm = $this->getView()->getHelperPluginManager()->getServiceLocator();
$adminConfig = $sm->get('ModuleManager')->loadModule('admin')->getConfig();
return $adminConfig;
}
}
I guess you are retrieving the Zend\View\HelperPluginManager instead of the correct ServiceManager.
Probably you are not injecting it as you should.
That makes sense if thats your complete LoggedCustomer code, since you are not saving the SM. As far as I know, if you implement the ServiceLocatorAwareInterface the SM will be injected, but you have to handle it.
UPDATE:
sorry, i didnt realize you had ServiceLocatorAwareTrait; thats the same.
But, reading http://framework.zend.com/manual/2.0/en/modules/zend.service-manager.quick-start.html
i see
By default, the Zend Framework MVC registers an initializer that will inject the ServiceManager instance, which is an implementation of
Zend\ServiceManager\ServiceLocatorInterface, into any class
implementing Zend\ServiceManager\ServiceLocatorAwareInterface. A
simple implementation looks like the following.
So, the service manager is only being injected ... if you implement ServiceLocatorAwareInterface in a controller.
So, you should manually inject the service manager.
for that, what i use to do is to create a factory in Module.php, instead of creating the invokable in the config. for that you implement this function:
public function getViewHelperConfig()
{
return array(
'factories' => array(
'loggedCustomer' => function($sm) {
$vh = new View\Helper\LoggedCustomer();
$vh->setServiceLocator($sm->getServiceLocator());
return $vh;
}
);
}
Also, i wont have the view helper implementing ServiceLocatorAwareInterface, so nothing else is automaticaly injected.
And with this it will work
It appears that the service manager that is injected into the view helper has only the services that are registered within the section 'view_manager' of module configs.
It is possible to inject the "main" service manager by registering the view helper as a factory like this:
'view_helpers' =>
[
'factories' =>
[
'loggedCustomer' => function($pluginManager)
{
$serviceLocator = $pluginManager->getServiceLocator();
$viewHelper = new View\Helper\LoggedCustomer();
$viewHelper->setServiceLocator($serviceLocator);
return $viewHelper;
},
]
],
But you have to make sure that you treat it in setServiceLocator method in the view helper. Otherwise the "limited" service manager will be injected into the view helper later on. Like this:
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
if($this->serviceLocator !== null)
return $this;
$this->serviceLocator = $serviceLocator;
return $this;
}
It fixes the problem, but it appears to be a tremendous hack to me.
In view helpers, if you want to access application services then use
$this->getServiceLocator()->getServiceLocator()
How to get translator in model?
Inside view we can get translator using this code
$this->translate('Text')
Inside controller we can get translator using this code
$translator=$this->getServiceLocator()->get('translator');
$translator->translate("Text") ;
But how to get translator in model?
I'd tried so many ways to get service locator in models
2 of those
1)Using MVC events
$e=new MvcEvent();
$sm=$e->getApplication()->getServiceManager();
$this->translator = $sm->get('translator');
if i pring $sm it is showing null. but it works fine in Model.php onBootstrap
2)Created one model which implements ServiceLocatorAwareInterface
SomeModel.php
<?php
namespace Web\Model;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class SomeModel implements ServiceLocatorAwareInterface
{
protected $services;
public function setServiceLocator(ServiceLocatorInterface $locator)
{
$this->services = $locator;
}
public function getServiceLocator()
{
return $this->services;
}
}
and used that inside my model
$sl = new SomeModel();
$sm=$sl->getServiceManager();
var_dump($sm); exit;
$this->translator = $sm->get('translator');
this is also printing null.
If you don't need the servicemanager instance in your model, simply inject translator instance to it.
For example:
// Your model's constructor
class MyModel {
// Use the trait if your php version >= 5.4.0
use \Zend\I18n\Translator\TranslatorAwareTrait;
public function __construct( $translator )
{
$this->setTranslator( $translator );
}
public function modelMethodWhichNeedsToUseTranslator()
{
// ...
$text = $this->getTranslator()->translate('lorem ipsum');
// ...
}
}
When you creating your model first time on service or controller level
class someClass implements ServiceLocatorAwareInterface {
public function theMethodWhichCreatesYourModelInstance()
{
// ...
$sm = $this->getServiceLocator();
$model = new \Namespace\MyModel( $sm->get('translator') )
// ...
}
}
If you need to instantiate your model (new MyModel();) on multiple methods/classes, consider to writing a factory for it.
Here is a nice article about Dependency Injection and PHP by Ralph Schindler for more detailed comments about this approach.
For your Model class to be ServiceLocatorAware, you not only need to implement the interface, you also need to make your model a service of the service manager, and fetch the model from there.
Add your model to the service manager, since it doesn't appear to need any constructor params, it's invokable, so you can add it to the invokables array in service manager config. You can do that by using the getServiceConfig() method in your Module class...
class Module
{
public function getServiceConfig()
{
return array(
'invokables' => array(
'SomeModel' => 'Fully\Qualified\ClassName\To\SomeModel',
),
);
}
}
Then, instead of calling the new keyword to create your model instance, you fetch it from the service manager, for instance, by calling the getServiceLocator() method in a controller action...
public function fooAction()
{
$sm = $this->getServiceLocator();
$model = $sm->get('SomeModel');
}
When your model is fetched from the service manager, a service initializer will look to see if it implements the ServiceLocatorAwareInterface and automatically call setServiceLocator() if it does, passing it an instance of the service manager.