More pondering about HABTM (let’s save new tags with a post)

There is a number of examples (some you can find even on this blog) on how to save a Post with a few Tags.
Most of them, so far, had shown how to easily save Tags when the ID’s are already known.

Now, we’ll take a look at how to save a Post and allow a user to enter a bunch of tags (hmmm… let’s say separated by a comma) and save it all together with very little hassle.

Let’s define some goals first:

  • We would like to allow the user to pick from a list of existing tags
  • We would like to allow the user to input a bunch of tags separated by comma
  • We need to ensure that we do not save already existing tags
  • If the user doesn’t select or input any tags, we should just go ahead and save the Post
  • If the user does something silly like input “some, some, some, tag”, we should only save two tags from the list

I think that should be good enough to get started…

First we’ll, build a simple a form add.ctp:


  echo $form->create();

  echo $form->inputs(array('name', 'body'));
  echo $form->input('Tag', array('multiple'=>'checkbox'));
  echo $form->input('Tag.new_tags');

  echo $form->end('Save');

As you see, we’ll have a bunch of checkboxes to allow the user to pick from existing tags, we’ll also allow a free-hand input for “new tags” $form->input('Tag.new_tags');

Now, let’s see our “add” action in the controller:


function add() {

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

  $this->set('tags', $this->Post->Tag->find('list'));
}

Nothing out of the ordinary so far. Note that, $this->set('tags', $this->Post->Tag->find('list')); will let CakePHP automagically build a bunch of checkboxes from existing tags in the DB.

Now, let’s add a little something-something to our Post model:

   function beforeSave() {
        
        $tagIds = $this->_getTagIds();
          
        if(!empty($tagIds)) {
            if(!isset($this->data['Tag']['Tag'])) {
                $this->data['Tag']['Tag'] = $tagIds;    
            }
            else {
               foreach($tagIds as $tagId) {
               	    $this->data['Tag']['Tag'][] = $tagId;
               }
            }  
        }
                        
        return true;
    }
    
    function _getTagIds() {
        $tags = explode(',', $this->data['Tag']['new_tags']);
                
        if(Set::filter($tags)) {
            foreach($tags as $tag) {
                
                $tag = strtolower(trim($tag));
                
                $existingTag = $this->Tag->find('first', array('conditions' => array('Tag.name' => $tag)));
                
                if(!$existingTag) {
                    $this->Tag->create();
                    $this->Tag->saveField('name', $tag);
                    
                    $tagIds[] = $this->Tag->id;    
                }
                else {
                    $tagIds[] = $existingTag['Tag']['id'];
                }
            }
                       
            return array_unique($tagIds); 
        }
        
        return false;      
    }

Let’s examine what’s happening here…

In beforeSave() we prepare the array of Tag ID’s to be saved, by relying on CakePHP’s HABTM conventions.
We’ve also created a custom function _getTagIds() to process the input from the user from the “new_tags” field (remember our add.ctp?)

So, if the user did not provide any input we simply go ahead and save the Post (and possibly some selected tags), otherwise we check if the tag already exists and if so, grab the ID. If it does not exist we save it, and then grab the ID.

Once all that is accomplished, we go ahead and properly prepare $this->data['Tag']['Tag'] to save our Post and Tag relationship.

Of course, any questions and suggestions about the code are as always welcomed.

P.S. If you are completely lost about what’s going on here, I suggest you check out this post, and then come back to review the code in more detail.

About these ads

18 Responses to More pondering about HABTM (let’s save new tags with a post)

  1. Kyo says:

    That’s a good post teknoid! By using your approach, it’s also possible to add AutoComplete functionality to the “new_tags” field.

  2. Pingback: More pondering about HABTM (let’s save new tags with a post) | Dailytuts.net

  3. red says:

    Great post, thank you!

  4. Lucian Lature says:

    I like this new layout, clean and very easy to scan and read.

  5. teknoid says:

    @Kyo
    Thanks, that definitely is a nicer approach than having a bunch of checkboxes for existing tags.

    @red
    Glad you liked, no problem ;)

    @Lucian Lature
    Yeah, I’ve finally found a theme that I’m more or less happy with.

  6. ianmcn says:

    thanks, just what I needed!

  7. teknoid says:

    @ianmcn

    You’re welcome

  8. Pingback: CakePHP Digest #13 | PseudoCoder.com

  9. Rui Cruz says:

    Woot! Adding Tag logic to the Post Model? :p

    I’m not a neebie in cakephp but I’m far from experienced so my question is this:

    Is it “correct” to add logic fom one Model (Tag) to another Model (Post) ??

    I’m curiosous about this because in these cases I end up writting this logic in the controller.

  10. teknoid says:

    @Rui Cruz

    Very good questions… I’ll answer them in reverse order, because it’s faster :)

    1. Logic in the controller = fat controller, you know that it is better to avoid that.

    2. Since we are saving the Post + Tag, I put the logic in the Post model, (plus I was in a bit of a rush and lazy).
    Now, that you’ve mentioned this… I have a doubt that it is the structurally the appropriate way to go.
    Since we are dealing with data arrays rather than object properties (for now), I don’t see too much harm in it, however it might be better to include the logic in the Tag model and trigger via something like: $this->Tag->processTags($blah) from the Post model.

    Thanks for bringing this up.

  11. Rui Cruz says:

    No prob :)

    I’ve been hearing the Fat Model and Skinny Controller but I’m having trouble in doing that since I’m handling several models in the save view so I really do get how to do that.

  12. teknoid says:

    @Rui Cruz

    Depends on the specific need. Of course, saveAll() is usually the answer for multiple models…
    I’d ask the specifics on the IRC channel or the google group.

  13. Xoubaman says:

    Could it be possible to validate tags somehow?

    Figure at least one tag is required and tags must be 2 characters long.

    I’m validating tags manually in the controller and showing a $tag_error, but I’m sure there must be a more elegant solution.

  14. teknoid says:

    @Xoubaman

    Certainly that should be handled in the model.
    I am finishing up a habtamable behavior, which allows to save two new habtm models (and their relation, of course) plus perform validation on both… should posted sometime next week. Until then, think about moving the validation to the model layer ;)

  15. Abhisek says:

    Thanks for the article. I made similar functions except I didn’t know about beforeSave() (I am a brand newbie with CakePHP). One thing I needed to change: I had to declare $this->data['data']['data'] as an array. It was giving me “Fatal Error: [] is not available for a string”.

  16. Lorenzo says:

    First of all, thanks for the post, your posts about HABTM should somehow make it into the official documentation, since they’re a real requirement to deal with join tables! :)

    Concerning where to put the logic: would it be the case to have an actual PostsTag model and put it there?

  17. izmanromli says:

    is it possible to insert all tags within a single query (without using foreach) ?

    imagine if someone insert so many new tags, is it effective to loop insertion query?

    thx …

  18. teknoid says:

    @izmanromli

    Well here we are doing a little more than just saving a bunch of tags, for one we are saving the Post model as well, secondly we are checking for existing tags and skip them so that our tags table doesn’t get overpopulated with repetitive data.
    Just because of that, the loop is pretty much required, IMO.

    Otherwise depending on your DB vendor you can do a simple multi-value insert (or whatever is the specific term for that).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 25 other followers

%d bloggers like this: