I am slowly building up my Zend skills by building some utility websites for my own use. I have been using Zend Forms and Form validation and so far have been happy that I have been understanding the Zend way of doing things. However I am a bit confused with how to use Zend_Validate_Db_NoRecordExists() in the context of an edit form and a field that maps to database column that has to be unique.
For example using this simple table
TABLE Test
(
ID INT AUTO_INCREMENT,
Data INT UNIQUE
);
If I was simply adding a new row to the Table Test, I could add a validator to the Zend Form element for the Data field as such:
$data = new Zend_Form_Element_Text('Data');
$data->addValidator( new Zend_Validate_Db_NoRecordExists('Test', 'Data') )
At form validation this validator will check that the contents of the Data element does not already exist in the table. Thus the insert into Test can go ahead without violating the Data fields UNIQUE qualifier.
However the situation is different when editing an existing row of the Test table. In that case the validator needs to check that the element value meets one of two mutually exclusive conditions conditions:
The user has changed the element value, and the new value does not currently
exist in the table.
The user has Not changed the element value. Thus the value does currently exist in the table (and this is OK).
The Zend Validation Docs talk about adding a parameter to the NoRecordExists() validator for the purpose of excluding records from the validation process. The idea being to "validate the table looking for any matching rows, but ignore any hits where the a field has this specific value". Such a use case is what is needed for the validating the element when editing a table. The pseudo code to do this in 1.9 is like so (actually I got this from the 1.9 source code - I think the current docs may be wrong):
$data = new Zend_Form_Element_Text('Data');
$data->addValidator( new Zend_Validate_Db_NoRecordExists('Test', 'Data',
array ('field'=>'Data', 'Value'=> $Value) );
The problem is that the value that is to be excluded ($Value) is bound to the validator at the time it is instantiated (also when the form is instantiated). But when the form is editing a record, that value needs to be bound to the contents of the $data field when the form was initially populated with data - IE the Data value initially read from the Test table row. But in typical Zend patterns a form is instantiated and populated in two separate steps which precludes binding the exclude value to the desired element value.
The following Zend psuedo code marks where I would like the binding of $Value to the NoRecordExists() validator to occur (and note that this is a common Zend controller pattern):
$form = new Form()
if (is Post) {
$formData = GetPostData()
if ($form->isValid($formData)) {
Update Table with $formData
Redirect out of here
} else {
$form->populate($formData)
}
} else {
$RowData = Get Data from Table
$form->populate($RowData) <=== This is where I want ('value' => $Value) bound
}
I could sub-class Zend_Form and override the populate() method to do a one-shot insertion of the NoRecordExists() validator on initial form population, but that seems like a huge hack to me. So I wanted to know what other people think and is there some pattern already written down that solves this problem?
Edit 2009-02-04
I've been thinking that the only decent solution to this problem is to write a custom validator and forget about the Zend version. My form has the record ID as hidden field, so that given the table and column names I could craft some SQL to test for uniqueness and exclude the row with an ID of such an such. Of course this started me thinking about how I would be tying the form to the dB layer that the Model is supposed to hide!
This is how it's done:
I your FORM, you add this validator (for example email field):
$email->addValidator('Db_NoRecordExists', true, array('table' => 'user', 'field' => 'email'));
Don't add custom error message for this since after that it didn't work for me, e.g.:
$email->getValidator('Db_NoRecordExists')->setMessage('This email is already registered.');
In your Controller add this:
/* Don't check for Db_NoRecordExists if editing the same field */
$form->getElement('email')
->addValidator('Db_NoRecordExists',
false,
array('table' => 'user',
'field' => 'email',
'exclude' => array ('field' => 'id', 'value' => $this->request->get('id'))));
And after this you do verifications, e.g.:
if ($this->getRequest()->isPost())
{
if($form->isValid($this->getRequest()->getPost()))
{
....
That's it!
This will also work :
$this->addElement('text', 'email', array(
'label' => 'Your email address:',
'required' => true,
'filters' => array('StringTrim'),
'validators' => array(
'EmailAddress',
array('Db_NoRecordExists', true, array(
'table' => 'guestbook',
'field' => 'email',
'messages' => array(
'recordFound' => 'Email already taken'
)
)
)
)
));
After reviewing the overwhelming response I've decided that I'm going with a custom validator
Look at this one:
Answer raised by me and well-solved by Dickie
private $_id;
public function setId($id=null)
{
$this->_id=$id;
}
public function init()
{
.....
if(isset($this->_id)){
$email->addValidator('Db_NoRecordExists', false, array('table' => 'user', 'field' => 'email','exclude' => array ('field' => 'id', 'value' => $this->_id) ));
$email->getValidator('Db_NoRecordExists')->setMessage('This email is already registered.');
}
Now u can use:
$form = new Form_Test(array('id'=>$id));
You could just call $form->getElement('input')->removeValidator('Zend_Validator_Db_NoRecordExists'); instead of supplying the exclusion.
I have just tried this example for email address uniqueness and it works perfectly with below stuffs :
1] In my form:
// Add an email element
$this->addElement('text', 'email', array(
'label' => 'Email :',
'required' => true,
'filters' => array('StringTrim'),
'validators' => array(
'EmailAddress',
)
));
Here's something special that I needed to add for unique email address to work:
$email = new Zend_Form_Element_Text('email');
$email->addValidator('Db_NoRecordExists', true, array('table' => 'guestbook', 'field' => 'email'));
2] In my controller:
$form->getElement('email')
->addValidator('Db_NoRecordExists',
false,
array('table' => 'guestbook',
'field' => 'email',
'exclude' => array ('field' => 'id', 'value' => $request->get('id'))));
if ($this->getRequest()->isPost()) {
if ($form->isValid($request->getPost())) {
Hope it helps you people !
Thanks
Related
A Zend_Form like this:
class Application_Form_Registration extends Zend_Form
{
public function init()
{
/* Form Elements & Other Definitions Here ... */
$$this->setMethod('post');
//first name
$this->addElement('text', 'email', array(
'label' => 'First name',
'required' => true,
'filters' => array('StringTrim'),
));
//last name
$this->addElement('text', 'lastname', array(
'label' => 'Last name',
'required' => true,
'filters' => array('StringTrim')
));
$this->addElement('submit', 'submit', array(
'ignore' => true,
'label' => 'Submit'
));
$this->addElement('hash', 'csrf', array(
'ignore' => true,
));
}
}
I read through the ZF1 1.12 API and reference document, but I can't find the meaning of the flag "ignore" in the Zend_Form::addElement() configure options.
The api doc is just this:
Surely I googled it and find it but this is not the way to work. How to I find the meaning of certain specific stuff. I don't suppose that I need to read the source code?
Just take this addElement() as an example, am I missing somewhere to look further? Nothing in Zend_Config class that I can find about ignore flag either.
As I know ignore flag defines if form values ($form->getValues()) will contain element value. If ignore is set to true for some element than form values ($form->getValues()) will not contain this element value.
ZF Documentation can be...lacking sometimes. The API docs for the ignore flag state:
getIgnore( ) : bool Get ignore flag (used when retrieving values at form level)
Which hints that the ignore flag has something to do with the behavior of Zend_Form GetValues() but it's not really spelled out.
In these cases I like to go straight to the source code so I can see for myself:
public function getValues($supressArrayNotation = false)
{
...
foreach ($this->getElements() as $key => $element) {
if (!$element->getIgnore()) {
...
}
You can see that the getValues() function in Zend_Form will check the ignore flag on each element before adding the value to the return array. If the flag is true, the value won't be included.
I am building an application using Symfony 2, and one of the pages contains a large form, which has a number of fixed fields but also a number of fields whose presence and number depends entirely on related data taken from the DB. More specifically, the form poses a number of questions, and the questions themselves come from the DB, having different text and IDs. The answers need to be stored in the DB, linked to the questions.
I've read up on how Symfony handles forms (Basic Symfony Forms), including the relationship between a Form object and the data object backing it, creating Fields from the Form object, and validating input according to rules applied to the Form object.
Since I don't want a fixed Form object I looked at how you're meant to dynamically modify Forms (Dynamic Symfony Forms), but the examples there seem to be about choosing whether to show certain pre-defined fields, or controlling the options in the fields, not completely generating new fields from scratch, which is what I want.
I had a look at the Symfony page on Data Transformers, and it seemed relevant, but I couldn't quite get my head around how it would help me. They seem more about mapping a Form object to an Entity than mapping complex form submissions to a Form object.
I did see this SO answer where the object backing the Form object is just an array, which also seemed relevant: I thought that maybe the answer was to generate some anonymous object with the dynamic fields in, and then make a form from that, but I don't see how I'd set up Symfony validation on it, and my form is not completely different each time, so doesn't need to be entirely anonymous.
What is the best way to achieve this using Symfony 2, ideally without giving up such framework benefits as easy validation etc?
EDIT I hadn't heard of Symfony Form Collections before, and it looks like this might do what I want - each Question in my case would map to an individual subform object. I'll update again if I try it out successfully.
Your question is pretty broad but this might help focus.
I have what I call a DynamicFormType which can add elements to a form based on a configuration items array.
class DynamicFormType extends AbstractType
{
protected $name;
protected $items;
public function getName() { return $this->name; }
public function __construct($name, $items)
{
$this->name = $name;
$this->items = $items;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
foreach($this->items as $name => $item)
{
switch($item['type'])
{
case 'radio':
$attr = array('class' => 'radio-medium');
$builder->add($name,'choice',array(
'label' => $item['label'],
'required' => false,
'attr' => $attr,
'empty_value' => false,
'expanded' => true,
'multiple' => false,
'choices' => $item['choices'],
));
break;
case 'select':
$attr = array();
$builder->add($name,'choice',array(
'label' => $item['label'],
'required' => false,
'attr' => $attr,
'empty_value' => false,
'expanded' => false,
'multiple' => false,
'choices' => $item['choices'],
));
break;
case 'text':
$attr = array();
if (isset($item['size'])) $attr['size'] = $item['size'];
$builder->add($name,'text',array(
'label' => $item['label'],
'required' => false,
'attr' => $attr,
));
break;
case 'textarea':
$attr = array();
if (isset($item['rows'])) $attr['rows'] = $item['rows'];
if (isset($item['cols'])) $attr['cols'] = $item['cols'];
$builder->add($name,'textarea',array(
'label' => $item['label'],
'required' => false,
'attr' => $attr,
));
break;
case 'collection':
$dynamicType = new DynamicFormType($name,$item['items']);
$builder->add($name,$dynamicType,array(
'label' => false,
'required' => false,
));
break;
}
}
return; if ($options);
}
}
I trust you can see how the items array is used to create form elements. In my application I use this with:
$basicType = new DynamicFormType('basic',$project->getPlan());
$builder->add('basic',$basicType, array('label' => false));
$project->getPlan() returns the items array which describes the various elements I want to add.
Hopefully, you can start with something like this and figure out what elements need to be added from your data structure. I didn't need validation here but it should be easy enough to add constraint information to you elements once you get things working.
OK, in the end what I wanted wasn't a truly dynamic form (but thanks to #Cerad for that code), but one using Collections where the number of certain child form items was variable.
So I essentially followed the Symfony Docs about Collections
Let's start this off with a short code snippet I will use to demonstrate my opinion:
$title = new Zend_Form_Element_Text('title', array(
'label' => 'Title',
'required' => false,
'filters' => array(
'StringTrim',
'HtmlEntities'
),
'validators' => array(
array('StringLength', false, array(3, 100))
),
));
This important line is:
'required' => false,
Which means that the input field is not required and you can submit the form without filling it. However, this also means that any filters and validators won't apply to it if you choose to fill in this field.
Common sense tells me that is an irrational behavior. The way I understand the word 'required' in relation with HTML input fields: an input field that is not required should return NULL if it is not filled in but if user decides to fill it both filters and validators should apply to it. That's what makes sense to me. Do you agree with me or is my common sense not so common?
Now more practical question, because this is how Zend_Form behaves, how can I achieve not required fields which would work as I described above (if nothing is typed in by user it returns NULL otherwise filters and validators normally apply).
Not really a complete answer to your question, but since comments don't have syntax formatting; here's a filter you can use to make your field values null if empty.
class My_Filter_NullIfEmpty implements Zend_Filter_Interface
{
public function filter( $value )
{
// maybe you need to expand the conditions here
if( 0 == strlen( $value ) )
{
return null;
}
return $value;
}
}
About the required part:
I'm not sure really. You could try to search the ZF mailinglists on Nabble:
http://www.nabble.com/Zend-Framework-Community-f16154.html
Or subscribe to their mailinglist, and ask them the question. Either through Nabble, or directly via the addresses on framework.zend.com:
http://tinyurl.com/y4f9lz
Edit:
Ok, so now I've done some tests myself, cause what you said all sounded counter intuitive to me. Your example works fine with me. This is what I've used:
<?php
class Form extends Zend_Form
{
public function init()
{
$title = new Zend_Form_Element_Text('title', array(
'label' => 'Title',
'required' => false,
'filters' => array(
'StringTrim',
'HtmlEntities',
'NullIfEmpty' // be sure this one is available
),
'validators' => array(
array('StringLength', false, array(3, 100))
),
));
$this->addElement( $title );
}
}
$form = new Form();
$postValues = array( 'title' => '' ); // or
$postValues = array( 'title' => ' ' ); // or
$postValues = array( 'title' => 'ab' ); // or
$postValues = array( 'title' => ' ab ' ); // or
$postValues = array( 'title' => '<abc>' ); // all work perfectly fine with me
// validate the form (which automatically sets the values in the form object)
if( $form->isValid( $postValues ) )
{
// retrieve the relevant value
var_dump( $form->getValue( 'title' ) );
}
else
{
echo 'form invalid';
}
?>
Actually, what you describe as your expectations are exactly how Zend_Form works. If you mark the element as not required, then the following happens: (a) if no value is passed, it skips validation, but if (b) a value is passed, then it must pass all validators in order to be valid.
BTW, best place to ask ZF questions is on the ZF mailing lists: http://framework.zend.com/archives
I'm using Zend Framework 1.62 (becuase we are deploying the finished product to a Red Hat instance, which doesn't have a hgih enough PHP version to support > ZF1.62).
When creating a Form using Zend Form, I add a select element, add some multi options.
I use the Zend Form as an in-object validation layer, passing an objects values through it and using the isValid method to determine if all the values fall within normal parameters.
Zend_Form_Element_Select works exactly as expected, showing invalid if any other value is input other than one of the multi select options I added.
The problem comes when I want to display the form at some point, I cant edit the error message created by the pre registered 'InArray' validator added automatically by ZF. I know I can disable this behaviour, but it works great apart from the error messages. I've tryed the following:
$this->getElement('country')->getValidator('InArray')->setMessage('The country is not in the approved lists of countries');
// Doesn't work at all.
$this->getElement('country')->setErrorMessage('The country is not in the approved lists of countries');
// Causes a conflict elswhere in the application and doesnt allow granular control of error messages.
Anyone have any ideas?
Ben
I usually set validators as per my example below:
$this->addElement('text', 'employee_email', array(
'filters' => array('StringTrim'),
'validators' => array(
array('Db_NoRecordExists', false, array(
'employees',
'employee_email',
'messages' => array(Zend_Validate_Db_Abstract::ERROR_RECORD_FOUND => 'A user with email address %value% already exists')
))
),
'label' => 'Email address',
'required' => true,
));
The validators array in the element options can take a validator name (string) or an array.
When an array is passed, the first value is the name, and the third is an array of options for the validator. You can specify a key messages with custom messages for your element in this array of options.
If your using Zend_Form_Element_Select (or any of the Multi subclasses), on validation the InArray validator will only be automatically added if there is not one present.
You can set a validator as so:
$options = array(...);
$this->addElement('select', 'agree', array(
'validators' => array(
array('InArray', true, array(
'messages' => array(
Zend_Validate_InArray::NOT_IN_ARRAY => 'Custom message here',
),
'haystack' => array_keys($options),
)),
'multiOptions' => $options,
));
and then your validator will be used instead of the automatically attached one.
$el = $this->addElement($name, $label, $require, 'select');
$validator = new Zend_Validate_InArray(array_keys(AZend_Geo::getStatesList()));
$validator->setMessage('Invalid US State.');
$el
->setMultiOptions(AZend_Geo::getStatesList())
->setRegisterInArrayValidator(false)
->addValidator($validator)
->addFilter(new Zend_Filter_StringToUpper())
->addFilter(new T3LeadBody_Filter_SetNull())
->setDescription('US State. 2 char.');
I've got a model in CakePHP that doesn't have a table, called Upload. I've got a validation in this Model for a field called source_id.
I've got a form that builds a nice looking $this-data, giving me a well formated set, including:
$this->data['Upload']['source_id']
However, the validation rule I have set doesn't seem to run at all. I copied this validation rule from another model where it does work, so I'm confident that it works:
var $validate = array(
'source_id' => array(
rule' => 'numeric',
'required' => true,
'allowEmpty' => false,
'message' => 'Error!.'
)
);
Can you not validate fields for a model that lacks a database table?
The form uses the Upload model, and submits to another controller action method.
CakePHP 1.2, PHP/MySQL 5, XAMPP.
I'm dumb. You have to trigger a validation check, either with a save() or
$this->Upload->set($this->data);
$this->Upload->validates();
Working now.
You can also fake the database structure by setting the $_schema array, like so:
var $useTable = false;
var $_schema = array(
'name' =>array('type'=>'string', 'length'=>100),
'email' =>array('type'=>'string', 'length'=>255),
'phone' =>array('type'=>'string', 'length'=>20),
'subject' =>array('type'=>'string', 'length'=>255),
'message' =>array('type'=>'text')
);