Timezones em PHP e MySQL
O fuso horário definido será usado pelas funções de data e timestamp do PHP, e é importante porque não apenas impactará nos cálculos de hora e data, por exemplo ao calcular a diferença entre uma data e hora informada e o momento atual, mas também na forma como os timestamps serão armazenados no MySQL, MariaDB ou outros sistemas de banco de dados.
O fuso horário do PHP é definido no arquivo php.ini, por exemplo:[Date]
date.timezone = "America/Argentina/Buenos_Aires"
E também pode ser definido em nível de sistema usando:date_default_timezone_set ('America/Argentina/Buenos_Aires');
Os diferentes timezones suportados pelo PHP podem ser vistos aqui.
No MySQL, em sistemas Linux, o fuso horário padrão pode ser definido de forma global no arquivo /etc/mysql/my.cnf, indicando a diferença GMT do fuso horário a ser definido:[mysqld]
default-time-zone = "-03:00"
Também é possível definir uma variável global, por exemplo:sudo mysql -e "SET GLOBAL time_zone = '-03:00';"
Se utilizamos PDO, também podemos definir o timezone por conexão, indicando igualmente o offset GMT:$conn->exec("SET time_zone='-03:00';");
Timezones no Laravel
No Laravel, o fuso horário padrão é UTC, e pode ser alterado modificando o valor de 'timezone' no arquivo config/app.php. Esse valor é acessado com config('app.timezone');.
Se o sistema for utilizado por usuários com diferentes fusos horários (o que é o mais provável), é uma boa prática usar UTC como o fuso horário de todo o sistema, de modo que todas as datas e marcas de tempo sejam armazenadas no banco de dados como UTC, independentemente do fuso horário do usuário (que pode mudar, por exemplo, se o usuário viajar e acessar de outra localização), e depois converter os timestamps para o fuso horário do usuário para que ele os visualize de acordo com o fuso horário configurado, mas sempre armazenando os timestamps de forma uniforme como UTC.
Embora existam diferentes abordagens para lidar com o gerenciamento de fusos horários no Laravel, este artigo tentará cobrir diferentes opções globais para que um usuário possa definir seu fuso horário e visualizar as datas e horas de acordo com suas preferências.
Adicionar a coluna timezone na tabela de usuários
Como cada usuário poderá definir seu fuso horário, precisamos criar uma nova coluna na tabela de usuários para armazenar essa informação.
php artisan make:migration add_timezone_field_to_users_table --table=users
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddTimezoneFieldToUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('timezone', 40)->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('timezone');
});
}
}Nesta abordagem, definimos a coluna como nullable, já que não queremos armazenar informações desnecessárias se o usuário selecionar UTC ou o fuso horário padrão do sistema, e armazenaremos apenas os fusos horários diferentes deste, caso o usuário o defina.
Adicionando um Accessor e um Mutator de timezone no Model de Usuários
Depois de adicionar timezone a $fillable[] no model User, vamos definir um Accessor e um Mutator para transformar esse novo atributo no model.
Como o campo timezone pode ser nulo (por exemplo, se for igual ao fuso horário do sistema, ou se adicionarmos essa funcionalidade a um sistema preexistente em que os usuários não têm um fuso horário associado), vamos criar um Accessor para que, caso o fuso horário do usuário esteja vazio, recebamos o fuso horário padrão do sistema ao acessar esse atributo, evitando possíveis erros.
No model User definiremos:
public function getTimeZoneAttribute ($value): string
{
return $value == config('app.timezone') || empty($value) ? config('app.timezone') : $value;
}Como não queremos armazenar no banco de dados fusos horários iguais ao definido por padrão no sistema, também adicionaremos um Mutator; assim, se o fuso horário selecionado pelo usuário for igual a este, ele não será armazenado no banco de dados, e depois, a partir do Accessor definido anteriormente, o fuso horário padrão da aplicação será retornado corretamente ao acessá-lo.
public function setTimeZoneAttribute($value) {
$this->attributes['timezone'] = $value == config('app.timezone') || is_null($value) ? null : $value;
}Helpers de Timezone
Para reutilizar o código relacionado aos fusos horários, criaremos alguns Helpers.
Há muitas formas de usar Helpers no Laravel; uma possibilidade é criar, por exemplo, um arquivo em app/Helpers/Helpers.php com a estrutura:
<?php
namespace App\Helpers;
use Illuminate\Support\Facades\App;
class Helpers {
//...
}Para incluir essa classe e seus métodos no bootstrap da aplicação, adicionaremos no arquivo composer.json, dentro de autoload e files, a referência a essa nova classe, por exemplo:
"files": [ "app/Helpers/Helpers.php" ]
Também adicionaremos no arquivo config/app.php, dentro de $aliases[], a referência a essa classe:
'aliases' => [ //.. 'Helpers' => App\Helpers\Helpers::class, ],
Em Helpers.php adicionaremos dois novos Helpers relacionados a timezone:
static public function getTimeZoneList()
{
return \Cache::rememberForever('timezones_list_collection', function () {
$timestamp = time();
foreach (timezone_identifiers_list(\DateTimeZone::ALL) as $key => $value) {
date_default_timezone_set($value);
$timezone[$value] = $value . ' (UTC ' . date('P', $timestamp) . ')';
}
return collect($timezone)->sortKeys();
});
}Esse novo método criará uma collection ordenada com os fusos horários disponíveis (utilizados como chave) e o fuso horário junto com o offset GMT, como mostra a imagem a seguir.
Como essa informação não mudará, armazenaremos essa collection em cache permanentemente.

Também podemos substituir esse método por uma versão simplificada e usar a função nativa do PHP timezone_identifiers_list(), que retornará um array com todos os fusos horários suportados.
Também adicionaremos o método getUserTimeZone(), que tentará obter o fuso horário definido pelo usuário, se existir, e caso contrário retornará o fuso horário padrão da aplicação.
Usaremos aqui o Helper do Laravel optional(), para evitar erros caso o usuário não tenha feito login.
static public function getUserTimeZone() {
return optional(auth()->user())->timezone ?? config('app.timezone');
}Seleção do fuso horário do usuário
Usando o Helper criado anteriormente, adicionaremos o formulário em que o usuário pode definir seu fuso horário. Por exemplo:
<div class="col-md-4">
<label class="required" for="timezone">{{ __('TimeZone') }}</label>
<select class="form-control" name="timezone" id="timezone">
@foreach(Helpers::getTimeZoneList() as $timezone => $timezone_gmt_diff)
<option value="{{ $timezone }}" {{ ( $timezone === old('timezone', $user->timezone)) ? 'selected' : '' }}>
{{ $timezone_gmt_diff }}
</option>
@endforeach
</select>
</div>O Laravel tem uma regra de validação nativa para timezone, portanto poderemos validar facilmente esse campo no controller ou no arquivo de form request com:
timezone' => ['required', 'timezone']
Obtendo o fuso horário do usuário
Se quisermos pré-selecionar o fuso horário do usuário no formulário, ou adicioná-lo como um campo oculto no processo de registro, podemos obter o fuso horário atual do cliente com a biblioteca javascript moment.
Devemos carregar as bibliotecas moment e moment-timezone, e usar o método moment.tz.guess(), por exemplo:
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.33/moment-timezone-with-data.min.js"></script>
<script>
$(document).ready(function() {
console.log(moment.tz.guess());
});
</script>Outra forma de acessar essa informação, sem o uso de bibliotecas externas, mas que pode não ser compatível com navegadores antigos, é:
Intl.DateTimeFormat().resolvedOptions().timeZone;
Utilizando o fuso horário do usuário em Controllers e Blade
Existem diferentes opções para transformar datas e marcas de tempo utilizando o fuso horário do usuário, em controllers e arquivos blade.
Se a seção sempre exigir que o usuário esteja logado, podemos usar, por exemplo:{{now(\Auth::user()->timezone)}}
Ou o Helper criado anteriormente, para contemplar também os casos em que se acessa sem login, retornando então o fuso horário padrão do sistema:{{ now(Helpers::getUserTimeZone()) }}
Para fazer o parse de um timestamp usando o fuso horário do usuário, simplesmente usaremos:Carbon::parse($model->timestamp_field)->setTimezone(Helpers::getUserTimeZone());
Salvar timestamps no banco de dados em UTC a partir do fuso horário do usuário
Será comum que o usuário adicione elementos com timestamp, selecionando a data e a hora a partir de uma biblioteca javascript ou outras, e usando seu próprio fuso horário, portanto precisamos transformar essa data para UTC para armazená-la de maneira uniforme em nosso banco de dados.
Suponhamos que o usuário possa adicionar elementos e definir a data de criação, e que tenha um fuso horário diferente do sistema; uma forma de transformá-la antes de salvar é:
$request->merge([
'created_at' => Carbon::parse($request->input('created_at'), Helpers::getUserTimeZone())
->setTimeZone(config('app.timezone'))
->format('Y-m-d H:i:s'),
]);Dessa forma indicamos ao método parse() do Carbon que o fuso horário do timestamp informado é de um tipo, e que o transformaremos para UTC, ou para o fuso horário padrão da nossa aplicação.
É importante que essa transformação seja realizada após a validação do formulário, pois caso contrário, se a validação falhar, ao chamar old() na view, o usuário verá a data em UTC e não em seu fuso horário.
Se o usuário puder editar posteriormente esse elemento, também vamos querer que ele seja exibido em seu fuso horário, portanto o transformaremos antes de enviá-lo para a view:
$model->created_at = Carbon::parse($model->created_at)->setTimezone(Helpers::getUserTimeZone());
Accessors de timezone do usuário em Models
Se quisermos que certos atributos sejam retornados por padrão com o fuso horário do usuário (ou o padrão do sistema se não existir, não estiver definido ou o usuário não estiver logado), podemos definir accessors no model:
public function getCreatedAtAttribute($value): Carbon
{
return Carbon::parse($value)->timezone(Helpers::getUserTimeZone());
}
public function getUpdatedAtAttribute($value): Carbon
{
return Carbon::parse($value)->timezone(Helpers::getUserTimeZone());
}Componente Blade para datas com o fuso horário do usuário
Para simplificar a transformação de timestamps com o fuso horário do usuário no Blade, podemos criar um Componente Blade.
php artisan make:component DateTimeZone --inline
Usamos a flag --inline para evitar criar um arquivo de view, já que, pela simplicidade deste componente, isso não é necessário, e a data transformada será retornada diretamente do método render() da nova classe criada em app/View/Components/DateTimeZone.php.
O código deste arquivo será:
<?php
namespace App\View\Components;
use App\Helpers\Helpers;
use Carbon\Carbon;
use Illuminate\View\Component;
class DateTimeZone extends Component
{
public Carbon $date;
public mixed $format;
/**
* Create a new component instance.
*
* @return void
*/
public function __construct(Carbon $date, $format = null)
{
$this->date = $date->setTimezone(Helpers::getUserTimeZone());
$this->format = $format;
}
protected function format()
{
return $this->format ?? 'Y-m-d H:i:s';
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return $this->date->format($this->format());
}
}
$format é definido como mixed, já que pode ser do tipo string ao definir um formato, ou null se esse parâmetro opcional não estiver presente.
Novamente utilizaremos o helper getUserTimeZone(), para obter o fuso horário definido pelo usuário, ou o padrão do sistema.
No Blade, podemos utilizar esse componente passando o timestamp a ser transformado e, opcionalmente, indicando um formato de saída, que por padrão, se esse parâmetro não estiver presente, será Y-m-d H:i:s.
<x-date-time-zone :date="$model->created_at" /> <x-date-time-zone :date="$model->created_at" format="d-m-Y H:i:s"/> <x-date-time-zone :date="$model->created_at" format="d/m/Y<\b\r>H:i"/>
Outras abordagens não recomendadas
Em algumas abordagens para trabalhar com múltiplos fusos horários no Laravel que vi online, cria-se um middleware em que o fuso horário padrão do sistema é definido com base no fuso horário do usuário com date_default_timezone_set().
Isso não é recomendado, já que dessa forma o fuso horário de todo o sistema é modificado para cada usuário, e seriam salvos no banco de dados timestamps não uniformes, com diferentes fusos horários.
Com a abordagem e os métodos deste artigo, podemos trabalhar com múltiplos fusos horários, definidos pelo usuário, mas armazenados uniformemente em UTC ou em outro fuso horário definido por padrão na aplicação, permitindo que a mesma informação seja visualizada de maneira diferente, mas sem alterar sua integridade.