ZF3 FormElementManager: how to get ServiceManager - php

I'm porting code from ZF2 to ZF3.
In ZF2 when I create a form via FormElementManager I can access the servicelocator on the init method and configure some stuff like this:
public function init()
{
$this->serviceLocator = $this->getFormFactory()->getFormElementManager()->getServiceLocator();
$this->translator = $this->serviceLocator->get('translator');
}
This is convenient in very large applications. In fact all my forms inherit from a BaseForm class.
In ZF3 this is bad pratic and serviceLocator are deprecated.
Which is the best way to get the same result ?
One way is to inject every form in the ControllerFactory or ServiceFactory with the stuff needed but this is very tedious.
Any help is appreciate.

First of, you should not have the ServiceManager and/or childs of it (like the FormElementManager) available in your Form objects.
Using the Factory pattern, you should create fully functional, stand-alone Form, Fieldset and InputFilter objects.
There will definitely be some tedious work, as you put it, but you need only do it once.
Let's say you want to create a Location. A Location consists of a name property and a OneToOne unidirectional Address reference. This creates the following needs:
LocationForm (-InputFilter)
LocationFieldset (-InputFilter)
AddressFieldset (-InputFilter)
Config for the above
Factory for each of the 6 classes above
In this answer I'll mash everything down to bare minimums and use classes and examples from my own repositories, so for full code you can go here and for examples here.
After the creation of the classes themselves, I'll show you the config you need for this use case and the Factories which tie all of it together.
AbstractFieldset
abstract class AbstractFieldset extends Fieldset
{
public function init()
{
$this->add(
[
'name' => 'id',
'type' => Hidden::class,
'required' => false,
]
);
}
}
AbstractInputFilter
abstract class AbstractFieldsetInputFilter extends AbstractInputFilter
{
public function init()
{
$this->add([
'name' => 'id',
'required' => false,
'filters' => [
['name' => ToInt::class],
],
'validators' => [
['name' => IsInt::class],
],
]);
}
}
AddressFieldset
class AddressFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
$this->add([
'name' => 'street',
'required' => true,
'type' => Text::class,
'options' => [
'label' => 'Address',
],
]);
}
}
AddressInputFilter
class AddressFieldsetInputFilter extends AbstractFieldsetInputFilter
{
public function init()
{
parent::init();
$this->add([
'name' => 'street',
'required' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 3,
'max' => 255,
],
],
],
]);
}
}
So far, easy. Now, we need to create the LocationFieldset and LocationFieldsetInputFilter. These will make use of the Address(-Fieldset) classes.
LocationFieldset
class LocationFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
$this->add([
'name' => 'name',
'required' => true,
'type' => Text::class,
'options' => [
'label' => 'Name',
],
]);
$this->add([
'type' => AddressFieldset::class,
'name' => 'address',
'required' => true,
'options' => [
'use_as_base_fieldset' => false,
'label' => 'Address',
],
]);
}
}
LocationFieldsetInputFilter
class LocationFieldsetInputFilter extends AbstractFieldsetInputFilter
{
/**
* #var AddressFieldsetInputFilter
*/
protected $addressFieldsetInputFilter;
public function __construct(AddressFieldsetInputFilter $addressFieldsetInputFilter)
{
$this->addressFieldsetInputFilter = $addressFieldsetInputFilter;
}
public function init()
{
parent::init();
$this->add($this->addressFieldsetInputFilter, 'address');
$this->add(
[
'name' => 'name',
'required' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 3,
'max' => 255,
],
],
],
]
);
}
}
Ok, so that's not very exciting yet. Do note, the LocationFieldset uses the AddressFieldset as a type. Instead, in the InputFilter class a full fledged class object (an InputFilter instance) is expected.
So, the Form. I also use an AbstractForm (BaseForm in your case) to handle a few defaults. In my complete one (in linked repo), there's a bit more, but for here this'll suffice. This adds CSRF protection to the Form and adds a submit button if the form does not have one. This only gets done if the Form class does not have either one when you call the init, so you can override these settings.
AbstractForm
abstract class AbstractForm extends \Zend\Form\Form implements InputFilterAwareInterface
{
protected $csrfTimeout = 900; // 15 minutes
public function __construct($name = null, $options = [])
{
$csrfName = null;
if (isset($options['csrfCorrector'])) {
$csrfName = $options['csrfCorrector'];
unset($options['csrfCorrector']);
}
parent::__construct($name, $options);
if ($csrfName === null) {
$csrfName = 'csrf';
}
$this->addElementCsrf($csrfName);
}
public function init()
{
if (!$this->has('submit')) {
$this->addSubmitButton();
}
}
public function addSubmitButton($value = 'Save', array $classes = null)
{
$this->add([
'name' => 'submit',
'type' => Submit::class,
'attributes' => [
'value' => $value,
'class' => (!is_null($classes) ? join (' ', $classes) : 'btn btn-primary'),
],
]);
}
public function get($elementOrFieldset)
{
if ($elementOrFieldset === 'csrf') {
// Find CSRF element
foreach ($this->elements as $formElement) {
if ($formElement instanceof Csrf) {
return $formElement;
}
}
}
return parent::get($elementOrFieldset);
}
protected function addElementCsrf($csrfName = 'csrf')
{
$this->add([
'type' => Csrf::class,
'name' => 'csrf',
'options' => [
'csrf_options' => [
'timeout' => $this->csrfTimeout,
],
],
]);
}
}
LocationForm
class LocationForm extends AbstractForm
{
public function init()
{
$this->add([
'name' => 'location',
'type' => LocationFieldset::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);
parent::init();
}
}
Now we have everything to make the Form. We still need the validation. Let's create these now:
AddressFieldsetInputFilter
class AddressFieldsetInputFilter extends AbstractFieldsetInputFilter
{
public function init()
{
parent::init();
$this->add([
'name' => 'street',
'required' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 3,
'max' => 255,
],
],
],
]);
}
}
LocationFieldsetInputFilter
class LocationFieldsetInputFilter extends AbstractFieldsetInputFilter
{
protected $addressFieldsetInputFilter;
public function __construct(AddressFieldsetInputFilter $addressFieldsetInputFilter)
{
$this->addressFieldsetInputFilter = $addressFieldsetInputFilter;
}
public function init()
{
parent::init();
$this->add($this->addressFieldsetInputFilter, 'address');
$this->add(
[
'name' => 'name',
'required' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 3,
'max' => 255,
],
],
],
]
);
}
}
LocationFormInputFilter
class LocationFormInputFilter extends AbstractFormInputFilter
{
/** #var LocationFieldsetInputFilter */
protected $locationFieldsetInputFilter;
public function __construct(LocationFieldsetInputFilter $filter)
{
$this->locationFieldsetInputFilter = $filter
}
public function init()
{
$this->add($this->locationFieldsetInputFilter, 'location');
parent::init();
}
}
Right, that's all of the classes themselves. Do you see how they'll be nested together? This creates re-usable components, which is why I said you'll need to do this only once. Next time you need an Address or a Location, you just make sure to load the AddressFieldset and set the InputFilter in the Factory. The latter, setting the right InputFilter, is done via Setter Injection the Factories. Shown below.
AbstractFieldsetFactory
abstract class AbstractFieldsetFactory implements FactoryInterface
{
/**
* #var string
*/
protected $name;
/**
* #var string
*/
protected $fieldset;
/**
* #var string
*/
protected $fieldsetName;
/**
* #var string
*/
protected $fieldsetObject;
public function __construct($fieldset, $name, $fieldsetObject)
{
$this->fieldset = $fieldset;
$this->fieldsetName = $name;
$this->fieldsetObject = $fieldsetObject;
$this->hydrator = new Reflection(); // Replace this with your own preference, either Reflection of ZF or maybe the Doctrine EntityManager...
}
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$fieldset = $this->fieldset;
$fieldsetObject = $this->fieldsetObject;
/** #var AbstractFieldset $fieldset */
$fieldset = new $fieldset($this->hydrator, $this->name ?: $this->fieldsetName);
$fieldset->setHydrator(
new DoctrineObject($this->hydrator)
);
$fieldset->setObject(new $fieldsetObject());
return $fieldset;
}
}
AddressFieldsetFactory
class AddressFieldsetFactory extends AbstractFieldsetFactory
{
public function __construct()
{
parent::__construct(AddressFieldset::class, 'address', Address::class);
}
}
LocationFieldsetFactory
class LocationFieldsetFactory extends AbstractFieldsetFactory
{
public function __construct()
{
parent::__construct(LocationFieldset::class, 'location', Location::class);
}
}
AbstractFieldsetInputFilterFactory
abstract class AbstractFieldsetInputFilterFactory implements FactoryInterface
{
/**
* #var ContainerInterface
*/
protected $container;
/**
* #var HydratorInterface
*/
protected $hydrator;
/**
* #var InputFilterPluginManager
*/
protected $inputFilterManager;
/**
* Use this function to setup the basic requirements commonly reused.
*
* #param ContainerInterface $container
* #param string|null $className
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
*/
public function setupRequirements(ContainerInterface $container, $className = null)
{
$this->inputFilterManager = $container->get(InputFilterPluginManager::class);
}
}
AddressFieldsetInputFilterFactory
class AddressFieldsetInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
parent::setupRequirements($container, Address::class);
return new AddressFieldsetInputFilter($this->hydrator);
}
}
LocationFieldsetInputFilterFactory
class LocationFieldsetInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
parent::setupRequirements($container, Location::class);
/** #var AddressFieldsetInputFilter $addressFieldsetInputFilter */
$addressFieldsetInputFilter = $this->inputFilterManager->get(AddressFieldsetInputFilter::class);
return new LocationFieldsetInputFilter(
$addressFieldsetInputFilter,
$this->hydrator
);
}
}
That takes care of the FieldsetInputFilterFactory classes. Just the Form left.
In my case I use the same abstract factory class as for the Fieldset classes.
LocationFormInputFilterFactory
class LocationFormInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
parent::setupRequirements($container);
/** #var LocationFieldsetInputFilter $locationFieldsetInputFilter */
$locationFieldsetInputFilter = $this->getInputFilterManager()->get(LocationFieldsetInputFilter::class);
return new LocationFormInputFilter(
$locationFieldsetInputFilter,
$this->hydrator
);
}
}
So, that's all of the classes done. It's a complete setup. You might encounter some bugs as I modified my own code to remove getters/setters, code comments/hinting, error, property and variable checking without testing. But it should work ;)
However, we're nearly done. We still need:
config
usage in a Controller
print/use Form in a View
The config is easy:
'form_elements' => [
'factories' => [
AddressFieldset::class => AddressFieldsetFactory::class,
LocationFieldset::class => LocationFieldsetFactory::class,
LocationForm::class => LocationFormFactory::class,
],
],
'input_filters' => [
'factories' => [
AddressFieldsetInputFilter::class => AddressFieldsetInputFilterFactory::class,
LocationFieldsetInputFilter::class => LocationFieldsetInputFilterFactory::class,
LocationFormInputFilter::class => LocationFormInputFilterFactory::class,
],
],
That's it. That little bit ties all of the above classes together.
To get the Form into a Controller, you would do something like this in the Factory:
class EditControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$hydrator = new Reflection(); // or $container->get('hydrator') or $container->get(EntityManager::class), or whatever you use
/** #var FormElementManagerV3Polyfill $formElementManager */
$formElementManager = $container->get('FormElementManager');
/** #var LocationForm $form */
$form = $formElementManager->get(LocationForm::class); // See :) Easy, and re-usable
return new EditController($hydrator, $form);
}
}
A typical "Edit" action would be like this (mind, this one uses Doctrine's EntityManager as the hydrator):
public function editAction()
{
$id = $this->params()->fromRoute('id', null);
/** #var Location $entity */
$entity = $this->getObjectManager()->getRepository(Location::class)->find($id);
/** #var LocationForm $form */
$form = $this->form;
$form->bind($entity);
/** #var Request $request */
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
/** #var Location $object */
$object = $form->getObject();
$this->getObjectManager()->persist($object);
try {
$this->getObjectManager()->flush();
} catch (Exception $e) {
// exception handling
}
return $this->redirect()->toRoute('route/name', ['id' => $object->getId()]);
}
}
return [
'form' => $form,
'validationMessages' => $form->getMessages() ?: '',
];
}
And the View Partial would look like this (based on the return in the above action):
form($form) ?>
So, that's it. Fully fledged, re-usable classes. Single setup. And in the end just a single line in the Factory for the Controller.
Please take note though:
Form, Fieldset and InputFilter use "address" input name. Very important to keep these the same throughout as Zend does some magic based on the names to match Fieldset with InputFilter.
If you have any more questions about how this works, please read through the documentation in the repo's I linked first, before asking below this question. There's more there that should help you out more, for example for Collection handling.

Related

How to load select option from database in zf2

I Have been following zf2 guide for blog I have created everything Controller, Factory, Form, Mapper, Model, Service, view etc
In my form I have a select element
$this->add(array(
'type' => 'select',
'name' => 'roleId',
'attributes' => array(
'id' => 'roleId',
'options' => array(
'1' => 'Admin',
'2' => 'Manager',
),
),
'options' => array(
'label' => 'Role',
),
));
Now in this form I want to load the option for the role from the database.
I tried loading the option by creating a simple function, which can be accessed in the element as below, but Am not able to fetch the result. I have already created Controller, Factory, Form, Mapper, Model, Service and view, Where I can do CRUD operation on Role.
$this->add(array(
'type' => 'select',
'name' => 'roleId',
'attributes' => array(
'id' => 'roleId',
'options' => $this->getAllRoles(),
),
'options' => array(
'label' => 'Role',
),
));
public function getAllRoles()
{
$roles = $this->getServiceLocator()->get('Admin\Service\RoleService');
$allRoles = $this->getAllTheRoles();
return $allroles;
}
Can anybody guide me how can I load all the Roles in option as listed in the IndexAction following Blog Post with ID and Name of the Role.
You could create a reusable form element that is pre-populated with the roles. To do so you must register the service with the form element manager in module.config.php.
return [
'form_elements' => [
'factories' => [
'RoleSelect' => 'MyModule\Form\Element\RoleSelectFactory',
],
],
];
There is not need to extend the standard select class as the changes are configuration only. This is best done in a factory class.
namespace MyModule\Form\Element;
use Zend\Form\Element\Select;
class RoleSelectFactory
{
public function __invoke($formElementManager, $name, $rname)
{
$select = new Select('role_id');
$select->setOptions(['label' => 'Role']);
$select->setAttributes(['id' => 'role_id']);
$serviceManager = $formElementManager->getServiceLocator();
$roleService = $serviceManager->get('Admin\Service\RoleService');
$options = [];
foreach($roleService->getAllTheRoles() as $role){
$options[$role->getId()] => $role->getName();
}
$select->setValueOptions($options);
return $select;
}
}
Adding the element within a form can then be updated to use the name of the service we registered.
$this->add([
'name' => 'role_id'
'type' => 'RoleSelect',
]);
One important point to remember is that the form using this element must be created using the $formElementManager->get('FormWithRoleSelect').
Finally found out the simple way to do this, Am really not sure if this is the correct way.
Added the Role service in the User Controller
Code in my userController.php
use Admin\Service\RoleServiceInterface;
class UserController extends AbstractActionController
{
/**
* #var \Admin\Service\UserServiceInterface
*/
protected $userService;
protected $roleService;
protected $userForm;
public function __construct(
UserServiceInterface $userService,
RoleServiceInterface $roleService,
FormInterface $userForm
)
{
$this->userService = $userService;
$this->roleService = $roleService;
$this->userForm = $userForm;
}
public function addAction()
{
$request = $this->getRequest();
$roles = $this->roleService->findAllRoles();
foreach ($roles as $role) {
if($role->getStatus() == 1) {
$allRoles[$role->getId()] = $role->getName();
}
}
$this->userForm->__construct(null, array('roleOptions'=>$allRoles));
}
}
My UserControllerFactory.php
<?php
namespace Admin\Factory;
use Admin\Controller\UserController;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class UserControllerFactory implements FactoryInterface
{
/**
* Create service
*
* #param ServiceLocatorInterface $serviceLocator
*
* #return mixed
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$realServiceLocator = $serviceLocator->getServiceLocator();
$userService = $realServiceLocator->get('Admin\Service\UserServiceInterface');
$roleService = $realServiceLocator->get('Admin\Service\RoleServiceInterface');
$userInsertForm = $realServiceLocator->get('FormElementManager')->get('Admin\Form\UserForm');
return new UserController(
$userService,
$roleService,
$userInsertForm
);
}
}
Finally the UserForm.php
<?php
namespace Admin\Form;
use Zend\Form\Form;
use Admin\Model\User;
use Zend\Stdlib\Hydrator\ClassMethods;
use Zend\Form\Element\Select;
class UserForm extends Form
{
public function __construct($name = null, $options = array())
{
$roleOptions = array();
if($options) {
$roleOptions = $options['roleOptions'];
}
parent::__construct($name, $options);
$this->setHydrator(new ClassMethods(false));
$this->setObject(new User());
$this->add(array(
'type' => 'hidden',
'name' => 'id'
));
$this->add(array(
'type' => 'select',
'name' => 'roleId',
'attributes' => array(
'id' => 'roleId',
'options' => $roleOptions
),
'options' => array(
'label' => 'Role',
),
));
}
}
This way using service manager I was successfully able to load the data in my for Select option.
Call getAllRoles() inside the controller then You can pass your custom array as parameter for form when you create form object. In form __construct function you can retrieve that array and set like this
'options' => $roles,

ZF2 Passing Route To Form Element

I am creating a form which needs dynamic options based on the route value of survey_question_reference
'main-surveyquestions'=> [
'type' => 'segment',
'options' => [
'route' => '/survey-questions[/:survey_question_reference][/:answer]',
'constraints' => [
'survey_question_reference' => '[0-9]*',
'answer' => '(answer)',
],
'defaults' => [
'controller' => 'Main\Controller\Main',
'action' => 'surveyquestions'
]
]
],
This is the Form code which calls the FormElement:
/**
* Init
*/
public function init()
{
/**
* Survey Answer
*/
$this->add(
[
'type' => 'Main\Form\Element\SurveyAnswerRadio',
'name' => 'survey_answer',
'options' => [
'label' => 'survey_answer'
],
'attributes' => [
'id' => 'survey_answer'
]
]
);
}
The following is the code from the Form Element. Where I have hard coded 'sqReference' => '1' the 1 needs to be replaced with the value of survey_question_reference from the route.
namespace Main\Form\Element;
use Doctrine\ORM\EntityManager;
use Zend\Form\Element\Radio;
/**
* Class SurveyAnswerRadio
*
* #package Main\Form\Element
*/
class SurveyAnswerRadio extends Radio
{
/**
* #var EntityManager $entityManager
*/
protected $entityManager;
/**
* #param EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* Get Value Options
*
* #return array
*
* #throws \Exception
*/
public function getValueOptions()
{
$array = [];
$result = $this->entityManager
->getRepository('AMDatabase\Entity\TheVerse\SA')
->findBy(
[
'sqReference' => '1'
],
[
'surveyAnswer' => 'ASC'
]
);
if (is_array($result) && count($result) > '0') {
/**
* #var \AMDatabase\Entity\TheVerse\SA $val
*/
foreach ($result as $val) {
$array[$val->getReference()] = $val->getSurveyAnswer();
}
}
return $array;
}
}
What you're looknig for is to inject the survey_question_reference parameter to your FormElement. You could do that as suggested by #kuldeep.kamboj in his answers. But if you don't want to change your approach and keep your custom SurveyAnswerRadio element, you have to make some fiew changes in your code :
Make SurveyAnswerRadio implements Zend\ServiceManager\ServiceLocatorAwareInterface so that you could implement setServiceLocator and getServiceLocator, which are required by the ServiceManager to automatically inject the service locator when the element is instantiated.
Your form should also implements Zend\ServiceManager\ServiceLocatorAwareInterface.
Implement the getFormElementConfig method in your Module.php.
Let’s look through the code now. You'll have something like this :
SurveyAnswerRadio :
class SurveyAnswerRadio extends Radio implements ServiceLocatorAwareInterface
{
//Add these two methods
public function setServiceLocator(ServiceLocatorInterface $sl)
{
$this->serviceLocator = $sl;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
public function getValueOptions()
{
$array = [];
$serviceManager = $this->serviceLocator->getServiceLocator();
$em = $serviceManager->get('Doctrine\ORM\EntityManager');
$sqReference = $serviceManager->get('application')->getMvcEvent()
->getRouteMatch()->getParam('survey_question_reference');
$result = $em->getRepository('AMDatabase\Entity\TheVerse\SA')
->findBy(
['sqReference' => $sqReference],
['surveyAnswer' => 'ASC']
);
if (is_array($result) && count($result) > '0') {
foreach ($result as $val) {
$array[$val->getReference()] = $val->getSurveyAnswer();
}
}
return $array;
}
}
Module.php :
Implement the getFormElementConfig method as follows. This allows the class ModuleName\Form\Element\SurveyAnswerRadio to be instantiated, or invoked, with the alias SurveyAnswerRadio.
class Module implements FormElementProviderInterface
{
// other stuff .....
public function getFormElementConfig()
{
return array(
'invokables' => array(
'SurveyAnswerRadio' => 'ModuleName\Form\Element\SurveyAnswerRadio'
)
);
}
}
No changes needed in the Form init method let it as it is.
Note that in your controller, you'll have to instantiate the Form via the FormElementManager :
$formManager = $this->serviceLocator->get('FormElementManager');
$form = $formManager->get('ModuleName\Form\YourForm');
Please see more details in the documentation
See also this post which exaplains how to manage dependencies within a custom Select Element in Form.
I will suggest to change approach. First do not try to extends Radio Element which is not necessary at all. You can do same in your Form Class. Second your entity manager also not work in Radio/Form class until your find mechanism to pass.
So I would suggest solutions like below.
First register your form class into as factory in module.config.php
'form_elements' => array(
'factories' => array(
'Main\Form\YourFormName' => function($sm) {
$form = new Form\YourFormName();
$form->setEntityManager($sm->getServiceLocator()->get('Doctrine\ORM\EntityManager'));
$form->setServiceLocator($sm->getServiceLocator());
return $form;
},
),
),
Then implement entityManager and serviceLocator into your form class.
use DoctrineModule\Persistence\ObjectManagerAwareInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class YourFormName extends Form implements ObjectManagerAwareInterface, ServiceLocatorAwareInterface
{
protected $entityManager;
protected $serviceLocator;
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
public function setEntityManager(ObjectManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function getEntityManager()
{
return $this->entityManager;
}
Then in init method you have serviceLocator/entityManager is already initialized.
public function init()
{
$routeMatch = $this->getServiceLocator()->get('Application')->getMvcEvent()->getRouteMatch();
$array = [];
$result = $this->entityManager
->getRepository('AMDatabase\Entity\TheVerse\SA')
->findBy(
[
'sqReference' => $routeMatch->getParam('survey_question_reference')
],
[
'surveyAnswer' => 'ASC'
]
);
if (is_array($result) && count($result) > '0') {
/**
* #var \AMDatabase\Entity\TheVerse\SA $val
*/
foreach ($result as $val) {
$array[$val->getReference()] = $val->getSurveyAnswer();
}
}
$this->add(
[
'type' => 'Zend\Form\Element\Radio',
'name' => 'survey_answer',
'options' => [
'label' => 'survey_answer',
'value_options' => $array,
],
'attributes' => [
'id' => 'survey_answer',
]
]
);

Extends User plugin by adding a profile does not render tab either new added fields in OctoberCMS

I've follow all the steps on the Extending User plugin screencast but for some reason I can not see "Profile" tab and either new added fields. Since I used the second approach, the easy one, this is what I've done:
Create the plugin and models and so on under Alomicuba namespace
Create and make the needed changes to the files as explained in video:
Plugin.php
<?php namespace Alomicuba\Profile;
use System\Classes\PluginBase;
use RainLab\User\Models\User as UserModel;
use RainLab\User\Controllers\Users as UsersController;
/**
* Profile Plugin Information File
*/
class Plugin extends PluginBase
{
public $requires = ['RainLab.User'];
/**
* Returns information about this plugin.
*
* #return array
*/
public function pluginDetails()
{
return [
'name' => 'Profile',
'description' => 'Add extra functionalities for Alomicuba WS by extends RainLab User',
'author' => 'DTS',
'icon' => 'icon-users'
];
}
public function boot()
{
UserModel::extend(function($model){
$model->hasOne['profile'] = ['Alomicuba\Profile\Models\Profile'];
});
UsersController::extendFormFields(function ($form, $model, $context){
if ($model instanceof UserModel)
return;
$form->addTabFields([
'pinCode' => [
'label' => 'PIN',
'tab' => 'Profile'
],
'phone2' => [
'label' => 'Teléfono (2)',
'tab' => 'Profile'
],
'phone3' => [
'label' => 'Teléfono (3)',
'tab' => 'Profile'
],
'phone4' => [
'label' => 'Teléfono (4)',
'tab' => 'Profile'
]
]);
});
}
}
add_profiles_fields_to_user_table.php
<?php namespace Alomicuba\Profile\Updates;
use Schema;
use October\Rain\Database\Updates\Migration;
class AddProfilesFieldsToUserTable extends Migration
{
public function up()
{
Schema::table('users', function($table)
{
$table->integer('pinCode')->unsigned();
$table->dateTime('pinCodeDateTime');
$table->integer('phone2')->unsigned()->nullable();
$table->integer('phone3')->unsigned()->nullable();
$table->integer('phone4')->unsigned()->nullable();
});
}
public function down()
{
$table->dropDown([
'pinCode',
'pinCodeDateTime',
'phone2',
'phone3',
'phone4'
]);
}
}
version.yaml
1.0.1: First version of Profile
1.0.2:
- Created profiles table
- create_profiles_table.php
- add_profiles_fields_to_user_table.php
Profile.php (Model)
<?php namespace Alomicuba\Profile\Models;
use Model;
/**
* Profile Model
*/
class Profile extends Model
{
/**
* #var string The database table used by the model.
*/
public $table = 'alomicuba_profile_profiles';
/**
* #var array Relations
*/
public $belongsTo = [
'user' => ['RainLab\User\Models\User']
];
// This method is not need anymore since I'll use the second approach
public static function getFromUser($user)
{
if ($user->profile)
return $user->profile;
$profile = new static;
$profile->user = $user;
$profile->save();
$user->profile = $profile;
return $profile;
}
}
But when I edit a existent user I didn't see the 'Profile' tab and also didn't see any new added field. See image below:
Any advice around this? Did I miss something?
Also I have a few question around plugin extends:
How do I add a required field to the register form?
How do I display each new added field on the account form?
I haved tested your code on my machine you need to write
$require instead of $requires in plugin.php
please check documentation
http://octobercms.com/docs/plugin/registration#dependency-definitions
and when extendFormFields() method called for UserController you need to specify that you only want to extends fields for UserModel not for other
if (!$model instanceof UserModel)
return;
so plugin.php code look like this
<?php namespace Alomicuba\Profile;
use System\Classes\PluginBase;
use RainLab\User\Models\User as UserModel;
use RainLab\User\Controllers\Users as UsersController;
/**
* Profile Plugin Information File
*/
class Plugin extends PluginBase
{
public $require = ['RainLab.User'];
/**
* Returns information about this plugin.
*
* #return array
*/
public function pluginDetails()
{
return [
'name' => 'Profile',
'description' => 'Add extra functionalities for Alomicuba WS by extends RainLab User',
'author' => 'DTS',
'icon' => 'icon-users'
];
}
public function boot()
{
UserModel::extend(function($model){
$model->hasOne['profile'] = ['Alomicuba\Profile\Models\Profile'];
});
UsersController::extendFormFields(function ($form, $model, $context){
if (!$model instanceof UserModel)
return;
$form->addTabFields([
'pinCode' => [
'label' => 'PIN',
'tab' => 'Profile'
],
'phone2' => [
'label' => 'Teléfono (2)',
'tab' => 'Profile'
],
'phone3' => [
'label' => 'Teléfono (3)',
'tab' => 'Profile'
],
'phone4' => [
'label' => 'Teléfono (4)',
'tab' => 'Profile'
]
]);
});
}
}
and in add_profiles_fields_to_user_table.php
for dropping column write following code
Schema::table('users', function($table)
{
$table->dropDown([
'pinCode',
'pinCodeDateTime',
'phone2',
'phone3',
'phone4'
]);
}

How to pass options/params to formCollection fieldset in ZendFramework 2?

I have a Form which contains a formCollection with a select element that I want to populate with values (from SQL) depending on a POST param.
Passing this param from controller to form wasn't a problem but now I can't find a way to set/read that param in target_element of formCollection. Any ideas on how make that work?
Here's my code:
Controller
class MyController extends AbstractActionController{
public function indexAction()
{
$form = $this->serviceLocator->get('FormElementManager')->get('Module\Form\myForm');
$form->init([
'param' => $this->params()->fromPost('param')
]);
}
}
Form
class myForm extends Form
{
private $sm;
public function __construct($sm = null)
{
parent::__construct();
$this->sm = $sm;
}
public function init($params=[])
{
$this->add([
'name' => 'choices',
'type' => 'Zend\Form\Element\Collection',
'options' => [
'label' => 'SelectLabel',
'count' => 1,
'should_create_template' => true,
'allow_add' => true,
'template_placeholder' => '__placeholder__',
'target_element' => [
'type' => 'Module\Form\choicesFieldset',
'options' => [
'param' => isset($params['param']) ? $params['param'] : 0,
]
],
],
]);
}
}
Fieldset
class choicesFieldset extends Fieldset{
private $sm;
public function __construct($sm = null){
parent::__construct();
$this->sm = $sm;
}
public function init(){
$param = $this->getOption('param');
$availableChoices = /* SQL_QUERY_BASED_ON_PARAM; */
$this->add([
'name' => 'choice_1',
'type' => 'Select',
'options' => [
'label' => 'First choice',
'value_options' => $availableChoices,
]
]);
}
}
Thanks in advance for your help.
All you would need to do is fetch the Request instance from the service manager; check the parameter you want and then 'inject' it into the form.
It would make more sense to do so in the form factory; rather than repeat yourself within controllers.
For example:
public function getFormElementConfig()
{
return array(
'factories' => array(
'MyModule\Form\MyForm' => function($formElementManager) {
$serviceManager = $formElementManager->getServiceLocator();
$request = $serviceManager->get('Request');
// defaults to 0 if not set
$param = $request->getPost('the_posted_variable_name', 0);
$options = array(
'my_custom_option_name' => $param,
);
// You should maintain the Zend\Form\Element::__construct() method signuture
// as it allows for the 'options' to be passed in.
// Alternatively you could use $form->setOption('param', $options)
// and inject the options as a soft dependency
$form = new Form\MyForm('my_form', $options, $serviceManager);
// ... other form stuff here
return $form;
},
),
);
}
Now you can use the option within the form using:
$param = $this->getOption('my_custom_option_name');

ZF2 form element collection validation

So I have a "simple" form
class SiteAddForm extends Form
{
public function __construct()
{
parent::__construct('add_site_form');
$site = new SiteFieldSet();
$this->add($site);
}
public function getTemplate()
{
return 'site_add.phtml';
}
}
The form it self does nothing. It adds a field_set and returns a template name.
The SiteFieldSet looks likes:
class SiteFieldSet
extends FieldSet
implements InputFilterProviderInterface
{
public function __construct()
{
parent::__construct('site');
$name = new Text('name');
$this->add($name);
$domains = new Collection('domains');
$domains->setTargetElement(new DomainFieldSet())
->setShouldCreateTemplate(true);
$this->add($domains);
}
public function getTemplate()
{
return 'site.phtml';
}
/**
* Should return an array specification compatible with
* {#link Zend\InputFilter\Factory::createInputFilter()}.
*
* #return array
*/
public function getInputFilterSpecification()
{
return [
'name' => [
'required' => true,
'validators' => [
new StringLength([
'min' => 200,
])
]
],
'domains' => [
'required' => true,
],
];
}
}
It adds a text and collection element to the fieldset. The field set implements InputFilterProviderInterface to validate the data thrown into it.
The name must be at least 200 chars (for testing) and the collection is required.
But now comes my problem. With the field set that is thrown into the collection, code:
class DomainFieldSet
extends FieldSet
implements InputFilterProviderInterface
{
public function __construct()
{
parent::__construct('domain');
$host = new Url('host');
$this->add($host);
$language = new Select('language', [
'value_options' => [
'nl_NL' => 'NL',
],
]);
$this->add($language);
$theme = new Select('theme', [
'value_options' => [
'yeti' => 'Yeti',
]
]);
$this->add($theme);
}
public function getTemplate()
{
return 'domain.phtml';
}
/**
* Should return an array specification compatible with
* {#link Zend\InputFilter\Factory::createInputFilter()}.
*
* #return array
*/
public function getInputFilterSpecification()
{
return [
'host' => [
'required' => true,
'validators' => [
new StringLength([
'min' => 200,
])
]
],
'language' => [
'required' => true,
],
'theme' => [
'required' => true,
],
];
}
}
Again nothing special. There are now three elements defined host, theme & language. Again the field set implements InputFilterProviderInterface. So there must be an getInputFilterSpecification in the class.
When I fill in the form
site[name] = "test"
site[domains][0][host] = 'test'
site[domains][0][theme] = 'yeti'
site[domains][0][language] = 'nl_NL'
It gives an error for site[name] saying it must be atleast 200 chars, so validations "works"
But it should also give an error on site[domains][0][host] that it needs to be atleast 200 chars (code was copy pasted, and the use is correct).
So why doesn't the validation kicks in, and or how can I solve the issue so a element/field set inside a collection is properly validated
Try using setValidationGroup in the form __construct method
like:
public function __construct()
{
$this->add(array(
'type' => 'Your\Namespace\SiteFieldSet',
'options' => array(
'use_as_base_fieldset' => true,
),
));
$this->setValidationGroup(array(
'site' => array(
'domain' => array(
'host',
'language',
'theme',
),
),
));
}
or this may also work...
$this->setValidationGroup(FormInterface::VALIDATE_ALL);

Categories