Working with forms

The Cookbook only covers the real basics.
I want to go over some common issues in a more detailed way for Cake1.3 apps.

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 – HABTM

For HABTM and multiple selects you usually have to duplicate the key since you are using echo $this->Form->input('Country'); (note the capital c to using the model name here) in the form which then becomes sth like <select name="data[Country][Country][]"...> via FormHelper.

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']['Country'] = array(1, 3, 9, ...);
}

Remember: You only pass down the keys (usually the primary id) of your records, not the labels/values.

A hot tip if you don’t remember how the default values are passed to the view for more complicated form setups:
Always post your form once and debug $this->request->data in your controller. Take a look at how the array is made up.
Then form your default array exactly this way prior to passing it down to the view and it will work. Neat, isn’t it?

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 Firebug or can at least be read out (which in most cases is not desired).
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.x 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. And it is usually cleaner than dragging those values unnecessarily through a form again.

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:

public 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():

public 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.

UPDATE 2012-09 – CakePHP 2

For CakePHP 2.x please use $this->request->data instead of $this->data in your controller actions.
Also try to avoid if (!empty($this->data)) {} and use this instead:

if ($this->request->is('post') || $this->request->is('put')) {}

Note that since 2.4 you can also use the array syntax now:

if ($this->request->is(array('post', 'put'))) {}

The rest should be still valid.

UPDATE 2013-03-06 – Custom controller validations

Since 2.x the validation errors are now arrays instead of strings. This also means that the validation does not stop anymore if you used invalidate() as it did in 1.3.
Even with last=>true in your rules it will still trigger the first rule.
You need to use the current master branch of my Tools plugin and the "fixed" invalidate() method as this:

$this->Post->invalidate('field', 'error message', true);

With true as third param you tell it to stop the validation here (similar to last=>true in the validate array) and not to trigger any more validation rules here.

UPDATE 2015-12 – CakePHP 3

With CakePHP 3.x the same still is true: Do not mess in the view with posted data, instead use the passed down entity and/or request data to let the FormHelper output the correct default values.

if ($this->request->is(['post', 'put'])) {
    // Validate/Save
} else {
    $this->request->data['field_name'] = 'defaultValue';
}
5.00 avg. rating (97% score) - 4 votes

4 Comments

  1. I’ve found that it might be worth pointing out that in later versions of CakePHP (I have tested this in 2.3.1), assignments to the data object in the controller must be done through $this->request->data instead of merely $this->data which is provided for read-only access. Thanks, this has definitely helped guide me in the right direction.

  2. Hi there, I’m on cake 2.5.1, and I’m a newbe that have made the mistakes you point out, that’s why I’m in need of help:
    My case: at edit I need one of the fields to be changed automatically from the previous value to a new one, field is

     enum(‘original’, ‘corrected’, ‘deleted’)

    so when retreiving data the form shows "original" at the "state" field on saving I need to change it automtically to "corrected", actually I’m not putting the "echo" on this fielld for the user not to touch it, trying to follow your suggestions I have come up with this code at the TermsController.php, which is not working:

     public function edit($id = null) {
                    if (!$this->Term->exists($id)) {
                            throw new NotFoundException(__('Invalid term'));
                    }
                    if ($this->request->is(array('post', 'put'))) {
                            if ($this->Term->save($this->request->data)) {
                                    $this->Session->setFlash(__('The term has been saved.'));
                                    return $this->redirect(array('action' => 'index'));
                            } else {
                                    $this->Session->setFlash(__('The term could not be saved. Please, try again.'));
                            }
                    } else {
                            $options = array('conditions' => array('Term.' . $this->Term->primaryKey => $id));
                            $this->request->data = $this->Term->find('first', $options);
                    }
                    if (!empty($this->data)) {
                            $this->Term->create();
                            if ($this->Term->save($this->data)) {
                    } else {
                            $this->data['Term']['state'] = 'corrected';
                    }
            }
    }

    for the add section I have made the way you say is wrongly made for newbes, but it is working, at file View/Terms/add.ctp:

     $this->Form->input('state', array('default'=>'original'));

    Thanks in advance for your help.

  3. SOLVED: if it helps somebody, I did next in model fiel:

    public function beforeSave($options = array()) {
                    if (isset($this->data['Oldcaterm']['status'])){
                            $this->data['Oldcaterm']['status'] = str_replace('original', 'corrected', $this->data['Oldcaterm']['status']);
                    }
                    return true;
            }

    and this did it, no extra code at any other file

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.