zen of coding

How to validate HABTM data…

Update: Since writing of this article the ‘rule’ => ‘multiple’ has been implemented in cake core.
It is much more convenient to use for HABTM validation.
You may consider this post deprecated and only to be used for historical purposes or if you are working with an older code-base.

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.

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

  • teknoid

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

  • 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?

  • @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 :)

  • Francisco

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

  • @Francisco

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

  • 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).

  • @Joaquin Windmüller

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

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

  • Pingback: Validación de datos HABTM en CakePHP()

  • @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…

  • brij bhushan

    Yes,it works

  • Joel Pearson

    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.

  • Joel Pearson

    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.

  • @Joel Pearson

    Excellent, thanks for sharing.

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

  • 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…

  • 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…

  • @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.

  • Javier

    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.

  • @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.

  • enthooz

    I just stumbled upon this post and read the comments first (from latest to earliest). Should have gone the other way and read Mark Story’s comment first. I just implemented the Validation::multiple rule. It looks something like this and seems to work:

    var $validate = array(
    ‘Tag’ => array(
    ‘rule’ => array(‘multiple’, array(‘min’ => 1)),
    ‘required’ => true
    )
    );

    • somehow using your technique isn’t working on my app, but i found a way to leave other rules ignored

      this is what i’ve done :

      var $validate = array
      (
      ‘post_id’ => array(‘rule’ => ‘notEmpty’), // this should be commented
      ‘category_id’ => array(‘rule’ => array(‘multiple’, array(‘min’ => 1)))
      );

  • supermork

    probably i am wrong, but with you approach it becomes impossible to save a Tag record alone.
    Maybe in checkTags() is advisable to check if isset( $this->data[‘Post’])…?

    bye and many thanks for your articles!

  • teknoid

    @supermork

    NP. as mentioned, it is best to use ‘rule’ => ‘multiple’ (since it has been implemented).

  • Bleh sorry to comment on something so old however the ‘multiple’ validation rule still doesn’t work for HABTM relationships:

    http://cakephp.lighthouseapp.com/projects/42648/tickets/400

    Bit of a hack solution can be found here:
    http://bakery.cakephp.org/articles/kogalex/2010/01/13/quick-fix-for-habtm-validation

%d bloggers like this: