nuts and bolts of cakephp

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

Posted in CakePHP by teknoid on April 21, 2009

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.

16 Responses

Subscribe to comments with RSS.

  1. Kyo said, on April 21, 2009 at 11:36 pm

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

  2. [...] Read the original post: More pondering about HABTM (let’s save new tags with a post) [...]

  3. red said, on April 22, 2009 at 3:08 am

    Great post, thank you!

  4. Lucian Lature said, on April 22, 2009 at 3:45 am

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

  5. teknoid said, on April 22, 2009 at 8:09 am

    @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 said, on April 23, 2009 at 3:12 am

    thanks, just what I needed!

  7. teknoid said, on April 23, 2009 at 8:26 am

    @ianmcn

    You’re welcome

  8. [...] If I linked to every new teknoid post there wouldn’t be room for any other blogs. I’ll pick there two: “Give all of your error messages a different layout” and “More pondering about HABTM“. [...]

  9. Rui Cruz said, on May 7, 2009 at 9:47 am

    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 said, on May 7, 2009 at 10:46 am

    @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 said, on May 7, 2009 at 1:06 pm

    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 said, on May 7, 2009 at 2:16 pm

    @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 said, on September 5, 2009 at 6:35 am

    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 said, on September 5, 2009 at 2:36 pm

    @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 said, on October 21, 2009 at 5:51 am

    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 said, on December 13, 2009 at 5:45 am

    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?


Leave a Reply