Relative dates in PHP
Guillaume Briday
3 minutes
Managing dates has never been an easy task, and the programming language doesn't change much about that challenge. There are libraries like Carbon that greatly simplify date manipulation, but it often takes some mental gymnastics to know what result is expected.
Natively, PHP allows you to create dates in a relative format with real English expressions. This is very convenient because you can read a date naturally.
I'll continue the article with examples in a Laravel project, but this would work in any PHP project, adapt as needed.
Before we start, I will create a helper to simplify the creation of a Carbon
instance:
- use Illuminate\Support\Carbon;
- $date = new Carbon();
- $date->toDateString();
+ carbon()->toDateString();
I will create a date.php
file in the App/Helpers
folder to include all the methods I will need, just like the other Laravel helpers. To make these methods available in the application, the file needs to be added to the composer autoload
:
"autoload": {
"classmap": [
"database/seeds",
"database/factories"
],
"psr-4": {
"App\\": "app/"
},
+ "files": [
+ "app/Helpers/date.php"
+ ]
}
And my file will contain the following method:
<?php
use Illuminate\Support\Carbon;
/**
* Return a Carbon instance.
*/
function carbon(string $parseString = '', string $tz = null): Carbon
{
return new Carbon($parseString, $tz);
}
This will also save me from including the Illuminate\Support\Carbon
namespace everywhere.
Using relative dates
In Laravel, there are two handy helpers for dates: now
and today
.
By reading the method names, you can explicitly understand their behavior, which is less the case for a call like this:
$date = new Carbon();
$date->subWeeks(3);
$date->startOfDay();
Even though you can quickly understand the goal behind these three lines, the reading is not natural, you can't read them like plain English.
Avoiding visual debt allows you to focus on the business aspect of the code!
With relative dates, we can transform this code to:
carbon('3 weeks ago midnight');
There are many practical cases.
For example, when you want to run a job every Monday:
- $date = now();
- $date->addWeek();
- $date->startOfWeek();
+ carbon('next monday');
If you want to get all the articles of the current month:
- $date = now();
- $date->startOfMonth();
+ carbon("first day of this month midnight");
It's still a Carbon object, so you can apply other methods to it if needed.
carbon("next friday")->diffForHumans();
=> "1 day from now"
You can also perform calculations combining multiple possible units:
carbon('monday this week +12 hours +30 minutes -20 seconds')
To avoid ambiguities or make modifications to an existing date, you can separate the calculations:
carbon('first day of january this year')->modify('+14 days');
A few things to know
The order of the elements is also important. Indeed, keywords that modify the time must be placed first if the time is customized; otherwise, they override the behavior.
carbon('tomorrow 11:00'); # Correct! Tomorrow at 11am
carbon('11:00 tomorrow'); # Incorrect! Tomorrow, but at 00:00
Another behavior that can be confusing is the first day of
notation. This format will always return the first day of the current month unless otherwise specified.
Thus, these three declarations will return the exact same date, namely the first day of the month:
carbon('first day of this month');
carbon('first day of');
carbon('first day of this year');
If you want the first day of the year, you need to specify the month as January
:
carbon('first day of january this year');
Similarly, for the first day of the week, you must use Monday
specifying the current week:
carbon('monday this week');
Without specifying the week, it will go to the next desired day.
Finally, relative expressions like +14 days
will always be processed after non-relative ones, regardless of their position in your query.
Understanding how it works
Now that we have seen the main specific cases, we can take a closer look at how it works.
We will need to use specific keywords to build our expression. As we have seen, there is an order and a certain logic to follow. The result will always be calculated relative to the current date.
All the keywords are available in the documentation: https://secure.php.net/manual/en/datetime.formats.relative.php
The overall logic is quite clear once you understand what we have discussed.
However, be careful of potentially incorrect results from overly complex queries, as the parser has its limits.
Practical use cases
I think it shouldn't be overused or used for creating all dates. Some expressions would be more complex to understand and maintain than in the standard declaration.
Nevertheless, there are several use cases that I find very practical, such as the examples I used above.
I also think of simple dates that read naturally in English, but for others, I prefer using standard methods.
For example, in tests, I really like this notation:
- $faker->dateTimeBetween(now()->subWeeks(3), now()->subWeek());
+ $faker->dateTimeBetween(carbon('3 weeks ago'), carbon('1 weeks ago'));
Or to add a delay to a job task:
- PrepareNewsletterSubscriptionEmail::dispatch()->delay(now()->addMonth());
+ PrepareNewsletterSubscriptionEmail::dispatch()->delay(carbon('next month'));
Conclusion
I think it's useful to know that this exists, it's up to you to decide when it will be necessary to use it.
Thank you!