isValidYearName($name)) { throw new InvalidArgumentException('School year must use YYYY-YYYY with consecutive years.'); } $existing = $this->schoolYearModel->where('name', $name); if ($exceptId !== null) { $existing->where('id !=', $exceptId); } if ($existing->first() !== null) { throw new InvalidArgumentException('That school year already exists.'); } $startsOn = $this->dateOrNull($payload['starts_on'] ?? null); $endsOn = $this->dateOrNull($payload['ends_on'] ?? null); if ($startsOn !== null && $endsOn !== null && $startsOn >= $endsOn) { throw new InvalidArgumentException('School year start date must be before the end date.'); } $registrationStarts = $this->dateOrNull($payload['registration_starts_on'] ?? null); $registrationEnds = $this->dateOrNull($payload['registration_ends_on'] ?? null); if ($registrationStarts !== null && $registrationEnds !== null && $registrationStarts > $registrationEnds) { throw new InvalidArgumentException('Registration start date must be on or before the registration end date.'); } } public function isValidYearName(string $value): bool { if (! preg_match('/^(\d{4})-(\d{4})$/', $value, $matches)) { return false; } return (int) $matches[2] === (int) $matches[1] + 1; } private function dateOrNull(mixed $value): ?DateTimeImmutable { $value = trim((string) $value); if ($value === '') { return null; } $date = DateTimeImmutable::createFromFormat('!Y-m-d', $value); if (! $date instanceof DateTimeImmutable || $date->format('Y-m-d') !== $value) { throw new InvalidArgumentException('Dates must use YYYY-MM-DD.'); } return $date; } }