requestedSchoolYearId($request); $requestedName = $this->legacyYearName($request); $userId = (int) (session()->get('user_id') ?? 0); if ($requestedId !== null && $requestedName !== '') { $row = $this->schoolYearModel->find($requestedId); if ($row === null || (string) ($row['name'] ?? '') !== $requestedName) { throw new InvalidSchoolYearSelectionException('Selected school-year parameters conflict.'); } return $this->authorizedContext($row, $userId, true); } if ($requestedId !== null) { $row = $this->schoolYearModel->find($requestedId); if ($row === null) { throw new SchoolYearNotFoundException('Selected school year was not found.'); } return $this->authorizedContext($row, $userId, true); } if ($requestedName !== '') { $row = $this->schoolYearModel ->where('name', $requestedName) ->first(); if ($row === null) { throw new SchoolYearNotFoundException('Selected school year was not found.'); } return $this->authorizedContext($row, $userId, true); } $sessionYearId = session('selected_school_year_id'); if (is_numeric($sessionYearId)) { $row = $this->schoolYearModel->find((int) $sessionYearId); if ($row !== null && $this->isSelectable($row, $userId)) { return $this->fromRow($row, true); } $this->clearSelection(); } return $this->active(); } public function selectableYears(bool $includeDraft = false): array { $builder = $this->schoolYearModel ->orderBy('name', 'DESC') ->orderBy('id', 'DESC'); if (! $includeDraft) { $builder->where('status !=', SchoolYearStatus::DRAFT); } $userId = (int) (session()->get('user_id') ?? 0); return array_values(array_filter( $builder->findAll(), fn (array $row): bool => $this->isSelectable($row, $userId) )); } public function select(int $schoolYearId): SchoolYearContext { $row = $this->schoolYearModel->find($schoolYearId); if ($row === null) { throw new SchoolYearNotFoundException('Selected school year was not found.'); } $userId = (int) (session()->get('user_id') ?? 0); $context = $this->authorizedContext($row, $userId, true); $previousSelection = session('selected_school_year_id'); if ($context->isActive()) { $this->clearSelection(); $context = $this->fromRow($row, false); } else { session()->set('selected_school_year_id', $context->id()); session()->remove('selected_school_year'); } if ((string) $previousSelection !== (string) session('selected_school_year_id')) { $this->clearDependentSessionState(); } return $context; } public function forYearName(string $yearName): SchoolYearContext { $yearName = trim($yearName); if ($yearName === '') { throw new SchoolYearNotFoundException('Selected school year was not found.'); } $row = $this->schoolYearModel ->where('name', $yearName) ->first(); if ($row === null) { throw new SchoolYearNotFoundException('Selected school year was not found.'); } $userId = (int) (session()->get('user_id') ?? 0); return $this->authorizedContext($row, $userId, true); } public function clearSelection(): void { session()->remove('selected_school_year_id'); session()->remove('selected_school_year'); $this->clearDependentSessionState(); } public function active(): SchoolYearContext { $active = $this->schoolYearModel->active(); if ($active !== null) { return $this->fromRow($active, false); } $closing = $this->schoolYearModel ->where('status', SchoolYearStatus::CLOSING) ->orderBy('closing_started_at', 'DESC') ->orderBy('id', 'DESC') ->findAll(2); if (count($closing) !== 1) { throw new SchoolYearConfigurationException('Exactly one active school year must be configured.'); } return $this->fromRow($closing[0], false); } private function normalizeInt(mixed $value): ?int { if ($value === null || $value === '' || ! ctype_digit((string) $value)) { return null; } return (int) $value; } private function requestedSchoolYearId(IncomingRequest $request): ?int { $ids = []; foreach (['school_year_id', 'schoolYearId', 'year_id'] as $key) { $value = $this->normalizeInt($request->getGet($key)); if ($value !== null) { $ids[$key] = $value; } if ($this->isWriteRequest($request)) { $value = $this->normalizeInt($request->getPost($key)); if ($value !== null) { $ids['post:' . $key] = $value; } } } if (count(array_unique($ids)) > 1) { throw new InvalidSchoolYearSelectionException('Conflicting school-year parameters were provided.'); } return $ids === [] ? null : (int) reset($ids); } private function legacyYearName(IncomingRequest $request): string { $names = []; foreach (['school_year', 'schoolYear', 'year'] as $key) { $value = trim((string) ($request->getGet($key) ?? '')); if ($value !== '') { $names[$key] = $value; } if ($this->isWriteRequest($request)) { $value = trim((string) ($request->getPost($key) ?? '')); if ($value !== '') { $names['post:' . $key] = $value; } } } if (count(array_unique($names)) > 1) { throw new InvalidSchoolYearSelectionException('Conflicting school-year parameters were provided.'); } if (isset($names['schoolYear']) || isset($names['year'])) { log_message('notice', 'Legacy school-year request alias used: {aliases}', [ 'aliases' => implode(',', array_keys($names)), ]); } return $names === [] ? '' : (string) reset($names); } private function isWriteRequest(IncomingRequest $request): bool { return in_array(strtoupper($request->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'], true); } private function authorizedContext(array $row, int $userId, bool $explicit): SchoolYearContext { if (! $this->isSelectable($row, $userId)) { throw new SchoolYearNotFoundException('Selected school year was not found.'); } return $this->fromRow($row, $explicit); } private function isSelectable(array $row, int $userId): bool { return $this->accessPolicy->canSelect($userId > 0 ? $userId : null, $row); } private function clearDependentSessionState(): void { session()->remove([ 'class_section_id', 'semester', 'active_semester', 'active_school_year', 'teacher_scores_selected_semester', 'grading_selected_semester', ]); } private function fromRow(array $row, bool $explicit): SchoolYearContext { return new SchoolYearContext( id: (int) $row['id'], yearName: (string) $row['name'], status: strtolower((string) $row['status']), explicitSelection: $explicit, ); } }