Symfony 4 + Twig的临时本地化

当产品增长到需要在不同时区工作的程度时,就需要对产品进行临时本地化(证据)。我想描述一个简单的想法的变体来解决这种情况。

背景是这样的:我们开发了一个小众的CRM / ERP系统,然后被告知明天他们将以从符拉迪沃斯托克(Vladivostok)到加里宁格勒(Kaliningrad)的特许经营权使用该系统。不幸的是,最初并未考虑过这种情况,因此我们开始学习如何以最小的成本和最大的便利性来做到这一点。

总的来说,三个任务被放大了:我们如何输出数据和如何输入数据,以及在它们之间的任务是如何存储所有数据。如您所知,由于时间相对来说是字面意义上的,所以决定像以前一样在莫斯科UTC + 3中存储时间,但是要在输入和输出中进行处理(请注意,参考点是UTC + 3)。当然,我们知道在此方向和其他方向上还有其他解决方案。您可以将所有现有条目转换为UTC + -0,也可以在存储时区的DBMS中使用专用类型,也可以自己编写此自定义类型,如果数据库突然不完全支持此类功能。但是在简单性原则的指导下,我们沿着建议的道路走了,而且乍一看,他并没有输给其他人很多东西,确定所需时区的逻辑非常简单。

在莫斯科成为参考点之后,我们为每个用户以及许多相关实体(组织,城市,应用程序,交易等)添加了自定义时区参数。这样就有可能明确地确定与他或他一起工作的用户或实体在哪个时区。那里的逻辑是标准的,而且恰恰通常是项目专用的。我们将此逻辑包装在服务中,并为您提供了一个时区

$localizationService->getTimezone();

在模板中本地化日期的决定如下:初始化Twig扩展名时,时区更改为所需的时区:

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

显示任何日期时间后,我们的情况变得更加复杂,必须下标“ 01.01.2020 12:30(Moscow)”,以便例如在与时区相关的条件订单/任务/事务中添加关于时间的信息带。出于实际原因,这是必需的,以便单个呼叫中心可以在任务/应用程序/事务的框架内舒适地与不同时区一起工作。

用于确定时区优先级的所有逻辑都连接到上述getTimezone中。

此外,我们面临这样一个事实,即如果您创建了树枝过滤器或函数,则需要更改一堆模板,但是您想避免这种情况。因此,经过一番询问后,我们决定重新定义标准的树枝过滤器日期

...
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 
}

标准日期过滤器代码可以在/ 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。

All Articles