Альтернативы hook_init ()

8

Я использую, hook_init()чтобы проверить время последнего доступа пользователей. Если последнее время доступа вчера, я увеличиваю счетчик и устанавливаю некоторые переменные.

Проблема в том, что hook_init()иногда выполняется более одного раза (я вижу это, используя dsm()) для одной и той же загрузки страницы, поэтому мой код выполняется несколько раз, что приводит к неправильным переменным.

Почему hook_init()выполняется более одного раза?
Каков наилучший подход к моей проблеме? Должен ли я использовать другой крючок?

Я еще немного покопался в этом: я ищу вызовы в hook_init () (искал строку 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 секунды.

Это версия d7 с php 5.3.10

Майк
источник
Используйте ddebug_backtrace (), это даст вам функцию backtrace. Если он действительно вызывается несколько раз, то эта функция сообщит вам, кем.
Бердир
3
Помните, что если вы видите несколько dsm (), это не означает, что хук вызывается несколько раз. Также возможно, что вы на самом деле выполняете несколько запросов (например, из-за отсутствия изображения, в результате которого страница 404 обрабатывается Drupal)
Бердир,
Заметьте, что между 11:22:28 и 11:20:34 разница составляет две минуты, а не две секунды. В этом случае ловушка не выполняется дважды в одном и том же запросе страницы, или значение for REQUEST_TIMEбудет одинаковым.
kiamlaluno
@kiamlaluno При втором выполнении, которое происходит через 2 минуты после первого, я вижу два REQUEST_TIME, текущее и старое время, которое происходит через 2 секунды после первого запроса. Это говорит мне, что код выполняется дважды. Не могу следовать вашей логике. Почему я вижу прошлое REQUEST_TIME для текущего запроса?
Майк
Я не могу ответить на это. Я могу только сказать, что, если REQUEST_TIMEисходить из того же запроса страницы, его значение одинаково; нет даже разницы в две секунды. Проверьте, нет ли кода, который изменяет значение REQUEST_TIME.
kiamlaluno

Ответы:

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()вызывается два раза подряд во время одного и того же запроса страницы, REQUEST_TIMEсодержит одно и то же значение, и функция вернется FALSE.

Код в mymodule_increase_counter()не оптимизирован; это просто чтобы показать пример. В реальном модуле я бы предпочел использовать таблицу базы данных, где сохраняются счетчик и другие переменные. Причина в том, что все переменные Drupal загружаются в глобальную переменную $confпри загрузке 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может быть обновлено во время начальной загрузки Drupal, прежде чем hook_init()вызываться. Обработчик записи сеанса, используемый в 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.inc и cache.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/status , hook_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_TIMEопределяется в bootstrap.inc следующей строкой.

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

Пока запрашиваемая в данный момент страница не возвращается в браузер, значение REQUEST_TIMEне изменяется. Если вы видите другое значение, то вы смотрите значение, назначенное на другой странице запроса.

киамлалуно
источник
Я сделал несколько тестов на основе ваших предложений. REQUEST_TIME не содержит того же значения, которое вы видите в обновленном вопросе. Я пытался найти вызовы hook_init (), но не нашел ничего, кроме одного в ядре. Может быть, я не смотрю в правильном направлении. Наконец, hook_exit (), кажется, делает свое дело, поэтому я приму этот ответ. Однако я ищу ответы о том, почему hook_init () вызывается дважды. В качестве дополнительного вопроса вы предлагаете использовать таблицу базы данных вместо variable_set / get. Почему это не рекомендуется, variable_set / get использует таблицу базы данных.
Майк
Переменные 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 или global redirect), но я не хочу показывать пальцем. Не совсем уверен, почему ваш ответ был опущен, хотя (или мой в этом отношении), это выглядит как хорошая информация для меня. Я проголосовал за восстановление баланса :)
Клайв
Я имею в виду, что Drupal не имеет такой проверки в своих hook_init()реализациях, и некоторые из них с радостью избегают выполнения дважды подряд. Также возможно, что OP hook_init()будет выполняться один раз в день, если счетчик подсчитывает количество последовательных дней, когда пользователи вошли на сайт.
kiamlaluno
1
Ах, хорошо, теперь я понимаю, что вы имеете в виду, да, вышеупомянутый статический шаблон - это тот, который я использовал в прошлом, чтобы обойти проблему его вызова дважды при одной и той же загрузке страницы; он не идеален (идеальным было бы выяснить, что вызывает его во второй раз), но в качестве быстрого решения это сработает. То, что вы говорите о последовательных днях, звучит правильно, вероятно, лучше, чем hook_initпроверки ОП, чтобы увидеть, запущен ли он уже один раз за день, и выручит ли он. Тогда все это становится не проблема в любом случае
Клайв
5

Вы можете обнаружить, что hook_init () вызывается несколько раз, если на странице происходит какой-либо AJAX (или вы загружаете изображения из частного каталога - хотя в этом я и не уверен). Есть несколько модулей, которые используют AJAX, чтобы помочь, например, обойти кеширование страниц для определенных элементов. Самый простой способ проверить это - открыть сетевой монитор в выбранном вами отладчике (Firefox или веб-инспектор) и проверить, есть ли какие-либо запросы. сделаны, которые могут запускать процесс начальной загрузки.

Вы получите dpm () только при загрузке следующей страницы, если это AJAX-вызов. Допустим, вы обновили страницу через 5 минут, вы получите вызов AJAX из сообщения инициализации 5 минут назад, а также свежее сообщение.

Альтернативой hook_init () является hook_boot (), который вызывается до того, как будет выполнено какое-либо кэширование. Модули еще не загружены, так что у вас действительно нет особой мощности, кроме установки глобальных переменных и запуска нескольких функций Drupal. Это полезно для обхода обычного уровня кэширования (но не будет обходить агрессивное кэширование).

Мартон Бодони
источник
1

В моем случае это было вызвано модулем Меню администрирования (admin_menu).

hook_init не вызывается при каждом запросе, но меню администратора может привести к тому, что / js / admin_menu / cache / 94614e34b017b19a78878d7b96ccab55 будет загружен браузером пользователя вскоре после основного запроса, что вызовет еще одну загрузку drupal.

Будут другие модули, которые делают подобные вещи, но admin_menu, вероятно, является одним из наиболее распространенных.

Риму Аткинсон
источник