nuts and bolts of cakephp

How to validate HABTM data…

Posted in CakePHP by teknoid on October 16, 2008

So we’ve got our Post and Tag models (Post hasAndBelongsToMany Tag).

We are about to save a new Post, but there is a need to ensure that a user selects at least one Tag…

Let’s see how we can accomplish such validation…

First, let’s build the form for saving our Post and some Tags.


echo $form->create('Post', array('action'=>'add'));
echo $form->input('title');
echo $form->input('post');
echo $form->input('Tag', array('multiple'=>'checkbox'));
echo $form->end('Add post');

Pretty generic stuff… In our controller we’ll get a list of tags and it will be automagically assigned to our ‘Tag’ input and turned into a bunch of checkboxes representing each Tag.
After submitting the form the Tag data will be sent back as an array of id’s.

Now, let’s add a validation rule to our Tag model:


class Tag extends AppModel {

	var $name = 'Tag';

	var $hasAndBelongsToMany = 'Post';

        var $validate = array('Tag'=>array('rule'=>'checkTags'));

	function checkTags() {
	   if(!empty($this->data['Tag']['Tag'])) {
	       return true;
	   }

	   return false;
	}
}

As you see, we are using a custom method checkTags() to ensure that at least one Tag has been selected. It might appear that we could use ‘notEmpty’ rule, but unfotunatelly it doesn’t work with arrays of data so we have to use our own validation method.

Last, but not least, we create an add() action in our Posts Controller:


function add() {

	    if(!empty($this->data)) {
	        $this->Post->Tag->set($this->data);
	        if($this->Post->Tag->validates()) {
	            $this->Post->save($this->data);
	        }
	    }

	    $this->set('tags', $this->Post->Tag->find('list', array('fields'=>array('id', 'tag'))));
 	}

Just to clarify…
First, we validate the Tag model, by using the data from the form to ensure that at least one Tag was selected. If so, we save the Post and the relevant Tags.
Also, as you see, we use find(’list’) to build our list of Tags for the view by relying on some good CakePHP magic.

21 Responses

Subscribe to comments with RSS.

  1. Mark Story said, on October 18, 2008 at 11:54 am

    Teknoid: You should also check out Validation::multiple() it was built to help validate Multi-select boxes, and its less code to write :)

  2. teknoid said, on October 18, 2008 at 4:54 pm

    @Mark Story

    Thanks for pointing that out, just checked it out in the latest build. Looks like the API need to be “refreshed” a little :)
    I’m going to do an update to this and it’ll be a good time to try out ‘multiple’ rule. Thanks for the advice.

  3. James said, on October 29, 2008 at 3:09 pm

    I’ve been looking for a solution to this problem for a while and this looks good, the only problem I can see is that the Post data doesn’t get validated until a Tag is chosen. I suppose you could also validate the Post by executing “$this->Post->validates()” but its not really a clean solution. Any ideas?

  4. teknoid said, on October 29, 2008 at 3:17 pm

    @James

    Off top of my head, try to use saveAll() then to validate both models… I’m sure there are other ways, but I’m too lazy to think right now :)

  5. Francisco said, on November 15, 2008 at 3:08 pm

    Great! i was looking how to do this. Did you find out how to use Validation::multiple() ?

  6. teknoid said, on November 15, 2008 at 5:55 pm

    @Francisco

    Good to hear it was of some help.
    … I have not looked into multiple yet, too much stuff going on at work ;)

  7. Joaquin Windmüller said, on November 17, 2008 at 5:47 pm

    Hey,

    I’m in this same situation, with the difference that my habtm is from Plugin to a app model. I have a Message (in the messaging plugin) and a Member in the core App. Any idea to how can this be done? (I cannot add the validation to the member model).

  8. teknoid said, on November 17, 2008 at 6:21 pm

    @Joaquin Windmüller

    Not off top of my head, double check at the google group.

  9. Joaquin Windmüller said, on November 18, 2008 at 11:55 pm

    This is what I did:


    function beforeValidate() {
    if (empty($this->data['Member']['Member'])) {
    $this->validationErrors['Member']['Member'] = 'Select one please';
    }
    return true;
    }

    The return true is to allow Cake to check local fields and validate those too.

  10. Validación de datos HABTM en CakePHP said, on November 19, 2008 at 12:26 am

    [...] Nuts and Bolts of cakephp encuentro una ayuda para hacer validación de datos de modelos que están en una relación has and [...]

  11. teknoid said, on November 19, 2008 at 9:44 am

    @Joaquin Windmüller

    Sounds good, although I’m not sure if doing validation in beforeValidate() is the best approach. After all, the purpose of this method is to handle data before validation takes place…

  12. brij bhushan said, on December 17, 2008 at 6:20 am

    Yes,it works

  13. Joel Pearson said, on March 4, 2009 at 2:15 am

    In regards to the question from @James some months ago about only doing partial validations when there are only validation errors in Tag, this is what I came up with.

    function add() {

    if(!empty($this->data)) {

    $this->Post->Tag->set($this->data);
    $validates = $this->Post->Tag->validates();

    $this->Post->set($this->data);
    $validates = $validates && $this->Post->validates();

    if($validates) {
    // Don’t revalidate the Post model because we have already validated earlier
    $this->Post->save($this->data, false);
    }
    }

    $this->set(’tags’, $this->Post->Tag->find(’list’, array(’fields’=>array(’id’, ‘tag’))));
    }

    This way everything is validated before trying to save, instead of only validating posts when the Tag model validates ok.

  14. Joel Pearson said, on March 4, 2009 at 2:30 am

    I found a problem with my post above.

    This line:
    $validates = $validates && $this->Post->validates();

    Should be:
    $validates = $this->Post->validates() && $validates;

    Otherwise the $this->Post->validates() function won’t run, because PHP short-circuits the operation.

  15. teknoid said, on March 4, 2009 at 3:36 pm

    @Joel Pearson

    Excellent, thanks for sharing.

  16. Yien Bin said, on March 5, 2009 at 2:11 am

    Hi, I would really like to see some examples about Validation::multiple(). It just simply doesn’t work for me.

  17. visskiss said, on March 10, 2009 at 10:58 am

    Hi Teknoid,

    This works, but it’s true that it doesn’t validate the whole form at once.

    It’s also…un-cakelike ;-) meaning the secondary model has to ‘know’ about the primary model. Which kind of defeats the point, no?

    There MUST be a better way. I tried fooling with multiple, but didn’t get ANYWHERE.

    PS your blog seems to pop up whenever I have a cake issue and do a search…

  18. visskiss said, on March 10, 2009 at 11:10 am

    Hi Again,

    Further to my previous comment, I think the best method for validating the data is simply to do this:

    if(empty($this->data['Tag']['Tag'])) {
    $this->Post->Tag->invalidate(’Tag’,'min_tags’);
    }

    Then proceed with a

    $this->Post->save($this->data);

    to validate the rest of the form.

    It’s simpler, requires no hacks and is much shorter…

  19. teknoid said, on March 10, 2009 at 1:22 pm

    @visskiss

    Secondary model knowing about primary model, is very much cake-like… the fact that it doesn’t validate the whole form is definitely a problem. Well, it sparked some good conversation and solutions so it was worthwhile to write about it, I guess :)

    And… thank you for sharing your solution as well.

  20. Javier said, on April 16, 2009 at 4:16 am

    Interesting debate.

    There’s one thing I don’t like in the original approach:

    We are about to save a new Post… let’s add a validation rule to our Tag model

    So, we want the *Post* to have at least one tag. We should be validating the Post model. Maybe there are 3 others models with a HABTM relationship with Tag, and their validation rules are different.

    Of course I don’t have a better idea; I’m just critizicing :-).

    What I’ve been using are manual validations in the controller or using the beforevalidate() method, but those have already been pointed out.

  21. teknoid said, on April 16, 2009 at 11:19 am

    @Javier

    No you are right, the original approach lacked some elegance… and had some flaws. Too bad I didn’t have much time to come back to it and test out some other things.


Leave a Reply