-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New PSR for ClockInterface #1224
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To ease usage with DateTimes I would rather only have one single method in that interface to retrieve a DateTimeImmutable. From a usability point we should not encourage people to handle timestamps or microtimes as integers as that inevitably leads to ...interesting results and usually is the reason for time-related problems. Whether we are talking about DST, Timezones, Leapseconds or other such things.
That is the reason why I would propose only one method dateTime
that returns a DateTimeImmutable
-instance. If it is really necessary I could also think about a DateTimeInterface
but using DateTimeImmutable
should by now be the prefered object.
If someone actually needs a timestamp or a microtime there is always the possibility to use DateTimeImmutable::format('U')
or DateTimeImmutable::format('U.u')
to get that value.
The Timezone can equally be retrieved using DateTimeImmutable::getTimezone()
.
That will make the interface much easier to implement as one does not need to implement methods that one does not need in their implementation just because the interface defines them.
Could it be a performance issue? Anyway, that probably needs benchmarks on |
Well. If such a performance impact is an issue, then we should probably think about whether abstracting things into Interfaces is such a good idea. Yes. That for sure can have a performance impact. But depending on the implementation the code that will then be executed on the implementing class might be And standards that the FIG propose should not think about performance impacts but about interoperability and allowing best possible usage under a multitude of possibilities. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is such a great proposal, on my point of view. I left some suggestions for typos.
I agree with @heiglandreas, a single |
I think this would be a very good idea to standardize. I've recently (read: past few years) been working on large legacy code base and one really common source of flaky tests is time. Mocking time is not something a lot of people intuitively seem to think about and it creates a whole bunch of issues due to assumptions that people make. For example, common incorrect assumption is that In my opinion, a standardized time interface, even if just simple From implementation point of view, I think a very simple interface would be best. The more methods there are, the more complicated it gets to mock it. If there is just a single method that returns There are few things, however, that I think need to be clearly defined in the standard interface:
Regarding performance, I also think that it shouldn't really be a concern here. An interface cannot guarantee performance of the implementing code. Thus, consumer of the interface should not make assumptions about the performance. And if performance is of great importance, it's likely that there is an use case for TL;DR; Time is definitely an major interoperability concern that should be seriously considered for standardization. |
When we return a
Here also we should not encourage people to expect a certain timezone. The |
Co-authored-by: Rafael Bernard Araújo <[email protected]>
Co-authored-by: Rafael Bernard Araújo <[email protected]>
Co-authored-by: Rafael Bernard Araújo <[email protected]>
Co-authored-by: Rafael Bernard Araújo <[email protected]>
The main issue I have with relying |
Removed DateTime object as a return type, seems DateTimeImmutable is the way to go.
I mostly agree with this statement, however, handling HTTP Cache/Cookie/etc Max-Age are built around manipulating timestamps with integer offsets. Code example from GuzzleHttp if (!$this->getExpires() && $this->getMaxAge()) {
// Calculate the Expires date
$this->setExpires(\time() + $this->getMaxAge());
} elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) {
$this->setExpires($expires);
} Not sure if the simplicity of this can be expressed as easily with DateTimeImmutable. |
(@cseufert invited me to share my opinion, so here I am 😅) I also agree with previous statements that one method returning a Since a clock always shows what it considers to be the current time (= now), I do think that Regarding performance, I 100% agree with @heiglandreas. If an integer is needed, |
If this would be handled with a ClockInterface instance the code would need only a minor adaption: if (!$this->getExpires() && $this->getMaxAge()) {
// Calculate the Expires date
$this->setExpires($this->clock->now()->getTimestamp() + $this->getMaxAge());
} elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) {
$this->setExpires($expires);
} So the (Always under the assumption that the good ideas of @jeromegamez are used to rename the function to |
Hello, I'm following this thread as well and I like the idea! I created also a project with a ClockInterface: https://github.com/loophp/nanobench/tree/master/src/Time If you want to add it to the list, feel free. Looking forward to see this happening! |
I'm inclined to agree with @localheinz above. Since every other possible format can be gotten off of a I am also highly worried about the "current timezone" concept. Timezones are about one third of everything that makes date and time hard. Any such spec needs to be very careful what it does with them, as that's exactly the sort of place that inconsistencies between libraries can creep in that are both nominally spec-compliant. My recommendation instead would be to allow now() to take a |
One caveat on the time zone bit is you can't set typehints to "DTI in UTC", so that piece is by convention. Agreed that passing a DateTimeZone would be useful, which would also explicitly solve the I always want this in UTC use case. |
As you get a DateTimeImmutable you can always use |
A timestamp is by definition Seconds since 1st of January 1970, 00:00:00 UTC. So 1147483647 is definitely before 1147483947. That is, if people used appropriate tooling to get that timestamp in the first place. And yes, timestamps are missing leap seconds but the algorithm on how to handle leap seconds in Unix-Timestamps is well defined. For more information about Unix Timestamp check out https://en.m.wikipedia.org/wiki/Unix_time Therefore using |
Side note about leap second, it just means the timestamp A little advantage of But I'm still a bit sad, we just say "timezone-agnostic" as an excuse because we let the point that need the most to be standardized when mocking time unsolved and implicitly allow clock to generate those object with any timezone (and even in this spec a clock that would generate different timezone each time you call As said before, timezone should be changed outside the clock so the clock output is really standard and unambiguous.
|
Nope, Nope and Nope. Your Business-Logic has to take care of the Timezone. When you need UTC, convert the DateTimeImmutable explicitly to UTC (via There are valid use-cases for not using UTC (more than a lot of people might think) and the ClockInterface allows that. And you can never, ever trust DateTime input in terms of timezone. Therefore make sure, that your business-logic sanitizes that input. Even if you are using a UTC-Clock. The |
If you want to make sure that nobody assumes a wrong timezone from final class TimezoneRouletteClock implements \Psr\Clock\ClockInterface
{
public function now(): \DateTimeImmutable
{
$names = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL);
shuffle($names);
return new \DateTimeImmutable(
'now',
new \DateTimeZone(array_pop($names))
);
}
} But imo this would be insane and we should define that the DTI must have the UTC Timezone. |
Repeating "nope" is quite weak as an argument. And obviously you'll work with timezones in your business-logic. That does not mean your clock should output objects with the final timezone already set. And rather than converting to UTC from any stupid timezone a clock can send you without you actually know it if this clock is in a third-party, it's way more logical to guarantee an UTC from the output point you want to be standard and reliable, and next in your app converting the other way to other timezones, when you're ready to display this date to an actual user you know is in this timezone. This is even more important in PHP due to many open bug for years around timezones unexpected behavior as soon as you do comparison/calculation/interval handling. Maintaining Carbon for 6 years let me not very confident about letting users pick wisely the timezone for the clock. What mostly can happen is having unknown timezone (default from php.ini / OS). And if I receive in a function a |
Here's what I signed up for in context of this PSR: It is about providing time consistently and ameliorating the interoperability between libraries, not about handling time itself. Imagine the following scenario: you work on a project that uses two libraries that both handle time. Library A uses nesbot/carbon, library B uses brick/date-time, and my project has a service that integrates both of them. When I'm running the application, there's no problem. I use Now, when I want to test all of this, I need to make a A common clock interface could solve that: I define a clock in my project and pass it to library A and library B, and all use Library A could do As for the timezones... if my project uses
As many have said before: the timezone is not the concern of the clock. The clock is also not a DateTime library. Its only job is to be an interface to provide interoperability for different libraries/projects/frameworks to access the same kind of now. |
I'm all with @kylekatarnls. Sure, the ClockInterface is not a date-time library, so it should be used by date-time libraries, which can extract only the timestamp if they want, and still force the user to provide their timezone explicitly. However, be sure that people will also use the ClockInterface directly, and if the implementation of a stupid clock is
Same experience for me, maintaining Note that if what you want is an output guaranteed to be in UTC timezone, you may as well just return the timestamp as I suggested. It's a 1-1 mapping between both representations. If you're returning a @heiglandreas You may manage to convince me that a Clock should return a time in a given timezone (after all, you have an opportunity to set this "default" timezone in your DI container, which is still better that OS / php.ini / |
To precise my position, I'm 99% in favor of the current proposal. I could be 100% convinced with just one more simple sentence like the one proposed by @Art4:
So if anyone would claim that |
Then people are using the interface wrongly and we need to address that specifically. That is what I mean with "Timezone-agnostic". The interface does not care about the timezone, that is in the domain of the user to make sure the timezone is what they expect it to be. If that is by use of convention because people use a UTCClock-implementation or use Wherever someone relies on the timezone of a DateTimeImmutable, that is an accident waiting to happen. @kylekatarnls: But |
@heiglandreas Our goal is that no one relies on the timezone of the returned DTI. But if we don't define that the returned DTI If we define that the DTI must be in UTC, then we make such implementations (rightly) impossible. A user is then forced to set a timezone (different from UTC) in his buisness logic. It is somewhat like the paradox of tolerance: to keep tolerance you must be intolerant to the intolerant. |
@Art4: Timezone-agnostic does not mean UTC. Timezone-agnostic means it is irrelevant, which timezone the DTI is in. It means the Users code, the business-logic has to make sure - explicitly or implicitly - that the DTI is used as is appropriate for the business-logic. That can mean to either use People shall never relly on the timezone of a DTI. The timezone always needs to be checked in the business-logic! |
@heiglandreas One could argue that UTC is very much timezone agnostic. Please correct me but there is no place in the world where UTC applies. There are only places whose timezone sometimes correlates with UTC (depending on DST and other politic reasons). So if you care about a timezone you have to use As you said:
I'm totally with you. But in the sentence before you said:
A convention could also be "always return timezone |
In terms of timezones UTC is a timezone as any other. We made it "special" by convention. But all other timezones are equaly valid as they allow to transpose from one timezone to another one. The fact that we are so obsessed with UTC is that a lot if people fail to understand that any DTI created "now" will describe the same point in time, but not on earth. But depending on the use-case that inormation might be relevant but will get lost when we transpose it to UTC. That's why it should be part of the domain logic to make sure that your DTI has the - for your business-case - correct timezone. Which is nothing that the ClockInterface can determine per se. |
I'm not sure if I'm doing anything wrong here. <?php
$tz = new DateTimeZone('Europe/Berlin');
$utcTZ = new DateTimeZone('UTC');
// clock yields localtime
$startDate = new DateTimeImmutable('2021-03-28 02:30:00', $tz);
// DST transition
$endDate = new DateTimeImmutable('2021-03-28 03:00:00', $tz);
var_dump($endDate->getTimestamp() - $startDate->getTimestamp()); // int(-1800)
var_dump($endDate->setTimezone($utcTZ)->getTimestamp() - $startDate->setTimezone($utcTZ)->getTimestamp()); // int(-1800)
// clock yields UTC
$startDate = new DateTimeImmutable('2021-03-28 02:30:00', $utcTZ);
$endDate = new DateTimeImmutable('2021-03-28 03:00:00', $utcTZ);
var_dump($endDate->getTimestamp() - $startDate->getTimestamp()); // int(1800) I know that a monotonic timer like Edit: |
@heiglandreas Sure but there are a lot of PSRs that define behavior that could not determine per se like throwing Exceptions. But if an implementation violates the PSR this will be a bug (or you can call it "it is not following the PSR") Maybe we should open a PR with proposing Btw: I've personally learned a lot from reading this discussion and would would like to thank everyone participating in this topic. ❤️ I changed my mind a few times (e.g. from "return |
@bcremer this is an oddity that can happen when you create a DTI with a predefined DateTime. That case will never happen when you are using Beware that this is still a bigger issue currently on the end of DST. But again: only if you arecreating the DTI with a dateTime, not when initializing it with "now". |
Thank you all for this interesting (and civil!) debate. I would just like to add a single thought. Up until now it seems that @heiglandreas is debating his "TZ agnostic" point of view against two of the lib maintainers. This highlights that IMHO both sides are right, but Andreas is right on the PSR. Basically, what I mean is that the PSR should embody the "TZ agnostic" stance, and each maintainer could then go ahead and force their libraries to return the DTI with the TZ that they like, and be opinionated in their on way. This is the beauty of PSRs and such approach: there's a common interface, but there's enough leeway for implementors to move in their preferred way. Do you agree? |
@Jean85 Whatever the outcome of the PSR, one can indeed write an adapter from the PSR's To be fair, anything returning one of PHP's current date-time objects is out of the question for me, they're too big of a footgun to be used as is IMO. Regarding the subject at hand, I'm starting to think that I misunderstood the debate. I thought we were debating whether a Clock's contract was to return a point in time + a timezone (allowing it to compute a local date + local time, just like your wall clock). I was even ready to take arguments in this direction, as even though I try to fight any kind of global state, your DI container is usually under your control so if a "default" timezone should come from somewhere, that would be the place. But as I understand, everyone seems to agree with my initial view that what's important is not returning a local date-time (and therefore a timezone), but returning a point in time. Even @heiglandreas agrees that the timezone coming from So, please tell me, if you agree that we cannot rely on the timezone returned: why on earth do you want to return a
Or did I miss something? |
I followed this topic and I think both version (with and without timezone) should be implemented as interfaces. There are applications that are timezone aware and there are other that are don't need to be. Naming is not my strong point but I think we can have something like:
The timezone not aware version could be designed in a separate PR but I think we can benefit the traction of the current discussion and fit it all in. |
@BenMorel IIRC I said something along the lines of "Taking any DTI and not making sure that the timezone aligns with my BusinessLogic is an accident waiting to happen" As I already said, a point in time is a snapshot of the earths rotation or a defined number of seconds based on an atomic clock. Whether that point in time was taken in Boston, Bratislava or Bejing is irrelevant. They are all the same point in time. |
I haven't chimed in on this discussion, but there seems to be a lot of FUD going around about timezones. In order to do any kind of reliable date calculation, a timezone is a requirement. Whatever anyone's beef with
My opinion is that the timezone not be declared by this PSR and be left to implementation, and that there only be one interface in this PSR: public function now(): DateTimeImmutable; |
You should definitely look at https://github.com/aeon-php /cc @norberttech |
Hello, I'm interested implementing this interface in Carbon, but I would actually need it to be released as a |
@kylekatarnls the working group is active on our Discord in the #psr-20 channel: https:://discord.gg/php-fig Feel free to join and ask directly! IIRC they still have to create the package but I didn't see many more discussion happening, so the only other step forward would be an approval vote, once the code is ready and tested. |
So how is this PSR going, what are the next steps? |
According to the workflow the editor (@cseufert) and the sponsor needs to agree whether the PSR is ready for a Readiness Vote within the working group. Should that pass the Review-Phase starts which takes at least 4 weeks before an Acceptance Vote can take place. But before all that can happen, the MR #1230 needs to be merged ;-) |
Just for the record, I had to use a |
I see @BenMorel last made point. I would say that by returning a On the other side, there is huge value in terms of ease of use on returning a value object. This object is widely used by many people (even though I would argue that most projects use Carbon, Chronos, Brick or other flavour). So, here are my two cents: we can have two interfaces. This is perfectly possible. If anyone sees a problem with using two interfaces, happy to discuss. interface ClockInterface
{
public function now(): \DateTimeImmutable
}
interface UnixClockInterface
{
public function unixNow(): int
} Implementors can implement both or just one. Developers type-hint to whatever they need. If they need Let's empower people so they can make their own choices and not to fall into a false dichotomy when it is perfectly possible to do both. |
Also, a |
As one can call |
Proposed new PSR for defining a ClockInterface that reads the clock.