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回実行されていることがわかります。ただし、1回目はコードの古いコピーを実行し、2回目は更新されたコピーを実行します。2秒の時間差もあります。

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


ddebug_backtrace()を使用すると、関数backtraceが得られます。それが実際に複数回呼び出された場合、その関数は誰があなたに教えてくれます。
Berdir 2012

3
複数のdsm()が表示されるからといって、フックが複数回呼び出されるという意味ではありません。また、実際には複数のリクエストを実行している可能性もあります(たとえば、Drupalによって処理される404ページとなる画像がないため)
Berdir

11:22:28と11:20:34の間の差は2秒ではなく2分であることに注意してください。その場合、同じページ要求でフックが2回実行されないか、またはの値REQUEST_TIMEが同じになります。
kiamlaluno

@kiamlaluno最初の2分後の2番目の実行時に、2つのREQUEST_TIME、現在の時刻と、最初の要求の2秒後の古い時刻が表示されます。これにより、コードが2回実行されることがわかります。あなたの論理に従うことはできません。現在のリクエストで過去のREQUEST_TIMEが表示されるのはなぜですか?
マイク

答えられません。REQUEST_TIME同じページリクエストからのものである場合、その値は同じです。2秒も違いません。の値を変更するコードがないことを確認してくださいREQUEST_TIME
kiamlaluno

回答:


20

hook_init()リクエストされたページごとに1回だけ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()、それが2回実行されている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()同じページ要求中に2回続けて呼び出された場合REQUEST_TIME、同じ値が含まれ、関数はを返しFALSEます。

のコードmymodule_increase_counter()は最適化されていません。例を示すだけです。実際のモジュールでは、カウンターと他の変数が保存されているデータベーステーブルを使用します。その理由は、Drupalの$confブートストラップ(_drupal_bootstrap_variables()variable_initialize()を参照)すると、Drupal変数がすべてグローバル変数にロードされるためです。Drupal変数を使用する場合、Drupalは、情報を保存したすべてのユーザーに関する情報をメモリにロードします。リクエストされた各ページについて、グローバル変数に保存されているユーザーアカウントは1つだけです$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では、template_preprocess_page()から呼び出されるフックであるhook_footer()を使用できます。何も返しませんが、カウンターを増やして変数を変更します。

Drupal 6およびDrupal 7では、hook_exit()を使用できます。フックは、ブートストラップが完了していないときにも呼び出されることに注意してください。モジュールから定義された関数や他の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()二回呼び出されます。各ページに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_TIME2つの値の違いが2分の場合にコードが表示される場合(例のように)、フックは2回呼び出されませんが、要求されたページごとに1回呼び出されます。

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

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

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


私はあなたの提案に基づいていくつかのテストを行いました。REQUEST_TIMEには、更新された質問で確認できるのと同じ値は含まれていません。hook_init()の呼び出しを見つけようとしましたが、コアに1つしか見つかりませんでした。多分私は正しい方法を探していません。最後に、hook_exit()がうまくいくようですので、この答えを受け入れます。ただし、hook_init()が2回呼び出される理由についての答えを探しています。副次的な質問として、variable_set / getの代わりにデータベーステーブルを使用することをお勧めします。なぜこれが推奨されないのか、variable_set / getはdbテーブルを使用します。
マイク

Drupal変数はデータベーステーブルを使用しますが、Drupalブートストラップ時にすべてメモリに読み込まれます。提供される各ページについて、Drupalは常にブートストラップし、ページ要求に関連付けられているユーザーアカウントのみがあります。Drupal変数を使用する場合、ユーザーアカウントの1つだけが使用されるため、不要なユーザーアカウントに関する情報をメモリにロードします。
kiamlaluno

8

これはDrupal 6で頻繁に発生したことを覚えています(まだDrupal 7で発生するかどうかはわかりません)が、その理由はわかりません。どこかでDrupalコアがこのフックを2回呼び出さないことを覚えているようです。

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

function MYMODULE_init() {
  static $code_run = FALSE;

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

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


断固として、それはDrupalがすることではありません。
kiamlaluno

2
それはコアがすることではありませんが、それは間違いなく起こります(私はちょうど3つのレガシーDrupal 6サイトで、ほとんどが異なるcontribモジュールを実行していることを確認しました)。それは本当の頭のスクラッチャーですが、現時点ではデバッグする時間がありません。私はそれがより頻繁に使用されるcontribモジュール(多分pathautoまたはグローバルリダイレクト)の1つであると思いますが、指を指したくありません。なぜあなたの答えが反対票を投じられたのか(あるいは私にとっては)本当にわからないのですが、私には良い情報のように見えます。私は少しバランスを回復することに賛成しました:)
クライヴ

つまり、Drupalのhook_init()実装にはそのようなチェックがなく、中には2回続けて実行されることを喜んで回避するものもあります。またhook_init()、ユーザーがサイトにログインしている連続した日数をカウンターがカウントする場合、OPが1日に1回実行される可能性もあります。
kiamlaluno

1
ええと、今の意味はわかります。ええ、上記の静的パターンは、同じページの読み込みで2回呼び出される問題を回避するために過去に使用したパターンです。これは理想的ではありません(2回目に何が呼び出されるのかを見つけることが理想的です)が、簡単な修正としてうまくいくでしょう。連続した日についてあなたが言うことは正しいように聞こえます。おそらく、OP hook_initがその日にすでに1度実行されているかどうかをチェックし、実行されている場合は救済することをお勧めします。とにかく、すべてが問題にならない
クライヴ

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はおそらく最も一般的にデプロイされるものの1つです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.