Alternative a hook_init ()


8

Uso hook_init()per controllare l'ora dell'ultimo accesso degli utenti. Se l'ultimo tempo di accesso è ieri, incremento un contatore e imposto alcune variabili.

Il problema è che a hook_init()volte viene eseguito più di una volta (posso vederlo utilizzando dsm()) per lo stesso caricamento della pagina, quindi il mio codice viene eseguito più volte con conseguente errata variabili.

Perché viene hook_init()eseguito più di una volta?
Quale sarebbe l'approccio migliore al mio problema? Dovrei usare un altro gancio?

Ho approfondito ulteriormente questo aspetto : cerco le chiamate a hook_init () (ho cercato la stringa module_invoke_all('init');) ma ho trovato solo la chiamata principale). Non so se questo può essere chiamato in modo diverso.

Questo è il mio 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.')');
}

e questo è l'output:

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

quindi, modificato il messaggio dsm () dsm('2nd execution');ed eseguito di nuovo, questo è l'output:

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

Puoi vedere che il codice viene eseguito due volte. Tuttavia, la prima volta esegue una vecchia copia del codice e la seconda volta la copia aggiornata. C'è anche una differenza di tempo di 2 secondi.

Questa è una versione d7 con php 5.3.10


Usa ddebug_backtrace (), ti darà la funzione backtrace. Se viene chiamato più volte, quella funzione ti dirà da chi.
Berdir,

3
Tieni presente che solo perché vedi più dsm (), ciò non significa che l'hook sia chiamato più volte. È anche possibile che tu stia effettivamente eseguendo più richieste (ad es. Perché manca un'immagine che risulta in una pagina 404 gestita da Drupal)
Berdir,

Da notare che tra le 11:22:28 e le 11:20:34 la differenza è di due minuti, non di due secondi. In tal caso, l'hook non viene eseguito due volte nella stessa richiesta di pagina o il valore per REQUEST_TIMEsarebbe lo stesso.
kiamlaluno

@kiamlaluno Alla seconda esecuzione che è 2 minuti dopo la prima, vedo due REQUEST_TIME, l'ora corrente e una più vecchia che risulta essere 2 secondi dopo la prima richiesta. Questo mi dice che il codice viene eseguito due volte. Non riesco a seguire la tua logica. Perché vedo REQUEST_TIME precedenti per la richiesta corrente?
Mike

Non posso rispondere. Posso solo dire che, se REQUEST_TIMEproviene dalla stessa richiesta di pagina, il suo valore è lo stesso; non c'è nemmeno una differenza di due secondi. Controlla che non ci sia codice che alteri il valore di REQUEST_TIME.
kiamlaluno

Risposte:


20

hook_init()viene invocato da Drupal solo una volta per ogni pagina richiesta; è l'ultimo passaggio fatto 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');
  }

Se hook_init()viene eseguito più di una volta, dovresti scoprire perché ciò accade. Per quanto posso vedere, nessuna delle hook_init()implementazioni in Drupal controlla che venga eseguita due volte (vedi ad esempio system_init () o update_init () ). Se questo è qualcosa che normalmente può accadere con Drupal, allora update_init()controlla prima se è già stato eseguito.

Se il contatore è il numero di giorni consecutivi in ​​cui un utente ha effettuato l'accesso, preferirei implementare hook_init()con un codice simile al seguente.

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

Se hook_init()viene richiamato due volte di fila durante la stessa richiesta di pagina, REQUEST_TIMEcontiene lo stesso valore e la funzione restituisce FALSE.

Il codice in mymodule_increase_counter()non è ottimizzato; è solo per mostrare un esempio. In un modulo reale, preferirei utilizzare una tabella di database in cui il contatore e le altre variabili vengono salvate. Il motivo è che le variabili Drupal sono tutte caricate nella variabile globale $confquando Drupal si avvia (vedi _drupal_bootstrap_variables () e variabile_initialize () ); se usi le variabili Drupal per questo, Drupal carica in memoria le informazioni su tutti gli utenti per i quali hai salvato le informazioni, quando per ogni pagina richiesta c'è un solo account utente salvato nella variabile globale $user.

Se stai contando il numero di pagine visitate dagli utenti in giorni consecutivi, implementerei il seguente codice.

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

Noterai che nel mio codice non uso $user->access. Il motivo è che $user->accesspotrebbe essere aggiornato durante l'avvio bootuprap di Drupal, prima che hook_init()venga invocato. Il gestore di scrittura della sessione utilizzato da Drupal contiene il seguente codice. (Vedi _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();
}

Come per un altro hook che puoi usare, con Drupal 7 puoi usare hook_page_alter () ; semplicemente non modifichi il contenuto di $page, ma aumenti il ​​tuo contatore e cambi le tue variabili.
Su Drupal 6, puoi usare hook_footer () , l'hook chiamato da template_preprocess_page () . Non si restituisce nulla, ma si aumenta il contatore e si modificano le variabili.

Su Drupal 6 e Drupal 7, puoi usare hook_exit () . Tenere presente che l'hook viene anche richiamato quando il bootstrap non è completo; il codice non può avere accesso alle funzioni definite dai moduli o altre funzioni Drupal e dovresti prima verificare che tali funzioni siano disponibili. Alcune funzioni sono sempre disponibili da hook_exit(), come quelle definite in bootstrap.inc e cache.inc . La differenza è che hook_exit()viene invocata anche per le pagine memorizzate nella cache, mentre hook_init()non viene invocata per le pagine memorizzate nella cache.

Infine, come esempio di codice utilizzato da un modulo Drupal, vedere statistics_exit () . Il modulo di statistiche registra statistiche di accesso per un sito, e come potete vedere, utilizza hook_exit()e non hook_init(). Per poter chiamare le funzioni necessarie, chiama drupal_bootstrap () passando il parametro corretto, come nel codice seguente.

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

Aggiornare

Forse c'è qualche confusione su quando hook_init()viene invocato.

hook_init()viene richiamato per ogni richiesta di pagina, se la pagina non viene memorizzata nella cache. Non viene invocato una volta per ogni richiesta di pagina proveniente dallo stesso utente. Se visiti, ad esempio, http://example.com/admin/appearance/update e quindi http://example.com/admin/reports/status , hook_init()verrà richiamato due volte: uno per ogni pagina.
"L'hook viene invocato due volte" significa che c'è un modulo che esegue il seguente codice, una volta che Drupal ha completato il suo bootstrap.

module_invoke_all('init');

In tal caso, la seguente implementazione hook_init()mostrerebbe lo stesso valore, due volte.

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

Se il tuo codice viene visualizzato per REQUEST_TIMEdue valori per i quali la differenza è di 2 minuti, come nel tuo caso, l'hook non viene invocato due volte, ma viene invocato una volta per ogni pagina richiesta, come dovrebbe accadere.

REQUEST_TIMEè definito in bootstrap.inc con la seguente riga.

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

Fino a quando la pagina attualmente richiesta non viene restituita al browser, il valore di REQUEST_TIMEnon cambia. Se vedi un valore diverso, stai osservando il valore assegnato in una pagina di richiesta diversa.


Ho fatto alcuni test in base ai tuoi suggerimenti. REQUEST_TIME non contiene lo stesso valore che puoi vedere nella domanda aggiornata. Ho provato a trovare invocazioni di hook_init () ma non ne ho trovata nessuna tranne una nel core. Forse non sto guardando nel modo giusto. Infine, hook_exit () sembra fare il trucco, quindi accetterò questa risposta. Cerco comunque risposte sul perché hook_init () viene chiamato due volte. Come domanda secondaria, suggerisci di usare una tabella di database invece di variabile_set / get. Perché questo non è raccomandato, variabile_set / get usa una tabella db.
Mike,

Le variabili Drupal utilizzano una tabella di database, ma vengono caricate tutte in memoria all'avvio di Drupal. Per ogni pagina pubblicata, Drupal si avvia sempre e c'è solo un account utente associato a una richiesta di pagina. Se usi le variabili Drupal, caricheresti in memoria informazioni sugli account utente che non sono necessarie, poiché viene utilizzato solo uno degli account utente.
kiamlaluno

8

Ricordo che questo accadeva molto in Drupal 6 (non sono sicuro che lo sia ancora in Drupal 7), ma non ho mai scoperto il perché. Mi sembra di ricordare di aver visto da qualche parte che il core di Drupal non chiama questo hook due volte però.

Ho sempre trovato il modo più semplice per aggirare il problema: usare una variabile statica per vedere se il codice è già stato eseguito:

function MYMODULE_init() {
  static $code_run = FALSE;

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

Ciò garantirà che venga eseguito una sola volta in un singolo caricamento di pagina.


In definitiva, non è quello che fa Drupal.
kiamlaluno

2
Non è ciò che fa il core , ma succede sicuramente (ho appena confermato che su tre siti Drupal 6 legacy, tutti con moduli contributivi per lo più diversi). È un vero grattacapo ma al momento non ho tempo per eseguirne il debug. Ho il sospetto che sia uno dei moduli contrib più utilizzati (forse pathauto o reindirizzamento globale), ma non voglio puntare il dito. Non sono sicuro del perché la tua risposta sia stata sottovalutata (o la mia per quella materia), mi sembra una buona informazione. Ho votato per ripristinare un po 'l'equilibrio :)
Clive

Voglio dire che Drupal non ha un simile controllo nelle sue hook_init()implementazioni e alcuni di loro eviterebbero volentieri di essere eseguiti due volte di fila. È anche probabile che l'OP voglia hook_init()essere eseguito una volta al giorno, se il contatore conta il numero di giorni consecutivi in ​​cui gli utenti hanno effettuato l'accesso al sito.
kiamlaluno

1
Ah ok capisco cosa intendi ora, sì, il modello statico sopra è solo quello che ho usato in passato per aggirare il problema di essere chiamato due volte nello stesso caricamento di pagina; non è l'ideale (l'ideale sarebbe scoprire cosa lo sta invocando la seconda volta) ma come soluzione rapida farà il trucco. Quello che dici dei giorni consecutivi suona bene, probabilmente meglio che il PO hook_initcontrolli per vedere se è già in esecuzione una volta per il giorno e se ne è uscito. Quindi l'intera cosa diventa comunque un problema
Clive

5

Potresti scoprire che hook_init () viene chiamato più volte se c'è qualche AJAX sulla pagina (o stai caricando immagini da una directory privata, anche se non ne sono sicuro). Esistono alcuni moduli che utilizzano AJAX per aiutare ad ignorare la memorizzazione nella cache della pagina per alcuni elementi, ad esempio - il modo più semplice per verificare è aprire il monitor di rete nel debugger di tua scelta (firefox o web inspector) e dare un'occhiata per vedere se ci sono richieste sono fatti che potrebbero innescare il processo bootstrap.

Otterrai il dpm () al caricamento della pagina successiva solo se si tratta di una chiamata AJAX. Quindi supponi di aggiornare la pagina 5 minuti dopo, riceverai la chiamata AJAX dal messaggio init di 5 minuti fa e anche quello nuovo.

Un'alternativa a hook_init () è hook_boot () che viene chiamato prima che venga eseguita qualsiasi cache. Nessun modulo è ancora caricato, quindi in realtà non hai molta potenza qui a parte l'impostazione delle variabili globali e l'esecuzione di alcune funzioni Drupal. È utile per ignorare la memorizzazione nella cache di livello normale (ma non ignorerà la memorizzazione nella cache aggressiva).


1

Nel mio caso, questo comportamento è stato causato dal modulo Menu amministrazione (admin_menu).

hook_init non veniva chiamato per ogni richiesta ma il menu di amministrazione causava il caricamento di / js / admin_menu / cache / 94614e34b017b19a78878d7b96ccab55 dal browser dell'utente poco dopo la richiesta principale, innescando un altro bootstrap drupal.

Ci saranno altri moduli che fanno cose simili ma admin_menu è probabilmente uno di quelli più comunemente distribuiti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.