ISO weekdays */ public function days(): array { $days = Settings::get(self::KEY_DAYS, self::DEFAULT_DAYS); if (! is_array($days)) { return self::DEFAULT_DAYS; } // Filtered rather than trusted: this is read on a schedule, and a // malformed setting must not be able to mean "every day" by accident. $days = array_values(array_unique(array_filter( array_map('intval', $days), fn (int $day) => $day >= 1 && $day <= 7, ))); return $days === [] ? self::DEFAULT_DAYS : $days; } public function from(): string { return $this->time(self::KEY_FROM, self::DEFAULT_FROM); } public function to(): string { return $this->time(self::KEY_TO, self::DEFAULT_TO); } private function time(string $key, string $default): string { $value = (string) Settings::get($key, $default); return preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $value) === 1 ? $value : $default; } /** * Is now inside the window? * * On the wall clock, not UTC (R19): an operator who writes 05:00 means five * in the morning where they live, and a window that drifts by an hour twice * a year is a window nobody trusts. * * A window that ends before it starts wraps past midnight — 23:00 to 02:00 * is a perfectly reasonable thing to ask for, and reading it as an empty * range would silently mean "never". */ public function isOpen(?Carbon $at = null): bool { $now = ($at ?? Carbon::now())->local(); if (! in_array((int) $now->isoWeekday(), $this->days(), true)) { // …unless the window wraps and we are in the tail of yesterday's. return $this->wraps() && in_array((int) $now->copy()->subDay()->isoWeekday(), $this->days(), true) && $now->format('H:i') < $this->to(); } $clock = $now->format('H:i'); $from = $this->from(); $to = $this->to(); return $this->wraps() ? ($clock >= $from || $clock < $to) : ($clock >= $from && $clock < $to); } private function wraps(): bool { return $this->to() <= $this->from(); } }