You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There are three distinct problems in DateTimeHelper — one parsing bug, one mutation side-effect, and one missing Config feature (regression from 1.x).
Investigation
PHP's DateTime constructor ignores the $timezone parameter when the input string already contains timezone info (Z, +00:00, etc.). For standard GPX timestamps like 2026-03-10T15:22:08Z, the 'Europe/London' default has no effect.
The setTimezone(date_default_timezone_get()) call changes the display representation but not the underlying Unix timestamp — so duration/speed computations are correct even on non-UTC servers. However, it creates surprising behavior and a real bug for edge cases.
Impact: Low for well-formed GPX, real bug for malformed GPX.
The $timezone parameter only matters when the input has no timezone info (e.g., 2026-06-15T15:22:08). During British Summer Time, Europe/London = UTC+1, so:
// During BST (e.g., June):new DateTime("2026-06-15T15:22:08", newDateTimeZone("Europe/London"))
// → interprets as 15:22 BST = 14:22 UTCnewDateTime("2026-06-15T15:22:08", newDateTimeZone("UTC"))
// → interprets as 15:22 UTC// 1 HOUR DIFFERENCE in timestamps — affects duration, speed, pace
Fix: Change default to 'UTC', remove setTimezone(date_default_timezone_get()):
publicstaticfunctionparseDateTime($value, string$timezone = 'UTC'): \DateTime
{
$timezone = new \DateTimeZone($timezone);
$datetime = new \DateTime($value, $timezone);
return$datetime;
}
Bug 2: formatDateTime() mutates the original DateTime object
formatDateTime() calls $datetime->setTimezone() which permanently modifies the DateTime object in the model. This is a side-effect — calling jsonSerialize() on a Stats or Point object changes the internal timezone of startedAt/finishedAt/time.
// Current code — MUTATES the inputpublicstaticfunctionformatDateTime($datetime, string$format = 'c', ?string$timezone = 'UTC'): ?string
{
if ($datetimeinstanceof \DateTime) {
$datetime->setTimezone(new \DateTimeZone($timezone ?? 'UTC')); // side effect!$formatted = $datetime->format($format);
}
return$formatted;
}
// Models use DateTimeImmutable — setTimezone returns new object, original unchangedpublic ?\DateTimeImmutable $time = null;
public ?\DateTimeImmutable $startedAt = null;
This is the more robust approach and aligns with modern PHP practices. DateTimeImmutable::setTimezone() returns a new object — the original is never modified.
Models (Stats, Point, Metadata) call formatDateTime() in their jsonSerialize() methods but don't currently have access to Config. Parsers call parseDateTime() without access to Config either. Options:
A. Inject Config into models/parsers — Engine sets Config on Stats when creating them; parsers receive Config from phpGPX
B. Keep serialization UTC-only — users who need custom formatting work with DateTime objects directly post-load
Summary
There are three distinct problems in
DateTimeHelper— one parsing bug, one mutation side-effect, and one missing Config feature (regression from 1.x).Investigation
PHP's
DateTimeconstructor ignores the$timezoneparameter when the input string already contains timezone info (Z,+00:00, etc.). For standard GPX timestamps like2026-03-10T15:22:08Z, the'Europe/London'default has no effect.The
setTimezone(date_default_timezone_get())call changes the display representation but not the underlying Unix timestamp — so duration/speed computations are correct even on non-UTC servers. However, it creates surprising behavior and a real bug for edge cases.Bug 1:
parseDateTime()fallback timezone'Europe/London'Impact: Low for well-formed GPX, real bug for malformed GPX.
The
$timezoneparameter only matters when the input has no timezone info (e.g.,2026-06-15T15:22:08). During British Summer Time,Europe/London= UTC+1, so:Fix: Change default to
'UTC', removesetTimezone(date_default_timezone_get()):Bug 2:
formatDateTime()mutates the original DateTime objectformatDateTime()calls$datetime->setTimezone()which permanently modifies the DateTime object in the model. This is a side-effect — callingjsonSerialize()on a Stats or Point object changes the internal timezone ofstartedAt/finishedAt/time.Fix — Option A: Clone before mutating:
Fix — Option B: Switch models to
DateTimeImmutable(PHP 8.1+ available):This is the more robust approach and aligns with modern PHP practices.
DateTimeImmutable::setTimezone()returns a new object — the original is never modified.Feature: Restore datetime configuration (1.x regression)
In 1.x, users could control output format and timezone:
In 2.x, all
formatDateTime()calls use hardcoded defaults — no way to configure output timezone or format:Proposed: Add three datetime properties to
Config:dateTimeDefaultTimezone— controls the fallback inparseDateTime()for malformed timestamps without timezone info (replaces hardcoded'Europe/London')dateTimeOutputTimezone— controlsformatDateTime()output timezone (restores 1.x$DATETIME_TIMEZONE_OUTPUT)dateTimeFormat— controlsformatDateTime()output format (restores 1.x$DATETIME_FORMAT, default usespspecifier forZsuffix — also fixes Fix <time> XML format: use Z suffix instead of +00:00 for UTC #82)Threading Config through serialization
Models (
Stats,Point,Metadata) callformatDateTime()in theirjsonSerialize()methods but don't currently have access toConfig. Parsers callparseDateTime()without access toConfigeither. Options:Migration from 1.x
phpGPX::$DATETIME_FORMAT = 'c'new Config(dateTimeFormat: 'Y-m-d\TH:i:sp')phpGPX::$DATETIME_TIMEZONE_OUTPUT = 'UTC'new Config(dateTimeOutputTimezone: 'UTC')phpGPX::$DATETIME_TIMEZONE_OUTPUT = 'America/New_York'new Config(dateTimeOutputTimezone: 'America/New_York')new Config(dateTimeDefaultTimezone: 'UTC')Related
Zvs+00:00format (addressed by changing default format to'Y-m-d\TH:i:sp')Related discussion: #67 (comment)