Lines 89.18% 33 / 37
Methods 70.00% 7 / 10
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 getId 100.00% 1 / 1 100.00% 1 / 1 1
 cancel 83.33% 5 / 6 0.00% 0 / 1 2.02
 isActive 100.00% 1 / 1 100.00% 1 / 1 1
 getNextRunAt 100.00% 1 / 1 100.00% 1 / 1 2
 rearmFromCache 100.00% 1 / 1 100.00% 1 / 1 1
 rearmFor 100.00% 11 / 11 100.00% 1 / 1 4
 startFollowing 80.00% 4 / 5 0.00% 0 / 1 2.03
 createScheduleFor 100.00% 5 / 5 100.00% 1 / 1 4
 runIfTimeIsCurrent 60.00% 3 / 5 0.00% 0 / 1 2.26
25final class EntityTimeTask implements ScheduledTask
26{
27    private EntityTime $armedTime;
28
29    private ?ScheduledTask $inner = null;
30
31    private ?Subscription $following = null;
32
33    private bool $cancelled = false;
34
35    /**
36     * @param Closure(ScheduledRun): void $handler
37     * @param Closure(self): void $onCancel
38     */
39    public function __construct(
40        private readonly string $id,
41        private readonly EntityId $entityId,
42        public readonly ResourceScope $scope,
43        private readonly LoggerInterface $logger,
44        private readonly Closure $handler,
45        private readonly ScheduleRegistry $registry,
46        private readonly StateCache $states,
47        private readonly Clock $clock,
48        private readonly Closure $onCancel,
49    ) {
50        $this->armedTime = EntityTime::none();
51    }
52
53    public function getId(): string
54    {
55        return $this->id;
56    }
57
58    public function cancel(): void
59    {
60        if ($this->cancelled) {
61            return;
62        }
63
64        $this->cancelled = true;
65        $this->inner?->cancel();
66        $this->following?->unsubscribe();
67        ($this->onCancel)($this);
68    }
69
70    public function isActive(): bool
71    {
72        return !$this->cancelled;
73    }
74
75    public function getNextRunAt(): ?Instant
76    {
77        return $this->cancelled ? null : $this->inner?->getNextRunAt();
78    }
79
80    public function rearmFromCache(): void
81    {
82        $this->rearmFor($this->states->find($this->entityId));
83    }
84
85    public function rearmFor(?EntityState $state): void
86    {
87        $time = EntityTime::fromEntityState($state, $this->clock->getTimeZone());
88
89        if ($this->cancelled || $time->equals($this->armedTime)) {
90            return;
91        }
92
93        $this->inner?->cancel();
94        $this->inner = null;
95        $this->armedTime = $time;
96        $schedule = $this->createScheduleFor($time);
97
98        if ($schedule === null) {
99            $this->logger->debug('Entity holds no upcoming time; waiting for its next change', ['entity' => $this->entityId->value]);
100
101            return;
102        }
103
104        $this->inner = $this->registry->armUnderTaskId($this->id, $this->scope, $schedule, $this->logger, $this->runIfTimeIsCurrent(...));
105    }
106
107    public function startFollowing(StateChangeStream $changes): void
108    {
109        $this->following = $changes->subscribe(function (StateChange $change): void {
110            $this->rearmFor($change->to);
111        });
112
113        if ($this->cancelled) {
114            $this->following->unsubscribe();
115        }
116    }
117
118    private function createScheduleFor(EntityTime $time): ?WallClockSchedule
119    {
120        if ($time->dailyAt !== null) {
121            return CalendarSchedule::dailyAt($time->dailyAt);
122        }
123
124        if ($time->moment !== null && $time->moment->isAfter($this->clock->getNow())) {
125            return OneShotSchedule::fromMoment($time->moment->toDateTime($this->clock->getTimeZone()));
126        }
127
128        return null;
129    }
130
131    private function runIfTimeIsCurrent(ScheduledRun $run): void
132    {
133        $current = $this->states->find($this->entityId);
134
135        // A paused app misses state changes, so the cache decides whether this run is still wanted.
136        if (!EntityTime::fromEntityState($current, $this->clock->getTimeZone())->equals($this->armedTime)) {
137            $this->rearmFor($current);
138
139            return;
140        }
141
142        ($this->handler)(new ScheduledRun($this, $run->scheduledFor, $run->firedAt, $run->missedOccurrences));
143    }
144}