> $weeks */ private function __construct( public \DateTimeImmutable $monthStart, public \DateTimeImmutable $gridStart, public \DateTimeImmutable $gridEnd, public array $weeks, ) { } /** * The grid containing $date, with "today" injectable so the flags can be tested without * waiting for the calendar to roll over. BookingHorizon::latest() takes the same optional * argument for the same reason. */ public static function around(\DateTimeImmutable $date, ?\DateTimeImmutable $today = null): self { $today = ($today ?? new \DateTimeImmutable('today'))->setTime(0, 0); $monthStart = $date->setTime(0, 0)->modify('first day of this month'); // "monday this week" is a no-op when the month already starts on a Monday, so this is the // whole leading-days calculation -- no weekday arithmetic needed. $gridStart = $monthStart->modify('monday this week'); $weeks = []; $cursor = $gridStart; for ($week = 0; $week < self::WEEKS; ++$week) { $days = []; for ($weekday = 0; $weekday < 7; ++$weekday) { $days[] = new MonthDay( date: $cursor, inMonth: $cursor->format('Y-m') === $monthStart->format('Y-m'), isToday: $cursor == $today, isPast: $cursor < $today, isWeekend: (int) $cursor->format('N') >= 6, ); $cursor = $cursor->modify('+1 day'); } $weeks[] = $days; } // The cursor has already stepped past the grid, hence the day back. return new self($monthStart, $gridStart, $cursor->modify('-1 day'), $weeks); } }