ZF2 Form: Customizing order of elements - php

I'm creating a form for a logged-in user to change their password, so I created a subclass of an existing password-reset form I have available. The forms will be identical except with an additional field for existing password. It's worked so far, except I can't figure out a way to manually set the order the new field; the only place I've gotten it to appear is at the end of the form. It seems that ZF2 requires you to add() form elements in the order that you want them rendered. I would do so, except the subclass form's constructor must the parent form's constructor before it can add new fields, by which point the parent form has already added its fields.
I have already tried setting the property order of my new field, but it did not work; I've tried several different combinations (I can't find the documentation for this feature anywhere, after lots of searching).
Subclass constructor snippet:
class ChangePassword extends ResetPassword implements InputFilterProviderInterface {
public function __construct() {
parent::__construct();
$this->add(array(
'type' => 'Zend\Form\Element\Password',
'name' => 'existingPassword',
'order' => 0,
'options' => array(
'label' => 'Existing Password',
'order' => 0,
),
'attributes' => array(
'required' => 'required',
'order' => 0,
)
));
}
Parent constructor snippet:
class ResetPassword extends Form implements InputFilterProviderInterface {
public function __construct() {
parent::__construct('reset-password');
$this->add(array(
'type' => 'Zend\Form\Element\Password',
'name' => 'password',
...

The key you're looking for which affects element order is named priority.
The form add() method accepts a second array containing $flags, and it's in this array that you must add the priority key/value pair.
Your constructor should end up looking something like this ...
class ChangePassword extends ResetPassword implements InputFilterProviderInterface {
public function __construct() {
parent::__construct();
$this->add(array(
'type' => 'Zend\Form\Element\Password',
'name' => 'existingPassword',
'options' => array(
'label' => 'Existing Password',
),
'attributes' => array(
'required' => 'required',
)
), // add flags array containing priority key/value
array(
'priority' => 1000, // Increase value to move to top of form
));
}
}

I have encouter this issue today, Crisp's answer helped but I think it would be nice to precise this :
In the view we have a lot of options to show our form :
<?= $this->form($form)?>
<?= $form->get('{element}') ?>
loop over $form->getIterator()
loop over $form->getElements()
etc...
I have to say i used a lot this structure in all of my projects :
<?php foreach ($form->get('fieldset')->getElements() as $elementName => $element): ?>
<?= $this->partial('partial/formElement', ['element' => $element])?>
<?php endforeach ?>
The problem is : getElements does not use priority, so its just give the element in order of when it was instanciated.
In the view we have to use the iteration method ($form->getIterator()) to get back this flag priority.

Related

ZF2 Form and Doctrine 2 modify the value_options

I am using Doctrine 2 in my Zend Framework 2 Project. I have now created a Form and create one of my Dropdowns with Values from the Database. My Problem now is that I want to change which values are used and not the one which I get back from my repository. Okay, here some Code for a better understanding:
$this->add(
array(
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'name' => 'county',
'options' => array(
'object_manager' => $this->getObjectManager(),
'label' => 'County',
'target_class' => 'Advert\Entity\Geolocation',
'property' => 'county',
'is_method' => true,
'empty_option' => '--- select county ---',
'value_options'=> function($targetEntity) {
$values = array($targetEntity->getCounty() => $targetEntity->getCounty());
return $values;
},
'find_method' => array(
'name' => 'getCounties',
),
),
'allow_empty' => true,
'required' => false,
'attributes' => array(
'id' => 'county',
'multiple' => false,
)
)
);
I want to set the value for my Select to be the County Name and not the ID. I thought that I would need the 'value_options' which needs an array. I tried it like above, but get the
Error Message: Argument 1 passed to Zend\Form\Element\Select::setValueOptions() must be of the type array, object given
Is this possible at all?
I was going to suggest modifying your code, although after checking the ObjectSelect code i'm surprised that (as far as I can tell) this isn't actually possible without extending the class. This is because the value is always generated from the id.
I create all form elements using factories (without the ObjectSelect), especially complex ones that require varied lists.
Alternative solution
First create a new method in the Repository that returns the correct array. This will allow you to reuse that same method should you need it anywhere else (not just for forms!).
class FooRepository extends Repository
{
public function getCounties()
{
// normal method unchanged, returns a collection
// of counties
}
public function getCountiesAsArrayKeyedByCountyName()
{
$counties = array();
foreach($this->getCounties() as $county) {
$counties[$county->getName()] = $county->getName();
}
return $counties;
}
}
Next create a custom select factory that will set the value options for you.
namespace MyModule\Form\Element;
use Zend\Form\Element\Select;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
class CountiesByNameSelectFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $formElementManager)
{
$element = new Select;
$element->setValueOptions($this->loadValueOptions($formElementManager));
// set other select options etc
$element->setName('foo')
->setOptions(array('foo' => 'bar'));
return $element;
}
protected function loadValueOptions(ServiceLocatorInterface $formElementManager)
{
$serviceManager = $formElementManager->getServiceLocator();
$repository = $serviceManager->get('DoctrineObjectManager')->getRepository('Foo/Entity/Bar');
return $repository->getCountiesAsArrayKeyedByCountyName();
}
}
Register the new element with the service manager by adding a new entry in Module.php or module.config.php.
// Module.php
public function getFormElementConfig()
{
return array(
'factories' => array(
'MyModule\Form\Element\CountiesByNameSelect'
=> 'MyModule\Form\Element\CountiesByNameSelectFactory',
),
);
}
Lastly change the form and remove your current select element and add the new one (use the name that you registered with the service manager as the type key)
$this->add(array(
'name' => 'counties',
'type' => 'MyModule\Form\Element\CountiesByNameSelect',
));
It might seem like a lot more code (because it is) however you will benefit from it being a much clearer separation of concerns and you can now reuse the element on multiple forms and only need to configure it in one place.

Zf2 Form dependent on userId

I want to create a dropdown unique to the users, and found a way to do that, but its not ok with the client.
here is what i`ve done:
public function __construct($name = null, EntityManager $em = null, $userId = null)
{
parent::__construct($name);
$this->setAttribute('method', 'post');
[...]
$this->add(array(
'name' => 'religionId',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => array(
'object_manager' => $this->getEntityManager(),
'target_class' => 'Religions\Entity\Religions',
'property' => 'name',
'disable_inarray_validator' => true,
'by_reference' => false,
'is_method' => true,
'find_method' => array(
'name' => 'findBy',
'params' => array(
'criteria' => array('reUserId' => $userId),
'orderBy' => array('name' => 'ASC'),
),
),
),
'attributes' => array(
'multiple' => false,
'required' => false,
)
));
}
This worked, as i was sending the variable reuserId when initializing the form, using $this->identity()
The client wants to inject the user entity and select from there....
Searched stackoverflow and google, but was not able to find anything...any help please? thanks!
To 'inject' anything in ZF2 you will need to use the ServiceManager and create service factories.
It is therefore important to ensure that you are always creating the form via the ServiceManager.
In a controller for instance:
$form = $this->getServiceLocator()->get('MyModule\Form\FooForm');
Then you would need to 'inject' the user buy creating a factory class or closure.
Module.php
public function getFormElementConfig() {
return array(
'factories' => array(
'MyModule\Form\FooForm' => function($fem) {
$serviceManager = $fem->getServiceLocator();
$entityManager = $serviceManager->get('objectmanager'); // Doctrine object manager
// Load the user
// Example is the zfcUser authentication service, however replace
// this with whatever you use to maintain the users id
$user = $serviceManager->get('zfcuser_auth_service')->getIdentity();
// Inject the user entity into the form constructor
$form = new FooForm($user);
return $form;
},
),
);
}
With that said I think you might need to think about the form dependencies. It seems to me that you do not depend on the user entity - but rather the user's id should be used in a database query that reduces the list of 'Religions'.
You could execute this query and then pass the result (The religion collection) to the form in the same way my example shows how to for the user entity - This would then mean you could use a 'normal' Zend\Form\Element\Select rather than the ObjectSelect - meaning no need to inject the ObjectManager etc.

ZF2 - Register custom form element

In ZF2, I've overridden the Text element with my own (call it My\Form\Element\Text). Now I want to make it so that when I add a text element to the form, it defaults to my overridden class and not Zend\Form\Element\Text:
$this->add([
'type' => 'text',
'name' => 'to',
]);
I know that I could use 'type' => 'My\Form\Element\Text' instead of just 'type' => 'text', but I'm trying to find out if I can avoid that and just use the custom element by default.
I've tried both of these techniques:
module.config.php
return [
'form_elements' => [
'invokables' => [
'text' => 'My\Form\Element\Text',
],
],
];
Module.php
class Module {
public function getFormElementConfig() {
return [
'invokables' => [
'text' => 'My\Form\Element\Text',
],
];
}
}
Neither of these worked (still getting an instance of Zend\Form\Element\Text). Is there some other way of registering the element so that the Zend\Form\Factory::create() method creates an instance of my custom element instead of the Zend version?
Although your config is correct, there are a couple of gotchas to be aware of when using custom elements, detailed in the docs here
Catch 1
If you are creating your form class by extending Zend\Form\Form, you must not add the custom element in the __construct-or, but rather in the init() method
Catch 2
You must not directly instantiate your form class, but rather get an instance of it through the Zend\Form\FormElementManager

How to inject the Doctrine ObjectManager into form element

I'm working on my custom User module, which basically uses ZfcUser as the foundation. Since every application is different and requires different meta information about users I want this to be easily configurable using a config array.
In my module's global config file I define the custom form fields and in the Module's onBootstrap I extend the ZfcUser registration form using the init event of ZfcUser\Form\Register. So in short I want to do something like this:
$sharedEvents->attach('ZfcUser\Form\Register',
'init',
function($e) use ($sm)
{
/* #var $form \ZfcUser\Form\Register */
$form = $e->getTarget();
// Get relevant config
$config = $sm->get('config');
if ( array_key_exists('redev_user', $config) && is_array($config['redev_user']) )
{
if ( array_key_exists('custom_fields', $config['redev_user']) && is_array($config['redev_user']['custom_fields']) )
{
foreach ($config['redev_user']['custom_fields'] as $curCustomField)
{
$form->add($curCustomField);
}
}
}
[...]
In my config file I then define the custom form fields like this:
<?php
return array(
'redev_user' => array(
'custom_fields' => array(
// Custom fields which will be added to the registration form
array(
'name' => 'firstname',
'type' => 'text',
'options' => array(
'label' => 'First name',
),
),
I do the same thing for the validators; they are being defined in the config file and attached to the form elements in the onBootstrap.
This all works nice and dandy, except when I need a Doctrine form element. In my specific case I would like to use a DoctrineModule\Form\Element\ObjectSelect for the country selectbox. In my config this would look like this:
array(
'name' => 'country',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => array(
'label' => 'Country',
//'object_manager' => $sm->get('Doctrine\ORM\EntityManager'),
'target_class' => 'RedevUser\Entity\Country',
'property' => 'countryname',
'is_method' => false,
'find_method' => array(
'name' => 'findBy',
'params' => array(
'criteria' => array(),
'orderBy' => array('countryname' => 'ASC'),
),
),
),
),
Note the commented out line for the object_manager. The ObjectSelect element obviously needs the ObjectManager. The question is how do I inject the ObjectManager while rendering the form based on the config.
I was thinking to render the form element myself and then check if it's an instance of some interface or base class of DoctrineModule\Form\Element. However it turns out there is no such base class or interface. The only thing those elements have in common is that they have a getProxy. Right now my code in onBootstrap looks like this:
foreach ($config['redev_user']['custom_fields'] as $curCustomField)
{
$formElemFactory = $form->getFormFactory();
$elem = $formElemFactory->createElement($curCustomField);
if ($elem instanceof \DoctrineModule\Form\Element\ObjectSelect)
{
// Inject ObjectManager
$elem->getProxy()->setObjectmanager($sm->get('Doctrine\ORM\EntityManager'));
}
$form->add($elem);
}
But I don't really want to check for the different Doctrine form element types. Also it seems a bit dirty to do it like this. Any opinions or ideas of how to do this better/cleaner?

Yii - CGridView - add own attribute

$this->widget('zii.widgets.grid.CGridView', array(
'dataProvider'=>$dataProvider,
'columns'=>array(
'title', // display the 'title' attribute
'category.name', // display the 'name' attribute of the 'category' relation
'content:html', // display the 'content' attribute as purified HTML
array( // display 'create_time' using an expression
'name'=>'create_time',
'value'=>'date("M j, Y", $data->create_time)',
),
array( // display 'author.username' using an expression
'name'=>'authorName',
'value'=>'$data->author->username',
//HERE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'htmlOptions'=>array('class'=>'$data->author->username', 'secondAttribute' => $data->author->id),
//HERE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
),
array( // display a column with "view", "update" and "delete" buttons
'class'=>'CButtonColumn',
),
),
));
In option value i can add variable from PHP, but for option htmlOptions this is not possible. Why?How can i make attribute with PHP variable?
When you add arrays in the columns collection without specifying a class property, the type of column being created is CDataColumn. The property CDataColumn::value is explicitly documented to be
a PHP expression that will be evaluated for every data cell and whose
result will be rendered as the content of the data cells.
Therefore, value has the special property that it gets eval'ed for each row and that's why you can set it "dynamically". This is an exception however, and almost nothing else supports the same functionality.
However, you are in luck because the property cssClassExpression is another special exception that covers exactly this usage case. So you can do it like this:
array(
'name'=>'authorName',
'value'=>'$data->author->username',
'cssClassExpression' => '$data->author->username',
),
Edit: I made a mistake while copy/pasting from your example and did not notice that you were trying to do the same thing for additional attributes inside htmlOptions (I have now deleted the relevant part of the code).
If you need to add more options for dynamic values you have no choice but to subclass CDataColumn and override the renderDataCell method (stock implementation is here).
Don't know if this still applies or not (given that there is an accepted answer), but there is a slightly better solution in the form of "rowHtmlOptionsExpression". This specifies an expression that will be evaluated for every row. If the result of the eval() call is an array, it will be used as the htmlOptions for the <tr> tag. So you can basically now use something like this:
$this->widget('zii.widgets.grid.CGridView', array
(
...
'rowHtmlOptionsExpression' => 'array("id" => $data->id)',
...
All your tags will have an id attribute with the records' PK.
Just modify the jQuery slightly to obtain the id from that tag instead of an extra column and you should be set.
Extend the class CDataColumn
Under protected/components/ create the file DataColumn.php with the following content:
/**
* DataColumn class file.
* Extends {#link CDataColumn}
*/
class DataColumn extends CDataColumn
{
/**
* #var boolean whether the htmlOptions values should be evaluated.
*/
public $evaluateHtmlOptions = false;
/**
* Renders a data cell.
* #param integer $row the row number (zero-based)
* Overrides the method 'renderDataCell()' of the abstract class CGridColumn
*/
public function renderDataCell($row)
{
$data=$this->grid->dataProvider->data[$row];
if($this->evaluateHtmlOptions) {
foreach($this->htmlOptions as $key=>$value) {
$options[$key] = $this->evaluateExpression($value,array('row'=>$row,'data'=>$data));
}
}
else $options=$this->htmlOptions;
if($this->cssClassExpression!==null)
{
$class=$this->evaluateExpression($this->cssClassExpression,array('row'=>$row,'data'=>$data));
if(isset($options['class']))
$options['class'].=' '.$class;
else
$options['class']=$class;
}
echo CHtml::openTag('td',$options);
$this->renderDataCellContent($row,$data);
echo '</td>';
}
}
We can use this new class like this:
$this->widget('zii.widgets.grid.CGridView', array(
'id' => 'article-grid',
'dataProvider' => $model->search(),
'filter' => $model,
'columns' => array(
'id',
'title',
array(
'name' => 'author',
'value' => '$data->author->username'
),
array(
'class' => 'DataColumn',
'name' => 'sortOrder',
'evaluateHtmlOptions' => true,
'htmlOptions' => array('id' => '"ordering_{$data->id}"'),
),
array(
'class' => 'CButtonColumn',
),
),
));

Categories