Alternatives à hook_init ()

8

J'utilise hook_init()pour vérifier le dernier temps d'accès des utilisateurs. Si le dernier temps d'accès est hier, j'incrémente un compteur et règle quelques variables.

Le problème est qu'il hook_init()est parfois exécuté plus d'une fois (je peux le voir en utilisant dsm()) pour le même chargement de page, donc mon code est exécuté plusieurs fois, ce qui entraîne des variables erronées.

Pourquoi est hook_init()exécuté plus d'une fois?
Quelle serait la meilleure approche de mon problème? Dois-je utiliser un autre crochet?

J'ai creusé un peu plus à ce sujet: je recherche des appels à hook_init () (recherche de chaîne module_invoke_all('init');) mais ne trouve que l'appel principal). Je ne sais pas si cela peut être appelé différemment.

Ceci est mon 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.')');
}

et voici la sortie:

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

puis, changé le message dsm () dsm('2nd execution');et exécuté à nouveau, voici la sortie:

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

Vous pouvez voir que le code est exécuté deux fois. Cependant, la première fois exécute une ancienne copie du code et la deuxième fois la copie mise à jour. Il y a aussi un décalage horaire de 2 secondes.

Ceci est une version d7 avec php 5.3.10

Mike
la source
Utilisez ddebug_backtrace (), cela vous donnera la fonction backtrace. Si elle est vraiment appelée plusieurs fois, cette fonction vous dira par qui.
Berdir
3
Gardez à l'esprit que ce n'est pas parce que vous voyez plusieurs dsm () que le hook est appelé plusieurs fois. Il est également possible que vous
exécutiez
A noter qu'entre 11:22:28 et 11:20:34 la différence est de deux minutes, pas de deux secondes. Dans ce cas, le hook n'est pas exécuté deux fois dans la même demande de page, ou la valeur de REQUEST_TIMEserait la même.
kiamlaluno
@kiamlaluno Lors de la deuxième exécution qui est 2min après la première, je vois deux REQUEST_TIME, l'heure actuelle et une heure plus ancienne qui se trouve être 2sec après la 1ère requête. Cela m'indique que le code est exécuté deux fois. Impossible de suivre votre logique. Pourquoi est-ce que je vois un REQUEST_TIME passé pour la demande actuelle?
Mike
Je ne peux pas répondre à cela. Je peux seulement dire que, si elle REQUEST_TIMEprovient de la même demande de page, sa valeur est la même; il n'y a même pas de différence de deux secondes. Vérifiez qu'aucun code ne modifie la valeur de REQUEST_TIME.
kiamlaluno

Réponses:

20

hook_init()n'est invoqué par Drupal qu'une seule fois pour chaque page demandée; c'est la dernière étape effectuée dans _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');
  }

Si hook_init()est exécuté plusieurs fois, vous devez découvrir pourquoi cela se produit. Pour autant que je puisse voir, aucune des hook_init()implémentations de Drupal ne vérifie qu'il est exécuté deux fois (voir par exemple system_init () ou update_init () ). Si c'est quelque chose qui peut normalement se produire avec Drupal, update_init()vérifiez d'abord s'il a déjà été exécuté.

Si le compteur est le nombre de jours consécutifs qu'un utilisateur s'est connecté, je préfère implémenter hook_init()avec un code similaire au suivant.

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

Si hook_init()est invoqué deux fois de suite lors de la même demande de page, REQUEST_TIMEcontient la même valeur et la fonction retournera FALSE.

Le code mymodule_increase_counter()n'est pas optimisé; c'est juste pour montrer un exemple. Dans un vrai module, je préfère utiliser une table de base de données où le compteur et les autres variables sont enregistrés. La raison en est que les variables Drupal sont toutes chargées dans la variable globale $conflorsque Drupal démarre ( voir _drupal_bootstrap_variables () et variable_initialize () ); si vous utilisez des variables Drupal pour cela, Drupal chargerait en mémoire des informations sur tous les utilisateurs pour lesquels vous avez enregistré des informations, alors que pour chaque page demandée, il n'y a qu'un seul compte d'utilisateur enregistré dans la variable globale $user.

Si vous comptez le nombre de pages visitées par les utilisateurs au cours des jours consécutifs, j'implémenterais le code suivant.

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

Vous remarquerez que dans mon code je n'utilise pas $user->access. La raison en est que cela $user->accesspourrait être mis à jour pendant le bootstrap Drupal, avant d' hook_init()être invoqué. Le gestionnaire d'écriture de session utilisé à partir de Drupal contient le code suivant. (Voir _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();
}

Quant à un autre hook que vous pouvez utiliser, avec Drupal 7, vous pouvez utiliser hook_page_alter () ; vous ne modifiez tout simplement pas le contenu de $page, mais augmentez votre compteur et modifiez vos variables.
Sur Drupal 6, vous pouvez utiliser hook_footer () , le hook appelé depuis template_preprocess_page () . Vous ne retournez rien, mais augmentez votre compteur et modifiez vos variables.

Sur Drupal 6 et Drupal 7, vous pouvez utiliser hook_exit () . Gardez à l'esprit que le crochet est également invoqué lorsque le bootstrap n'est pas terminé; le code ne peut pas avoir accès aux fonctions définies à partir des modules ou à d'autres fonctions Drupal, et vous devez d'abord vérifier que ces fonctions sont disponibles. Certaines fonctions sont toujours disponibles à partir de hook_exit(), comme celles définies dans bootstrap.inc et cache.inc . La différence est qu'elle hook_exit()est invoquée également pour les pages en cache, tandis qu'elle hook_init()n'est pas invoquée pour les pages en cache.

Enfin, comme exemple de code utilisé à partir d'un module Drupal, voir statistics_exit () . Le module Statistiques enregistre les statistiques d'accès pour un site, et comme vous le voyez, il utilise hook_exit(), non hook_init(). Pour pouvoir appeler les fonctions nécessaires, il appelle drupal_bootstrap () en passant le paramètre correct, comme dans le code suivant.

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

Mise à jour

Il y a peut-être une certaine confusion sur le moment où hook_init()est invoqué.

hook_init()est invoqué pour chaque demande de page, si la page n'est pas mise en cache. Il n'est pas invoqué une seule fois pour chaque demande de page provenant du même utilisateur. Si vous visitez, par exemple, http://example.com/admin/appearance/update , puis http://example.com/admin/reports/status , hook_init()sera invoqué deux fois: un pour chaque page.
«Le hook est appelé deux fois» signifie qu'il existe un module qui exécute le code suivant, une fois que Drupal a terminé son amorçage.

module_invoke_all('init');

Si tel est le cas, l'implémentation suivante de hook_init()afficherait la même valeur, deux fois.

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

Si votre code s'affiche pour REQUEST_TIMEdeux valeurs pour lesquelles la différence est de 2 minutes, comme dans votre cas, le crochet n'est pas appelé deux fois, mais il est appelé une fois pour chaque page demandée, comme cela devrait se produire.

REQUEST_TIMEest défini dans bootstrap.inc avec la ligne suivante.

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

Tant que la page actuellement demandée n'est pas renvoyée au navigateur, la valeur de REQUEST_TIMEne change pas. Si vous voyez une valeur différente, vous regardez la valeur attribuée dans une autre page de demande.

kiamlaluno
la source
J'ai fait quelques tests sur la base de vos suggestions. REQUEST_TIME ne contient pas la même valeur que celle que vous pouvez voir dans la question mise à jour. J'ai essayé de trouver des invocations de hook_init () mais n'en ai trouvé aucune sauf une dans le noyau. Peut-être que je ne regarde pas dans le bon sens. Enfin, hook_exit () semble faire l'affaire, donc j'accepterai cette réponse. Je cherche cependant des réponses pour expliquer pourquoi hook_init () est appelé deux fois. En guise de question secondaire, vous proposez d'utiliser une table de base de données au lieu de variable_set / get. Pourquoi n'est-ce pas recommandé, variable_set / get utilise une table db.
Mike
Les variables Drupal utilisent une table de base de données, mais elles sont toutes chargées en mémoire lors du démarrage de Drupal. Pour chaque page servie, Drupal démarre à tout moment et il y a juste un compte utilisateur associé à une demande de page. Si vous utilisez des variables Drupal, vous chargez en mémoire des informations sur les comptes d'utilisateurs qui ne sont pas nécessaires, car un seul des comptes d'utilisateurs est utilisé.
kiamlaluno
8

Je me souviens que cela se passait beaucoup dans Drupal 6 (je ne sais pas si c'est toujours le cas dans Drupal 7), mais je n'ai jamais su pourquoi. Je semble me souvenir d'avoir vu quelque part que le noyau Drupal n'appelle pas ce crochet deux fois.

J'ai toujours trouvé que le moyen le plus simple était d'utiliser une variable statique pour voir si le code avait déjà été exécuté:

function MYMODULE_init() {
  static $code_run = FALSE;

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

Cela garantira qu'il ne sera exécuté qu'une seule fois dans un seul chargement de page.

Clive
la source
En définitive, ce n'est pas ce que fait Drupal.
kiamlaluno
2
Ce n'est pas ce que fait le noyau , mais cela se produit définitivement (je viens de confirmer que sur trois sites Drupal 6 hérités, tous exécutant principalement des modules contrib différents). C'est un vrai casse-tête mais je n'ai pas le temps de les déboguer pour le moment. Je soupçonne que c'est l'un des modules contrib les plus régulièrement utilisés (peut-être pathauto ou redirection globale), mais je ne veux pas pointer du doigt. Je ne sais pas vraiment pourquoi votre réponse a été rejetée (ou la mienne d'ailleurs), cela ressemble à de bonnes informations pour moi. J'ai voté pour rétablir un peu l'équilibre :)
Clive
Je veux dire que Drupal n'a pas une telle vérification dans ses hook_init()implémentations, et certains d'entre eux éviteraient volontiers d'être exécutés deux fois de suite. Il est également probable que l'OP souhaite hook_init()être exécuté une fois par jour, si le compteur compte le nombre de jours consécutifs de connexion des utilisateurs sur le site.
kiamlaluno
1
Ah ok, je comprends ce que vous voulez dire maintenant, ouais le modèle statique ci-dessus est juste celui que j'ai utilisé dans le passé pour contourner le problème de son appel deux fois dans le même chargement de page; ce n'est pas l'idéal (l'idéal serait de savoir ce qui l'invoque la deuxième fois) mais comme solution rapide, il fera l'affaire. Ce que vous dites à propos des jours consécutifs semble juste, probablement mieux que le PO hook_initvérifie s'il est déjà exécuté une fois pour la journée et annule si c'est le cas. Ensuite, le tout devient un non-problème de toute façon
Clive
5

Vous pourriez trouver hook_init () est appelé plusieurs fois s'il y a un AJAX sur la page (ou si vous chargez des images à partir d'un répertoire privé - bien que je n'en sois pas sûr). Il y a quelques modules qui utilisent AJAX pour aider à contourner la mise en cache des pages pour certains éléments par exemple - le moyen le plus simple de vérifier est d'ouvrir le moniteur net dans le débogueur de votre choix (firefox ou inspecteur web) et d'avoir un regard pour voir si des demandes sont faites qui pourraient déclencher le processus d'amorçage.

Vous n'aurez que le dpm () lors du chargement de la page suivante si c'est un appel AJAX. Supposons donc que vous actualisiez la page 5 minutes plus tard, vous obtiendrez l'appel AJAX du message d'init d'il y a 5 minutes ainsi que du nouveau.

Une alternative à hook_init () est hook_boot () qui est appelée avant toute mise en cache. Aucun module n'est encore chargé, donc vous n'avez vraiment pas beaucoup de pouvoir ici à part définir des variables globales et exécuter quelques fonctions Drupal. Il est utile pour contourner la mise en cache de niveau normal (mais ne contournera pas la mise en cache agressive).

Marton Bodonyi
la source
1

Dans mon cas, ce comportement est dû au module du menu d'administration (admin_menu).

hook_init n'était pas appelé à chaque demande, mais le menu d'administration entraînerait le chargement de / js / admin_menu / cache / 94614e34b017b19a78878d7b96ccab55 par le navigateur de l'utilisateur très peu de temps après la demande principale, déclenchant un autre amorçage drupal.

Il y aura d'autres modules qui font des choses similaires, mais admin_menu est probablement l'un des plus couramment déployés.

Rimu Atkinson
la source