Working with date-time values¶
Shopsys Platform internally works with dates in UTC timezone. That is for better portability and integration with other systems. Also, it allows you to work with time values more freely. It's easy to implement show dates that suites your needs, for example, each user has its own timezone.
Getting Current Time with Clock¶
Shopsys Platform uses the Symfony Clock component for all time-related operations.
Never use new DateTime() or new DateTimeImmutable() when you need to get the current time.
Why Use Clock Instead of DateTime¶
Using new DateTime() directly creates several problems:
- Untestable code - Tests cannot control what "now" means, leading to flaky tests
- Non-deterministic behavior - The same code can produce different results depending on when it runs
In Services (with Dependency Injection)¶
In services, facades, repositories, and form types, inject ClockInterface and use $this->clock->now():
use Psr\Clock\ClockInterface;
class OrderService
{
public function __construct(
private readonly ClockInterface $clock,
) {}
public function createOrder(OrderData $orderData): Order
{
$orderData->createdAt = $this->clock->now();
// ...
}
}
In Entities, Data Objects, Fixtures, and Tests¶
In places where dependency injection is not available (entities, data objects, data fixtures, tests), use new DatePoint():
use Symfony\Component\Clock\DatePoint;
// Getting current time
$now = new DatePoint();
// Getting relative time from now (note the parentheses around new DatePoint())
$yesterday = (new DatePoint())->modify('-1 day');
$nextWeek = (new DatePoint())->modify('+1 week');
$twoHoursAgo = (new DatePoint())->modify('-2 hours');
When to Use Each Approach¶
| Scenario | Use |
|---|---|
| In services (DI available) | $this->clock->now() |
| In entities/data objects/fixtures | new DatePoint() |
| In tests | new DatePoint() |
| Relative time (with DI) | $this->clock->now()->modify('-30 days') |
| Relative time (without DI) | (new DatePoint())->modify('-30 days') |
| Parsing stored datetime string | new DatePoint($storedValue) |
| Fixed/constant dates | new DatePoint('2024-01-01') |
| Creating from format | DatePoint::createFromFormat(...) |
In Tests¶
Tests use new DatePoint() for time calculations:
use Symfony\Component\Clock\DatePoint;
// Set entity timestamp relative to current time
$cart->setModifiedAt((new DatePoint())->modify('-131 days'));
// For services with injected ClockInterface, mock it:
$clock = $this->createMock(ClockInterface::class);
$clock->method('now')->willReturn(new DatePoint());
Configuration¶
What timezone will be used is controlled by the implementation of DisplayTimeZoneProviderInterface.
Default implementation DisplayTimeZoneProvider takes into account domain timezone setting from config/domains.yaml file and convert all the dates into this timezone.
DisplayTimeZoneProvider also provides the timezone for the admin that is set using shopsys.admin_display_timezone parameter.
Display dates¶
In the admin¶
All date values should be presented to the admin from Twig templates, where are three filters at your hand
formatDateformatTimeformatDateTime
All filters are aware of DisplayTimeZoneProvider and internally convert the values to the desired admin display timezone when rendering date-times.
Note
PHP does not have any Date object and even the dates are internally instance of DateTime class.
On the storefront¶
The dates are sent to the storefront in UTC timezone from the frontend API.
The domain timezone is provided for the storefront via the settings.displayTimezone GraphQL query.
Custom useFormatDate hook is then used for proper date formatting while taking the domain timezone into account.
As a safety net, there is publicRuntimeConfig.domains.fallbackTimezone in the next.config.js file, which is used when the domain timezone is not available via API.
Filling the dates¶
When the admin enters any date-time value, it should be in a currently used admin display timezone.
Shopsys Platform comes with two FormTypes ready to handle dates properly – DatePickerType and DateTimeType.
Both of them are aware of DisplayTimeZoneProvider and convert the values to the desired admin display timezone when the input is submitted.
Even when you need to store only the date, it should be persisted as a DateTime in the database.
Consider following. User in Phoenix (UTC-7) creates an article and set the date of creation to some date. This date should be visible near the article.
Due to limitations of PHP, the value is in variable of theDateTimetype with zero time (midnight). Presenting such date back to the user results into date shift (one day back), because this "midnight DateTime" is converted to the display timezone. Storing the dates in the database as a DateTime type prevents it.
Filling the dates programmatically¶
Setting Current Time¶
When you need to set a timestamp to "now" in entities or data objects, use DatePoint:
use Symfony\Component\Clock\DatePoint;
$entity->setCreatedAt(new DatePoint());
$entity->setModifiedAt((new DatePoint())->modify('-1 hour'));
In services with injected ClockInterface, use $this->clock->now() instead.
Parsing External Dates¶
When storing dates from external sources (e.g., from 3rd party application), parse them with DateTimeImmutable and convert to UTC timezone:
$dateFromOtherSource = '2020-08-24 18:30:02';
$dateTime = new \DateTimeImmutable($dateFromOtherSource, new \DateTimeZone('Europe/Prague'));
$dateTimeUtc = $dateTime->setTimezone(new \DateTimeZone('UTC'));
Parsing External Dates¶
When storing dates from external sources (e.g., from 3rd party application), parse them with DatePoint and convert to UTC timezone:
use Symfony\Component\Clock\DatePoint;
$dateFromOtherSource = '2020-08-24 18:30:02';
$dateTime = new DatePoint($dateFromOtherSource, new \DateTimeZone('Europe/Prague'));
$dateTimeUtc = $dateTime->setTimezone(new \DateTimeZone('UTC'));
Note
Always use DatePoint instead of DateTimeImmutable for consistency across the codebase.
Exceptions¶
When to use Date¶
As described in previous paragraph, the best way to store date is to use DateTime.
There are exceptions to that, e.g. storing of historical data like birthdays, historical events etc.
We use this approach for storing internal days and holidays (represented by Shopsys\FrameworkBundle\Model\Store\ClosedDay\ClosedDay::$date).
In this case, the holiday date is bound to a particular domain, and it is not necessary to convert it to the UTC timezone.
Storing time values¶
A store opening and closing times (Shopsys\FrameworkBundle\Model\Store\OpeningHours\OpeningHoursRange) are persisted in the database as string values, without any relation to a particular date.
The values represent the information like "On mondays, the store is open from 8:00 to 18:00" and this is not affected by a timezone as it is always considered as a local time of the particular store.