From 2df12b5509ef770cd0c4e8157d7f0b0d1ccc044f Mon Sep 17 00:00:00 2001 From: tomwalder Date: Wed, 7 Jul 2021 11:52:16 +0100 Subject: [PATCH] Timezone support updates and additional tests --- README.md | 28 +++++ src/GDS/Gateway/RESTv1.php | 2 +- src/GDS/Mapper.php | 8 +- src/GDS/Mapper/GRPCv1.php | 10 +- src/GDS/Mapper/RESTv1.php | 41 +++++-- tests/RESTv1MapperTest.php | 143 +++++++++++++++++++++++-- tests/TimeZoneTest.php | 214 +++++++++++++++++++++++++++++++++++++ 7 files changed, 418 insertions(+), 28 deletions(-) create mode 100644 tests/TimeZoneTest.php diff --git a/README.md b/README.md index 6bff201..65608e7 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ If you need to continue running applications on that infrastructure, stick to ve * [Getting Started](#getting-started) including installation with Composer and setup for GDS Emulator * [Defining Your Model](#defining-your-model) * [Creating Records](#creating-records) +* [Timezones and DateTime](#updated-timezone-support) * [Geopoint Support](#geopoint) * [Queries, GQL & The Default Query](#queries-gql--the-default-query) * [Multi-tenant Applications & Data Namespaces](#multi-tenant-applications--data-namespaces) @@ -143,6 +144,33 @@ Code: https://github.com/tomwalder/php-gds-demo * Remove PHP 5 support * Remove App Engine first-generation runtime support (inc direct Protocol Buffer API) +### Updated Timezone Support ### + +In 5.1, timezone support has been improved for `DateTime` objects going in & out of Datastore. + +#### How the data is stored +Datstore keeps the data recorded as UTC. When you browse data in the Google Cloud Console, they represent it in your locale. + +#### Data coming out through PHP-GDS as Entities +You can now expect any `DateTime` object coming out of Datastore from PHP-GDS to have your current PHP default timezone applied. Example follows: + +```php +date_default_timezone_set('America/New_York'); + +$obj_store = new GDS\Store('Book'); +$obj_book = $obj_store->fetchOne(); +echo $obj_book->published->format('c'); // 2004-02-12T15:19:21-05:00 +echo $obj_book->published->getTimezone()->getName(); // America/New_York +``` + +#### Data going in - multi format support +If you pass in a `DateTime` object (or anything matching `DateTimeInterface`), we will respect the timezone set on it. + +Any other string-based value passed in for a `datetime` field will be converted to a `DateTimeImmutable` object before being converted to UTC, using the standard PHP methods: +https://www.php.net/manual/en/datetime.construct.php + +This means that unless using a timestamp value (e.g. `@946684800`), or a value with a timezone already stated (e.g. `2010-01-28T15:00:00+02:00`), we will assume the value is in your current timezone context. + ## Changes in Version 4 ## * More consistent use of `DateTime` objects - now all result sets will use them instead of `Y-m-d H:i:s` strings diff --git a/src/GDS/Gateway/RESTv1.php b/src/GDS/Gateway/RESTv1.php index cc3521b..4582a38 100644 --- a/src/GDS/Gateway/RESTv1.php +++ b/src/GDS/Gateway/RESTv1.php @@ -432,7 +432,7 @@ protected function configureObjectValueParamForQuery($obj_val, $mix_value) /** @var Entity $mix_value */ $obj_val->keyValue = $this->applyPartition((object)['path' => $this->createMapper()->buildKeyPath($mix_value)]); } elseif ($mix_value instanceof \DateTimeInterface) { - $obj_val->timestampValue = $mix_value->format(\GDS\Mapper\RESTv1::DATETIME_FORMAT); + $obj_val->timestampValue = $mix_value->format(\GDS\Mapper\RESTv1::DATETIME_FORMAT_ZULU); } elseif (method_exists($mix_value, '__toString')) { $obj_val->stringValue = $mix_value->__toString(); } else { diff --git a/src/GDS/Mapper.php b/src/GDS/Mapper.php index a777837..cbe10e5 100755 --- a/src/GDS/Mapper.php +++ b/src/GDS/Mapper.php @@ -28,13 +28,9 @@ abstract class Mapper /** * Datetime formats */ - const DATETIME_FORMAT_UU = 'Uu'; const DATETIME_FORMAT_UDOTU = 'U.u'; - - /** - * Microseconds in a second - */ - const MICROSECONDS = 1000000; + const TZ_UTC = 'UTC'; + const TZ_UTC_OFFSET = '+00:00'; /** * Current Schema diff --git a/src/GDS/Mapper/GRPCv1.php b/src/GDS/Mapper/GRPCv1.php index 14f0d4a..7497e2e 100755 --- a/src/GDS/Mapper/GRPCv1.php +++ b/src/GDS/Mapper/GRPCv1.php @@ -343,13 +343,19 @@ private function configureGooglePropertyValue(array $arr_field_def, $mix_value) /** * Extract a datetime value * + * Attempt to retain microsecond precision + * * @param Value $obj_property * @return mixed */ protected function extractDatetimeValue($obj_property) { - // Attempt to retain microsecond precision - return $obj_property->getTimestampValue()->toDateTime(); + $obj_dtm = $obj_property->getTimestampValue()->toDateTime(); + $str_default_tz = date_default_timezone_get(); + if (self::TZ_UTC === $str_default_tz || self::TZ_UTC_OFFSET === $str_default_tz) { + return $obj_dtm; + } + return $obj_dtm->setTimezone(new \DateTimeZone($str_default_tz)); } /** diff --git a/src/GDS/Mapper/RESTv1.php b/src/GDS/Mapper/RESTv1.php index 86dbe41..3e54abe 100644 --- a/src/GDS/Mapper/RESTv1.php +++ b/src/GDS/Mapper/RESTv1.php @@ -30,7 +30,7 @@ class RESTv1 extends \GDS\Mapper * * A timestamp in RFC3339 UTC "Zulu" format, accurate to nanoseconds. Example: "2014-10-02T15:01:23.045123456Z". */ - const DATETIME_FORMAT = 'Y-m-d\TH:i:s.u\Z'; + const DATETIME_FORMAT_ZULU = 'Y-m-d\TH:i:s.u\Z'; /** * Auto detect & extract a value @@ -71,22 +71,40 @@ protected function extractAutoDetectValue($obj_property) /** * Extract a datetime value * - * We will lose accuracy - * - past seconds in version 3.0 - * - past microseconds (down from nanoseconds) in version 4.0 + * Response values are ... + * A timestamp in RFC3339 UTC "Zulu" format, accurate to nanoseconds. Example: "2014-10-02T15:01:23.045123456Z". + * + * Construct as UTC, then apply default timezone before returning + * + * PHP cannot handle more that 6 d.p. (microseconds), so we parse out as best we can with preg_match() * * @param $obj_property * @return mixed */ protected function extractDatetimeValue($obj_property) + { + return $this->buildLocalisedDateTimeObjectFromUTCString((string) $obj_property->timestampValue); + } + + /** + * Build and return a DateTime, with the current timezone applied + * + * @param string $str_datetime + * @return \DateTime + * @throws \Exception + */ + public function buildLocalisedDateTimeObjectFromUTCString(string $str_datetime): \DateTime { $arr_matches = []; - if(preg_match('/(.{19})\.?(\d{0,6}).*Z/', $obj_property->timestampValue, $arr_matches) > 0) { - $obj_dtm = new \DateTime($arr_matches[1] . '.' . $arr_matches[2] . 'Z'); - } else { - $obj_dtm = new \DateTime($obj_property->timestampValue); + if(preg_match('/(.{19})\.?(\d{0,6}).*Z/', $str_datetime, $arr_matches) > 0) { + $str_datetime = $arr_matches[1] . '.' . $arr_matches[2] . 'Z'; + } + $str_default_tz = date_default_timezone_get(); + if (self::TZ_UTC === $str_default_tz || self::TZ_UTC_OFFSET === $str_default_tz) { + new \DateTime($str_datetime); } - return $obj_dtm; + return (new \DateTime($str_datetime, new \DateTimeZone(self::TZ_UTC))) + ->setTimezone(new \DateTimeZone($str_default_tz)); } /** @@ -399,7 +417,10 @@ protected function createPropertyValue(array $arr_field_def, $mix_value) $obj_dtm = new \DateTimeImmutable($mix_value); } // A timestamp in RFC3339 UTC "Zulu" format, accurate to nanoseconds. Example: "2014-10-02T15:01:23.045123456Z". - $obj_property_value->timestampValue = $obj_dtm->format(self::DATETIME_FORMAT); + $obj_property_value->timestampValue = \DateTime::createFromFormat( + self::DATETIME_FORMAT_UDOTU, + $obj_dtm->format(self::DATETIME_FORMAT_UDOTU) + )->format(self::DATETIME_FORMAT_ZULU); break; case Schema::PROPERTY_DOUBLE: diff --git a/tests/RESTv1MapperTest.php b/tests/RESTv1MapperTest.php index b9b5052..464d529 100644 --- a/tests/RESTv1MapperTest.php +++ b/tests/RESTv1MapperTest.php @@ -122,6 +122,63 @@ public function testDateTimeMapToGoogle() } + /** + * Test data going into Datastore has been correctly converted to UTC when operating in another TZ + */ + public function testDateTimeMapToGoogleWithTimezone() + { + // Let's use a timezone with no Daylight savings + // This is -03:00 hours + $str_existing_tz = date_default_timezone_get(); + date_default_timezone_set('America/Cayenne'); + + $obj_schema = (new \GDS\Schema('Person'))->addDatetime('retirement'); + + $obj_mapper = new \GDS\Mapper\RESTv1(); + $obj_mapper->setSchema($obj_schema); + + $obj_gds_entity = new \GDS\Entity(); + $obj_gds_entity->setSchema($obj_schema); + $obj_gds_entity->setKind('Person'); + + $obj_gds_entity->zoned = new DateTime('2021-02-04 08:30:00'); // takes on default timezone + $obj_gds_entity->dob = new DateTime('1979-02-05T08:30:00+09:00'); // timezone specified + $obj_gds_entity->exact = new DateTime('1979-02-05T08:30:00.12345678Z'); // UTC assumed + $obj_gds_entity->ts = new DateTime('@946684800'); // UTC assumed + $obj_gds_entity->retirement = '2050-01-01 09:00:00'; // takes on default timezone + + $obj_rest_entity = $obj_mapper->mapToGoogle($obj_gds_entity); + + $this->assertEquals( + '2021-02-04T11:30:00.000000Z', + $obj_rest_entity->properties->zoned->timestampValue, + '08:30 (-3) => 11:30 UTC' + ); + + // 1979-02-05T08:30:00+09:00 => previous day, 23:30 + $this->assertEquals( + '1979-02-04T23:30:00.000000Z', + $obj_rest_entity->properties->dob->timestampValue, + 'Previous day, 23:30' + ); + + // '1979-02-05T08:30:00.123457Z' 6 OR 7, depending on PHP version (>= 7.2, cuts not rounds) + $this->assertTrue(in_array($obj_rest_entity->properties->exact->timestampValue, [ + '1979-02-05T08:30:00.123456Z', // PHP >= 7.2 + '1979-02-05T08:30:00.123457Z', // PHP up to 7.1 + ])); + + // + $this->assertEquals( + '2050-01-01T12:00:00.000000Z', + $obj_rest_entity->properties->retirement->timestampValue, + '-3 hours from Y-m-d H:i:s' + ); + + // Reset the timezone + date_default_timezone_set($str_existing_tz); + } + /** * Ensure arrays of lat/lon pairs are supported for geopoints */ @@ -432,14 +489,82 @@ public function testAncestryFromArray() $this->assertEquals('Child', $obj_path_last->kind); } + /** + * Confirm we correctly extract DateTime objects from REST responses + * + * @throws Exception + */ + public function testMapDatetimeFromGoogle() + { + $obj_schema = (new \GDS\Schema('Event'))->addDatetime('when'); + $obj_mapper = new \GDS\Mapper\RESTv1(); + $obj_mapper->setSchema($obj_schema); + $obj_entity = $obj_mapper->mapOneFromResult($this->buildFakeResponse()); + $this->assertInstanceOf('\\DateTime', $obj_entity->when); + $this->assertInstanceOf('\\DateTime', $obj_entity->then); + $str_php_micros = '1412262083.045123'; + $this->assertEquals($str_php_micros, $obj_entity->when->format(\GDS\Mapper::DATETIME_FORMAT_UDOTU)); + $this->assertEquals('2014-10-02 15:01:23', $obj_entity->when->format('Y-m-d H:i:s')); + $this->assertEquals('2015-11-03 16:02:24', $obj_entity->then->format('Y-m-d H:i:s')); + } + + /** + * Confirm we correctly extract DateTime objects from REST responses + * + * @throws Exception + */ + public function testMapDatetimeFromGoogleInTimezone() + { + $str_existing_tz = date_default_timezone_get(); + date_default_timezone_set('America/Cayenne'); -// public function testMapToGoogle() -// { -// $obj_mapper = new \GDS\Mapper\RESTv1(); -// $obj_gds_entity = new \GDS\Entity(); -// $obj_gds_entity->setKind('Person'); -// $obj_rest_entity = $obj_mapper->mapToGoogle($obj_gds_entity); -// $this->assertEquals('expected', $obj_rest_entity->actual); -// } + $obj_schema = (new \GDS\Schema('Event'))->addDatetime('when'); + $obj_mapper = new \GDS\Mapper\RESTv1(); + $obj_mapper->setSchema($obj_schema); + $obj_entity = $obj_mapper->mapOneFromResult($this->buildFakeResponse()); + $this->assertInstanceOf('\\DateTime', $obj_entity->when); + $this->assertInstanceOf('\\DateTime', $obj_entity->then); + $str_php_micros = '1412262083.045123'; + $this->assertEquals($str_php_micros, $obj_entity->when->format(\GDS\Mapper::DATETIME_FORMAT_UDOTU)); + $this->assertEquals('2014-10-02 12:01:23', $obj_entity->when->format('Y-m-d H:i:s')); + $this->assertEquals('2015-11-03 13:02:24', $obj_entity->then->format('Y-m-d H:i:s')); + $this->assertEquals('America/Cayenne', $obj_entity->when->getTimezone()->getName()); + $this->assertEquals('America/Cayenne', $obj_entity->then->getTimezone()->getName()); + + // Reset the timezone + date_default_timezone_set($str_existing_tz); + } -} \ No newline at end of file + /** + * Build a fake REST response payload + * + * @return stdClass + */ + private function buildFakeResponse(): \stdClass + { + return (object)[ + 'entity' => (object) [ + 'key' => (object)[ + "partitionId" => (object)[ + "projectId" => 'test-project', + "namespaceId" => 'test-namespace', + ], + 'path' => [ + (object)[ + "kind" => 'Event', + "id" => '123456789', + ] + ] + ], + 'properties' => (object)[ + 'when' => (object)[ + "timestampValue" => '2014-10-02T15:01:23.045123456Z', + ], + 'then' => (object)[ + "timestampValue" => '2015-11-03T16:02:24.055123456Z', + ], + ], + ] + ]; + } +} diff --git a/tests/TimeZoneTest.php b/tests/TimeZoneTest.php new file mode 100644 index 0000000..4f0ed55 --- /dev/null +++ b/tests/TimeZoneTest.php @@ -0,0 +1,214 @@ + + */ +class TimeZoneTest extends \PHPUnit\Framework\TestCase { + + const FORMAT_YMDHIS = 'Y-m-d H:i:s'; + + const DTM_KNOWN_8601 = '2004-02-12T15:19:21+00:00'; + + /** + * Validate that creating datetime objects from 'U.u' format always results in UTC TZ + */ + public function testCreateDateTimeFromFormatZone() + { + $str_existing_tz = date_default_timezone_get(); + date_default_timezone_set('America/Cayenne'); + + // New datetimes should be in the current timezone + $obj_dtm = new DateTime(); + $this->assertEquals('America/Cayenne', $obj_dtm->getTimezone()->getName()); + + // Using timestamp should be in UTC (or '+00:00') + $obj_dtm = new DateTime('@1625652400'); + $this->assertTrue(in_array($obj_dtm->getTimezone()->getName(), [Mapper::TZ_UTC, Mapper::TZ_UTC_OFFSET])); + + // Using string with zone + $obj_dtm = new DateTime('2004-02-12T15:19:21+00:00'); + $this->assertTrue(in_array($obj_dtm->getTimezone()->getName(), [Mapper::TZ_UTC, Mapper::TZ_UTC_OFFSET])); + + // Using string with zone + $obj_dtm = new DateTime('2004-02-12T15:19:21+04:00'); + $this->assertEquals('+04:00', $obj_dtm->getTimezone()->getName()); + + // Using 'U.u' should be in UTC (or '+00:00') + $obj_dtm = DateTime::createFromFormat(Mapper::DATETIME_FORMAT_UDOTU, '1625652400.123456'); + $this->assertTrue(in_array($obj_dtm->getTimezone()->getName(), [Mapper::TZ_UTC, Mapper::TZ_UTC_OFFSET])); + + // Using 'U.u' should be in UTC (or '+00:00') + $obj_dtm = DateTime::createFromFormat(Mapper::DATETIME_FORMAT_UDOTU, '1625652400.000000'); + $this->assertTrue(in_array($obj_dtm->getTimezone()->getName(), [Mapper::TZ_UTC, Mapper::TZ_UTC_OFFSET])); + + // Using 'U.u' should be in UTC (or '+00:00') + $obj_dtm = DateTime::createFromFormat(Mapper::DATETIME_FORMAT_UDOTU, '1625652400.0'); + $this->assertTrue(in_array($obj_dtm->getTimezone()->getName(), [Mapper::TZ_UTC, Mapper::TZ_UTC_OFFSET])); + + // Too many DP of data should fail + $this->assertFalse(DateTime::createFromFormat(Mapper::DATETIME_FORMAT_UDOTU, '1625652400.999999999')); + + // And back to local TZ use cases + $obj_dtm = new DateTime('2021-01-05 15:34:23'); + $this->assertEquals('America/Cayenne', $obj_dtm->getTimezone()->getName()); + $obj_dtm = new DateTime('2021-01-05'); + $this->assertEquals('America/Cayenne', $obj_dtm->getTimezone()->getName()); + $obj_dtm = new DateTime('01:30'); + $this->assertEquals('America/Cayenne', $obj_dtm->getTimezone()->getName()); + $obj_dtm = new DateTime('now'); + $this->assertEquals('America/Cayenne', $obj_dtm->getTimezone()->getName()); + $obj_dtm = new DateTime('yesterday'); + $this->assertEquals('America/Cayenne', $obj_dtm->getTimezone()->getName()); + + // Reset the timezone + date_default_timezone_set($str_existing_tz); + } + + /** + * Validate understanding and truncation of RFC3339 UTC "Zulu" format + */ + public function testMicroConversions() + { + // This is the example from the Google API docs, with more accuracy than PHP can handle + $str_rfc_3339 = '2014-10-02T15:01:23.045123456Z'; + + // Some expected conversions + $str_php_micros = '1412262083.045123'; + $str_php_c = '2014-10-02T15:01:23+00:00'; + $str_php_rfc_3339 = '2014-10-02T15:01:23.045123Z'; + + $obj_mapper = new RESTv1(); + $obj_dtm = $obj_mapper->buildLocalisedDateTimeObjectFromUTCString($str_rfc_3339); + + $this->assertEquals($str_php_micros, $obj_dtm->format(Mapper::DATETIME_FORMAT_UDOTU)); + $this->assertEquals($str_php_c, $obj_dtm->format('c')); + $this->assertEquals($str_php_rfc_3339, $obj_dtm->format(RESTv1::DATETIME_FORMAT_ZULU)); + } + + /** + * Validate understanding and truncation of RFC3339 UTC "Zulu" format + */ + public function testMicroConversionsWithTimezone() + { + $str_existing_tz = date_default_timezone_get(); + date_default_timezone_set('America/Cayenne'); + + // This is the example from the Google API docs, with more accuracy than PHP can handle + $str_rfc_3339 = '2014-10-02T15:01:23.045123456Z'; + + // Some expected conversions + $str_php_micros = '1412262083.045123'; + $str_php_c = '2014-10-02T12:01:23-03:00'; + + $obj_mapper = new RESTv1(); + $obj_dtm = $obj_mapper->buildLocalisedDateTimeObjectFromUTCString($str_rfc_3339); + + $this->assertEquals('America/Cayenne', date_default_timezone_get()); + $this->assertEquals('America/Cayenne', $obj_dtm->getTimezone()->getName()); + + $this->assertEquals($str_php_micros, $obj_dtm->format(Mapper::DATETIME_FORMAT_UDOTU)); + $this->assertEquals($str_php_c, $obj_dtm->format('c')); + + // Reset the timezone + date_default_timezone_set($str_existing_tz); + } + + /** + * Confirm known behaviour - which is that unadjusted zulu formats are not equal + * + * @throws Exception + */ + public function testZuluFormat() + { + $obj_tz_utc = new DateTimeZone('UTC'); + $obj_dtm1 = new DateTime('now', $obj_tz_utc); + $str_utc_ts = $obj_dtm1->format(Mapper::DATETIME_FORMAT_UDOTU); + + $obj_tz_london = new DateTimeZone('Europe/London'); + $obj_dtm1->setTimezone($obj_tz_london); + $str_london_ts = $obj_dtm1->format(Mapper::DATETIME_FORMAT_UDOTU); + $str_london_zulu = $obj_dtm1->format(RESTv1::DATETIME_FORMAT_ZULU); + + $obj_tz_nyc = new DateTimeZone('America/New_York'); + $obj_dtm1->setTimezone($obj_tz_nyc); + $str_nyc_ts = $obj_dtm1->format(Mapper::DATETIME_FORMAT_UDOTU); + $str_nyc_zulu = $obj_dtm1->format(RESTv1::DATETIME_FORMAT_ZULU); + + // Timestamps always match + $this->assertEquals($str_utc_ts, $str_london_ts); + $this->assertEquals($str_utc_ts, $str_nyc_ts); + + // London and NYC never match in unadjusted zulu format + $this->assertNotEquals($str_london_zulu, $str_nyc_zulu); + } + + public function testZoneConversion() + { + $obj_tz_utc = new DateTimeZone('UTC'); + $obj_tz_nyc = new DateTimeZone('America/New_York'); + $obj_tz_xmas = new DateTimeZone('Indian/Christmas'); + + $obj_dtm_utc = (new DateTime(self::DTM_KNOWN_8601))->setTimezone($obj_tz_utc); + $obj_dtm_nyc = (new DateTime(self::DTM_KNOWN_8601))->setTimezone($obj_tz_nyc); + $obj_dtm_xmas = (new DateTime(self::DTM_KNOWN_8601))->setTimezone($obj_tz_xmas); + + // Timestamps match + $this->assertEquals($obj_dtm_utc->format('U'), $obj_dtm_nyc->format('U')); + $this->assertEquals($obj_dtm_utc->format('U'), $obj_dtm_xmas->format('U')); + + // Unadjusted Zulu mismatch + $this->assertNotEquals($obj_dtm_utc->format(RESTv1::DATETIME_FORMAT_ZULU), $obj_dtm_nyc->format(RESTv1::DATETIME_FORMAT_ZULU)); + $this->assertNotEquals($obj_dtm_utc->format(RESTv1::DATETIME_FORMAT_ZULU), $obj_dtm_xmas->format(RESTv1::DATETIME_FORMAT_ZULU)); + $this->assertNotEquals($obj_dtm_nyc->format(RESTv1::DATETIME_FORMAT_ZULU), $obj_dtm_xmas->format(RESTv1::DATETIME_FORMAT_ZULU)); + + // Adjust value to UTC, then the outputs should match + $str_zulu_utc_adjusted = $obj_dtm_utc->setTimezone($obj_tz_utc)->format(RESTv1::DATETIME_FORMAT_ZULU); + $str_zulu_nyc_adjusted = $obj_dtm_nyc->setTimezone($obj_tz_utc)->format(RESTv1::DATETIME_FORMAT_ZULU); + $str_zulu_xmas_adjusted = $obj_dtm_xmas->setTimezone($obj_tz_utc)->format(RESTv1::DATETIME_FORMAT_ZULU); + $this->assertEquals($str_zulu_utc_adjusted, $str_zulu_nyc_adjusted); + $this->assertEquals($str_zulu_utc_adjusted, $str_zulu_xmas_adjusted); + + // And confirm new UTC-based objects with these values match + $obj_dtm_utc_from_zulu1 = DateTime::createFromFormat(RESTv1::DATETIME_FORMAT_ZULU, $str_zulu_utc_adjusted, $obj_tz_utc); + $this->assertEquals( + $obj_dtm_utc->format('U'), + $obj_dtm_utc_from_zulu1->format('U') + ); + $this->assertEquals( + $obj_dtm_utc->format(self::FORMAT_YMDHIS), + $obj_dtm_utc_from_zulu1->format(self::FORMAT_YMDHIS) + ); + $this->assertEquals( + $obj_dtm_utc->getTimezone()->getName(), + $obj_dtm_utc_from_zulu1->getTimezone()->getName() + ); + } + +} \ No newline at end of file