The Cookbook only covers the real basics. I want to go over some common issues in a more detailed way.
Dummy Values
Dummy values for select fields are usually added in the view:
/* (1) */ echo $this->Form->input('select_field', array('empty'=>'please select')); /* (2) */ echo $this->Form->input('select_field', array('empty'=>array(0=>'please select')));(1) is used if validation is “notEmpty” for this field (key = empty string) or if it is “numeric” and the user has to chose a value other than the dummy one. (2) is used if validation is “numeric” and you want to allow the dummy value as default value.
Of course, there are other cases, too. Note: We assume that the controller passes a variable named “selectField” with the options array to the view. You could also manually add options with the ‘options=>array(…)’ param.
Default Values
Many beginners make the mistake to use the options params “value”, “selected” or “checked” for the default value. But this field will then be populated with the same default value every time after posting the form. This breaks the logic of most forms. The validation errors are supposed to show what is wrong and the posted content should remain in the form fields. So how is it done right?
Use the controller for populating the form with default values
/* inside add/edit actions */ if (!empty($this->data)) { $this->Country->create(); if ($this->Country->save($this->data)) { ... } else { ... } } else { /* Now here you can put your default values */ $this->data['Country']['active'] = 1; $this->data['Country']['lat'] = 0.0; ... }It doesn’t matter what type they are (checkbox, select, radio, textarea, text). For checkboxes and radio buttons it should be either 0 (not checked – by default) or 1 (checked).
Default Values – hidden!
Many make the mistake to use hidden fields for some values that are not supposed to be edited by users. But they still can be modified using Firefug etc. So what is the more correct approach here?
if (!empty($this->data)) { $this->Post->create(); # add the content before passing it on to the model $this->data['Post']['status'] = '2'; if ($this->Post->save($this->data)) { ... } }We don’t even create hidden inputs in the form but add the hidden values in the controller “on demand”. Right before the model gets them and processes them. No tempering possible then. No overhead in the forms.
2.0 Note: Cake2 Security Component does now hash the hidden input content (!), as well. But if you don’t want or can’t use this component the above still applies.
Disallowing some fields to be edited
Most beginners would make the (huge) mistake to simply mark the input fields as read-only or hidden field. Well, again, Firebug can easily modify those fields. If you really want to use this strategy, make sure (!) that they are not passed on to the model. This can be accomplished with the third parameter in the save() method of the model:
# we dont want the "read-only" fields to be edited (like e.g. 'approved') if ($this->Post->save($this->data, true, array('name', 'title', 'content'))) {Now only those 3 fields get modified, no matter what. See my security post for details how to secure forms.
A cleaner approach usually is to simply echo the content (without any inputs). For this to work it is important NOT to just pass it along $this->data. After posting the form those fields are not in the data array anymore and will cause errors. The correct approach (I even baked my templates this way) would be to pass the record itself to the view as well:
function edit($id = null) { if (empty($id) || !($post = $this->Post->find('first', array('conditions'=>array('Post.id'=>$id))))) { //ERROR and redirect } if (!empty($this->data)) { $this->data['Post']['id'] = $post['Post']['id']; if ($this->Post->save($this->data, true, array('id', 'title', 'content', ...))) { //OK and redirect } else { //ERROR + validation errors without redirect } } if (empty($this->data)) { $this->data = $post; } $this->set(compact('post')); }Now we have full access to the record even after posting the form: echo $post['Post']['approved']; etc.
Validating first (manually)
This comes in handy, if you need to do something prior to saving the posted data:
# very important if you use validates() $this->Post->set($this->data); if ($this->Post->validates()) { // do something here # false = no need to validate again (already done in validates()!) $this->Post->User->save(null, false); ... } else { // ERROR }This is also very useful if you don’t want to save the record. Maybe you want to write an email or something.
Note: In save() we do not pass $this->data again because callback functions inside the model could already have altered the passed data. We would override this. So we pass “null” instead. If you actually need to alter $this->data here, be sure all your important modifications happen inside beforeSave() – as beforeValidate() is now not called anymore with “false” as second param.
Custom controller validations
This is useful if the result of a component matters for the validation process and you want to manually invalidate a field:
$this->Post->set($this->data); if (!$this->MyComponent->foo()) { $this->Post->invalidate('field', 'error message'); } # if foo() returns false, validates() will never return true if ($this->Post->validates()) { $this->Post->User->save(null, false); ... } else { // ERROR }
Modifications prior to validation/saving
Sometimes you want to make sure, that your display field (e.g. “name”/”title”) is uppercase for the first letter, and lowercase for the rest. As any other modification as well you would want to put this in the model as beforeValidate() or beforeSave(). Usually the first one is the better choice. After validation there should only be necessary changes that don’t affect the validated fields if possible. And if so, make those don’t break the validation rules. In our case we even wont to auto-prefix urls in order to properly validate them. So here we have no choice other than using beforeValidate():
function beforeValidate() { if (!empty($this->data[$this->alias]['name'])) { $this->data[$this->alias]['name'] = ucfirst(mb_strtolower($this->data[$this->alias]['name'])); } /* adding http:// if necessary (if only www... was submitted) */ if (!empty($this->data[$this->alias]['homepage'])) { $this->data[$this->alias]['homepage'] = CustomComponent::autoPrefixUrl($this->data[$this->alias]['homepage']); } /* very important! */ return true; }If you don’t return true, the saving process will abort. This usually doesn’t make sense in beforeValidate() – and is mainly done in beforeSave().
Note: Most generic beforeSave() and beforeValidate() functionality could easily be transformed into a behavior.
