Drupal - Alternatives to hook_init()

hook_init() is invoked by Drupal only once for each requested page; it is the last step done in _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');
  }

If hook_init() is being executed more than once, you should discover why that happens. As far as I can see, none of the hook_init() implementations in Drupal checks it is being executed twice (see for example system_init(), or update_init()). If that is something that can normally happen with Drupal, then update_init() would first check if it has been already executed.

If the counter is the number of consecutive days a user logged in, I would rather implements hook_init() with code similar to the following one.

// 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;
}

If hook_init() is invoked two times in row during the same page request, REQUEST_TIME contains the same value, and the function would return FALSE.

The code in mymodule_increase_counter() is not optimized; it is just to show an example. In a real module, I would rather use a database table where the counter, and the other variables are saved. The reason is that Drupal variables are all loaded in the global variable $conf when Drupal bootstraps (see _drupal_bootstrap_variables(), and variable_initialize()); if you use Drupal variables for that, Drupal would load in memory information about all the users for which you saved information, when for each requested page there is only one user account saved in the global variable $user.

If you are counting the number of page visited from the users in consecutive days, then I would implement the following code.

// 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;
}

You will notice that in my code I don't use $user->access. The reason is that $user->access could be updated during Drupal bootstrap, before hook_init() is invoked. The session write handler used from Drupal contains the following code. (See _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();
}

As for another hook you can use, with Drupal 7 you can use hook_page_alter(); you just don't alter the content of $page, but increase your counter, and change your variables.
On Drupal 6, you could use hook_footer(), the hook called from template_preprocess_page(). You don't return anything, but increase your counter, and change your variables.

On Drupal 6, and Drupal 7, you could use hook_exit(). Bear in mind that the hook is also invoked when the bootstrap is not complete; the code could not have access to functions defined from modules, or other Drupal functions, and you should first check those functions are available. Some functions are always available from hook_exit(), such as the ones defined in bootstrap.inc, and cache.inc. The difference is that hook_exit() is invoked also for cached pages, while hook_init() is not invoked for cached pages.

Finally, as example of code used from a Drupal module, see statistics_exit(). The Statistics module logs access statistics for a site, and as you see, it uses hook_exit(), not hook_init(). To be able to call the necessary functions, it calls drupal_bootstrap() passing the correct parameter, such as in the following code.

  // 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();
  }

Update

Maybe there is some confusion about when hook_init() is invoked.

hook_init() is invoked for each page request, if the page is not cached. It is not invoked once for each page request coming from the same user. If you visit, for example, http://example.com/admin/appearance/update, and then http://example.com/admin/reports/status, hook_init() will be invoked twice: one for each page.
"The hook is invoked twice" means there is a module that executes the following code, once Drupal has completed its bootstrap.

module_invoke_all('init');

If that is the case, then the following implementation of hook_init() would show the same value, twice.

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

If your code shown for REQUEST_TIME two values for which the difference is 2 minutes, as in your case, then the hook is not invoked twice, but it is invoked once for each requested page, as it should happen.

REQUEST_TIME is defined in bootstrap.inc with the following line.

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

Until the currently requested page is not returned to the browser, the value of REQUEST_TIME doesn't change. If you see a different value, then you are watching the value assigned in a different request page.


I remember this happening a lot in Drupal 6 (not sure if it still does in Drupal 7), but I never found out why. I seem to remember seeing somewhere that Drupal core does not call this hook twice though.

I always found the easiest way around it was use a static variable to see if the code has already been run:

function MYMODULE_init() {
  static $code_run = FALSE;

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

That'll ensure it only gets run once in a single page load.


You might find hook_init() is called multiple times if there is any AJAX happening on the page (or you're loading images from a private directory - though I'm not sure about that as much). There are a few modules that use AJAX to help bypass page caching for certain elements for example - easiest way to check is to open up the net monitor in your debugger of choice (firefox or web inspector) and having a look to see if any requests are made that could be triggering the bootstrap process.

You'll only get the dpm() on the next page load though if it is an AJAX call. So say you refresh the page 5 minutes later, you'll get the AJAX call from 5 minutes ago's init message as well as the fresh one.

An alternative to hook_init() is hook_boot() which is called before any caching is done at all. No modules are loaded in yet either, so you really don't have much power here apart from setting global variables and running a few Drupal functions. It's useful for bypassing regular level caching (but won't bypass agressive caching).

Tags:

7

Hooks