Timezone Middleware

I’m trying to build an application in which everyone can see each other favorite datetime but in their own timezone, so I have the following structure:

  • Table users: email, password, favorite_date (datetime), timezone (string, e.g: ‘UTC’, ‘Europe/Madrid’, ‘America/Argentina/Buenos_Aires’, etc)
  • File config/App.php: defaultTimezone UTC
  • File templates/Users/view:
<?= $this->Time->format($user->favorite_date, null, false, $this->Identity->get('timezone')); ?>
  • File templates/Users/add:
<?= $this->Form->control('favorite_date', ['type' => 'datetime', 'default' => new FrozenTime('now', $this->Identity->get('timezone'))]); ?>
  • File templates/Users/edit:
<?= $this->Form->control('favorite_date', ['type' => 'datetime', 'value' => $user->fecha_ingreso->setTimezone($this->Identity->get('timezone'))]); ?>
  • File src/Controller/UsersController/add and src/Controller/UsersController/add:

if ($this->request->is(‘post’)) {
$data = $this->request->getData();
$data[‘favorite_date’] = new FrozenTime($data[“favorite_date”], $this->Authentication->getIdentity()->get(‘timezone’));
$user = $this->Users->patchEntity($user, $data);
$this->Tropas->save($user)
}

This is working fine (showing everything in the user timezone but saving it as UTC), but now I’m trying to keep my code DRY.

I’ve been reading Date & Time - 4.x and Internationalization & Localization 4.x and then I implemented the DateTimeMiddleware from the example,

  • File src/Controllers/Middleware/DateTimeMiddleware.php:

class DateTimeMiddleware
implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$user = $request->getAttribute(‘identity’);
if ($user) {
TypeFactory::build(‘datetime’)
->useLocaleParser()
->setUserTimezone($user->timezone);
}
return $handler->handle($request);
}
}

Now, I’m not sure of which part of my previous code I should be able to remove adding this middleware since I dont fully understand how this works. Can I get some help?

When request data is being parsed/coverted in the marshalling stage (this happens when you patch or create an entity with data), the user timezone config is used for constructing the datetime object from the request data, and then it is converted to the default timezone (which should equal your app/PHP default timezone at the time of the database type object being constructed).

So say your type’s user timezone is Europe/Madrid, and your app/PHP default timezone is UTC, the flow would be:

  1. request data is marshalled
  2. date is parsed/interpreted in user timezone (Europe/Madrid)
  3. date is converted to default timezone (UTC)
  4. date is converted to database timezone if configured (\Cake\Database\Type\DateTimeType::setDatabaseTimezone())

When reading data from the database, the user timezone config is not involved, as the database type object doesn’t know what you’re going to do with the data. Instead the received value is created using the database timezone if configured, and then converted to the default timezone, so:

  1. date is read from database
  2. date is parsed/interpreted in database timezone if configured, otherwise default timezone (UTC)
  3. date is converted to default timezone (UTC)

So, what you should be able to drop from your code is passing the value and setting the timezone in your edit.php template, and the creation and injection of the datetime object in your controller’s add() and edit() methods.

The add.php template would still need it for the default value, as creating datetime objects is only aware of the default timezone. The view.php template would also still need it, as what you get from the database is not being converted into the user timezone (since you’re using the time helper, you could use the helper’s unfortunaltey undocumented outputTimezone option in case you want to apply the user timezone to all usages of the helper).

1 Like

Hey guys,

i have some questions to this old topic, because i have several problems.
My application’s default timezone is UTC, serverside and in database columns.
I added a DateTimeMiddleware like described in the documentations:

class DatetimeMiddleware implements MiddlewareInterface {
    
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
        
        $tenant = $request->getAttribute('tenant');
        if ($tenant) {
            // set timezone from tenant's config, e.g. 'Europe/Berlin'
            $timezone = $tenant->config->timezone? $tenant->config->timezone: Configure::read('App.defaultTimezone');
            
            TypeFactory::build('datetime')
                //->useLocaleParser()
                ->setUserTimezone($timezone);
        } 

        return $handler->handle($request);
    }
}

Now, when i receive some data with timezone “Europe/Berlin” from API or POST method, the timezone is translated to UTC and saved to database.
How can I output this data in the frontend in the correct tenent-based time zone?
I don’t want to output it several times with code like that:

$date->i18nFormat(\IntlDateFormatter::FULL, 'Europe/Berlin')

Is it possible to do this in a more central way?

I tried in the AppView.php, but it doesn’t work:

        $tenant = $this->getRequest()->getAttribute('tenant');
        $timezone = $tenant->config->timezone? $tenant->config->timezone: Configure::read('App.defaultTimezone');
        $this->loadHelper('Time', [ 'outputTimezone' => $timezone ]);

OK, I think I did it.

Of course i have to use the TimeHelper in view, when you set the timezone attribute.
Unfortunately, this is not possible when outputting the direct FrozenTime type.

$this->Time->i18nFormat($date, [\IntlDateFormatter::NONE, \IntlDateFormatter::SHORT])

Please, i need help againg…

Now that I’m working with UTC on the server side and passing the data to the view, I’m encountering issues again with my form controls. I can’t convert the timezone to “Europe/Berlin” or anything else. There is no option for form helper.

So here is my question:

What is best practise in CakePHP 4.X for international timezones?
I think in database you have to save the time in UTC, right?

But what about the server side, i.e. the defaultTimezone of the PHP settings?

My config/app.php configures both App.timezone.name and Datasources.default.timezone to the name of the timezone, and then config/bootstrap.php does date_default_timezone_set(Configure::read('App.timezone.name'));. I don’t think there’s anything else required. Everything is stored in the database as UTC, and is converted to the local timezone when read back out into FrozenTime objects, so there’s no need for views to do anything at all with the timezone.

I don’t know about best practices in CakePHP but I also save everything in UTC. In the form control you can set the option timezone for datetime fields:

<?= $this->Forms->control('date', ['type' => 'datetime', 'default' => 'now', 'timezone' => $this->Identity->get('timezone')]); ?>