Drupal专业开发指南 第17章 创建模块

老葛的Drupal培训班 Think in Drupal

在一个文本编辑器中打开空的plusone.module,并添加标准的Drupal头部文件:
 
<?php
// $Id$
/**
 * @file
 * A simple +1 voting widget.
 */
 
    接下来我们将逐个的添加我们用到的Drupal钩子。一个比较简单的就是hook_perm(),它让你向Drupal的基于角色的访问控制页面添加一个“rate content”权限。使用这一权限,你就可以阻止那些没有账号或者没有登录的匿名用户进行投票了。
 
/**
 * Implementation of hook_perm().
 */
function plusone_perm() {
    return array('rate content');
}
 
    现在,我们开始实现一些AJAX功能。jQuery的一个重要特性,就是它能够提交自己的HTTP GET 或 POST请求,使用这一特性你就可以将投票提交给Drupal,而不用刷新整个页面了。jQuery将拦截投票链接上的点击事件,并向Drupal发送一个请求,让其保存投票并返回更新后的总分数。jQuery将使用这个新值来更新页面上的分数。图17-9展示了整个流程的概览。
    一旦jQuery拦截了对投票链接的点击,它需要通过一个URL来调用一个Drupal函数。我们使用hook_menu(),将由jQuery提交的投票URL映射到一个Drupal PHP函数上。这个PHP函数将投票保存到数据库中,并以JSON (JavaScript Object Notation)的形式为jQuery返回一个新分数(噢,这样我们就没有使用XML,因此这也就不是严格意义上的AJAX了)。
 
/**
 * Implementation of hook_menu().
 */
function plusone_menu() {
    $items['plusone/vote'] = array(
        'page callback' => 'plusone_vote',
        'access arguments' => array('rate content'),
        'type' => MENU_CALLBACK,
    );
    return $items;
}
 
    在前面的函数中,当路径为plusone/vote的请求进来以后,如果请求该路径的用户拥有“rate content”权限,那么函数plusone_vote()就会处理这个请求。
   
17-9.投票更新流程概览
 
注意 如果发送请求的用户没有“rate content”权限,Drupal将返回一个拒绝访问页面。然而,我们将确保动态的构建我们的投票小部件,这样那些无权投票的用户就看不到投票链接了。对于那些恶意的用户,他们可能会绕过我们的小部件,直接访问http://example.com/?q=plusone/vote,此时Drupal的权限系统仍然会保护我们免受他们的攻击。
 
    路径plusone/vote/3翻译成了PHP函数调用plusone_vote(3)(关于Drupal的菜单/回调系统的更多详细,参看第4章)。
 
/**
 * Called by jQuery, or by browser if JavaScript is disabled.
 * Submits the vote request. If called by jQuery, returns JSON.
 * If called by the browser, returns page with updated vote total.
 */
function plusone_vote($nid) {
    global $user;
    $nid = (int)$nid;
 
    // Authors may not vote on their own posts. We check the node table
    // to see if this user is the author of the post.
    $is_author = db_result(db_query('SELECT uid FROM {node} WHERE nid = %d AND
        uid = %d', $nid, $user->uid));
 
    if ($nid > 0 && !$is_author) {
        // Get current vote count for this user.
        $vote_count = plusone_get_vote($nid, $user->uid);
        if (!$vote_count) {
        // Delete existing vote count for this user.
        db_query('DELETE FROM {plusone_votes} WHERE uid = %d AND nid = %d',
            $user->uid, $nid);
        db_query('INSERT INTO {plusone_votes} (uid, nid, vote_count) VALUES
            (%d, %d, %d)', $user->uid, $nid, $vote_count + 1);
        watchdog('plusone', 'Vote by @user on node @nid.', array(
            '@user' => $user->name, '@nid' => $nid));
        }
    }
    // Get new total to display in the widget.
    $total_votes = plusone_get_total($nid);
    // Check to see if jQuery made the call. The AJAX call used
    // the POST method and passed in the key/value pair js = 1.
    if (!empty($_POST['js'])) {
        // jQuery made the call.
        // This will return results to jQuery's request.
        drupal_json(array(
            'total_votes' => $total_votes,
            'voted' => t('You voted')
        )
    );
    exit();
    }
 
    // It was a non-JavaScript call. Redisplay the entire page
    // with the updated vote total by redirecting to node/$nid
    // (or any URL alias that has been set for node/$nid).
    $path = drupal_get_path_alias('node/'. $nid);
    drupal_goto($path);
}
 
    前面的plusone_vote()函数,保存了当前的投票,并向jQuery返回信息;信息的形式是一个关联数组,里面包含了新总分和字符串You voted(你已投票),这个字符串用于替换投票小部件下面的Vote(投票文本。这个数组将被传给drupal_json(),这样就可以将PHP变量转化为它们的JavaScript等价物,在这里,就是将一个PHP关联数组转化为了一个JavaScript对象,并将HTTP头部设置为Content-type: text/javascript。关于JSON的更多详细,参看http://en.wikipedia.org/wiki/JSON
    注意,在前面的函数中,我们是如何处理浏览器禁用了JavaScript这种情况的。当我们编写jQuery代码时,我们将确保,来自jQuery的AJAX调用会传递一个名为js的参数并使用POST方法。如果没有js参数的话,我们就知道此时是用户点击了投票链接,浏览器本身请求了该路径----例如,plusone/vote/3。在这种情况下,因为浏览器期望的是一个普通的HTML页面,所以我们不会返回JSON。相反,我们更新了投票总分来反映用户投票的事实,接着,我们将浏览器重定向到最初的页面,Drupal将负责重新构建该页面并显示新的投票总分。
    在前面的代码中,我们调用了plusone_get_vote()和plusone_get_total(),现在让我们创建这两个函数:
 
/**
 * Return the number of votes for a given node ID/user ID pair.
 */
function plusone_get_vote($nid, $uid) {
    return (int)db_result(db_query('SELECT vote_count FROM {plusone_votes} WHERE
        nid = %d AND uid = %d', $nid, $uid));
}
 
/**
 * Return the total vote count for a node.
 */
function plusone_get_total($nid) {
    return (int)db_result(db_query('SELECT SUM(vote_count) FROM {plusone_votes}
        WHERE nid = %d', $nid));
}
 
    现在,让我们集中精力将投票小部件显示在文章旁边。这包括两部分。首先,我们在plusone_widget()函数内部定义一些变量。接着我们将这些变量传递给一个主题函数。下面是第一部分:
 
/**
 * Create voting widget to display on the web page.
 */
function plusone_widget($nid) {
    global $user;
 
    $total = plusone_get_total($nid);
    $is_author = db_result(db_query('SELECT uid FROM {node} WHERE nid = %d
        AND uid = %d', $nid, $user->uid));
    $voted = plusone_get_vote($nid, $user->uid);
 
    return theme('plusone_widget', $nid, $total, $is_author, $voted);
}
 
    还记不记得,当我们需要一个可主题化的项目时,我们需要使用hook_theme()来向Drupal声明它,这样就将其包含在了主题注册表中。下面就是这个钩子函数:
 
/**
 * Implementation of hook_theme().
 * Let Drupal know about our theme function.
 */
function plusone_theme() {
    return array(
        'plusone_widget' => array(
            'arguments' => array('nid', 'total', 'is_author', 'voted'),
        ),
    );
}
 
    接着,我们就需要实际的主题函数了。注意,在这里我们加载了我们的JavaScript和CSS文件。
 
/**
 * Theme for the voting widget.
 */
function theme_plusone_widget($nid, $total, $is_author, $voted) {
    // Load the JavaScript and CSS files.
    drupal_add_js(drupal_get_path('module', 'plusone') .'/plusone.js');
    drupal_add_css(drupal_get_path('module', 'plusone') .'/plusone.css');
 
    $output = '<div class="plusone-widget">';
    $output .= '<div class="score">'. $total .'</div>';
 
    $output .= '<div class="vote">';
    if ($is_author) {
        // User is author; not allowed to vote.
        $output .= t('Votes');
    }
    elseif ($voted) {
        // User already voted; not allowed to vote again.
        $output .= t('You voted');
    }
    else {
        // User is eligible to vote.
        $output .= l(t('Vote'), "plusone/vote/$nid", array(
            'attributes' => array('class' => 'plusone-link')
            ));
    }
    $output .= '</div>'; // Close div with class "vote".
    $output .= '</div>'; // Close div with class "plusone-widget".
 
    return $output;
}
 
    在前面代码的plusone_widget()函数中,我们先设置了一些变量,接着将小部件的主题化委托给了我们创建的自定义主题函数theme_plusone_widget()。记住theme('plusone_widget')实际上调用的就是theme_plusone_widget()(更多详细,可参看第8章)。创建一个单独的主题函数,而不是在plusone_widget()函数内部构建HTML,这样设计者在想要修改外观时就能覆写这个函数了。
    在我们的主题函数theme_plusone_widget()中,一定要为关键的HTML元素添加CSS类属性,这样在jQuery中就可以非常方便的定位这些元素了。还有,看一下链接的URL。它指向了plusone/vote/$nid,其中$nid是文章的当前节点ID。当用户点击这个链接时,由于我们使用jQuery监听该链接上的onClick事件,所以jQuery将代替Drupal来拦截并处理这个事件。看到没有,我们在构建链接时,是如何定义CSS选择器plusone-link的?在我们后面的Javascript中,找到该选择器出现的地方,这就是a.plus1-link。它就是说,一个带有css类plusone-link的HTML元素<a>。
    显示在页面http://example.com/?q=node/4中的小部件的HTML,应该是这样的:
 
<div class="plusone-widget">
    <div class="score">0</div>
    <div class="vote">
        <a class="plusone-link" href="/plusone/vote/4">Vote</a>
    </div>
</div>
 
    theme_plusone_widget()函数用来生成发送给浏览器的小部件。我们想让这个小部件显示在节点视图中,这样当用户查看节点时,就可以进行投票了。你能猜一下,我们将使用哪个Drupal钩子呢?这就是我们的老朋友hook_nodeapi(),它允许我们修改正被构建的任意节点。
 
/**
 * Implementation of hook_nodeapi().
 */
function plusone_nodeapi(&$node, $op, $teaser, $page) {
    switch ($op) {
        case 'view':
        // Show the widget, but only if the full node is being displayed.
        if (!$teaser) {
            $node->content['plusone_widget'] = array(
                '#value' => plusone_widget($node->nid),
                '#weight' => 100,
            );
        }
        break;
 
        case 'delete':
        // Node is being deleted; delete associated vote data.
        db_query('DELETE FROM {plusone_vote} WHERE nid = %d', $node->nid);
        break;
    }
}
 
    我们将weight元素的值设置为一个大的(或者“重的”)数字,这样就确保了小部件显示在文章的底部,而不是顶部。我们还偷偷的加了一个delete情况,这样当节点被删除时,该节点的投票记录也将被一同删除。
这就是plusone.module的全部内容了。我们的模块马上就要完工了,现在就剩下填写plusone.js了,在里面填写我们的jQuery代码,用于执行AJAX调用,更新投票总分,并将字符串Vote修改为You voted
 
// $Id$
 
// Only run if we are in a supported browser.
if (Drupal.jsEnabled) {
    // Run the following code when the DOM has been fully loaded.
    $(document).ready(function () {
        // Attach some code to the click event for the
        // link with class "plusone-link".
        $('a.plusone-link').click(function () {
            // When clicked, first define an anonymous function
            // to the variable voteSaved.
            var voteSaved = function (data) {
                // Update the number of votes.
                $('div.score').html(data.total_votes);
                // Update the "Vote" string to "You voted".
                $('div.vote').html(data.voted);
            }
            // Make the AJAX call; if successful the
            // anonymous function in voteSaved is run.
            $.ajax({
                type: 'POST', // Use the POST method.
                url: this.href,
                dataType: 'json',
                success: voteSaved,
                data: 'js=1' // Pass a key/value pair.
            });
            // Prevent the browser from handling the click.
            return false;
        });
    });
}
 
    你应该把你所有的jQuery代码都包装在一个Drupal.jsEnabled测试中。这个测试将确保当前的浏览器支持特定的DOM方法(如果不支持的话,那么将不会执行我们的JavaScript)。
    这个JavaScript向a.plusone-link添加了一个事件侦听器(还记不记得我们将plusone-link定义为了CSS类选择器?),这样当用户点击链接时,它将触发一个HTTP POST请求,来请求它指向的URL。前面的代码还演示了,jQuery是如何处理从Drupal中传递回来的数据的。当AJAX请求完成以后,返回值(从Drupal中返回)将作为data参数传递到匿名函数中,我们把这个函数赋值给了变量voteSaved。关联数组中的键所引用的数组,最初是在Drupal内部的plusone_vote()函数中构建的。最后,Javascript更新了分数,并将文本“Vote”修改为了“You Voted”。
    为了阻止加载整个页面(因为JavaScript负责处理点击事件),在JavaScript jQuery函数中,我们把返回值设置为了false

Drupal版本: