Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Clock] Add ClockAwareTrait to help write time-sensitive classes #48362

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

Merged
merged 1 commit into from
Dec 5, 2022

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Nov 28, 2022

Q A
Branch? 6.3
Bug fix? no
New feature? yes
Deprecations? no
Tickets -
License MIT
Doc PR -

Add the trait and use $this->now() inside the class, autowiring will do the magic 💪

@carsonbot carsonbot added this to the 6.2 milestone Nov 28, 2022
@nicolas-grekas nicolas-grekas force-pushed the time-aware branch 2 times, most recently from dc8a762 to b1a36b0 Compare November 28, 2022 14:39
@chalasr
Copy link
Member

chalasr commented Nov 28, 2022

I wouldn't use this trait myself (given the benefits over a regular constructor parameter is not that significant IMHO) and I think there is no need to rush, better discuss this for 6.3.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Nov 28, 2022

Now targeting 6.3. About adding a constructor argument for the clock, I think many will find that overkill and will decide to just not make their clock injectable. That just happened to me. Relying on the ambient clock is too convenient to warrant the boilerplate of adding a clock everywhere.

But making it settable through the trait does reduce this design overhead. It provides a DX closer to the current ambient one, without scarifying anything that DI provides (testability/etc).

Note that I'm not saying that everybody should use the trait after this is possibly merged. I'm just proposing a way to reduce the friction of using a clock, thus improving the adoption of the concept.

Another alternative to solve the DX concern would be to provide a global static clock. That's why e.g. Chronos are so convenient to use. But I'm not ready yet to give up to DI :)

@wouterj
Copy link
Member

wouterj commented Nov 28, 2022

As this was discussed in a private chat rather than GitHub, I'll list my feelings about adding a class like this, so everyone can take this in consideration for 6.3 :)

Looking at it from impact on actual code, the approach can be compared as:

Current situationWith trait
  class Store
  {
      public function __construct(
          // ...
+         private ClockInterface $clock
      ) {
      }

      public function isOpen(): bool
      {
-         $now = new \DateTimeImmutable();
+         $now = $this->clock->now();
  
          return $this->openTime > $now && ...;
      }
  }
  class Store
  {
+     use ClockAwareTrait;

      public function __construct(
          // ...
      ) {
      }

      public function isOpen(): bool
      {
-         $now = new \DateTimeImmutable();
+         $now = $this->now();
  
          return $this->openTime > $now && ...;
      }
  }

I fail to see the actual difference. In my opinion, if the DX on the left side is bad, so would the DX on the right side be.

On a design point of view, the trait is contradicting a few things that I consider important. Important enough to at least raise them in this discussion: 😉

  • The internal API of the object now covers more concerns than the concerns of the object itself with the addition of a now().
  • It advocates the use of setter injection (and the need of ?=) whereas constructor injection is recommended in these cases.
  • It hides dependencies of this class.

Note that these points are not only important from a design point of view, but also affect cognitive load of the class. Using constructor injection, it is immediately clear by reading a single file (a) who is responsible for providing current time and (b) how to change the clock implementation.

From a Symfony maintainer perspective it is unclear to me what is special about the clock service compared to other services. The only class I recall that comes close to "facade methods" like used here is AbstractController, and we've recently became more strict on what facade methods to allow there. If we were to accept a trait like this, would we accept e.g. EventDispatcherAwareTrait::dispatch()?

At last, there is nothing wrong with using new DateTimeImmutable() in your code. Using any clock/time library instead of new DateTimeImmutable() is useful to make your classes easier to test. I feel like education (talks, blog posts, documentation) in how to test time-based logic in much more of a key factor of adaption here than a small bit of boilerplate.
And the great thing about having 101 libraries doing mostly the same thing: people can decide whether they want the DI-based approach (e.g. symfony/clock) or the global state based approach (e.g. Chronos).

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Nov 28, 2022

Thanks for writing your thoughts.

  • About internal API, having a property or a method is the same, in terms of concerns managed by of the object.
  • About setter injection, #[Required] makes them just fine, allowing to differentiate instantiation-time methods vs the others. Nothing is hidden, quite the contrary.
  • About $this->now(); vs $this->clock->now();, people do use DateTime instead DateTimeImmutable because it is boring to type, even with IDE autocompletion.
  • About constructor injection, this is a significant overhead to me, vs doing nothing when using new DateTimeImmutable inline. I don't mean from a typing pov, but from a dependency management pov: now, I have another argument to deal with when e.g. refactoring the order of my arguments + all corresponding tests.
  • The setter injection is perfect for the test-ability need: usually do not care, it's not even needed to have DI in place, but in a time-sensitive test case, there is a door to mock the clock. Constructor injection puts this possibility on the front door, while only a side door is needed.

Also, I think using new DateTimeImmutable is not fine. In our own code bases, I see a weird mix of DateTime, DateTimeInterface and DateTimeImmutable, and that's not really fun to deal with. I'd better have all places use a time factory and make everything consistent and easier to reason about. But if this goes with costly boilerplate, it will never happen.

@GromNaN
Copy link
Member

GromNaN commented Nov 30, 2022

I don't like setter injection because it allows to update the service instances after their initialization. This is too risked for side-effects, especially in tests if the DIC is reused between tests (for performances) and the clock is updated on some of them (with a different mocked value).

From @wouterj's example, the clock can easily be optional as last argument of the constructor.

      public function __construct(
          // ...
-         private ClockInterface $clock
+         private readonly ClockInterface $clock = new NativeClock()
      ) {
      }

@stof
Copy link
Member

stof commented Dec 1, 2022

@GromNaN as the trait defines the property as readonly, you can only call the setter once (the second time will get an error from PHP)

@GromNaN
Copy link
Member

GromNaN commented Dec 1, 2022

Oh, good point @stof, the problem I am raising may not exist. The service instance cannot be updated.

@GromNaN
Copy link
Member

GromNaN commented Dec 4, 2022

This trait could be used for changes like #48098. Would we use it internally?

@nicolas-grekas
Copy link
Member Author

It'd make sense to me yes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants