Alternativas ao hook_init ()

8

Eu uso hook_init()para verificar o último horário de acesso dos usuários. Se o último horário de acesso for ontem, incremento um contador e defino algumas variáveis.

O problema é que hook_init()algumas vezes é executado mais de uma vez (posso ver isso usando dsm()) para o mesmo carregamento de página, portanto, meu código é executado várias vezes, resultando em variáveis ​​incorretas.

Por que é hook_init()executado mais de uma vez?
Qual seria a melhor abordagem para o meu problema? Devo usar outro gancho?

Eu pesquisei um pouco mais sobre isso: procuro chamadas para o hook_init () (procurei por string module_invoke_all('init');), mas encontrei apenas a chamada principal). Não sei se isso pode ser chamado de maneira diferente.

Este é o meu 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 esta é a saída:

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

então, alterou a mensagem dsm () para dsm('2nd execution');e executou novamente, esta é a saída:

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

Você pode ver que o código é executado duas vezes. No entanto, a primeira vez que executa uma cópia antiga do código e a segunda vez a cópia atualizada. Há também uma diferença de tempo de 2 segundos.

Esta é uma versão d7 com o php 5.3.10

Mike
fonte
Use ddebug_backtrace (), ele fornecerá a função backtrace. Se realmente for chamado várias vezes, essa função informará por quem.
Berdir
3
Lembre-se de que apenas porque você vê vários dsm (), isso não significa que o gancho seja chamado várias vezes. Também é possível que você está no fato de executar várias solicitações (por exemplo, porque uma imagem está faltando, que resulta em uma página 404 que é tratado pelo Drupal)
Berdir
Observe que entre 11:22:28 e 11:20:34 a diferença é de dois minutos, não de dois segundos. Nesse caso, o gancho não é executado duas vezes na mesma solicitação de página ou o valor para REQUEST_TIMEseria o mesmo.
Kiamlaluno
@kiamlaluno Na segunda execução, que ocorre 2 minutos após a primeira, vejo duas REQUEST_TIME, a hora atual e a anterior, que ocorre 2 segundos após a 1ª solicitação. Isso me diz que o código é executado duas vezes. Não é possível seguir sua lógica. Por que vejo REQUEST_TIME do passado para a solicitação atual?
97612 Mike
Eu não posso responder isso. Só posso dizer que, se REQUEST_TIMEvier da mesma solicitação de página, seu valor é o mesmo; não há nem dois segundos de diferença. Verifique se não há código que altere o valor de REQUEST_TIME.
Kiamlaluno

Respostas:

20

hook_init()é invocado pelo Drupal apenas uma vez para cada página solicitada; é o último passo realizado em _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()estiver sendo executado mais de uma vez, você deve descobrir por que isso acontece. Até onde posso ver, nenhuma das hook_init()implementações no Drupal verifica se está sendo executada duas vezes (veja, por exemplo, system_init () ou update_init () ). Se isso é algo que normalmente pode acontecer com o Drupal, update_init()verifique primeiro se ele já foi executado.

Se o contador for o número de dias consecutivos em que um usuário efetuou login, eu preferiria implementar hook_init()com código semelhante ao seguinte.

// 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()for chamado duas vezes consecutivas durante a mesma solicitação de página, REQUEST_TIMEconterá o mesmo valor e a função retornará FALSE.

O código in mymodule_increase_counter()não é otimizado; é apenas para mostrar um exemplo. Em um módulo real, eu preferiria usar uma tabela de banco de dados em que o contador e as outras variáveis ​​sejam salvas. O motivo é que as variáveis ​​Drupal são todas carregadas na variável global $confquando o Drupal é iniciado ( consulte _drupal_bootstrap_variables () e variable_initialize () ); se você usar variáveis ​​do Drupal para isso, o Drupal carregará na memória informações sobre todos os usuários para os quais você salvou as informações, quando para cada página solicitada houver apenas uma conta de usuário salva na variável global $user.

Se você estiver contando o número de páginas visitadas dos usuários em dias consecutivos, implementaria o código a seguir.

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

Você notará que no meu código eu não uso $user->access. O motivo é que $user->accesspoderia ser atualizado durante a inicialização do Drupal, antes de hook_init()ser chamado. O manipulador de gravação de sessão usado no Drupal contém o seguinte código. (Consulte _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();
}

Quanto a outro gancho que você pode usar, com o Drupal 7 você pode usar hook_page_alter () ; você simplesmente não altera o conteúdo de $page, mas aumenta seu contador e altera suas variáveis.
No Drupal 6, você pode usar hook_footer () , o gancho chamado de template_preprocess_page () . Você não retorna nada, mas aumenta seu contador e altera suas variáveis.

No Drupal 6 e Drupal 7, você pode usar hook_exit () . Lembre-se de que o gancho também é chamado quando a inicialização não está completa; o código não pode ter acesso a funções definidas a partir de módulos ou outras funções do Drupal, e você deve primeiro verificar se essas funções estão disponíveis. Algumas funções estão sempre disponíveis hook_exit(), como as definidas em bootstrap.inc e cache.inc . A diferença é que hook_exit()é invocado também para páginas em cache, enquanto hook_init()não é invocado para páginas em cache.

Por fim, como exemplo de código usado em um módulo Drupal, consulte statistics_exit () . O módulo Estatísticas registra as estatísticas de acesso de um site e, como você vê, ele usa hook_exit(), não hook_init(). Para poder chamar as funções necessárias, ele chama drupal_bootstrap () passando o parâmetro correto, como no código a seguir.

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

Atualizar

Talvez haja alguma confusão sobre quando hook_init()é invocado.

hook_init()é chamado para cada solicitação de página, se a página não estiver em cache. Não é chamado uma vez para cada solicitação de página proveniente do mesmo usuário. Se você visitar, por exemplo, http://example.com/admin/appearance/update e http://example.com/admin/reports/status , hook_init()será chamado duas vezes: um para cada página.
"O gancho é chamado duas vezes" significa que há um módulo que executa o código a seguir, depois que o Drupal conclui sua inicialização.

module_invoke_all('init');

Se for esse o caso, a seguinte implementação de hook_init()mostraria o mesmo valor, duas vezes.

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

Se o seu código for exibido para REQUEST_TIMEdois valores para os quais a diferença é de 2 minutos, como no seu caso, o gancho não será chamado duas vezes, mas será chamado uma vez para cada página solicitada, como deve acontecer.

REQUEST_TIMEé definido em bootstrap.inc com a seguinte linha.

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

Até que a página solicitada no momento não seja retornada ao navegador, o valor de REQUEST_TIMEnão será alterado. Se você vir um valor diferente, estará observando o valor atribuído em uma página de solicitação diferente.

kiamlaluno
fonte
Fiz alguns testes com base nas suas sugestões. REQUEST_TIME não contém o mesmo valor que você pode ver na pergunta atualizada. Tentei encontrar invocações de hook_init (), mas não encontrei nenhuma, exceto uma no núcleo. Talvez eu não esteja olhando da maneira certa. Finalmente, o hook_exit () parece fazer o truque, então aceitarei esta resposta. No entanto, estou procurando respostas sobre o motivo pelo qual hook_init () é chamado duas vezes. Como questão secundária, você sugere usar uma tabela de banco de dados em vez de variable_set / get. Por que isso não é recomendado, variable_set / get usa uma tabela db.
Mike
As variáveis ​​do Drupal usam uma tabela de banco de dados, mas são carregadas todas na memória quando o Drupal é iniciado. Para cada página exibida, o Drupal é iniciado o tempo todo e há apenas uma conta de usuário associada a uma solicitação de página. Se você usar variáveis ​​Drupal, carregaria na memória informações sobre contas de usuário desnecessárias, pois apenas uma das contas de usuário é usada.
kiamlaluno
8

Lembro-me disso acontecendo muito no Drupal 6 (não tenho certeza se ainda acontece no Drupal 7), mas nunca descobri o porquê. Eu me lembro de ter visto em algum lugar que o núcleo Drupal não chama esse gancho duas vezes.

Sempre achei que a maneira mais fácil de contornar isso era usar uma variável estática para verificar se o código já foi executado:

function MYMODULE_init() {
  static $code_run = FALSE;

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

Isso garantirá que ele seja executado apenas uma vez em um único carregamento de página.

Clive
fonte
Definitivamente, não é isso que Drupal faz.
kiamlaluno
2
Não é o que o núcleo faz, mas definitivamente acontece (eu apenas confirmei que em três sites herdados do Drupal 6, todos executando principalmente módulos de contribuição diferentes). É realmente um arranhão de cabeça, mas não tenho tempo para depurá-las no momento. Eu suspeito que seja um dos módulos de contribuição mais usados ​​regularmente (talvez pathauto ou redirecionamento global), mas não quero apontar o dedo. Não tenho muita certeza do motivo pelo qual sua resposta foi negada (ou a minha, nesse caso), parece uma boa informação para mim. Voto a favor para restaurar um pouco a balança :)
Clive
Quero dizer que o Drupal não tem essa verificação em suas hook_init()implementações, e algumas delas evitariam com prazer serem executadas duas vezes seguidas. Também é provável que o OP deseje hook_init()ser executado uma vez por dia, se o contador contar o número de dias consecutivos em que os usuários efetuaram login no site.
kiamlaluno
11
Ah, ok, entendi o que você quer dizer agora, sim, o padrão estático acima é exatamente o que eu usei no passado para contornar o problema de ser chamado duas vezes no mesmo carregamento de página; não é o ideal (o ideal seria descobrir o que está chamando na segunda vez), mas como uma solução rápida, ele fará o truque. O que você diz sobre os dias consecutivos parece certo, provavelmente melhor do que o OP hook_initverifique se já correu uma vez durante o dia e falha se tiver. Então a coisa toda se torna um problema de qualquer maneira
Clive
5

Você pode achar que hook_init () é chamado várias vezes se houver algum AJAX acontecendo na página (ou você estiver carregando imagens de um diretório privado - embora eu não tenha muita certeza disso). Existem alguns módulos que usam o AJAX para ajudar a ignorar o cache da página para certos elementos, por exemplo - a maneira mais fácil de verificar é abrir o monitor de rede no seu depurador de escolha (firefox ou inspetor da web) e ver se há alguma solicitação são feitas que podem estar acionando o processo de inicialização.

Você só receberá o dpm () na página seguinte se for uma chamada AJAX. Digamos que você atualize a página 5 minutos depois, você receberá a chamada AJAX da mensagem de inicialização de 5 minutos atrás e a nova.

Uma alternativa para hook_init () é hook_boot (), que é chamado antes que qualquer cache seja feito. Ainda não há módulos carregados, então você realmente não tem muita força aqui além de definir variáveis ​​globais e executar algumas funções do Drupal. É útil para ignorar o cache de nível regular (mas não ignora o cache agressivo).

Marton Bodonyi
fonte
1

No meu caso, esse comportamento foi causado pelo módulo Administration Menu (admin_menu).

hook_init não estava sendo chamado a cada solicitação, mas o menu admin faria com que / js / admin_menu / cache / 94614e34b017b19a78878d7b96ccab55 fosse carregado pelo navegador do usuário logo após a solicitação principal, acionando outra inicialização drupal.

Existem outros módulos que fazem coisas semelhantes, mas o admin_menu é provavelmente um dos mais comuns.

Rimu Atkinson
fonte