From 89bfc23d323f6f3237a19a8b5907d52d4c776de4 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 2 Jan 2018 16:27:58 -0500 Subject: [PATCH] Standardize date normalization to immutables Also move date formats to the ValueInfo class Standardizing on immutables avoids any possible ambiguity in the API of the resultant value, as well as any ambiguity as to whether a DateTime output instance is the same instance or a clone (they had been clones) --- lib/Database.php | 2 +- lib/Feed.php | 15 ++++----- lib/Misc/ValueInfo.php | 52 ++++++++++++++++++------------ tests/cases/Misc/TestValueInfo.php | 16 +++++++-- 4 files changed, 53 insertions(+), 32 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index fdede55..ea39f21 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -827,7 +827,7 @@ class Database { $limit = Date::normalize("now"); if (Arsse::$conf->purgeFeeds) { // if there is a retention period specified, compute it; otherwise feed are deleted immediatelty - $limit->sub(new \DateInterval(Arsse::$conf->purgeFeeds)); + $limit = Date::sub(Arsse::$conf->purgeFeeds, $limit); } $out = (bool) $this->db->prepare("DELETE from arsse_feeds where orphaned <= ?", "datetime")->run($limit); // commit changes and return diff --git a/lib/Feed.php b/lib/Feed.php index ce1a88d..54b32a2 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -328,12 +328,12 @@ class Feed { return [$new, $edited]; } - protected function computeNextFetch(): \DateTime { + protected function computeNextFetch(): \DateTimeImmutable { $now = Date::normalize(time()); if (!$this->modified) { $diff = $now->getTimestamp() - $this->lastModified->getTimestamp(); $offset = $this->normalizeDateDiff($diff); - $now->modify("+".$offset); + return $now->modify("+".$offset); } else { // the algorithm for updated feeds (returning 200 rather than 304) uses the same parameters as for 304, // save that the last three intervals between item dates are computed, and if any two fall within @@ -347,20 +347,19 @@ class Feed { $offsets[] = $this->normalizeDateDiff($diff); } if ($offsets[0]==$offsets[1] || $offsets[0]==$offsets[2]) { - $now->modify("+".$offsets[0]); + return $now->modify("+".$offsets[0]); } elseif ($offsets[1]==$offsets[2]) { - $now->modify("+".$offsets[1]); + return $now->modify("+".$offsets[1]); } else { - $now->modify("+ 1 hour"); + return $now->modify("+ 1 hour"); } } else { - $now->modify("+ 1 hour"); + return $now->modify("+ 1 hour"); } } - return $now; } - public static function nextFetchOnError($errCount): \DateTime { + public static function nextFetchOnError($errCount): \DateTimeImmutable { if ($errCount < 3) { $offset = "5 minutes"; } elseif ($errCount < 15) { diff --git a/lib/Misc/ValueInfo.php b/lib/Misc/ValueInfo.php index 870dad0..3f226f5 100644 --- a/lib/Misc/ValueInfo.php +++ b/lib/Misc/ValueInfo.php @@ -19,7 +19,7 @@ class ValueInfo { // strings const EMPTY = 1 << 2; const WHITE = 1 << 3; - //normalization types + // normalization types const T_MIXED = 0; // pass through unchanged const T_NULL = 1; // convert to null const T_BOOL = 2; // convert to boolean @@ -28,11 +28,23 @@ class ValueInfo { const T_DATE = 5; // convert to DateTimeInterface instance const T_STRING = 6; // convert to string const T_ARRAY = 7; // convert to array - //normalization modes + // normalization modes const M_NULL = 1 << 28; // pass nulls through regardless of target type const M_DROP = 1 << 29; // drop the value (return null) if the type doesn't match const M_STRICT = 1 << 30; // throw an exception if the type doesn't match const M_ARRAY = 1 << 31; // the value should be a flat array of values of the specified type; indexed and associative are both acceptable + // symbolic date and time formats + const DATE_FORMATS = [ // in out + 'iso8601' => ["!Y-m-d\TH:i:s", "Y-m-d\TH:i:s\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets + 'iso8601m' => ["!Y-m-d\TH:i:s.u", "Y-m-d\TH:i:s.u\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets + 'microtime' => ["U.u", "0.u00 U" ], // NOTE: the actual input format at the user level matches the output format; pre-processing is required for PHP not to fail + 'http' => ["!D, d M Y H:i:s \G\M\T", "D, d M Y H:i:s \G\M\T"], + 'sql' => ["!Y-m-d H:i:s", "Y-m-d H:i:s" ], + 'date' => ["!Y-m-d", "Y-m-d" ], + 'time' => ["!H:i:s", "H:i:s" ], + 'unix' => ["U", "U" ], + 'float' => ["U.u", "U.u" ], + ]; public static function normalize($value, int $type, string $dateInFormat = null, $dateOutFormat = null) { $allowNull = ($type & self::M_NULL); @@ -131,14 +143,14 @@ class ValueInfo { if (is_string($value)) { return $value; } - $dateOutFormat = $dateOutFormat ?? "iso8601"; - $dateOutFormat = isset(Date::FORMAT[$dateOutFormat]) ? Date::FORMAT[$dateOutFormat][1] : $dateOutFormat; - if ($value instanceof \DateTimeImmutable) { - return $value->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat); - } elseif ($value instanceof \DateTime) { - $out = clone $value; - $out->setTimezone(new \DateTimeZone("UTC")); - return $out->format($dateOutFormat); + if ($value instanceof \DateTimeInterface) { + $dateOutFormat = $dateOutFormat ?? "iso8601"; + $dateOutFormat = isset(self::DATE_FORMATS[$dateOutFormat]) ? self::DATE_FORMATS[$dateOutFormat][1] : $dateOutFormat; + if ($value instanceof \DateTimeImmutable) { + return $value->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat); + } elseif ($value instanceof \DateTime) { + return \DateTimeImmutable::createFromMutable($value)->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat); + } } elseif (is_float($value) && is_finite($value)) { $out = (string) $value; if (!strpos($out, "E")) { @@ -168,13 +180,11 @@ class ValueInfo { if ($value instanceof \DateTimeImmutable) { return $value->setTimezone(new \DateTimeZone("UTC")); } elseif ($value instanceof \DateTime) { - $out = clone $value; - $out->setTimezone(new \DateTimeZone("UTC")); - return $out; + return \DateTimeImmutable::createFromMutable($value)->setTimezone(new \DateTimeZone("UTC")); } elseif (is_int($value)) { - return \DateTime::createFromFormat("U", (string) $value, new \DateTimeZone("UTC")); + return \DateTimeImmutable::createFromFormat("U", (string) $value, new \DateTimeZone("UTC")); } elseif (is_float($value)) { - return \DateTime::createFromFormat("U.u", sprintf("%F", $value), new \DateTimeZone("UTC")); + return \DateTimeImmutable::createFromFormat("U.u", sprintf("%F", $value), new \DateTimeZone("UTC")); } elseif (is_string($value)) { try { if (!is_null($dateInFormat)) { @@ -187,28 +197,28 @@ class ValueInfo { throw new \Exception; } } - $f = isset(Date::FORMAT[$dateInFormat]) ? Date::FORMAT[$dateInFormat][0] : $dateInFormat; + $f = isset(self::DATE_FORMATS[$dateInFormat]) ? self::DATE_FORMATS[$dateInFormat][0] : $dateInFormat; if ($dateInFormat=="iso8601" || $dateInFormat=="iso8601m") { - // DateTime::createFromFormat() doesn't provide one catch-all for ISO 8601 timezone specifiers, so we try all of them till one works + // DateTimeImmutable::createFromFormat() doesn't provide one catch-all for ISO 8601 timezone specifiers, so we try all of them till one works if ($dateInFormat=="iso8601m") { - $f2 = Date::FORMAT["iso8601"][0]; + $f2 = self::DATE_FORMATS["iso8601"][0]; $zones = [$f."", $f."\Z", $f."P", $f."O", $f2."", $f2."\Z", $f2."P", $f2."O"]; } else { $zones = [$f."", $f."\Z", $f."P", $f."O"]; } do { $ftz = array_shift($zones); - $out = \DateTime::createFromFormat($ftz, $value, new \DateTimeZone("UTC")); + $out = \DateTimeImmutable::createFromFormat($ftz, $value, new \DateTimeZone("UTC")); } while (!$out && $zones); } else { - $out = \DateTime::createFromFormat($f, $value, new \DateTimeZone("UTC")); + $out = \DateTimeImmutable::createFromFormat($f, $value, new \DateTimeZone("UTC")); } if (!$out) { throw new \Exception; } return $out; } else { - return new \DateTime($value, new \DateTimeZone("UTC")); + return new \DateTimeImmutable($value, new \DateTimeZone("UTC")); } } catch (\Exception $e) { if ($strict && !$drop) { diff --git a/tests/cases/Misc/TestValueInfo.php b/tests/cases/Misc/TestValueInfo.php index 87c6f01..8c2f12d 100644 --- a/tests/cases/Misc/TestValueInfo.php +++ b/tests/cases/Misc/TestValueInfo.php @@ -506,6 +506,18 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { list($type, $value, $exp) = $test; $this->assertEquals($exp, I::normalize($value, $type | I::M_ARRAY, "iso8601"), "Failed test #$index"); } + // Date-to-string format tests + $test = new \DateTimeImmutable("now", new \DateTimezone("UTC")); + $exp = $test->format(I::DATE_FORMATS['iso8601'][1]); + $this->assertSame($exp, I::normalize($test, I::T_STRING, null), "Failed test for null output date format"); + foreach (I::DATE_FORMATS as $name => $formats) { + $exp = $test->format($formats[1]); + $this->assertSame($exp, I::normalize($test, I::T_STRING, null, $name), "Failed test for output date format '$name'"); + } + foreach (["U", "M j, Y (D)", "r", "c"] as $format) { + $exp = $test->format($format); + $this->assertSame($exp, I::normalize($test, I::T_STRING, null, $format), "Failed test for output date format '$format'"); + } } protected function d($spec, $local, $immutable): \DateTimeInterface { @@ -517,7 +529,7 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { } } - protected function t(float $spec): \DateTime { - return \DateTime::createFromFormat("U.u", sprintf("%F", $spec), new \DateTimeZone("UTC")); + protected function t(float $spec): \DateTimeImmutable { + return \DateTimeImmutable::createFromFormat("U.u", sprintf("%F", $spec), new \DateTimeZone("UTC")); } }