Welcome to part two of Hell is Programming a Calendar, where I will dive into some of the more nitty-gritty details about handling time specifically in Drupal. If you haven’t already, I highly recommend checking out part one where I explain why time, in a more general programming sense, is so complicated in the first place.
Anatomy of a date field
Let’s start by breaking down Drupal’s core date field into its smaller components to examine how it works.
In my mind, the core date field can be broken down into four major components.
- Storage: Stores the value in ISO8601 format.
- UI: Handles a user inputting the data, which is then used by both storage and render.
- Render: Displays the data in whatever format is requested.
- Calculation: Sits underneath the rest, providing an easy means of changing between different ways of representing the date.
The following section outlines each major component and the classes they use. To illustrate these transitions I’ve made a date field and saved it with the value of June 7, 2022 at 1:45:00 pm.
Let’s start at the storage layer, which sets up the field so that it can properly interact with the other layers. When building the test date field Drupal invoked the DateTimeItem class which has two properties, a DateTimeIso8601 and a DateTimeComputed (we’ll come back to the computed field later). When I save the value of June 7, 2022 at 1:45:00 pm into this field, DateTimeIso8601 calls the calculation layer to convert it to 2022-06-07T20:45:00 and saves it in the database. All dates are stored in UTC in the database.
- DateTimeItem: Creates the field and its storage and differentiates between storing a datetime or just a date.
- DateTimeIso8601: Sets and gets datetimes in UTC and stores them in the ISO8601 format (YYYY-MM-DDTHH:MM:SS The time part is optional).
Next we have the UI layer which builds a UI for the end user that can accept and change a date value. DateTimeWidgetBase and DateTimeDefaultWidget work together to build a UI for the user to input and change the date value.
- DateTimeComputed: Calculate the date value into a format that can be displayed in the widget.
- DateTimeWidgetBase: Creates the basic UI and ensures that the date is using the correct timezone. Uses the DateTimezone base PHP class to do so.
- DateTimeDefaultWidget: Extends the previous class and dictates what date and time the elements the UI will display for the user.
Then, we come to rendering which displays the properly formatted date to the end user. DateTimeDefaultFormatter and DateTimeFormatterBase work together to output a properly formatted render array.
- DateTimeFormatterBase: Gets the correct timezone to display the date. This is normally the user’s timezone but it allows for overrides as well. It then converts the stored date from UTC to the correct timezone and outputs a render array of that data (still in ISO8601 format).
- DateTimeDefaultFormatter: Takes the ISO8601 formatted date from the base class and converts it to whatever format the field widget expects.
Finally we have the calculation layer which is called by the other three layers to help smoothly translate data between them.
- DateTimeItemInterface: Defines default values (timezone, datetime format, date format) that are needed for creating DrupalDateTime objects.
- DateTimePlus: Wraps PHP’s DateTime class so that it can create dates from more parameters (date object, timestamp, etc) and adds error handling.
- DrupalDateTime: Extends DateTimePlus to add Drupal-specific requirements. Namely integrating Drupal translation into the date format and pulling Drupal’s timezone system into the timezone calculation.
What did we learn?
Dissecting the core date field reveals a number of things about how dates work and how we can navigate them when writing our own custom code. First, the data is stored consistently (ISO format, UTC time, no am/pm) and then adjusted on the fly for the end user depending on their timezone and preferred format. Second, all of the above functionality depends on the PHP Datetime class wrapped by DateTimePlus and DrupalDateTime. Also, by default the formatters output both the converted date (Tue, 06/07/2022 - 13:45) AND the datetime value that it’s derived from (2022-06-07T20:45:00Z). This is really useful if you need to do some more calculation on the template layer.
These are all good practices to emulate in your own code. Storing the data in a consistent format makes it easier for multiple different systems and endpoints to manipulate it because they can all assume the same starting point. Always using datetime objects creates cleaner and more flexible code than just the standard strtotime and date functions (see the cheatsheet below for examples). Also, outputting your date data in both the intended and ISO format makes your front-end code more flexible.
But what about? (Stay tuned for Part 3)
Hopefully you now have a better understanding of how the date field works and how you can use its components to your advantage. However, I feel like in answering so many questions I’ve just created a whole lot more. What’s the best way to compare date values within this framework? How do other date-reliant elements of Drupal use this system? If unix time is so important for modern computing, why doesn’t Drupal’s core date field use it?
So, perhaps unsurprisingly, there’s going to be a part three to this series in which I attempt to answer these questions. If you have any questions or points you’d like me to explore in the next post, please reach out to me on the official Drupal Slack or my LinkedIn profile.
Addendum: Cheat Sheet
Creating DateTime Objects
use Drupal\Core\Datetime\DrupalDateTime; // Note: This is for creating NEW datetime objects. // Generally if you do not pass a Timezone it will default to UTC $utc_timezone = new \DateTimeZone("UTC"); $iso_date = '2022-06-07T20:45:00'; // CreateFromDateTime // Uses the base PHP datetime class to create a DrupalDateTime. $date_time = new \DateTime($iso_date, $utc_timezone); $create_from_date_time = DrupalDateTime::createFromDateTime($date_time); // CreateFromArray // Pass an array of numeric date values to create a DrupalDateTime. $date_array = [ 'year' => 2022, 'month' => 6, 'day' => 7, 'hour' => 20, 'minute' => 45, 'second' => 0, ]; $create_from_array = DrupalDateTime::createFromArray($date_array, $utc_timezone); // CreateFromTimeStamp // Must get a timestamp (in this case: 1654634700) from an existing PHP Datetime (or DrupalDateTime) object. $timestamp = $date_time->getTimestamp(); $create_from_timestamp = DrupalDateTime::createFromTimestamp($timestamp, $utc_timezone); // CreateFromFormat // Pass a datetime format and a matching datetime to get a DrupalDateTime object. // Note: The format and date value MUST match. If I were to put in 'Y-m-d' for the format I would get an error. $iso_date = '2022-06-07T20:45:00'; $create_from_format = DrupalDateTime::createFromFormat('Y-m-d\TH:i:s', $iso_date, $utc_timezone); // For more examples, check out /core/tests/Drupal/Tests/Core/Datetime/DrupalDateTimeTest.php in Drupal Core.
Manipulating DateTime Objects
use Drupal\Core\Datetime\DrupalDateTime; // Note: all of these examples are technically altering the same object. // For simplicity sake my "returns" are assuming that the listed function was // the only one run. $date_time = new \DateTime('2022-06-07T20:45:00', new \DateTimeZone("UTC")); $drupal_date_time = DrupalDateTime::createFromDateTime($date_time); // Add or subtract time. Uses php.net/manual/en/class.dateinterval.php for interval notation. // returns: 2022-06-08T20:45:00 $add_one_day = $drupal_date_time->add(new \DateInterval('P1D')); // returns: 2022-06-07T18:45:00 $subtract_two_hours = $drupal_date_time->sub(new \DateInterval('PT2H')); // Diff // returns the difference between two DrupalDateTime objects. $drupal_datetime_2 = DrupalDateTime::createFromFormat('Y-m-d\TH:i:s', '2022-06-08T20:45:00', new \DateTimeZone("UTC")); $diff = $drupal_date_time->diff($drupal_datetime_2); // returns: 1 $day_difference = $diff->d; // Get the object in a different format // returns: 2022-06-07 $new_format = $drupal_date_time->format('Y-m-d'); // Get the offset (depends on the timezone type) // returns: 0 $offset = $drupal_date_time->getOffset(); // Get the timezone // returns: ['timezone_type' => 3, 'timezone => 'UTC'] $timezone = $drupal_date_time->getTimezone(); // Get the timestamp // returns: 1654634700 $timestamp = $drupal_date_time->getTimestamp(); // Change the day to July 9th, 2023 // returns: 2023-07-09T20:45:00 $set_new_date = $drupal_date_time->setDate(2023, 7, 9); // Change the time to 9:30 pm. // returns: 2022-06-07T21:30:00 $set_new_time = $drupal_date_time->setTime(21, 30); $drupal_date_time->setTimezone(new \DateTimeZone("America/Los_Angeles")); // For more examples, check out web/core/tests/Drupal/Tests/Component/Datetime/DateTimePlusTest.php in core
- PHP DateTime: https://www.php.net/manual/en/class.datetime.php
- ISO 6801: https://www.iso.org/iso-8601-date-and-time-format.html
- PHP DateTimeZone: https://www.php.net/manual/en/class.datetimezone.php
- PHP DateInterval: https://www.php.net/manual/en/class.dateinterval.php
- PHP DateTime Format: https://www.php.net/manual/en/datetime.format.php