Working with DateTime Instances

There are many nitty gritty details when working with PHPs DateTime instances. You have to know their inner workings pretty well not to make mistakes with date handling. This cookbook entry holds several interesting pieces of information on how to work with PHP DateTime instances in ORM.

PHP の DateTime インスタンスを操作する際には、多くの重要な詳細があります。日付の処理を間違えないように、内部構造をよく知っておく必要があります。このクックブック エントリには、ORM で PHP の DateTime インスタンスを操作する方法に関する興味深い情報がいくつか含まれています。

DateTime changes are detected by Reference

When calling EntityManager#flush() Doctrine computes the changesets of all the currently managed entities and saves the differences to the database. In case of object properties (@Column(type=”datetime”) or @Column(type=”object”)) these comparisons are always made BY REFERENCE. That means the following change will NOT be saved into the database:

EntityManager#flush() を呼び出すと、Doctrine は現在管理されているすべてのエンティティの変更セットを計算し、差分をデータベースに保存します。オブジェクト プロパティ (@Column(type="datetime") または @Column(type="object")) の場合、これらの比較は常に参照によって行われます。つまり、次の変更はデータベースに保存されません。

<?php

use DateTime;

#[Entity]
class Article
{
    #[Column(type='datetime')]
    private DateTime $updated;

    public function setUpdated(): void
    {
        // will NOT be saved in the database
        $this->updated->modify("now");
    }
}

The way to go would be:

行く方法は次のとおりです。

<?php
use DateTime;

class Article
{
    public function setUpdated(): void
    {
        // WILL be saved in the database
        $this->updated = new DateTime("now");
    }
}

Default Timezone Gotcha

By default Doctrine assumes that you are working with a default timezone. Each DateTime instance that is created by Doctrine will be assigned the timezone that is currently the default, either through the date.timezone ini setting or by calling date_default_timezone_set().

デフォルトでは、Doctrine はデフォルトのタイムゾーンで作業していると想定します。 Doctrine によって作成された各 DateTime インスタンスには、date.timezone ini 設定または date_default_timezone_set() を呼び出すことによって、現在デフォルトであるタイムゾーンが割り当てられます。

This is very important to handle correctly if your application runs on different servers or is moved from one to another server (with different timezone settings). You have to make sure that the timezone is the correct one on all this systems.

アプリケーションが異なるサーバーで実行されている場合、またはあるサーバーから別のサーバーに移動されている場合 (異なるタイムゾーン設定で)、これを正しく処理することが非常に重要です。このすべてのシステムでタイムゾーンが正しいことを確認する必要があります。

Handling different Timezones with the DateTime Type

If you first come across the requirement to save different timezones you may be still optimistic about how to manage this mess, however let me crush your expectations fast. There is not a single database out there (supported by Doctrine ORM) that supports timezones correctly. Correctly here means that you can cover all the use-cases that can come up with timezones. If you don’t believe me you should read up on Storing DateTime in Databases.

異なるタイムゾーンを保存するという要件に最初に遭遇した場合、この混乱を管理する方法についてまだ楽観的かもしれませんが、私はあなたの期待をすぐに打ち砕かせてください.タイムゾーンを正しくサポートする (Doctrine ORM でサポートされている) 単一のデータベースは存在しません。ここで正しく言うと、タイムゾーンを考え出すことができるすべてのユースケースをカバーできるということです。信じられない場合は、DateTime をデータベースに保存するを読んでください。

The problem is simple. Not a single database vendor saves the timezone, only the differences to UTC. However with frequent daylight saving and political timezone changes you can have a UTC offset that moves in different offset directions depending on the real location.

問題は簡単です。単一のデータベース ベンダーがタイムゾーンを保存するわけではなく、UTC との違いだけを保存します。ただし、頻繁な夏時間と政治的なタイムゾーンの変更により、実際の場所に応じて異なるオフセット方向に移動する UTC オフセットを持つことができます。

The solution for this dilemma is simple. Don’t use timezones with DateTime and Doctrine ORM. However there is a workaround that even allows correct date-time handling with timezones:

このジレンマの解決策は簡単です。 DateTime と Doctrine ORM でタイムゾーンを使用しないでください。ただし、タイムゾーンを使用した正しい日時処理を可能にする回避策もあります。

  1. Always convert any DateTime instance to UTC.

    DateTime インスタンスは常に UTC に変換します。

  2. Only set Timezones for displaying purposes

    表示目的でタイムゾーンのみを設定する

  3. Save the Timezone in the Entity for persistence.

    永続化のためにエンティティにタイムゾーンを保存します。

Say we have an application for an international postal company and employees insert events regarding postal-package around the world, in their current timezones. To determine the exact time an event occurred means to save both the UTC time at the time of the booking and the timezone the event happened in.

国際郵便会社の申請書があり、従業員が現在のタイムゾーンで世界中の郵便小包に関するイベントを挿入するとします。イベントが発生した正確な時刻を特定するということは、予約時の UTC 時間とイベントが発生したタイムゾーンの両方を保存することを意味します。

<?php

namespace DoctrineExtensions\DBAL\Types;

use DateTimeZone;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\DateTimeType;

class UTCDateTimeType extends DateTimeType
{
    private static DateTimeZone $utc;

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value instanceof \DateTime) {
            $value->setTimezone(self::getUtc());
        }

        return parent::convertToDatabaseValue($value, $platform);
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if (null === $value || $value instanceof \DateTime) {
            return $value;
        }

        $converted = \DateTime::createFromFormat(
            $platform->getDateTimeFormatString(),
            $value,
            self::getUtc()
        );

        if (! $converted) {
            throw ConversionException::conversionFailedFormat(
                $value,
                $this->getName(),
                $platform->getDateTimeFormatString()
            );
        }

        return $converted;
    }

    private static function getUtc(): DateTimeZone
    {
        return self::$utc ??= new DateTimeZone('UTC');
    }
}

This database type makes sure that every DateTime instance is always saved in UTC, relative to the current timezone that the passed DateTime instance has.

このデータベース タイプは、渡された DateTime インスタンスの現在のタイムゾーンを基準にして、すべての DateTime インスタンスが常に UTC で保存されるようにします。

To actually use this new type instead of the default datetime type, you need to run following code before bootstrapping the ORM:

デフォルトの日時型の代わりにこの新しい型を実際に使用するには、ORM をブートストラップする前に次のコードを実行する必要があります。

<?php

use Doctrine\DBAL\Types\Type;
use DoctrineExtensions\DBAL\Types\UTCDateTimeType;

Type::overrideType('datetime', UTCDateTimeType::class);
Type::overrideType('datetimetz', UTCDateTimeType::class);

To be able to transform these values back into their real timezone you have to save the timezone in a separate field of the entity requiring timezoned datetimes:

これらの値を実際のタイムゾーンに変換できるようにするには、タイムゾーンを必要とするエンティティの別のフィールドにタイムゾーンを保存する必要があります。

<?php
namespace Shipping;

/**
 * @Entity
 */
class Event
{
    /** @Column(type="datetime") */
    private $created;

    /** @Column(type="string") */
    private $timezone;

    /**
     * @var bool
     */
    private $localized = false;

    public function __construct(\DateTime $createDate)
    {
        $this->localized = true;
        $this->created = $createDate;
        $this->timezone = $createDate->getTimeZone()->getName();
    }

    public function getCreated()
    {
        if (!$this->localized) {
            $this->created->setTimeZone(new \DateTimeZone($this->timezone));
        }
        return $this->created;
    }
}

This snippet makes use of the previously discussed “changeset by reference only” property of objects. That means a new DateTime will only be used during updating if the reference changes between retrieval and flush operation. This means we can easily go and modify the instance by setting the previous local timezone.

このスニペットは、前述のオブジェクトの「参照のみによる変更セット」プロパティを利用しています。つまり、取得操作とフラッシュ操作の間で参照が変更された場合、新しい DateTime は更新中にのみ使用されます。これは、以前のローカル タイムゾーンを設定することで、インスタンスに簡単にアクセスして変更できることを意味します。

Table Of Contents

Previous topic

Validation of Entities

エンティティの検証

Next topic

Mysql Enums

Mysql 列挙型

This Page

Fork me on GitHub