PHP
Article

Drupal goes Social: Building a “Liking” Module in Drupal

By Abbas Suterwala

In this article, we are going to look at how we can create a Drupal module which will allow your users to like your posts. The implementation will use jQuery to make AJAX calls and save this data asynchronously.

logo_drupal

Creating your Drupal like module

Let’s start by creating the new Drupal module. To do that we should first create a folder called likepost in the sites\all\modules\custom directory of your Drupal installation as shown below:

Initial folder structure

Inside this folder, you should create a file called likepost.info with the following contents:

name = likepost
description = This module allows the user to like posts in Drupal.
core = 7.x

This file is responsible for providing metadata about your module. This allows Drupal to detect and load its contents.

Next, you should create a file called as likepost.module in the same directory. After creating the file, add the following code to it:

/**
 * @file
 * This is the main module file.
 */

 /**
 * Implements hook_help().
 */
function likepost_help($path, $arg) {

    if ($path == 'admin/help#likepost') {
        $output = '<h3>' . t('About') . '</h3>';
        $output .= '<p>' . t('This module allows the user to like posts in Drupal.') . '</p>';
        return $output;
    }
}

Once you have completed this you can go to the modules section in your Drupal administration and should be able to see the new module. Do not enable the module yet, as we will do so after adding some more functionality.

Creating the schema

Once you have created the module file, you can create a likepost.install file inside the module root folder. Inside, you will define a table schema which is needed to store the likes on each post for each user. Add the following code to the file:

<?php

/**
* Implements hook_schema().
*/
function likepost_schema() {
    $schema['likepost_table_for_likes'] = array(
        'description' => t('Add the likes of the user for a post.'),
        'fields' => array(
            'userid' => array(
                'type' => 'int',
                'not null' => TRUE,
                'default' => 0,
                'description' => t('The user id.'),
            ),

            'nodeid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t('The id of the node.'),
                ),

        ),

        'primary key' => array('userid', 'nodeid'),
    );
    return $schema;
}

In the above code we are are implementing the hook_schema(), in order to define the schema for our table. The tables which are defined within this hook are created during the installation of the module and are removed during the uninstallation.

We defined a table called likepost_table_for_likes with two fields: userid and nodeid. They are both integers and will store one entry per userid – nodeid combination when the user likes a post.

Once you have added this file, you can install the module. If everything has gone correctly, your module should be enabled without any errors and the table likepost_table_for_likes should be created in your database. You should also see the help link enabled in the module list next to your likepost module. If you click on that you should be able to see the help message you defined in the hook_help() implementation.

Help Message

Creating a menu callback to handle likes

Once we have enabled the module, we can add a menu callback which will handle the AJAX request to add or delete the like. To do that, add the following code to your likepost.module file

/**
* Implements hook_menu().
*/
function likepost_menu() {
    $items['likepost/like/%'] = array(
        'title' => 'Like',
        'page callback' => 'likepost_like',
        'page arguments' => array(2),
        'access arguments' => array('access content'),
        'type' => MENU_SUGGESTED_ITEM,
    );
    return $items;
}


function likepost_like($nodeid) {
    $nodeid = (int)$nodeid;
    global $user;

    $like = likepost_get_like($nodeid, $user->uid);

    if ($like !== 0) {
        db_delete('likepost_table_for_likes')
        ->condition('userid', $user->uid)
        ->condition('nodeid', $nodeid)
        ->execute();
        //Update the like value , which will be sent as response
        $like = 0;
    } else {
        db_insert('likepost_table_for_likes')
        ->fields(array(
        'userid' => $user->uid,
        'nodeid' => $nodeid
        ))
        ->execute();
        //Update the like value , which will be sent as response
        $like = 1;
    }

    $total_count = likepost_get_total_like($nodeid);
    drupal_json_output(array(
        'like_status' => $like,
        'total_count' => $total_count
        )
    );

}

/**
* Return the total like count for a node.
*/
function likepost_get_total_like($nid) {
    $total_count = db_query('SELECT count(*) from {likepost_table_for_likes} where nodeid = :nodeid',
    array(':nodeid' => $nid))->fetchField();
    return (int)$total_count;
}

/**
* Return whether the current user has liked the node.
*/
function likepost_get_like($nodeid, $userid) {
    $like = db_query('SELECT count(*) FROM {likepost_table_for_likes} WHERE
    nodeid = :nodeid AND userid = :userid', array(':nodeid' => $nodeid, ':userid' => $userid))->fetchField();
    return (int)$like;
}

In the above code, we are implementing hook_menu() so that whenever the path likepost/like is accessed with the node ID, it will call the function likepost_like().

Inside of likepost_like() we get the node ID and the logged in user’s ID and pass them to the function likepost_get_like(). In the function likepost_get_like() we check our table likepost_table_for_likes to see if this user has already liked this post. In case he has, we will delete that like, otherwise we will insert an entry. Once that is done, we call likepost_get_total_like() with the node ID as a parameter, which calculates the total number of likes from all users on this post. These values are then returned as JSON using the drupal_json_output() API function.

This menu callback will be called from our JQuery AJAX call and will update the UI with the JSON it receives.

Displaying the Like button on the node

Once we have created the callback, we need to show the like link on each of the posts. We can do so by implementing hook_node_view() as below:

/**
 * Implementation of hook_node_view
 */
function likepost_node_view($node, $view_mode) {
    if ($view_mode == 'full'){
        $node->content['likepost_display'] =  array('#markup' => display_like_post_details($node->nid),'#weight' => 100);

        $node->content['#attached']['js'][] = array('data' => drupal_get_path('module', 'likepost') .'/likepost.js');
        $node->content['#attached']['css'][] = array('data' => drupal_get_path('module', 'likepost') .'/likepost.css');
    } 

}

/**
* Displays the Like post details.
*/
function display_like_post_details($nid) {

    global $user;
    $totalLike =  likepost_get_total_like($nid);
    $hasCurrentUserLiked = likepost_get_like($nid , $user->uid);

    return theme('like_post',array('nid' =>$nid, 'totalLike' =>$totalLike, 'hasCurrentUserLiked' => $hasCurrentUserLiked));
    
}
/**
* Implements hook_theme().
*/
function likepost_theme() {
    $themes = array (
        'like_post' => array(
            'arguments' => array('nid','totalLike','hasCurrentUserLiked'),
        ),
    );
    return $themes;
}

function theme_like_post($arguments) {
    $nid = $arguments['nid'];
    $totalLike = $arguments['totalLike'];
    $hasCurrentUserLiked = $arguments['hasCurrentUserLiked'];
    global $base_url;
    $output = '<div class="likepost">';
    $output .= 'Total number of likes on the post are ';
    $output .= '<div class="total_count">'.$totalLike.'</div>';

    if($hasCurrentUserLiked == 0) {
        $linkText = 'Like';
    } else {
        $linkText = 'Delete Like';
    }

    $output .= l($linkText, $base_url.'/likepost/like/'.$nid, array('attributes' => array('class' => 'like-link')));

    $output .= '</div>'; 
    return $output;
    
}

Inside likepost_node_view() we check for when the node is in the full view mode and we add the markup returned by the function display_like_post_details(). We also attached our custom JS and CSS file when the view is rendered using the attached property on the node content. In function display_like_post_details() we get the total number of likes for the post and whether or not the current user has liked the post. Then we call the theme function which will call the function theme_like_post() which we have declared in the implementation of ‘hook_theme’ but will allow the designers to override if required. In theme_like_post(), we create the HTML output accordingly. The href on the link is the $base_url and the path to our callback appended to it. The node ID is also attached to the URL which will be passed as a parameter to the callback.

Once this is done, add a file likepost.css to the module root folder with the following contents:

.likepost {
    border-style: dotted;
    border-color: #98bf21;
    padding: 10px;
}

.total_count {
    font-weight: bold;
}

.like-link {
    color:red;
}

.like-link:hover {
    color: red;
}

Now if you go to the complete page of a post you will see the Like post count as shown below.

Adding the jQuery logic

Now that we see the like link displayed, we will just have to create the likepost.js file with the following contents:

jQuery(document).ready(function () {

    jQuery('a.like-link').click(function () {
        jQuery.ajax({
            type: 'POST', 
            url: this.href,
            dataType: 'json',
            success: function (data) {
                if(data.like_status == 0) {
                    jQuery('a.like-link').html('Like');
                }
                else {
                    jQuery('a.like-link').html('Delete Like');
                }

                jQuery('.total_count').html(data.total_count);
            },
            data: 'js=1' 
        });

        return false;
    });
});

The above code binds the click event to the like link and makes an AJAX request to the URL of our callback menu function. The latter will update the like post count accordingly and then return the new total count and like status, which is used in the success function of the AJAX call to update the UI.

Updated UI with Like count

Conclusion

jQuery and AJAX are powerful tools to create dynamic and responsive websites. You can easily use them in your Drupal modules to add functionality to your Drupal site, since Drupal already leverages jQuery for its interface.

Have feedback? Let us know in the comments!

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

Comments
joshirohit100

Instead of doing this through custom module, you cn use flag module with almost no coding to implement like functionality.

megazoid

Thanks for the article but I wonder is it normal to have HTML right inside the module code?
Can you show how can we move that markup into the external theme files?

nod_

Biggest issue here is that this module present a CRSF vunerability, there is no verification when accessing the like action callback, It should be a form to prevent abuse. Here is how to do things properly: Create forms in a safe way to avoid cross-site request forgeries (CSRF).

Usually we should stop here and fix the thing (or better, as was said earlier, use flag module and be done with it).

But let's take it all the way, this is a vulgarization article so I'd rather people learn the right things (if not in the article, in the comments):

  1. There ought to be a custom permission for this, access content is way to permissive and un-customizable. Combine that with the CRSF issue and watch your DB melt.
  2. As far as I can see all anonymous users gets one vote and step on each others, clearly a bug (anon users have the user ID 0).
  3. User text is not translatable, the t() function needs to be used.
  4. Also, it would be great to use Drupal coding standards when writing Drupal code:
    • Indentation is 2 spaces.
    • else on a new line.
  5. There is no need to use $base_url, that's what the l() function is for.

  6. There is a straight up bug in the JS, try displaying several likes on the page and things will get messed up. It is also neither using Drupal JS API nor JS standards.

// wrote the code without running it, should work but no guarantees.
(function ($) {
  "use strict";
  // jQuery ready doesn't work for content added through ajax,
  // always use Drupal.behaviors.
  Drupal.behaviors.likeLink = {
    attach: function (context) {
      // The jquery once plugin make sure initialization happens only once.
      // It is completely different from jQuery.one()
      $(context).find('likepost').once('like-link').each(function () {
        var $likepost = $(this);
        $likepost.find('a.like-link').click(function (event) {
          $.ajax({
            type: 'POST',
            url: this.href,
            dataType: 'json',
            success: function (data) {
              var $like = $(event.target);
              if (data.like_status === 0) {
                $like.html('Like');
              }
              else {
                $like.html('Delete Like');
              }
              $likepost.find('.total_count').html(data.total_count);
            },
            data: 'js=1'
          });

          return false;
        });
      });
    },
    // When removing the content during ajax calls, clean up things.
    detach: function (context, settings, trigger) {
      if (trigger === 'unload') {
        $(context).find('likepost').removeOnce('like-link')
          .find('a.like-link').off('click');
      }
    }
  };
})(jQuery);

On the plus side, using a theme function is good and special thumbs up for using #attached instead of the dirty drupal_add_js lots of kudos from me there smile.

There ought to be at least a tiny amount of peer review to avoid code presenting security vulnerability make it on sitepoint.com. Wouldn't want people to complain about Drupal when it's only fault may be hard to find documentation.

swader

@nod_ thanks for the detailed feedback! We do have a peer review system in place, but sadly, we lack Drupalistas. Would you be interested in joining the peer review repo? All the upcoming posts are there first, for at least two weeks before publication, often longer, so if you could find the time to take a look from time to time and drop a comment or two, it'd be much appreciated. All I need is your Github username. A bit more info here.

nod_

I'm stretched for time but I can help a little. This feed is published on the Drupal planet after all. My github username is theodoreb.

swader

You should now have access to the repo. I figure you're probably as stretched for time as the rest of us, but even if you just briefly skim a post when waiting in line in a shop or something and leave a comment like "this and this should be like this and that", it's already a huge help. Thanks!

nod_

Yup, all good. Thanks. I'll look out for Drupal posts in the queue smile

swader

@abbass786 any feedback on this, as the author? Or @upchuk as our Drupal regular?

zviryatko

For like/dislike I always use Flag module, because he support views

upchuk

Not much from me. I agree with @nod_ on his points.

abbass786

Yup Agreed.
The idea of this tutorial was not to just make that module.
But learn how pieces of which work together for instance how to use jQuery etc so that it can be used in any custom Drupal module you planning to make.

abbass786

Thanks nod_ with the detailed feedback.
I agree with all the comments.

Just as a a beginner , when one is not use to coding in Drupal then too many concepts in one post (like using the T function etc) cause too much to digest. Though I completely agree that a final production code written for Drupal should have all the best practices mentioned by you.

But as a beginner if some one reads a post too many new concepts make one even not understand the core concept the the article is trying to highlight.

Having you in the review team will be awesome.
Looking forward to that and improve my Drupal skills smile

abbass786

Thanks for catching the drupal_add_js and recommending #attached in the peer review smile

ki11d0z3r

You little complicated task.
Instead of writing custom js code you can use class 'ajax-link' and use ajax command. Here example how to use this features of Drupal http://www.computerminds.co.uk/drupal-code/make-link-use-ajax-drupal-7-its-easy . Also there is a little performance improvements: you need place nid column in primary key first then uid (some info about this: https://dev.mysql.com/doc/refman/5.0/en/multiple-column-indexes.html )
And of course you have problems with anonymous users (you can use session_api module if you want to give permission for anonymous users to vote).

michaelmd

can this be used together with something like the ostatus module ?
...or any project that might be able to do (or aiming to do) decentralised federation for posts?

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.