Course Module Navigation
Overview
⚙ What
Adds a navigation block to the module-detail API response (GET /api/v1/courses/{courseId}/modules/{moduleId}) carrying the previous and next reachable activity in whole-course order. Each link exposes the module's id, name, and modname so the frontend can render prev/next controls.
⏰ When
Runs synchronously during every module-detail request. The ModuleNavigationService is invoked after the module is resolved and authorized, walking outward from the current module to find the nearest visible and available neighbours. Best-effort: failures yield null links, never a 500.
★ Why
The PM requested the "previous / next" component on course module pages (matching Moodle's activity navigation). The endpoint previously returned no navigation data, leaving the frontend with nothing to render the control from. This is a backend-only change enabling the frontend team to build the UI.
📁 Where
Lives in app/Modules/Course/. New files: DTOs/ModuleNavLinkDTO, DTOs/ModuleNavigationDTO, Services/ModuleNavigationService. Modified: Controllers/ModuleController, Resources/ModuleDetailResource. Reuses the shared CourseModinfoService and Availability module.
🛠 How
The controller calls ModuleNavigationService::resolve(), which fetches the cached whole-course structure via CourseModinfoService, locates the current module's index, then walks outward in both directions (-1 for previous, +1 for next). Each candidate is tested for reachability: visible, not deleted, not a skip-type (label, attendance), and passes the AvailabilityService evaluation. Course-wide completion/section maps are pre-computed once and shared across all evaluations so cm=-1 resolution works correctly across section boundaries. The result is attached to ModuleDetailResource via withNavigation() and serialized as a navigation JSON block.
Sequence Diagram
sequenceDiagram
participant B as Browser
participant C as ModuleController
participant R as ShowModuleRequest
participant MS as ModuleService
participant NS as ModuleNavigationService
participant MI as CourseModinfoService
participant AS as AvailabilityService
participant DB as Database
participant RES as ModuleDetailResource
B->>C: GET /api/v1/courses/{courseId}/modules/{moduleId}
C->>R: validate & authorize
R-->>C: ShowModuleDTO
C->>MS: show(dto)
MS->>DB: query course_modules + relations
DB-->>MS: MoodleCourseModule
MS-->>C: module
C->>NS: resolve(courseId, cmId, studentId)
NS->>MI: get(courseId)
MI->>DB: query course_sections, course_modules (cached)
DB-->>MI: CourseModinfoDTO
MI-->>NS: modinfo
NS->>DB: query course_modules_completion (bulk)
DB-->>NS: completionMap
Note over NS: Walk backward from current index
loop Find previous
NS->>AS: evaluate(availability, context)
AS-->>NS: AvailabilityResult
end
Note over NS: Walk forward from current index
loop Find next
NS->>AS: evaluate(availability, context)
AS-->>NS: AvailabilityResult
end
NS-->>C: ModuleNavigationDTO(previous, next)
C->>RES: new ModuleDetailResource(module)
C->>RES: withNavigation(navigation)
RES-->>C: JSON response with navigation block
C-->>B: 200 { data: { ..., navigation: { previous, next } } }
Flowchart
flowchart TD
A[GET /courses/courseId/modules/moduleId] --> B{FormRequest valid?}
B -- No --> B1[422 Validation Error]
B -- Yes --> C[ModuleService::show]
C --> D{Module found?}
D -- No --> D1[404 Not Found]
D -- Yes --> E{Module available?}
E -- No --> E1[423 Locked]
E -- Yes --> F[Dispatch ModuleViewed event]
F --> G[Get completion record]
G --> H[ModuleNavigationService::resolve]
H --> I[Fetch modinfo from cache]
I --> J{Current module in list?}
J -- No --> K[Return empty navigation]
J -- Yes --> L[Build shared context maps]
L --> M[Walk backward for previous]
M --> N{Candidate reachable?}
N -- "deleted / hidden / label" --> M
N -- "availability locked" --> M
N -- "boundary reached" --> O[previous = null]
N -- Yes --> P[previous = NavLinkDTO]
O --> Q[Walk forward for next]
P --> Q
Q --> R{Candidate reachable?}
R -- "deleted / hidden / label" --> Q
R -- "availability locked" --> Q
R -- "boundary reached" --> S[next = null]
R -- Yes --> T[next = NavLinkDTO]
S --> U[Build ModuleDetailResource]
T --> U
K --> U
U --> V[Attach withNavigation + withFilters]
V --> W[200 JSON Response]
style B1 fill:#ef4444,color:#fff
style D1 fill:#ef4444,color:#fff
style E1 fill:#f59e0b,color:#fff
style W fill:#10b981,color:#fff
style K fill:#64748b,color:#fff
Files Changed
| File Path | Layer | Description |
|---|---|---|
| app/Modules/Course/DTOs/ModuleNavLinkDTO.php | DTO | New — final readonly DTO carrying a single nav link (id, name, modname) |
| app/Modules/Course/DTOs/ModuleNavigationDTO.php | DTO | New — carries previous + next (nullable) nav links; has empty() factory |
| app/Modules/Course/Services/ModuleNavigationService.php | Service | New — resolves nearest reachable neighbours via outward walk, builds shared availability context, evaluates visibility + availability per candidate |
| app/Modules/Course/Resources/ModuleDetailResource.php | Resource | Modified — added withNavigation() method and navigation block in toArray() |
| app/Modules/Course/Controllers/ModuleController.php | Controller | Modified — injects ModuleNavigationService, calls resolve(), passes result via withNavigation() |
| tests/Feature/Course/ModuleNavigationTest.php | Test | New — 7 feature tests covering course order, boundaries, hidden/label skip, availability lock, cross-section navigation |
| docs/openapi.yaml | Docs | Modified — documents the navigation block in the module response schema |
| docs/postman_collection.json | Docs | Modified — updated example response and test script to include navigation |
Rules Applied
- Architecture — Thin controllers:
ModuleController::show()stays under 15 lines; all navigation logic lives inModuleNavigationService. No business logic in the controller. - Architecture — Layer separation: Controller → Service → Repository/Model flow respected. The service orchestrates modinfo + availability; the resource handles serialization; the controller wires them together.
- Architecture — DTOs between layers:
ModuleNavLinkDTOandModuleNavigationDTOarefinal readonlyclasses with typed constructor-promoted properties, carrying data from service to resource layer. - Architecture — Non-fatal navigation:
resolve()wraps the entire build in a try/catch returningModuleNavigationDTO::empty()on any failure, ensuring navigation never causes a 500 on the module endpoint. - Coding style — PHP 8.3 features:
final readonlyDTOs, typed properties, constructor promotion,match-style const arrays, named arguments, strict types declared in every file. - Coding style — Laravel first: Uses
Collection::mapWithKeys()for completion map,array_maponly on simple value arrays.Cachefacade used by modinfo.DB::table()and Eloquent for all queries. - Coding style — PHPDoc: Every class and public method has a brief PHPDoc block.
@throwsomitted where the method explicitly catches all throwables internally. - Coding style — final by default: All new classes (
ModuleNavLinkDTO,ModuleNavigationDTO,ModuleNavigationService) and the test class arefinal. - Coding style — Naming conventions: Service suffix on service class, DTO suffix on DTOs,
test_it_prefix on test methods, camelCase methods, PascalCase classes. - Testing — Feature tests with real DB: Uses
RefreshDatabasetrait. Seeds Moodle tables viaDB::table()->insert()(no factories for Moodle models). Tests the full HTTP request/response cycle. - Testing — AAA pattern: Every test follows Arrange → Act → Assert with clear separation and one assertion concept per test.
- Testing — Coverage: 7 tests covering: happy path (prev+next), boundary nulls (first/last), label skip, hidden skip, availability lock skip, cross-section navigation.
- Security — Constructor injection:
ModuleNavigationServicereceivesCourseModinfoServiceInterfaceandAvailabilityServicevia constructor DI. Noapp()calls. - Security — Moodle read-only: Navigation queries only read from Moodle tables (course_modules, course_sections, course_modules_completion). No writes to Moodle data.
- Architecture — Content filters:
ModuleDetailResourcecontinues to callwithFilters()for all HTML fields through the filter pipeline, as required by the content filters rule. - Git workflow — Conventional commits: Feature scoped to
coursemodule, single logical change, tests included with implementation.