ZF: Form array field - how to display values in the view correctly - zend-framework

Let's say I have a Zend_Form form that has a few text fields, e.g:
$form = new Zend_Form();
$form->addElement('text', 'name', array(
'required' => true,
'isArray' => true,
'filters' => array( /* ... */ ),
'validators' => array( /* ... */ ),
));
$form->addElement('text', 'surname', array(
'required' => true,
'isArray' => true,
'filters' => array( /* ... */ ),
'validators' => array( /* ... */ ),
));
After rendering it I have following HTML markup (simplified):
<div id="people">
<div class="person">
<input type="text" name="name[]" />
<input type="text" name="surname[]" />
</div>
</div>
Now I want to have the ability to add as many people as I want. I create a "+" button that in Javascript appends next div.person to the container. Before I submit the form, I have for example 5 names and 5 surnames, posted to the server as arrays. Everything is fine unless somebody puts the value in the field that does not validate. Then the whole form validation fails and when I want to display the form again (with errors) I see the PHP Warning:
htmlspecialchars() expects parameter 1 to be string, array given
Which is more or less described in ticket: http://framework.zend.com/issues/browse/ZF-8112
However, I came up with a not-very-elegant solution. What I wanted to achieve:
have all fields and values rendered again in the view
have error messages only next to the fields that contained bad values
Here is my solution (view script):
<div id="people">
<?php
$names = $form->name->getValue(); // will have an array here if the form were submitted
$surnames= $form->surname->getValue();
// only if the form were submitted we need to validate fields' values
// and display errors next to them; otherwise when user enter the page
// and render the form for the first time - he would see Required validator
// errors
$needsValidation = is_array($names) || is_array($surnames);
// print empty fields when the form is displayed the first time
if(!is_array($names))$names= array('');
if(!is_array($surnames))$surnames= array('');
// display all fields!
foreach($names as $index => $name):
$surname = $surnames[$index];
// validate value if needed
if($needsValidation){
$form->name->isValid($name);
$form->surname->isValid($surname);
}
?>
<div class="person">
<?=$form->name->setValue($name); // display field with error if did not pass the validation ?>
<?=$form->surname->setValue($surname);?>
</div>
<?php endforeach; ?>
</div>
The code work, but I want to know if there is an appropriate, more comfortable way to do this? I often hit this problem when there is a need for a more dynamic - multivalue forms and have not find better solution for a long time.

Having no better idea, I have created a view helper that handles the logic presented above. It can be found here.
If the helper is available in the view, it can be used in the following way (with the form from the question):
<?=
$this->formArrayElements(
array($form->name, $form->surname),
'partials/name_surname.phtml'
);
?>
The contents of the application/views/partials/name_surname.phtml partial view are:
<div class="person">
<?= $this->name ?>
<?= $this->surname ?>
</div>
The fields are rendered according to the posted form and validation messages are shown only next to the values that failed validation.
The helper's code is far from perfect (I just rewrote the idea from the question) but is easy to use and can be considered as good starting point.

Related

Troubleshooting CakePHP form submission

I recently set up the ability to tag posts on my site. I had everything working fine. Then as I was wrapping up I tested all my admin side forms again. The Add Tag form no longer does anything. It doesn't even flash an error or redirect after submission. The page just reloads at the same URL. The only changes to the site I have made since initial testing was move the forms to the admin side of the dev site. Here is some code to hopefully reveal what the mystery is. Also my edit tag form is doing similar thing. It has no flash message but redirects back to the index, like its supposed to but with no changes made to the tag. Ill include the edit code as well.
Add.ctp in src/Template/Admin/Tags/Add.ctp
<div class="tags form large-9 medium-8 columns content">
<?= $this->Form->create($tag) ?>
<div class="form-group">
<fieldset>
<h1 class="page-header">New Tag</h1>
<?php
echo $this->Form->input('name', ['class' => 'form-control']);
?>
</fieldset>
</div>
<?= $this->Form->button(__('Submit'), ['class' => 'btn btn-primary']) ?>
<?= $this->Form->end() ?>
</div>
Here is my Add funciton in my TagsController:
public function add()
{
$this->viewBuilder()->layout('admin');
$tag = $this->Tags->newEntity();
if ($this->request->is('post')) {
$tag = $this->Tags->patchEntity($tag, $this->request->data);
if ($this->Tags->save($tag)) {
$this->Flash->success(__('The tag has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The tag could not be saved. Please, try again.'));
}
$this->set(compact('tag'));
$this->set('_serialize', ['tag']);
}
Here is my Edit funciton in my TagsController:
public function edit($id = null)
{
$this->viewBuilder()->layout('admin');
$tag = $this->Tags->get($id, [
'contain' => []
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$tag = $this->Tags->patchEntity($tag, $this->request->data);
if ($this->Tags->save($tag)) {
$this->Flash->success(__('The tag has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The tag could not be saved. Please, try again.'));
}
$this->set(compact('tag'));
$this->set('_serialize', ['tag']);
}
Edit.ctp in src/Template/Admin/Tags/Edit.ctp
<div class="tags form large-9 medium-8 columns content">
<?= $this->Form->create($tag) ?>
<div class="form-group">
<fieldset>
<h1 class="page-header">Edit Tag</h1>
<?php
echo $this->Form->input('name', array('class' => 'form-control'));
?>
</fieldset>
</div>
<?= $this->Form->button(__('Submit'), ['class' => 'btn btn-primary']) ?>
<?= $this->Form->end() ?>
</div>
Just as a side note. I started getting errors when creating a new post as well.
General error: 1364 Field 'section_id' doesn't have a default value
I did go into my DB and give the field a default value. But then when I fill out the form for a new post again, the error just moves to the next table column. I am assuming they are some how related since they popped up at the same time and because tags and posts are related to each other.
TagsTable:
class TagsTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('tags');
$this->displayField('name');
$this->primaryKey('id');
$this->hasMany('PostsTags', [
'foreignKey' => 'tag_id'
]);
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->integer('id')
->allowEmpty('id', 'create');
$validator
->requirePresence('name', 'create')
->notEmpty('name');
return $validator;
}
}
Tags Entity:
class Tag extends Entity
{
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* #var array
*/
protected $_accessible = [
'*' => false,
'id' => false
];
}
When I place <?php debug($tag); ?> into my add.ctp view this is the out put it gives me:
object(App\Model\Entity\Tag) {
'[new]' => true,
'[accessible]' => [],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Tags'
}
Again, in question always post debug pathEntity output, in your case debug($tag), also Tag Entity, your validation code, and how looks your db tags table.
Answer:
General error: 1364 Field 'section_id' doesn't have a default value
This means that you have not passed a value for this field.
You can change that table field to accept null or empty value and/or set default if not passed from application, or make validation in your TagsTable to be sure if submitted data valid before send to db.
After question updated:
protected $_accessible = [
'*' => false, <---- should be true
'id' => false
];
This means that all fields except id are accessible

Form Type Default Value for empty value in rendered form

I found some weird behavior with a rendered controller with displays a edit form for my entity.
But first things first:
I'm rendering a template with displays a entity. If the logged in user is the same user as the owner of that entity i also render another controller hidden with contains the edit form for this entity. The User can access this via a button which fires a jQuery toggle.
The entity has 2 textfields which can be empty, description and situation.
So if one of the two or both are empty the edit form will display in the textfield (null) by default. I do not want that! How can i fix this so that the textfields are empty like the value of the field (so that my placeholder will be shown).
Here's an image to visualize this:
But further: This entity (Poi) belongs to another Entity (Turn), so 1 Turn -> many Pois. You can navigate through the pois in my website.
But if the owner navigtes through them (keep in mind, the edit form will be rendered, but not displayed until the button was klicked) all description and situation fields now display (null), even he did not saved the edit. It just happen by itself.
Here an image which shows it
Why does this happen? What can i do against it? Is there maybe something like an empty value option in the form type?
I searched for a solution, but i couldn't find anything that is nearly simliar with my situation.
The form build from my Form Type:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'text', array(
'required' => false
))
->add('situation', 'textarea', array(
'required' => false
))
->add('description', 'textarea', array(
'required' => false
))
->add('isPrivateText', 'checkbox', array(
'required' => false
))
->add('isPrivateImage', 'checkbox', array(
'required' => false
))
;
}
The relevant part of my edit.html.twig
<p class="edit_form"><span class="edit_left">{{ form_label(edit_form.situation, 'Situation') }} </span>
<span class="edit_right">{{ form_widget(edit_form.situation, { attr: {'placeholder': 'Törn Situation'} }) }}</span></p>
<p class="edit_form"><span class="edit_left">{{ form_label(edit_form.description, 'Beschreibung') }} </span>
<span class="edit_right">{{ form_widget(edit_form.description, { attr: {'placeholder': 'Törn Beschreibung'} }) }}</span></p>
Where my showPoi.html.twig renderes the form controller:
<div class="col-md-6 col-sm-6 toggle_edit" style="display: none;">
<div>
{% render controller('MysailinglogMysailinglogBundle:Poi:edit', { id: poi[0].id , poi: poi}) %}
<!-- Don't worry about the 2 divs, i just shortened up the code -->
</div>
</div>
After lots of more research i found a solution that is working fine
I'm adding a listener to my formType which leads to the following function:
function onPreSetData(FormEvent $event) {
$data = $event->getData();
if($data->getDescription() == "(null)"){
$data->setDescription('');
}
if($data->getSituation() == "(null)"){
$data->setSituation('');
}
return $event->setData($data);
}
It just takes the data from the event which will build the form and is nothing more then the Poi Entity. There i simply check if the value is (null) and if it is i set it to a empty string.
Registering the listener is also easy, it`s done with this simple line of code:
$builder->addEventListener(FormEvents::PRE_SET_DATA, array($this, 'onPreSetData'));
This must be done with a instance of the FormBuilder, the "onPreSetData" must be the same name as the function above which will be triggered by the event.
It's important to mention that the Event must be the PRE_SET_DATA event in this situation because i wanted to manipulate the data before they're written into the form!
You can set up an empty data attribute in the Form type:
Symfony documentation
$builder->add('description', 'textarea', array(
'required' => false,
'empty_value' => 'Choose your gender',
'empty_data' => null
));

Symfony 2 This form should not contain extra fields

I created a form using formBuilder in Symfony. I add some basic styling to the form inputs using an external stylesheet and referencing the tag id. The form renders correctly and processes information correctly.
However, it outputs an unwanted unordered list with a list item containing the following text: This form should not contain extra fields.
I am having a really hard time getting rid of this notice. I was wondering if someone can help me understand why it being rendered with my form and how to remove it?
Many thanks in advance!
Controller
$form = $this->createFormBuilder($search)
->add('searchinput', 'text', array('label'=>false, 'required' =>false))
->add('search', 'submit')
->getForm();
$form->handleRequest($request);
Twig Output (form is outputted and processed correctly
This form should not contain extra fields.
Rendered HTML
<form method="post" action="">
<div id="form">
<ul>
<li>This form should not contain extra fields.</li>
</ul>
<div>
<input type="text" id="form_searchinput" name="form[searchinput]" />
</div>
<div>
<button type="submit" id="form_search" name="form[search]">Search</button>
</div>
<input type="hidden" id="form__token" name="form[_token]" value="bb342d7ef928e984713d8cf3eda9a63440f973f2" />
</div>
</form>
It seems to me that you have the problem because of the token field. If it is so, try to add options to createFormBuilder():
$this->createFormBuilder($search, array(
'csrf_protection' => true,
'csrf_field_name' => '_token',
))
->add('searchinput', 'text', array('label'=>false, 'required' =>false))
->add('search', 'submit')
->getForm();
To find out the extra field use this code in controller, where you get the request:
$data = $request->request->all();
print("REQUEST DATA<br/>");
foreach ($data as $k => $d) {
print("$k: <pre>"); print_r($d); print("</pre>");
}
$children = $form->all();
print("<br/>FORM CHILDREN<br/>");
foreach ($children as $ch) {
print($ch->getName() . "<br/>");
}
$data = array_diff_key($data, $children);
//$data contains now extra fields
print("<br/>DIFF DATA<br/>");
foreach ($data as $k => $d) {
print("$k: <pre>"); print_r($d); print("</pre>");
}
$form->bind($data);
This message is also possible if you added/changed fields in your createFormBuilder() and press refresh in your browser...
In this case it's ok after sending the form again ;-)
I got the same message while having multiple forms on the same page. Turns out, symfony defaults to the name 'form' for all of them. Instead of using createFormBuilder, you can change the name of the form to avoid conflicts using
public FormBuilderInterface createNamedBuilder(string $name, string|FormTypeInterface $type = 'form', mixed $data = null, array $options = array(), FormBuilderInterface $parent = null)
See https://stackoverflow.com/a/13366086/1025437 for an example.
I ran into this error when creating a multi-step form.
When the step 1 form is submitted, $request->request contains acme_mybundle_myform array. This created a validation error and stopped the back, forward and form fields from populating correctly. Not to mention "this-form-should-not-contain-extra-fields"
I discovered this thanks to the code by nni6.
The solution in my case was inside the controller:
if ($form->isValid())
{
if($form->has('nextStep') && $form->get('nextStep')->isClicked())
{
$session->getFlashBag()->set('notice', 'Next clicked');
$registerType->incrementStep();
$request->request->remove('acme_mybundle_myform');
return $this->forward("AcmeMyBundle:Default:register", array($request));
}
....
}
I had the same error.
It was because I had a form which, by mistake, had a NULL name.
In the HTML, the name attribute would look like this:
<form name href="..." action"..."></form>
As simple as that.
Might not be the case for everyone, but worth to check.

CakePHP: allowing database update with button click

I have a product search page with the form below. The search result is displayed on the same page with search bar at the top.
echo $this->Form->create('Searches', array('action'=>'products', 'type' => 'get', 'name' => 'textbox1'));
echo $form->input($varName1, array('label' => false));
echo $form->end('Locate');
I also have a little box next to the search result that allows (it doesn't work yet) the user to flag using checkboxes a product and accordingly update its database (table products and using model Product) with a button click. Note that I have a Searches controller for this search page.
<form method="link" action="/myapp/product/test_update_db>
<label><input type="checkbox" name="flag1" <?php echo $preCheckBox1; ?>>Flag 1</input></label>
<label><input type="checkbox" name="flag2" <?php echo $preCheckBox2; ?>>Flag 2</input></label>
<input type="submit" value="Update">
</form>
I'm having difficulty with this approach figuring out how to perform this check-box-and-DB-update routine. I'm getting to the link I'd like to go (/myapp/product/test_update_db), but I don't know how to take variables flag1 and flag2, along with row ID of this result ($results['Product']['id'])) to the new page.
Could someone guide me on how to perform this neatly? Is this general approach correct? If not, what route should I be taking? I'd prefer not to use javascript at this time, if possible.
EDIT: I think I can make this work if I use the URL for passing data.. but I'd still like to know how this could be done "under the hood" or in MVC. I feel like I'm hacking at the CakePHP platform.
UPDATE: So, I ended up using the URL parameters for retrieving information pieces like flag1 and flag2. I'm still looking for an alternative method.
To see where your is-checkbox-checked data is located, do the following in your controller:
// Cake 2.0+
debug($this->request->data);
// previous versions
debug($this->data);
If you want to pass data to your search controller from the current page, you can always add the data to your form:
$this->input
(
'Product.id',
array
(
'type' => 'hidden',
'value' => $yourProductId
)
);
I ended up using information embedded in the URL for getting submission data. Something like below..
In Products controller, when the form with flag1 and flag2 are submitted:
public function test_update_db() {
// Get variables from URL, if any, and save accordingly
$result = $this->Product->updateProduct($this->params['url'], 'url');
if ($result) {
$this->Session->setFlash('Successfully updated!', 'default', array('class' => 'success'));
$this->redirect($this->referer());
}
else {
$this->Session->setFlash('Update was unsuccessful!', 'default', array('class' => 'error'));
$this->redirect($this->referer());
}
}
This works for doing what I needed to do. I feel like there's a more proper way to do this though.
if ($result) {
$this->Session->setFlash('Successfully updated!', 'default', array('class' => 'success'));
$this->redirect($this->referer());
}

How to validate and render dynamically extensible zend form

i need help with my dynamically extensible zend form.
I have form with subform, which contains two elements:
<form>
<fieldset class="itemGroup">
<label>
Question
<input type="text" name="items[questions][]" value="">
</label>
<label>
Answer
<input type="text" name="items[answers][]" value="">
</label>
</fieldset>
</form>
I obtained it with following procedure:
$itemsSubform = new Zend_Form_SubForm();
$form->addSubForm($itemsSubform, 'items');
$itemsSubform->setElementsBelongTo('items');
$itemQuestion = new Zend_Form_Element_Text('questions', array(
'label' => 'Question',
'isArray' => true,
'filters' => array(
'stringTrim',
),
'validators' => array(
array('stringLength', array('max' => 255)),
),
));
$itemAnswer = new Zend_Form_Element_Text('answers', array(
'label' => 'Answer',
'isArray' => true,
'filters' => array(
'stringTrim',
),
'validators' => array(
array('stringLength', array('max' => 255)),
),
));
$itemsSubform->addDisplayGroup(array($itemQuestion, $itemAnswer), 'itemGroup');
If is needed, i just copy all fieldset for extend form by javascript.
All is working correct, until i submit form. During validation is no element validated, and during rendering form, populated by such data, i get error message from class Zend_View_Abstract that escaping value is array instead of string (this method is called during rendering element for escape its value).
For compltion, if i call $form->getValues(); after validation, (by javascript another fieldset is added) i get this:
Array
(
[items] => Array
(
[questions] => Array
(
[0] => lorem
[1] => dolor
)
[answers] => Array
(
[0] => ipsum
[1] => sit
)
)
)
Could someone advise me how to behave to form? The ideal solution would be when form themselves validate each value separately and find how many times should he render fieldset (displayGroup).
First your elements fail to validate because you set isArray => TRUE and the validator you chose evaluates strings.
Next you issue with populating the form I believe is likely due to the fact that you need to supply the data to populate in the same multidimensional array as the form produces (the arrays should map one to one).
The link below is to an example of how to build a form dynamically, you should be able to use it as a template to produce one to fill your needs.
Example of generating form elements with loop