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