hook_init()的替代方法


8

hook_init()用来检查用户的上次访问时间。如果最后访问时间是昨天,我将增加一个计数器并设置一些变量。

问题在于,对于相同的页面加载,hook_init()有时执行的次数超过一次(我可以使用看到dsm()),因此我的代码多次执行,导致变量错误。

为什么hook_init()执行不止一次?
解决我的问题的最佳方法是什么?我应该使用另一个挂钩吗?

我对此进行了更多研究: 我搜索了对hook_init()的调用(搜索了string module_invoke_all('init');),但只找到了核心调用)。我不知道这可以用不同的方式称呼。

这是我的hook_init()

function episkeptis_achievements_init(){
    dsm('1st execution');
    dsm('REQUEST_TIME: '.format_date(REQUEST_TIME, 'custom', 'd/m/Y H:i:s').' ('.REQUEST_TIME.')');
}

这是输出:

1st execution
REQUEST_TIME: 09/07/2012 11:20:32 (1341822032)

然后,将dsm()消息更改为dsm('2nd execution');并再次执行,这是输出:

1st execution
REQUEST_TIME: 09/07/2012 11:20:34 (1341822034)
2nd execution
REQUEST_TIME: 09/07/2012 11:22:28 (1341822148)

您可以看到该代码执行了两次。但是,第一次执行代码的旧副本,第二次执行更新的副本。也有2秒的时差。

这是php 5.3.10的d7版本


使用ddebug_backtrace(),它将为您提供函数backtrace。如果确实被多次调用,则该函数将告诉您谁。
Berdir

3
请记住,仅因为看到多个dsm(),并不意味着该挂钩被多次调用。实际上,您也有可能执行多个请求(例如,由于缺少图像而导致404页由Drupal处理)
Berdir 2012年

请注意,在11:22:28和11:20:34之间的区别是两分钟,而不是两秒。在这种情况下,挂钩不会在同一页面请求中执行两次,否则其值REQUEST_TIME将相同。
kiamlaluno

@kiamlaluno在第二次执行(即第一次执行后2分钟)时,我看到两个REQUEST_TIME,即当前时间和较旧的时间,恰好是第一次请求后的2秒。这告诉我代码执行了两次。无法遵循您的逻辑。为什么我看到当前请求的过去REQUEST_TIME?
Mike

我无法回答。我只能说,如果REQUEST_TIME来自同一个页面请求,则它的值是相同的。甚至没有两秒钟的差异。检查是否没有更改的值的代码REQUEST_TIME
kiamlaluno

Answers:


20

hook_init()对于每个请求的页面,Drupal仅调用一次;这是_drupal_bootstrap_full()中完成的最后一步。

  // Drupal 6
  //
  // Let all modules take action before menu system handles the request
  // We do not want this while running update.php.
  if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
    module_invoke_all('init');
  }
  // Drupal 7
  //
  // Let all modules take action before the menu system handles the request.
  // We do not want this while running update.php.
  if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
    // Prior to invoking hook_init(), initialize the theme (potentially a custom
    // one for this page), so that:
    // - Modules with hook_init() implementations that call theme() or
//   theme_get_registry() don't initialize the incorrect theme.
    // - The theme can have hook_*_alter() implementations affect page building
//   (e.g., hook_form_alter(), hook_node_view_alter(), hook_page_alter()),
//   ahead of when rendering starts.
    menu_set_custom_theme();
    drupal_theme_initialize();
    module_invoke_all('init');
  }

如果hook_init()执行不止一次,则应该找出原因。据我所知hook_init(),Drupal中的任何实现都没有对其执行两次进行检查(例如,请参见system_init()update_init())。如果那是Drupal通常会发生的事情,那么update_init()将首先检查它是否已经执行过。

如果计数器是用户登录的连续天数,我宁愿hook_init()使用类似于以下代码的代码来实现。

// Drupal 7
function mymodule_init() {
  global $user;

  $result = mymodule_increase_counter($user->uid); 
  if ($result[0]) {
    // Increase the counter; set the other variables.
  }
  elseif ($result[1] > 86400) {
    // The user didn't log in yesterday.
  }
}

function mymodule_date($timestamp) {
  $date_time = date_create('@' . $timestamp);
  return date_format($date_time, 'Ymd');
}

function mymodule_increase_counter($uid) {
  $last_timestamp = variable_get("mymodule_last_timestamp_$uid", 0);
  if ($last_timestamp == REQUEST_TIME) {
    return array(FALSE, 0);
  }

  $result = array(
    mymodule_date($last_timestamp + 86400) == mymodule_date(REQUEST_TIME),
    REQUEST_TIME - $last_timestamp,
  );
  variable_set("mymodule_last_timestamp_$uid", REQUEST_TIME);

  return $result;
}
// Drupal 6
function mymodule_init() {
  global $user;

  $result = mymodule_increase_counter($user->uid); 
  if ($result[0]) {
    // Increase the counter; set the other variables.
  }
  elseif ($result[1] > 86400) {
    // The user didn't log in yesterday.
  }
}

function mymodule_increase_counter($uid) {
  $last_timestamp = variable_get("mymodule_last_timestamp_$uid", 0);
  $result = array(FALSE, time() - $last_timestamp);

  if (time() - $last_timestamp < 20) {
    return $result;
  }

  $result[0] = (mymodule_date($last_timestamp + 86400) == mymodule_date(REQUEST_TIME));
  variable_set("mymodule_last_timestamp_$uid", time());

  return $result;
}

如果hook_init()在同一页面请求期间连续两次调用if ,则REQUEST_TIME包含相同的值,并且该函数将返回FALSE

mymodule_increase_counter()未优化代码;这只是为了展示一个例子。在实际的模块中,我宁愿使用数据库表来保存计数器以及其他变量。原因是$conf在Drupal引导时,会将 Drupal变量全部加载到全局变量中(请参见_drupal_bootstrap_variables()variable_initialize());如果为此使用Drupal变量,则Drupal会在内存中加载有关您为其保存信息的所有用户的信息,而对于每个请求的页面,在全局变量中仅保存一个用户帐户$user

如果您要计算连续几天用户访问的页面数,那么我将实现以下代码。

// Drupal 7
function mymodule_init() {
  global $user;

  $result = mymodule_increase_counter($user->uid); 
  if ($result[0]) {
    // Increase the counter; set the other variables.
  }
  elseif ($result[1] > 86400) {
    // The user didn't log in yesterday.
  }
}

function mymodule_date($timestamp) {
  $date_time = date_create('@' . $timestamp);
  return date_format($date_time, 'Ymd');
}

function mymodule_increase_counter($uid) {
  $last_timestamp = variable_get("mymodule_last_timestamp_$uid", 0);
  if ($last_timestamp == REQUEST_TIME) {
    return array(FALSE, 0);
  }

  $result = array(
    mymodule_date($last_timestamp + 86400) == mymodule_date(REQUEST_TIME),
    REQUEST_TIME - $last_timestamp,
  );
  variable_set("mymodule_last_timestamp_$uid", REQUEST_TIME);

  return $result;
}
// Drupal 6
function mymodule_init() {
  global $user;

  $result = mymodule_increase_counter($user->uid); 
  if ($result[0]) {
    // Increase the counter; set the other variables.
  }
  elseif ($result[1] > 86400) {
    // The user didn't log in yesterday.
  }
}

function mymodule_increase_counter($uid) {
  $last_timestamp = variable_get("mymodule_last_timestamp_$uid", 0);
  $result = array(FALSE, time() - $last_timestamp);

  if (time() - $last_timestamp < 20) {
    return $result;
  }

  $result[0] = (mymodule_date($last_timestamp + 86400) == mymodule_date(REQUEST_TIME));
  variable_set("mymodule_last_timestamp_$uid", time());

  return $result;
}

您会注意到在我的代码中我没有使用$user->access。原因是$user->access可以在hook_init()调用之前在Drupal引导期间进行更新。Drupal使用的会话写处理程序包含以下代码。(请参见_drupal_session_write()。)

// Likewise, do not update access time more than once per 180 seconds.
if ($user->uid && REQUEST_TIME - $user->access > variable_get('session_write_interval', 180)) {
  db_update('users')
    ->fields(array(
    'access' => REQUEST_TIME,
  ))
    ->condition('uid', $user->uid)
    ->execute();
}

至于可以使用的另一个钩子,在Drupal 7中可以使用hook_page_alter();您只是不更改的内容$page,而是增加计数器,并更改变量。
在Drupal 6上,可以使用hook_footer(),这是从template_preprocess_page()调用的钩子。您什么都不返回,但是增加计数器,并更改变量。

在Drupal 6和Drupal 7上,可以使用hook_exit()。请记住,当引导程序未完成时,也会调用该挂钩。代码无法访问从模块定义的功能或其他Drupal功能,因此您应该首先检查那些功能是否可用。有些功能始终可用hook_exit(),例如bootstrap.inccache.inc中定义的功能。不同之处在于,hook_exit()还为缓存页面hook_init()调用了,而没有为缓存页面调用。

最后,作为Drupal模块中使用的代码示例,请参见statistics_exit()。统计信息模块记录站点的访问统计信息,如您所见,它使用hook_exit(),而不是hook_init()。为了能够调用必要的函数,它调用了drupal_bootstrap()并传递了正确的参数,例如下面的代码。

  // When serving cached pages with the 'page_cache_without_database'
  // configuration, system variables need to be loaded. This is a major
  // performance decrease for non-database page caches, but with Statistics
  // module, it is likely to also have 'statistics_enable_access_log' enabled,
  // in which case we need to bootstrap to the session phase anyway.
  drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES);
  if (variable_get('statistics_enable_access_log', 0)) {
    drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION);

    // For anonymous users unicode.inc will not have been loaded.
    include_once DRUPAL_ROOT . '/includes/unicode.inc';
    // Log this page access.
    db_insert('accesslog')
      ->fields(array(
      'title' => truncate_utf8(strip_tags(drupal_get_title()), 255), 
      'path' => truncate_utf8($_GET['q'], 255), 
      'url' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', 
      'hostname' => ip_address(), 
      'uid' => $user->uid, 
      'sid' => session_id(), 
      'timer' => (int) timer_read('page'), 
      'timestamp' => REQUEST_TIME,
    ))
      ->execute();
  }

更新资料

关于何时hook_init()调用,可能有些混乱。

hook_init()如果未缓存页面,则为每个页面请求调用。不会为来自同一用户的每个页面请求调用一次。例如,如果您访问http://example.com/admin/appearance/update,然后http://example.com/admin/reports/statushook_init()则会被调用两次:每页一个。
“挂钩被调用两次”表示一旦Drupal完成其引导程序,就有一个模块将执行以下代码。

module_invoke_all('init');

如果是这样,则以下的实现hook_init()将显示相同的值两次。

function mymodule_init() {
  watchdog('mymodule', 'Request time: !timestamp', array('!timestamp' => REQUEST_TIME), WATCHDOG_DEBUG);
}

如果您的代码显示了REQUEST_TIME两个值(相差2分钟)(如您的情况),则该钩子不会被调用两次,而是应为每个请求的页面调用一次。

REQUEST_TIMEbootstrap.inc中用以下行定义。

define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']);

在当前请求的页面没有返回到浏览器之前,的值REQUEST_TIME不变。如果看到不同的值,那么您正在查看在不同请求页面中分配的值。


我根据您的建议做了一些测试。REQUEST_TIME与您在更新的问题中看到的值不同。我试图找到hook_init()的调用,但除了内核中的任何一个都没有找到。也许我没有找到正确的方法。最后,hook_exit()似乎可以解决问题,因此我将接受此答案。但是,我正在寻找有关为何两次调用hook_init()的答案。作为附带问题,建议您使用数据库表,而不要使用variable_set / get。为什么不建议这样做,variable_set / get使用db表。
Mike

Drupal变量使用数据库表,但是当Drupal引导时,它们全部加载到内存中。对于每个服务的页面,Drupal始终引导,并且只有与该页面请求关联的用户帐户。如果使用Drupal变量,则将不必要的关于用户帐户的信息加载到内存中,因为仅使用了一个用户帐户。
kiamlaluno

8

我记得在Drupal 6中经常发生这种情况(不确定在Drupal 7中是否仍会发生),但是我从未发现原因。我似乎记得记得在某个地方,Drupal核心并未两次调用此钩子。

我总是发现最简单的方法是使用静态变量来查看代码是否已经运行:

function MYMODULE_init() {
  static $code_run = FALSE;

  if (!$code_run) {
    run_some_code();
    $code_run = TRUE;
  }
}

这样可以确保它在单个页面加载中仅运行一次。


确实,那不是Drupal所做的。
kiamlaluno

2
它不是核心功能,但肯定会发生(我刚刚确认,在三个旧的Drupal 6站点上,所有站点都运行大多数contrib模块)。这是一个真正的难题,但是我现在没有时间调试它们。我怀疑它是更常用的contrib模块之一(也许是pathauto或全局重定向),但我不想指责。虽然不太确定为什么您的答案被否决了(或者是我的话),但对我来说,这似乎是很好的信息。我upvoted恢复平衡了一下:)
克莱夫

我的意思是Drupal在其hook_init()实现中没有这样的检查,其中一些会很乐意避免连续执行两次。hook_init()如果计数器计算用户在站点上连续登录的天数,则OP可能希望每天执行一次。
kiamlaluno

1
好的,我明白了你的意思,是的,上面的静态模式只是我过去用来解决在同一页面加载中被两次调用的静态模式;这不是理想的方法(理想情况是第二次找出正在调用它的是什么),但是作为快速修复,它可以解决问题。您所说的有关连续几天的声音听起来似乎是正确的,可能最好是OP hook_init进行检查以查看其是否已经在一天中运行一次,并等待运行。那么整个事情变成一个非问题呢
克莱夫

5

如果页面上发生任何AJAX,您可能会发现hook_init()被多次调用(或者您正在从私有目录中加载图像-尽管我不太确定)。有一些模块使用AJAX来帮助绕过某些元素的页面缓存-例如,最简单的检查方法是在所选的调试器(Firefox或Web检查器)中打开网络监视器,并查看是否有任何请求可能触发了引导过程。

即使是AJAX调用,您只会在下一页加载时获得dpm()。因此,假设您在5分钟后刷新了页面,那么您将在5分钟前的初始化消息中收到AJAX调用,以及新的消息。

hook_init()的替代方法是hook_boot(),它在所有缓存完成之前被调用。也没有任何模块被加载,因此,除了设置全局变量和运行一些Drupal函数之外,您实际上没有很多功能。这对于绕过常规级别的缓存很有用(但不会绕过积极的缓存)。


1

就我而言,此行为是由“管理菜单”模块(admin_menu)引起的。

hook_init不会被每个请求调用,但是在主请求之后不久,管理菜单将导致/ js / admin_menu / cache / 94614e34b017b19a78878d7b96ccab55由用户的浏览器加载,从而触发另一个drupal引导程序。

会有其他模块执行类似的操作,但是admin_menu可能是更常用的模块之一。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.