web-dev-qa-db-ja.com

hook_init()の代替

hook_init()を使用して、ユーザーの最終アクセス時間を確認します。最終アクセス時刻が昨日である場合は、カウンターをインクリメントしていくつかの変数を設定します。

問題は、同じページの読み込みに対してhook_init()が2回以上実行されることがあり(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回実行されていることがわかります。ただし、最初はコードの古いコピーを実行し、2回目は更新されたコピーを実行します。 2秒の時間差もあります。

これはphp 5.3.10のd7バージョンです

8
Mike

hook_init()は、リクエストされたページごとにDrupalによって1回だけ呼び出されます。 _ 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()が複数回実行されている場合は、それが発生する理由を発見する必要があります。私が見る限り、Drupalのhook_init()実装は、2回実行されていることを確認しません(たとえば、 system_init() 、または-を参照)。 pdate_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()が続けて2回呼び出された場合、REQUEST_TIMEには同じ値が含まれ、関数はFALSEを返します。

mymodule_increase_counter()のコードは最適化されていません。例を示すだけです。実際のモジュールでは、カウンターと他の変数が保存されているデータベーステーブルを使用します。その理由は、Drupalブートストラップ( _ drupal_bootstrap_variables() および variable_initialize()を参照)すると、Drupal変数がすべてグローバル変数$confにロードされるためです。 );そのために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を使用していないことに気づくでしょう。その理由は、hook_init()が呼び出される前に、Drupalブートストラップ中に$user->accessが更新される可能性があるためです。 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では、 template_preprocess_page() から呼び出されるフック hook_footer() を使用できます。何も返さないが、カウンターを増やし、変数を変更します。

Drupal 6およびDrupal 7では、 hook_exit() を使用できます。このフックは、bootstrapが完了していないときにも呼び出されることに注意してください。モジュールから定義された関数、または他のDrupal関数にコードがアクセスできなかったため、最初にそれらの関数が使用可能かどうかを確認する必要があります。 bootstrap.inccache.inc で定義されている関数など、一部の関数は常にhook_exit()から利用できます。違いは、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()は、ページがキャッシュされていない場合、ページリクエストごとに呼び出されます。同じユーザーからのページ要求ごとに1回呼び出されることはありません。たとえば、 http://example.com/admin/appearance/update にアクセスし、次に http://example.com/admin/reports/status にアクセスした場合、hook_init()は2回呼び出されます:各ページに1つ。
「フックが2回呼び出される」とは、Drupalがブートストラップを完了すると、次のコードを実行するモジュールがあることを意味します。

module_invoke_all('init');

その場合、以下のhook_init()の実装は、同じ値を2回表示します。

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

あなたの場合のように、コードがREQUEST_TIMEの2つの値の差が2分の場合、フックは2回呼び出されませんが、要求されたページごとに1回呼び出されます。

REQUEST_TIMEbootstrap.inc に次の行で定義されています。

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

現在リクエストされているページがブラウザに返されない限り、REQUEST_TIMEの値は変化しません。別の値が表示される場合は、別のリクエストページで割り当てられた値を監視しています。

20
kiamlaluno

これはDrupal 6(Drupal 7)でも発生するかどうかは不明)で多く発生していることを覚えていますが、その理由はわかりません。覚えているようです。 Drupalコアがこのフックを2回呼び出すことはありません。

私は常に、最も簡単な方法は、静的変数を使用してコードが既に実行されているかどうかを確認することでした。

function MYMODULE_init() {
  static $code_run = FALSE;

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

これにより、1ページの読み込みで1回だけ実行されるようになります。

8
Clive

ページ上でAJAXが発生している場合は hook_init ()が複数回呼び出される可能性があります(またはプライベートディレクトリから画像を読み込んでいる場合- mそれだけはわからない)AJAXなど)を使用するモジュールがいくつかあります。たとえば、特定の要素のページキャッシュをバイパスするのに役立ちます。確認する最も簡単な方法は、選択したデバッガー(FirefoxまたはWebインスペクター)と、bootstrapプロセスをトリガーしている可能性のある要求が行われたかどうかを確認するための調査。

AJAX呼び出しの場合は、次のページの読み込み時に dpm ()しか取得しません。したがって、5分後にページを更新するとします。 AJAX 5分前の初期メッセージからの呼び出しと新しいメッセージを取得します。

hook_init ()の代わりに hook_boot ()を使用して、キャッシュを実行する前に呼び出します。モジュールもまだロードされていないので、グローバル変数を設定していくつかのDrupal関数を実行することを除けば、実際にはそれほど多くの能力はありません。通常のレベルのキャッシュをバイパスするのに役立ちます(しかし、 tアグレッシブキャッシングをバイパスします)。

5
Marton Bodonyi

私の場合、この動作は管理メニューモジュール(admin_menu)が原因でした。

hook_initは各リクエストを呼び出されませんでしたが、管理メニューはメインのリクエストの直後にユーザーのブラウザーによって/ js/admin_menu/cache/94614e34b017b19a78878d7b96ccab55をロードし、別のdrupalブートストラップをトリガーしました。

同様のことを行う他のモジュールもありますが、admin_menuはおそらく最も一般的にデプロイされるものの1つです。

1
Rimu Atkinson