Dates have always been a tricky thing to manage in Drupal. Even in PHP. PHP 5.2 introduced the DateTimeInterface
which makes handling dates, date ranges, intervals, comparisons etc much easier. However, we always still have the complication of data storage, formatting and different timezones management.
In this article we are going to look at how we can run some entity queries in Drupal 8 using the Date field in our conditions. The requirement for returning entities which have a date field with a value between certain hours is definitely not an edge case, and although seems like an easy task, it can be tricky.
Imagine a simple date field on the Node entity which stores date and time. By default in Drupal 8, the storage for this date is in the format Y-m-d\TH:i:s
and the timezone is UTC. However, the site timezone is rarely UTC and we very well may have users choosing their own timezones. So we need to construct our node queries carefully if we want reliable results.
Running a db_query()
type of query for returning nodes with the date in a certain range would be a pain at best and impossible at worst. Luckily though, we can, and should always in Drupal 8 try to rely on the entity.query
service when looking for entities.
So let's see a couple of examples.
First, an easy one: how do we query for all the nodes which have the field_date
value in the future.
$now = new DrupalDateTime('now');
$query = \Drupal::entityQuery('node');
$query->condition('field_date', $now->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT), '>=');
$results = $query->execute();
A few things to notice. First, we are using the Drupal wrapper of \DateTime
and constructing an object to represent our current time. Then we create our entity query and for the date field condition we pass the storage format so that it can be compared to what is being stored. And the regular operators here allow us to find the right entities.
There is one problem with this though. When creating the DrupalDateTime
, the site default timezone is used. So if our timezone is not UTC, the query will suffer because we are essentially comparing apples with oranges. And the further away from UTC we are, the more apples start to become compared to cars and airplanes.
To fix this, we need to set the timezone to UTC before running the query.
$now = new DrupalDateTime('now');
$now->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE));
And then use $now
in the query. The subtle difference to understand is that we are creating $now
totally relative to where we are (the site timezone) because we are interested in finding nodes in the future from us, not from from another timezone. However, we then convert it so that we can have them compared properly in the query.
A more complex example could be a range of times. Let's say we want all the nodes with the time of today between 16:00 and 18:00 (a 2 hour span).
I prefer to work directly with \DateTime
and then wrap it into the Drupal wrapper just because i can have all the native methods highlighted by my IDE. So we can do something like this:
$timezone = drupal_get_user_timezone();
$start = new \DateTime('now', new \DateTimeZone($timezone));
$start->setTime(16,0);
$start->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE));
$start = DrupalDateTime::createFromDateTime($start);
$end = new \DateTime('now', new \DateTimezone($timezone));
$end->setTime(18, 0);
$end->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE));
$end = DrupalDateTime::createFromDateTime($end);
$query = \Drupal::entityQuery('node');
$query
->condition('field_date', $start->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT), '>=')
->condition('field_date', $end->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT), '<=');
$results = $query->execute();
So first, we get the user timezone. drupal_get_user_timezone()
returns for us the string representation of the timezone the current user has selected, or if they haven't, the site default timezone. Based on that, we create our native date object that represents the current point in time but set the actual time to 16:00. After that we set the storage timezone and create our Drupal wrapper so that we can format it for the query.
For the end date we do the same thing but we set a different time. Then we expectedly write our query conditions and ask for the entities which have a date between those 2 times.
The order of setting the time and timezone on the date object is important. We want to set the time before we set the timezone because the times we are looking for are relative to the current user, not to the storage timezone.
So that is pretty much it. Now you can query for entities and play with date fields without issues (hopefully).
Daniel Sipos
Danny founded WEBOMELETTE in 2012 as a passion project, mostly writing about Drupal problems he faced day to day, as well as about new technologies and things that he thought other developers would find useful. Now he now manages a team of developers and designers, delivering quality products that make businesses successful.
Comments
Make sure to include DrupalDateTime
Thanks for this, super simple.
For anyone finding that DrupalDateTime is undefined, make sure to include it in your class:
Thanks Danny!
Now I got all my datetimes storing correctly and I even think I figured out how to have my run-once-after-local-midnight-cron-hook-comparing-UTC-times to fire and work correctly!
Appreciated :)
Thanks for this!
Covers everything I needed mate, just wanted to say thanks!
A DrupalDateTime challenge.
Hi Danny! Picture this: I have a user who sits in Dallas, Texas and manages apartment complexes from New York to Los Angeles. I can't use the user time zone, nor the "site" time zone to display content created in each of the separate zones. The user selects the apartment (which has a tzone indicator on the entity like America/Chicago) but when the views detail records display (and when displaying a node) how can the system display the proper time for the records in the zone? Scratching my head... Also, have benefitted from your posts in the past and really appreciate your insight.
Field Type Date Range
Hi Danny !.
There are two values for these fields: value and end_value
Could you please ask how to write a query condition where end_value less than or equal current time for instance ?
field_name to use in entity query for date range
Query field for today based on current user date
I have a custom date field (no time), and was wondering how I could query it for entities that match 'today'.
I tried setting;
but that didn't work, and also tried
Any help would be greatly appreciated
Update this article to remove deprecated code
The use of DATETIME_STORAGE_TIMEZONE and DATETIME_STORAGE_FORMAT is deprecated (See https://www.drupal.org/node/2912980).
You now should use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::STORAGE_TIMEZONE and Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATETIME_STORAGE_FORMAT
Could you please update this article because it is a good reference for this problem. This is number one search result in Google and it would be cool to show best practices here :)
Also a TLDR; at the start could help... (I didn't read to the part with the timezone and wondered why it wasnt working :D)
In reply to Update this article to remove deprecated code by bahuma20 (not verified)
Thank you. Updated!
Thank you. Updated!
The code has the typos:
The code has the typos:
Class DateTimeZone referenced with incorrect case: DateTimezone
Should be:
new \DateTimeZone()
In reply to The code has the typos: by Anonymous (not verified)
Thank you!
Thank you!
Thanks! (Also: deprecated code)
Big thanks for the article! Helped me filter out entities correctly in a query where I needed to get for this year, but an entity for 2022-01-01 was included.
Oh and btw, drupal_get_user_timezone() was deprecated in 8.8.x and removed in Drupal 9. We're supposed to use the built-in date_default_timezone_get() now.
Reference: https://www.drupal.org/node/3009387
Add new comment