Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

PHP, MySQL e fusos horários


Esta resposta foi atualizada para acomodar a recompensa. A resposta original e não editada está abaixo da linha.

Quase todos os pontos de pergunta adicionados pelo proprietário da recompensa são em relação a como os datetimes do MySQL e do PHP devem interagir, no contexto de fusos horários.

O MySQL ainda tem patético suporte a fuso horário , o que significa que a inteligência deve ser do lado do PHP.
  • Defina seu fuso horário de conexão do MySQL para UTC conforme documentado no link acima. Isso fará com que todos os datetimes manipulados pelo MySQL, incluindo NOW() , para ser tratado com sensatez.
  • Sempre use DATETIME , nunca use TIMESTAMP a menos que você exija expressamente o comportamento especial em um TIMESTAMP . Isso é menos doloroso do que costumava ser.
    • Está ok para armazenar o tempo de época do Unix como um inteiro se você tiver para, como para fins de legado. A época é UTC.
    • O formato de data e hora preferido do MySQL é criado usando a string de formato de data PHP Y-m-d H:i:s
  • Converter todos PHP datetimes para UTC ao armazená-los no MySQL, o que é uma coisa trivial, conforme descrito abaixo
  • Datetimes retornados do MySQL podem ser entregues com segurança ao construtor PHP DateTime. Certifique-se de passar em um fuso horário UTC também!
  • Converter o PHP DateTime para o fuso horário local do usuário no echo , não antes. Felizmente, a comparação e a matemática de DateTime com outros DateTimes levarão em consideração o fuso horário em que cada um está.
  • Você ainda está de acordo com os caprichos do banco de dados DST fornecido com o PHP. Mantenha seus patches do PHP e do SO atualizados! Mantenha o MySQL no estado feliz do UTC para remover um possível aborrecimento do horário de verão.

Isso aborda a maioria dos pontos.

A última coisa é um doozy:
  • O que alguém deve fazer se tiver inserido dados anteriormente (por exemplo, usando NOW() ) sem se preocupar com o fuso horário para garantir que tudo permaneça consistente?

Este é um verdadeiro aborrecimento. Uma das outras respostas apontou o CONVERT_TZ , embora eu pessoalmente tivesse feito isso pulando entre os fusos horários nativos do servidor e UTC durante seleções e atualizações, porque sou hardcore assim.

o aplicativo também deve ser capaz de definir/escolher o horário de verão de acordo com cada usuário.

Você não precisa e não deve fazer isso na era moderna.

As versões modernas do PHP têm o DateTimeZone classe, que inclui a capacidade de listar fusos horários nomeados . Os fusos horários nomeados permitem que o usuário selecione sua localização real e tenha o sistema automaticamente determinar suas regras de horário de verão com base nesse local.

Você pode combinar DateTimeZone com DateTime para algumas funcionalidades simples, mas poderosas. Você pode simplesmente armazenar e usar todos de seus carimbos de data/hora em UTC por padrão , e convertê-los para o fuso horário do usuário em exibição.
// UTC default
    date_default_timezone_set('UTC');
// Note the lack of time zone specified with this timestamp.
    $nowish = new DateTime('2011-04-23 21:44:00');
    echo $nowish->format('Y-m-d H:i:s'); // 2011-04-23 21:44:00
// Let's pretend we're on the US west coast.  
// This will be PDT right now, UTC-7
    $la = new DateTimeZone('America/Los_Angeles');
// Update the DateTime's timezone...
    $nowish->setTimeZone($la);
// and show the result
    echo $nowish->format('Y-m-d H:i:s'); // 2011-04-23 14:44:00

Ao usar esta técnica, o sistema irá automaticamente selecione as configurações de horário de verão corretas para o usuário, sem perguntar ao usuário se ele está ou não no horário de verão.

Você pode usar um método semelhante para renderizar o menu de seleção. Você pode reatribuir continuamente o fuso horário para o único objeto DateTime. Por exemplo, este código listará as zonas e seus horários atuais, neste momento:
$dt = new DateTime('now', new DateTimeZone('UTC')); 
foreach(DateTimeZone::listIdentifiers() as $tz) {
    $dt->setTimeZone(new DateTimeZone($tz));
    echo $tz, ': ', $dt->format('Y-m-d H:i:s'), "\n";
}

Você pode simplificar bastante o processo de seleção usando alguma mágica do lado do cliente. Javascript tem um irregular, mas funcional Classe de data, com um método padrão para obter o deslocamento UTC em minutos . Você pode usar isso para ajudar a diminuir a lista de fusos horários prováveis, sob a suposição cega de que o relógio do usuário está certo.

Vamos comparar este método para fazer você mesmo. Você precisaria realmente realizar cálculos de data todas as vezes você manipula uma data e hora, além de empurrar uma escolha para o usuário com a qual ele realmente não se importará. Isso não é apenas sub-ótimo, é insano de bat-guano. Forçar os usuários a dizer quando querem suporte de horário de verão é causar problemas e confusão.

Além disso, se você quiser usar a estrutura moderna do PHP DateTime e DateTimeZone para isso, precisará use obsoleto Etc/GMT... seqüências de fuso horário em vez de fusos horários nomeados. Esses nomes de zona podem ser removidos de futuras versões do PHP, então não seria prudente fazer isso. Digo tudo isso por experiência.

tl;dr :Use o conjunto de ferramentas moderno, poupe-se dos horrores da matemática de datas. Apresente ao usuário uma lista de nomeados fusos horários. Armazene suas datas em UTC , que não será afetado pelo horário de verão de forma alguma. Converter datas para o fuso horário nomeado selecionado do usuário na exibição , não mais cedo.

Conforme solicitado, aqui está um loop sobre os fusos horários disponíveis exibindo seu deslocamento GMT em minutos. Selecionei minutos aqui para demonstrar um fato lamentável:nem todos os deslocamentos são em horas inteiras! Alguns realmente mudam meia hora à frente durante o horário de verão, em vez de uma hora inteira. O deslocamento resultante em minutos deve corresponde ao Date.getTimezoneOffset do Javascript .
$utc = new DateTimeZone('UTC');
$dt = new DateTime('now', $utc); 
foreach(DateTimeZone::listIdentifiers() as $tz) {
    $local = new DateTimeZone($tz);
    $dt->setTimeZone($local);
    $offset = $local->getOffset($dt); // Yeah, really.
    echo $tz, ': ', 
         $dt->format('Y-m-d H:i:s'),
         ', offset = ',
         ($offset / 60),
         " minutes\n";
}