Alternativen zu hook_init ()

8

Ich verwende hook_init(), um die letzte Zugriffszeit der Benutzer zu überprüfen. Wenn die letzte Zugriffszeit gestern ist, erhöhe ich einen Zähler und setze einige Variablen.

Das Problem ist, dass hook_init()manchmal mehr als einmal (ich kann dies mit verwenden dsm()) für dieselbe Seitenladung ausgeführt wird, so dass mein Code mehrmals ausgeführt wird, was zu falschen Variablen führt.

Warum wird hook_init()mehr als einmal ausgeführt?
Was wäre der beste Ansatz für mein Problem? Soll ich einen anderen Haken verwenden?

Ich habe etwas mehr darüber nachgedacht : Ich suche nach Aufrufen von hook_init () (nach Zeichenfolge gesucht module_invoke_all('init');), habe aber nur den Kernaufruf gefunden). Ich weiß nicht, ob dies anders genannt werden kann.

Das ist mein 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.')');
}

und das ist die Ausgabe:

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

Dann wurde die Nachricht dsm () in geändert dsm('2nd execution');und erneut ausgeführt. Dies ist die Ausgabe:

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

Sie können sehen, dass der Code zweimal ausgeführt wird. Beim ersten Mal wird jedoch eine alte Kopie des Codes ausgeführt, beim zweiten Mal die aktualisierte Kopie. Es gibt auch einen Zeitunterschied von 2 Sekunden.

Dies ist eine d7-Version mit PHP 5.3.10

Mike
quelle
Verwenden Sie ddebug_backtrace (), um die Funktion backtrace zu erhalten. Wenn es wirklich mehrmals aufgerufen wird, sagt Ihnen diese Funktion, von wem.
Berdir
3
Denken Sie daran, dass nur, weil Sie mehrere dsm () sehen, dies nicht bedeutet, dass der Hook mehrmals aufgerufen wird. Es ist auch möglich, dass Sie tatsächlich mehrere Anforderungen ausführen (z. B. weil ein Bild fehlt, das zu einer 404-Seite führt, die von Drupal verarbeitet wird)
Berdir
Um festzustellen, dass zwischen 11:22:28 und 11:20:34 der Unterschied zwei Minuten und nicht zwei Sekunden beträgt. In diesem Fall wird der Hook nicht zweimal in derselben Seitenanforderung ausgeführt, oder der Wert für REQUEST_TIMEwäre der gleiche.
Kiamlaluno
@kiamlaluno Bei der zweiten Ausführung, die 2 Minuten nach der ersten ist, sehe ich zwei REQUEST_TIME, die aktuelle Zeit und eine ältere Zeit, die zufällig 2 Sekunden nach der ersten Anforderung liegt. Dies sagt mir, dass der Code zweimal ausgeführt wird. Kann deiner Logik nicht folgen. Warum wird für die aktuelle Anforderung eine vergangene REQUEST_TIME angezeigt?
Mike
Das kann ich nicht beantworten. Ich kann nur sagen, dass REQUEST_TIMEder Wert der Seite gleich ist , wenn er von derselben Seitenanforderung stammt. Es gibt nicht einmal einen Unterschied von zwei Sekunden. Überprüfen Sie, ob es keinen Code gibt, der den Wert von ändert REQUEST_TIME.
Kiamlaluno

Antworten:

20

hook_init()wird von Drupal nur einmal für jede angeforderte Seite aufgerufen; Dies ist der letzte Schritt 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');
  }

Wenn hook_init()es mehr als einmal ausgeführt wird, sollten Sie herausfinden, warum dies geschieht. Soweit ich sehen kann, hook_init()überprüft keine der Implementierungen in Drupal, ob sie zweimal ausgeführt wird (siehe zum Beispiel system_init () oder update_init () ). Wenn dies normalerweise mit Drupal passieren kann, update_init()prüfen Sie zunächst, ob es bereits ausgeführt wurde.

Wenn der Zähler die Anzahl der aufeinanderfolgenden Tage ist, an denen sich ein Benutzer angemeldet hat, würde ich lieber einen hook_init()Code implementieren , der dem folgenden ähnlich ist.

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

Wenn hook_init()es während derselben Seitenanforderung zweimal hintereinander aufgerufen wird, REQUEST_TIMEdenselben Wert enthält und die Funktion zurückgegeben wird FALSE.

Der Code in mymodule_increase_counter()ist nicht optimiert. Es soll nur ein Beispiel zeigen. In einem realen Modul würde ich lieber eine Datenbanktabelle verwenden, in der der Zähler und die anderen Variablen gespeichert sind. Der Grund dafür ist, dass $confbeim Drupal-Bootstraps alle Drupal-Variablen in die globale Variable geladen werden (siehe _drupal_bootstrap_variables () und variable_initialize () ). Wenn Sie dafür Drupal-Variablen verwenden, lädt Drupal Speicherinformationen über alle Benutzer, für die Sie Informationen gespeichert haben, wenn für jede angeforderte Seite nur ein Benutzerkonto in der globalen Variablen gespeichert ist $user.

Wenn Sie die Anzahl der von den Benutzern an aufeinanderfolgenden Tagen besuchten Seiten zählen, würde ich den folgenden Code implementieren.

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

Sie werden feststellen, dass ich in meinem Code keine verwende $user->access. Der Grund ist, dass $user->accessdies während des Drupal-Bootstraps aktualisiert werden könnte, bevor hook_init()es aufgerufen wird. Der von Drupal verwendete Session Write Handler enthält den folgenden Code. (Siehe _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();
}

Für einen anderen Hook, den Sie verwenden können, können Sie mit Drupal 7 hook_page_alter () verwenden . Sie ändern einfach nicht den Inhalt von $page, sondern erhöhen Ihren Zähler und ändern Ihre Variablen.
In Drupal 6 können Sie hook_footer () verwenden , den Hook, der von template_preprocess_page () aufgerufen wird . Sie geben nichts zurück, sondern erhöhen Ihren Zähler und ändern Ihre Variablen.

Auf Drupal 6 und Drupal 7 können Sie hook_exit () verwenden . Beachten Sie, dass der Hook auch aufgerufen wird, wenn der Bootstrap nicht vollständig ist. Der Code konnte keinen Zugriff auf Funktionen haben, die von Modulen oder anderen Drupal-Funktionen definiert wurden, und Sie sollten zuerst überprüfen, ob diese Funktionen verfügbar sind. Einige Funktionen sind immer verfügbar hook_exit(), z. B. die in bootstrap.inc und cache.inc definierten . Der Unterschied besteht darin, dass er hook_exit()auch für zwischengespeicherte Seiten hook_init()aufgerufen wird , während er nicht für zwischengespeicherte Seiten aufgerufen wird.

Ein Beispiel für Code, der von einem Drupal-Modul verwendet wird, finden Sie unter statistics_exit () . Das Statistikmodul protokolliert Zugriffsstatistiken für eine Site und verwendet, wie Sie sehen hook_exit(), nicht hook_init(). Um die erforderlichen Funktionen aufrufen zu können, wird drupal_bootstrap () aufgerufen, wobei der richtige Parameter übergeben wird, wie im folgenden 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();
  }

Aktualisieren

Vielleicht gibt es einige Verwirrung darüber, wann hook_init()aufgerufen wird.

hook_init()wird für jede Seitenanforderung aufgerufen, wenn die Seite nicht zwischengespeichert ist. Es wird nicht einmal für jede Seitenanforderung aufgerufen, die vom selben Benutzer stammt. Wenn Sie besuchen zum Beispiel http://example.com/admin/appearance/update und dann http://example.com/admin/reports/status , hook_init()wird zweimal aufgerufen werden: eine für jede Seite.
"Der Hook wird zweimal aufgerufen" bedeutet, dass es ein Modul gibt, das den folgenden Code ausführt, sobald Drupal seinen Bootstrap abgeschlossen hat.

module_invoke_all('init');

Wenn dies der Fall ist, würde die folgende Implementierung von hook_init()zweimal denselben Wert anzeigen.

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

Wenn Ihr Code für REQUEST_TIMEzwei Werte angezeigt wird , für die die Differenz wie in Ihrem Fall 2 Minuten beträgt, wird der Hook nicht zweimal aufgerufen, sondern einmal für jede angeforderte Seite, wie es passieren sollte.

REQUEST_TIMEwird in bootstrap.inc mit der folgenden Zeile definiert.

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

Bis die aktuell angeforderte Seite nicht an den Browser zurückgegeben wird, REQUEST_TIMEändert sich der Wert von nicht. Wenn Sie einen anderen Wert sehen, beobachten Sie den auf einer anderen Anforderungsseite zugewiesenen Wert.

kiamlaluno
quelle
Ich habe einige Tests basierend auf Ihren Vorschlägen durchgeführt. REQUEST_TIME enthält nicht den gleichen Wert, den Sie in der aktualisierten Frage sehen können. Ich habe versucht, Aufrufe von hook_init () zu finden, aber keine außer einer im Kern gefunden. Vielleicht sehe ich nicht richtig aus. Schließlich scheint hook_exit () den Trick zu tun, also werde ich diese Antwort akzeptieren. Ich suche jedoch nach Antworten, warum hook_init () zweimal aufgerufen wird. Als Nebenfrage schlagen Sie vor, anstelle von variable_set / get eine Datenbanktabelle zu verwenden. Warum wird dies nicht empfohlen? Variable_set / get verwendet eine DB-Tabelle.
Mike
Drupal-Variablen verwenden eine Datenbanktabelle, werden jedoch beim Drupal-Bootstraps alle in den Speicher geladen. Für jede bereitgestellte Seite wird Drupal jederzeit gebootet, und einer Seitenanforderung ist nur ein Benutzerkonto zugeordnet. Wenn Sie Drupal-Variablen verwenden, laden Sie Speicherinformationen zu Benutzerkonten, die nicht erforderlich sind, da nur eines der Benutzerkonten verwendet wird.
Kiamlaluno
8

Ich erinnere mich, dass dies in Drupal 6 viel passiert ist (ich bin mir nicht sicher, ob es in Drupal 7 immer noch passiert), aber ich habe nie herausgefunden, warum. Ich erinnere mich an einen Ort, an dem Drupal Core diesen Hook nicht zweimal aufruft.

Ich fand es immer am einfachsten, eine statische Variable zu verwenden, um festzustellen, ob der Code bereits ausgeführt wurde:

function MYMODULE_init() {
  static $code_run = FALSE;

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

Dadurch wird sichergestellt, dass es beim Laden einer einzelnen Seite nur einmal ausgeführt wird.

Clive
quelle
Das ist definitiv nicht das, was Drupal tut.
Kiamlaluno
2
Es ist nicht das, was der Kern tut, aber es passiert definitiv (ich habe gerade bestätigt, dass auf drei älteren Drupal 6-Sites, auf denen meist unterschiedliche Contrib-Module ausgeführt werden). Es ist ein echter Head-Scratcher, aber ich habe im Moment keine Zeit, sie zu debuggen. Ich vermute, es ist eines der am häufigsten verwendeten Contrib-Module (vielleicht Pathauto oder Global Redirect), aber ich möchte nicht mit den Fingern zeigen. Ich bin mir nicht sicher, warum Ihre Antwort abgelehnt wurde (oder meine), aber es scheint mir eine gute Information zu sein. Ich habe mich dafür entschieden, das Gleichgewicht ein wenig wiederherzustellen :)
Clive
Ich meine, Drupal hat keine solche Überprüfung in seinen hook_init()Implementierungen, und einige von ihnen würden gerne vermeiden, zweimal hintereinander ausgeführt zu werden. Es ist auch wahrscheinlich, dass das OP hook_init()einmal pro Tag ausgeführt werden soll, wenn der Zähler die Anzahl der aufeinander folgenden Tage zählt, an denen sich die Benutzer auf der Site angemeldet haben.
Kiamlaluno
1
Ah ok, ich verstehe, was du jetzt meinst. Ja, das obige statische Muster ist nur das, das ich in der Vergangenheit verwendet habe, um das Problem zu umgehen, dass es zweimal beim Laden derselben Seite aufgerufen wird. Es ist nicht ideal (ideal wäre es herauszufinden, was es beim zweiten Mal aufruft), aber als schnelle Lösung reicht es aus. Was Sie über die aufeinander folgenden Tage sagen, klingt richtig, wahrscheinlich besser, als hook_initwenn das OP prüft, ob es bereits einmal für diesen Tag ausgeführt wird, und wenn dies der Fall ist, wird es gerettet. Dann wird das Ganze sowieso kein Thema
Clive
5

Möglicherweise wird hook_init () mehrmals aufgerufen, wenn auf der Seite AJAX auftritt (oder Sie Bilder aus einem privaten Verzeichnis laden - obwohl ich mir da nicht so sicher bin). Es gibt einige Module, die AJAX verwenden, um beispielsweise das Zwischenspeichern von Seiten für bestimmte Elemente zu umgehen. Die einfachste Möglichkeit, dies zu überprüfen, besteht darin, den Netzmonitor in einem Debugger Ihrer Wahl (Firefox oder Web Inspector) zu öffnen und nach Anforderungen zu suchen gemacht werden, die den Bootstrap-Prozess auslösen könnten.

Sie erhalten dpm () jedoch nur beim Laden der nächsten Seite, wenn es sich um einen AJAX-Aufruf handelt. Angenommen, Sie aktualisieren die Seite 5 Minuten später. Sie erhalten den AJAX-Anruf sowohl aus der Init-Nachricht vor 5 Minuten als auch aus der neuen.

Eine Alternative zu hook_init () ist hook_boot (), das aufgerufen wird, bevor überhaupt ein Caching durchgeführt wird. Es sind auch noch keine Module geladen, so dass Sie hier wirklich nicht viel Leistung haben, außer globale Variablen festzulegen und einige Drupal-Funktionen auszuführen. Es ist nützlich, um das reguläre Level-Caching zu umgehen (aber das aggressive Caching wird nicht umgangen).

Marton Bodonyi
quelle
1

In meinem Fall wurde dieses Verhalten durch das Verwaltungsmenü-Modul (admin_menu) verursacht.

hook_init wurde nicht bei jeder Anfrage aufgerufen, aber das Admin-Menü führte dazu, dass / js / admin_menu / cache / 94614e34b017b19a78878d7b96ccab55 sehr kurz nach der Hauptanforderung vom Browser des Benutzers geladen wurde und einen weiteren Drupal-Bootstrap auslöste.

Es wird andere Module geben, die ähnliche Aufgaben ausführen, aber admin_menu ist wahrscheinlich eines der am häufigsten bereitgestellten.

Rimu Atkinson
quelle