Временная локализация на Symfony 4 + Twig

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Потребность во временной локализации продукта возникает, когда продукт вырастает до таких масштабов, при которых необходима работа в разных временных зонах (очевидность). Хочется описать вариант простой идеи решения этого кейса.

Предыстория такова: разрабатывали нишевую CRM/ERP-систему, а потом нам сказали, что буквально завтра с этой системой будут работать по франшизе от Владивостока до Калининграда. К сожалению, изначально такой сценарий продуман не был, и мы начали изучать, как сделать это сделать с минимальными затратами и максимальным удобством.

Итого, укрупненно получилось три задачи: как мы выводим данные и как вводим, а между ними задача как все это храним. Поскольку время, как известно, относительно в прямом и переносном смысле, было решено хранить время как раньше по Москве UTC+3, но обрабатывать его на входе и выходе (и везде иметь в виду, что точка отсчета — UTC+3). Конечно, мы понимали, что есть и другие решения в этом и других направлениях. Можно преобразовать все существующие записи к UTC+-0, а также использовать специализированные типы в СУБД, которые хранят временную зону, можно самим написать этот кастомный тип, если вдруг база не в полной мере поддерживает такие фичи. Но руководствуясь принципом простоты, пошли по предложенному пути, тем более, он, на первый взгляд, существенно ничем не проигрывает остальным, и логика по определению нужной временной зоны была довольно проста.

После того как точкой отсчета стала Москва, добавили настраеваемый параметр временной зоны каждому пользователю, а также — в ряд связанных сущностей (организация, город, заявки, сделки и т.д.). После чего можно было однозначно устанавливать, в каком временном поясе пользователь или сущность, с которой он работает. Логика там стандартная и точно часто специфичная для проектов. Обернули эту логику в сервис и получали временную зону, где нужно

$localizationService->getTimezone();

Решение по локализации дат в шаблонах было следующим: при инициализации Twig расширений меняли временную зону на нужную:

function __construct(Environment $twig, LocalizationService $localizationService) {
    $twig->getExtension('Twig_Extension_Core')->setTimezone($localizationService->getTimezone());
}

Наша ситуация осложнялась еще тем, что после вывода любой даты-времени необходимо делать приписку «01.01.2020 12:30 (Москва)», чтобы, например, в условной заявке/задаче/сделке, которая привязана к часовому поясу, выводилась информация о часовом поясе. Из практических соображений это нужно, чтобы единый колл-центр мог комфортно работать с разными временными зонами в рамках задачи/заявки/сделки.

Вся логика по определению приоритета временных зон была зашита в вышеупомянутый getTimezone.

Далее столкнулись с тем, что если делать свой twig-фильтр или -функцию, то необходимо будет изменять кучу шаблонов, а этого хотелось избежать. Поэтому, немного приценившись, мы решили переопределить стандартный twig-фильтр date

...
new TwigFilter('date', [$this, 'date'], ['needs_environment' => true]),
...

function date(Twig_Environment $env, $date, $format = null, $timezone = null)
{
    $appendix = '';
    if (format && strpos($format, 'H:i') !== false)
        $appendix = ' ('.DateTimeFunctions::getRussianAbbrev($this->localizationService->getTimezone()).')';   
...
    // стандартный код фильтра date записывающийся в $result
...
   return $result.$appendix;
}

Также раз мы заняли стандартный фильтр, старую версию определили заново:

...
new TwigFilter('native_date', [$this, 'nativeDate'], [ 'needs_environment' => true]),
...
public function nativeDate(Twig_Environment $env, $date, $format = null, $timezone = null)
{
    // стандартный код фильтра date 
}

Стандартный код фильтра date можно найти в /twig/twig/lib/Twig/Extension/Core::twig_date_format_filter. Хотя на самом деле в большинстве случаев сгодится простейший, не сильно отличающийся вариант:

$date->setTimeZone($timezone)
$result = $date->format($format);

Конечно, также можно сделать форк или переопределить более существенную часть Twig, но если функционал стандартного фильтра устраивает, то можно просто вынести его отдельно и ничего не потерять.

Осталось решить проблему ввода даты-времени. Один из вариантов решения:

private function getOffsetHours()
{
    if (!$this->isInit)
        $this->init();

    $local = new \DateTime('now', new \DateTimeZone($this->getTimezone()));
    $user = new \DateTime('now');

    $localOffset = $local->getOffset() / 3600;
    $globalOffset = $user->getOffset() / 3600;

    $diff = $globalOffset - $localOffset;
    return $diff;
}

public function toGlobalTime(\DateTimeInterface $dateTime): \DateTimeInterface 
{
    if (!$this->isInit)
        $this->init();

    $offsetHours = $this->getOffsetHours();

    if ($offsetHours > 0) {
        return $dateTime->modify('+ '.$offsetHours.' hours');
    } else  if($offsetHours < 0){
        return $dateTime->modify($offsetHours.' hours');
    }

    return $dateTime;
}

После чего вызывать перед сохранением даты-времени, например, в слушателях. Нам этот вариант подошел, так как в основном время и дата в проекте фиксируются по определенным событиям, а не вводятся вручную. Для другого крайнего случая, где постоянно вводится время через формы, возможно, решение будет неоптимальным.

В качестве бонуса. В проекте для вывода таблиц используется Omines datatables-bundle. Там решение оказалось еще проще. Вместо DateTimeColumn для локализации использовался:

class CustomDateTimeColumn extends DateTimeColumn
{
    
    private $localizationService;
    private $timeZone;
    
    public function __construct(LocalizationService $localizationService)
    {
        $this->localizationService = $localizationService;
        $this->timeZone = $localizationService->getTimezoneObject();
    }
    
   
    public function normalize($value)
    {
        $value->setTimeZone($this->timeZone);
        return parent::normalize($value);
    }
}

Спасибо за потраченное время. Если кто-то поможет улучшить базовые вещи решения, то буду весьма благодарен. Речь идет про базовые, так как понятно, что код вакуумный и в реальности имеет куда больший DI и всякие плюшки для внутреннего пользования в проекте.

Резюмируя. Представлена идея простого решения по быстрой временной локализации проекта. От версий не зависит или если зависит, то слабо. Это решение успешно перекочевало из Symfony 4.2 в 5.
Источник: https://habr.com/ru/post/495604/


Интересные статьи

Интересные статьи

Недавно на проекте интегрировал модуль CRM Битрикса c виртуальной АТС Ростелеком. Делал по стандартной инструкции, где пошагово показано, какие поля заполнять. Оказалось, следование ей не гаран...
У некоторых бизнес-тренеров в области е-коммерса и консультантов по увеличению интернет-продаж на многие вопросы часто можно слышать универсальную отмазку — «надо тестировать» или другую (чтобы не...
Передать нужный код для каждого браузера – непростая задача. В этой статье рассмотрим несколько вариантов, как эту задачу можно решить. Передача современного кода современным браузеро...
Тема статьи навеяна результатами наблюдений за методикой создания шаблонов различными разработчиками, чьи проекты попадали мне на поддержку. Порой разобраться в, казалось бы, такой простой сущности ка...
Один из самых острых вопросов при разработке на Битрикс - это миграции базы данных. Какие же способы облегчить эту задачу есть на данный момент?