Files
alrahma_sunday_school/app/Services/SchoolYearContextService.php
T
root 1ef2800f12
Tests / PHPUnit (push) Successful in 1m26s
fix enrollment for the new school-year
2026-08-07 23:43:31 -04:00

276 lines
8.5 KiB
PHP

<?php
namespace App\Services;
use App\Exceptions\SchoolYear\InvalidSchoolYearSelectionException;
use App\Exceptions\SchoolYear\SchoolYearConfigurationException;
use App\Exceptions\SchoolYear\SchoolYearNotFoundException;
use App\Models\SchoolYearModel;
use App\Support\SchoolYear\SchoolYearStatus;
use App\Support\SchoolYear\SchoolYearContext;
use CodeIgniter\HTTP\IncomingRequest;
final class SchoolYearContextService
{
public function __construct(
private readonly SchoolYearModel $schoolYearModel,
private readonly SchoolYearAccessPolicy $accessPolicy = new SchoolYearAccessPolicy(),
) {
}
public function resolve(
IncomingRequest $request,
?int $routeSchoolYearId = null
): SchoolYearContext {
$requestedId = $routeSchoolYearId ?? $this->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,
);
}
}