How can I reuse DateTime and DateTimeZone objects in PHP? - php

I made a function to convert datestrings to other timezones, but my function creates new DateTime and DateTimeZone objects every time it's called. This is bad practice so I looked into how I could reuse the objects.
DateTime has a method called setDate, but it requires a year, month and day as arguments instead of a datestring. DateTimeZone has no such method.
Is there a way I can reuse my DateTime and DateTimeZone objects?
Edit: This function will be called anywhere from 4 to hundreds of times in a single POST, depending on how many dates are being returned from the server.
/**
* Converts a datestring to another timezone
*
* #arg $ds (String) - The datestring to be converted
* #arg $tz (String) - The timezone to convert the datestring to
*
* #return (String) - The converted datetime
*/
function convertDateTime($ds, $tz) {
$fooDateTime = new DateTime($ds);
$fooTimeZone = new DateTimeZone($tz);
$newDate = $fooDateTime->setTimezone($fooTimeZone);
return $newDate->format('Y-m-d H:i:s');
}

Compared to the work that is going on in the DateTime and DateTimeZone constructors, the overhead of initialising a new object is miniscule. What is really happening when you call those functions?
The input is being parsed, validated, deconstructed, and in case of DateTimeZone timezone information is being loaded from somewhere.
That data is being stored somewhere (in object properties, but might as well be in variables, an array or whatever else).
You get a handle back to the result of the work which just happened (your new object).
Since every date is unique, this work needs to happen anyway for each date. Whether this is dressed up as an object or as a regular function call which returns an array or something is irrelevant and makes little difference.
Arguably you could memoize the DateTimeZone objects, since identical timezones are identical and immutable anyway. Something like:
static $timezones = [];
if (!isset($timezones[$tz])) {
$timezones[$tz] = new DateTimeZone($tz);
}
But really, for all you know PHP is already optimising this behind the scenes. It probably won't make any appreciable difference in practice.
What you should certainly not do is mutate objects and set entirely new values on them. If you read the list of things that happen above again, it should make sense why; because you don't know what work exactly PHP did there, and you may create inconsistent objects which screw with your business logic due to stupid mistakes.

First of all, confirm it IS a problem. Benchmark your app with current code, then change convertDateTime to return a hardcoded string:
function convertDateTime($ds, $tz) {
return '2016-02-03 15:54:00';
}
and measure the difference.
If there is any, you can probably memoize responses, but it will increase memory consumption, which IMHO is more valuable.
One more thing to keep in mind. One said:
Premature optimization is the root of all evil.
In PHP it is extremely true, as the language itself is far from optimal.

I don't see any bad practice creating multiple DateObject instances, but if you prefer a unique instance, you can extend the DateTime class:
class MyDateTime extends DateTime
{
public function convertDateTime( $ds, $tz )
{
$fooTimeZone = new DateTimeZone( $tz );
$newDate = $this->setTimezone( $fooTimeZone );
return $newDate->format( 'Y-m-d H:i:s' );
}
}
It remains the problem of DateTimeZone class...
Please note that this is only an example to partially answer to your question ( «How can I reuse DateTime» ) without getting into the arguments.
It's easy also add more code to reuse DateTimeZone, i.e. setting an array and then populating it when new TimeZone is called. (Edit: see example in deceze answer)

Related

Why do I get error when try to convert Carbon to DateTime?

I am developing a Laravel project. I try to create a DateTime object by using Carbon. This is what I tried:
Carbon::createFromFormat('Y-m-d H:i:s', '2021-10-01T00:01:00')->toDateTime();
But my phpstan complains : Cannot call method toDateTime() on Carbon\Carbon|false.
Why is this error? What is the correct way to convert Carbon to a DateTime object?
Your format is incorrect, so Carbon cannot create the time. You're missing the T, which needs to be escaped.
Carbon::createFromFormat('Y-m-d\TH:i:s', '2021-10-01T00:01:00')->toDateTime();
If PHPStan complains, that's because the static analysis (which does not execute the code) cannot determine the types properly. As Carbon extends DateTime, the PHP documentation can help for this method call:
Returns a new DateTime instance or false on failure.
So, to ensure that the code is sound in terms of static analysis, you need to split it up:
$object = Carbon::createFromFormat('Y-m-d H:i:s', '2021-10-01T00:01:00');
if (!$object instanceof Carbon) {
throw new RuntimeException('could not parse date');
}
$object->toDateTime();
The difference: now, PHPStan can safely assume that $object is of type Carbon when toDateTime() is called
As others pointed out: running that code would also yield an error, as the date format you try to parse from and the input date do not match. But that is out of scope for PHPStan, which does not execute the code
Carbon objects are already DateTime objects.
class Carbon extends \DateTime
You may want toDateTimeString, but if you really want a DateTime object, you've already got one, just with a little Carbon syntax sugar sprinkled on top.
If you really need a DateTime object, the ->toDate() function is what you're looking for.
https://carbon.nesbot.com/docs/
Return native DateTime PHP object matching the current instance.

How can I create a relative PHP DateTime for the next instance of a particular time?

So it seems like the PHP DateTime class allows you to use simple relative language strings such a next Monday 12:00 to create objects associated with relative dates.
The Problem
The use case I'd like to achieve is to omit the day ("Monday") from the above string, and create a DateTime object for the next instance of 06:00.
This presents a challenge because the code may be running at 07:00, in which case the string 06:00 will return a date which is in the past, whereas if we run the code at 05:00 (or anytime before 06:00), we will get the correct result.
Note: As an addendum, I don't want to have to do a bunch of complicated arithmetic and want to leverage the DateTime class to its fullest to keep myself from any sneaky little TimeZone or leap year issues.
My Question
How can I use the PHP DateTime class to obtain a DateTime object for the next instance of a certain time (without supplying the day as well)?
I created a PHP extension for the DateTime API called dt. You can find it here. Using it for this task is very simple. Some Examples:
$dt = dt::create("today 06:00");
if($dt->isPast()) {
$dt->modify("+ 1Day");
}
echo $dt;
Optionally, you can always set a time zone as the second parameter.
$time = "06:00";
$dt = dt::create("today $time","Africa/Johannesburg");
//:
The class works with special chains which can contain conditions. This allows you to create an expression that does the job with the create method alone.
$dt = dt::create("today 6:00|{{?YmdHis<NOW}}+1 Day");
The class also works with cron expressions.
$cron = "0 6 * * 1-5";//every Mo-Fr at 6:00
$dt = dt::create('now')->nextCron($cron);
Always delivers the next time 6:00 for Monday to Friday. At the weekend, the next time is Monday 6:00.
The Solution
Here's how I ended up solving this...
/*
* Simple function that returns a DateTime object representing the next instance of the time
* supplied by the $timeString parameter.
*
* $timeString: A string representing the time you wish to process, in 24h notation (i.e.
* '06:00' for 6:00am).
*/
function getNextTimeInstance($timeString){
// First, grab an instance of the current moment as a DateTime object ('now' is optional).
$rightNow = new DateTime('now');
// Basic initializations for the object we want to use.
$processedTime = new DateTime();
// Change the Timezone if you need to do so.
$processedTime->setTimezone(new DateTimeZone('Africa/Johannesburg'));
// Set the time for our working DateTime to the time we want (i.e. '06:00').
$processedTime->modify($timeString);
// If the next instance of this time we're trying to process is in the past
// (meaning it is "less than" the current moment).
if($rightNow > $processedTime){
// Simply add one day to our working DateTime object.
$processedTime->modify('+1 day');
}
// Return the DateTime objects that represents the next instance of that time.
return $processedTime;
}
I relied heavily on the following documentation to reach this result:
The PHP DateTime class.
The list of date formats that the PHP DateTime class will accept as inputs.
The list of supported TimeZone strings that PHP will accept.
I hope this helps anyone else stuck in the same situation as myself, and if anyone else has a more elegant solution, please feel free to contribute.

How to detect Ambiguous and Invalid DateTime in PHP?

When dealing with local DateTime values provided by a user, it's quite possible to have a time that is either invalid or ambiguous, due to Daylight Saving Time transitions.
In other languages and frameworks, there are often methods such as isAmbiguous and isValid, on some representation of the time zone. For example in .NET, there is TimeZoneInfo.IsAmbiguousTime and TimeZoneInfo.IsInvalidTime.
Plenty of other time zone implementations have similar methods, or functionality to address this concern. For example, in Python, the pytz library will throw an AmbiguousTimeError or InvalidTimeError exception that you can trap.
PHP has excellent time zone support, but I can't seem to find anything to address this. The closest thing I can find is DateTimeZone::getTransitions. That provides the raw data, so I can see that some methods could be written on top of this. But do they exist already somewhere? If not, can anyone provide a good implementation? I would expect them to work something like this:
$tz = new DateTimeZone('America/New_York');
echo $tz->isValidTime(new DateTime('2013-03-10 02:00:00')); # false
echo $tz->isAmbiguousTime(new DateTime('2013-11-03 01:00:00')); # true
I 'm not aware of any existing implementations and I haven't had cause to use advanced date/time features such as these as yet, so here is a clean room implementation.
To enable the syntax illustrated in the question we are going to extend DateTimeZone as follows:
class DateTimeZoneEx extends DateTimeZone
{
const MAX_DST_SHIFT = 7200; // let's be generous
// DateTime instead of DateTimeInterface for PHP < 5.5
public function isValidTime(DateTimeInterface $date);
public function isAmbiguousTime(DateTimeInterface $date);
}
To keep distracting details from cluttering the implementation, I am going to assume that the $date arguments have been created with the proper time zone; this is in contrast to the example code given in the question.
That is to say, the correct result will not be produced by this:
$tz = new DateTimeZoneEx('America/New_York');
echo $tz->isValidTime(new DateTime('2013-03-10 02:00:00'));
but instead by this:
$tz = new DateTimeZoneEx('America/New_York');
echo $tz->isValidTime(new DateTime('2013-03-10 02:00:00', $tz));
Of course since $tz is already known to the object as $this, it should be easy to extend the methods so that this requirement is removed. In any case, making the interface super user friendly is out of the scope of this answer; going forward I will focus on the technical details.
isValidTime
The idea here is to use getTransitions to see if there are any transitions around the date/time we are interested in. getTransitions will return an array with either one or two elements; the timezone situation for the "begin" timestamp will always be there, and another element will exist if a transition occurs shortly after it. The value of MAX_DST_SHIFT is small enough that there is no chance of getting a second transition/third element.
Let's see the code:
public function isValidTime(DateTime $date)
{
$ts = $date->getTimestamp();
$transitions = $this->getTransitions(
$ts - self::MAX_DST_SHIFT,
$ts + self::MAX_DST_SHIFT
);
if (count($transitions) == 1) {
// No DST changes around here, so obviously $date is valid
return true;
}
$shift = $transitions[1]['offset'] - $transitions[0]['offset'];
if ($shift < 0) {
// The clock moved backward, so obviously $date is valid
// (although it might be ambiguous)
return true;
}
$compare = new DateTime($date->format('Y-m-d H:i:s'), $this);
return $compare->modify("$shift seconds")->getTimestamp() != $ts;
}
The final point of the code depends on the fact that PHP's date functions calculate timestamps for invalid date/times as if wall clock time had not shifted. That is, the timestamps calculated for 2013-03-10 02:30:00 and 2013-03-10 03:30:00 will be identical on the New York timezone.
It's not difficult to see how to take advantage of this fact: create a new DateTime instance equal to the input $date, then shift it forward in wall clock time terms an amount equal to the DST shift in seconds (it is imperative that DST not be taken into account to make this adjustment). If the timestamp of the result (here the DST rules come into play) is equal to the timestamp of the input, then the input is an invalid date/time.
isAmbiguousTime
The implementation is quite similar to isValidTime, only a few details change:
public function isAmbiguousTime(DateTime $date)
{
$ts = $date->getTimestamp();
$transitions = $this->getTransitions(
$ts - self::MAX_DST_SHIFT,
$ts + self::MAX_DST_SHIFT);
if (count($transitions) == 1) {
return false;
}
$shift = $transitions[1]['offset'] - $transitions[0]['offset'];
if ($shift > 0) {
// The clock moved forward, so obviously $date is not ambiguous
// (although it might be invalid)
return false;
}
$shift = -$shift;
$compare = new DateTime($date->format('Y-m-d H:i:s'), $this);
return $compare->modify("$shift seconds")->getTimestamp() - $ts > $shift;
}
The final point depends on another implementation detail of PHP's date functions: when asked to produce the timestamp for an ambiguous date/time, PHP produces the timestamp of the first (in absolute time terms) occurrence. This means that the timestamps of the latest ambiguous time and the earliest non-ambiguous time for a given DST change will differ by an amount larger than the DST offset (specifically, the difference will be in the range [offset + 1, 2 * offset], where offset is an absolute value).
The implementation takes advantage of this by again doing a "wall clock shift" forward and checking the timestamp difference between the result and the input $date.
See the code in action.

How to create DateTime object from string in symfony2/php

In a DB table I have several fields with datetime as field type. So I need to persist data only as date time object.
From a form I get date time as string like
2012-10-05 17:45:54
Now when ever I persist my entity I get following error:
Fatal error: Call to a member function format() on a non-object in
..\DateTimeType.php on line 44
I tried with
$protocol->setStartedAt(strtotime($post['started_at']));
or
$from = \DateTime::createFromFormat('yy-mm-dd hh:mm:ss', $post['started_at']);
$protocol->setStartedAt($from);
or just
$from = new \DateTime($post['started_at']);
$protocol->setStartedAt($from);
The last code works but it does not uses the timestamp passed as arguement but just gets the current time.
Any ideas?
I always create a DateTime object with its constructor, in your case it would be:
$protocol->setStartedAt(new \DateTime($post['started_at']));
if this works but does not use the timestamp posted you probably do not have the value in $post['started_at']. Try debugging it or just do the dirty trick:
die($post['started_at']);
For the sake of future readers who surely will someday encounter this problem (this is the first post if you google "symfony 2 datetime from string"), keep in mind that in Symfony 2 the DateTime object does NOT accept a string with that format : "d/m/Y H:i:s", and probably doesn't support many others either.
For the sake of not becoming mad at that, I've actually found out that the easiest and safest solution to avoid such errors is this one:
First, get your date string from whatever kind of request you are doing (In my case a generic AJAX request) and convert it to a DateTime Object, this example assumes that we need to create a dateTime object for 25/04/2015 15:00, which is the format of the jQuery UI italian DateTimePicker (that's just an example):
$literalTime = \DateTime::createFromFormat("d/m/Y H:i","25/04/2015 15:00");
(note: use \ to use php's DateTime object, else you will be using Symfony's datetime object that will throw you an exception)
Then, once you did it, create a date string using the comfort format function, by giving to the first parameter the output format expected (Y-m-d H:i:s):
$expire_date = $literalTime->format("Y-m-d H:i:s");
In this way you are 100% sure that whatever kind of format you are passing or receiving this will properly be converted and you won't get any kind of exception from the DateTime symfony object, as long as you provide what you are expecting as an input.
Knowing that this post is actually quite old, I've just decided to post that because I didn't find any other valuable source but this one to understand where the problem could have been.
Please note that the best solution is still to send the datetime string in the correct format already, but if you literally have no ways to do that the safest way to convert such a string is the one above.
How about createFromFormat?
http://uk.php.net/manual/en/datetime.createfromformat.php
$from = DateTime::createFromFormat($post['started_at'], 'Y-m-d H:i:s');

PHP DateTime for .NET developers

PHP's DateTime class has the two methods add() and sub() to add or subtract a time span from a DateTime object. This looks very familiar to me, in .NET I can do the same thing. But once I tried it, it did very strange things. I found out that in PHP, those methods will modify the object itself. This is not documented and the return value type indicates otherwise. Okay, so in some scenarios I need three lines of code instead of one, plus an additional local variable. But then PHP5's copy by reference model comes into play, too. Just copying the object isn't enough, you need to explicitly clone it to not accidently modify the original instance still. So here's the C# code:
DateTime today = DateTime.Today;
...
if (date < today.Add(TimeSpan.FromDays(7)) ...
Here's the PHP equivalent:
$today = new DateTime('today');
...
$then = clone $today;
$then->add(new DateInterval('P7D'));
if ($date < $then) ...
(I keep a copy of today to have the same time for all calculations during the method runtime. I've seen it often enough that seconds or larger time units change in that time...)
Is this it in PHP? I need to write a wrapper class for that if there's no better solution! Or I'll just stay with the good ol' time() function and just use DateTime for easier parsing of the ISO date I get from the database.
$today = time() % 86400;
...
if ($date < $today + 7 * 86400) ...
Time zones not regarded in these examples.
Update: I just found out that I can use the strtotime() function as well for parsing such dates. So what's the use for a DateTime class in PHP after all if it's so complicated to use?
Update^2: I noticed that my $today = ... thing above is garbage. Well, please just imagine some correct way of getting 'today' there instead...
It looks like you will definitely have to make a clone of the object. There does not seem to be a way to create a copy of the DateTime object any other way.
As far as I can see, you could save one line:
$today = new DateTime('today');
...
$then = clone $today;
if ($then->add(new DateInterval('P7D')) < $then) ...
I agree this isn't perfect. But do stick with DateTime nevertheless - write a helper class if need be. It is way superior to the old date functions: It doesn't have the year 2038 bug, and it makes dealing with time zones much, much easiert.

Categories