Calendar Module
5W Analysis
What
A read-only Calendar module that exposes Moodle calendar events to authenticated students via two REST endpoints. Supports paginated listing with optional date-range filters and single-event detail retrieval.
When
Triggered on every authenticated HTTP request to GET /api/v1/calendar/events or GET /api/v1/calendar/events/{id}. Events are filtered at query-time: only visible = 1 records are returned. Optional start_date and end_date parameters further narrow results by timestart. Results are ordered by timesort ascending.
Why
Students need a single unified view of upcoming deadlines, course events, and personal reminders without logging into Moodle. This module bridges Moodle's event data to the new student portal, respecting Moodle's visibility and enrolment rules without duplicating or altering any Moodle table.
Where
- Module:
app/Modules/Calendar/ - Shared model:
app/Shared/Models/MoodleCalendarEvent.php - Moodle source table:
mdl_event(read-only) - Routes: registered via
app/Modules/Calendar/routes.php - Tests:
tests/Feature/Calendar/andtests/Unit/Services/
How
The request flows through the standard layered architecture: Route → Middleware (auth:sanctum + moodle.active) → EventController → ListEventsRequest / ShowEventDTO → CalendarService → EventRepository → MoodleCalendarEvent.
The core mechanism is a multi-branch OR scope inside EventRepository::applyStudentScope() that determines which events the authenticated student can see:
- User events —
eventtype = 'user' AND userid = $studentId - Site events —
eventtype = 'site'(visible to all) - Course / module events —
eventtype IN ('course','due','open','close') AND courseid IN (enrolled courses)via a subquery onuser_enrolments JOIN enrol - Category events —
eventtype = 'category' AND categoryid IN (categories of enrolled courses) - Group events —
eventtype = 'group' AND groupid IN (groups the student belongs to)
Table aliases (ue, e, c, gm) are used in all subqueries to remain DB_TABLE_PREFIX–agnostic. The CalendarEventResource normalises Moodle's Unix timestamp integers to ISO 8601 strings and converts Moodle sentinel values (zero / empty string) to null.
Sequence Diagram
Request Flowchart
Files Changed
| File Path | Layer | Description |
|---|---|---|
| app/Shared/Models/MoodleCalendarEvent.php | Model | Read-only Eloquent model for Moodle's event table. Extends MoodleModel (write-protected), casts Unix timestamps, defines course and user BelongsTo relationships. |
| app/Modules/Calendar/Enums/CalendarEventType.php | Enum | PHP 8.1 string-backed enum mapping all eight Moodle event type values (user, site, course, group, category, due, open, close) used in query scoping. |
| app/Modules/Calendar/Enums/CalendarErrorCode.php | Enum | Integer-backed enum for Calendar module error codes. Defines NotFound = 4001 returned in the API envelope's code field on 404 responses. |
| app/Modules/Calendar/Exceptions/CalendarEventNotFoundException.php | Exception | Domain exception thrown when an event is not found or not accessible by the student. Carries a CalendarErrorCode readonly property, maps to HTTP 404. |
| app/Modules/Calendar/DTOs/ListEventsDTO.php | DTO | final readonly DTO carrying validated list query parameters (studentId, optional Carbon start/end dates, perPage) from the FormRequest to the service layer. |
| app/Modules/Calendar/DTOs/ShowEventDTO.php | DTO | final readonly DTO carrying eventId and studentId for single-event detail lookups. Constructed inline in the controller from the route parameter and authenticated user. |
| app/Modules/Calendar/Requests/ListEventsRequest.php | Request | FormRequest validating start_date, end_date (must be ≥ start_date), and per_page (1–100). Provides toDTO() that builds a ListEventsDTO using Carbon for date parsing. |
| app/Modules/Calendar/Repositories/EventRepositoryInterface.php | Repository | Contract defining listForStudent(ListEventsDTO) and findForStudent(int, int). Bound in AppServiceProvider — service depends on this interface, not the concrete class. |
| app/Modules/Calendar/Repositories/EventRepository.php | Repository | Queries MoodleCalendarEvent with a multi-branch OR scope (applyStudentScope) covering personal, site, course/module, category, and group events. Uses table aliases in subqueries to be prefix-agnostic. Translates ModelNotFoundException to CalendarEventNotFoundException. |
| app/Modules/Calendar/Services/CalendarService.php | Service | Business logic orchestrator. Accepts ListEventsDTO or ShowEventDTO and delegates to EventRepositoryInterface. Thin by design — all query complexity lives in the repository. |
| app/Modules/Calendar/Resources/CalendarEventResource.php | Resource | Extends ApiResource. Maps Moodle model fields to camelCase API keys, converts Unix timestamps to ISO 8601 via Carbon, and normalises Moodle sentinel values (0, empty string) to null. |
| app/Modules/Calendar/Controllers/EventController.php | Controller | Thin controller (≤15 lines per method). index delegates to service via ListEventsRequest::toDTO(). show builds ShowEventDTO inline, catches CalendarEventNotFoundException, returns 404 with error code. |
| app/Modules/Calendar/routes.php | Route | Registers GET /v1/calendar/events and GET /v1/calendar/events/{id} under auth:sanctum + moodle.active middleware. Named routes follow api.v1.calendar.* convention. |
| app/Providers/AppServiceProvider.php | Provider | Binds EventRepositoryInterface → EventRepository in the service container so CalendarService receives the concrete implementation via constructor injection. |
| routes/api.php | Route | Loads the Calendar module's routes.php into the main API router, registering endpoints under the /api prefix. |
| tests/Feature/Calendar/CalendarEventTest.php | Test | 18 feature tests covering both endpoints: 401 on unauthenticated, envelope structure, site/personal/course/module event visibility, invisible event exclusion, date-range filtering, pagination, resource shape, 404 for inaccessible events, and Moodle sentinel value normalisation. |
| tests/Unit/Services/CalendarServiceTest.php | Test | 3 unit tests for CalendarService: verifies list() delegates to repository, show() returns the correct model, and show() re-throws CalendarEventNotFoundException from the repository. |
| docs/openapi.yaml | Docs | Calendar endpoints appended to the project OpenAPI 3.0 spec: both GET paths with query parameters, response schemas, and error codes documented. |
| docs/postman_collection.json | Docs | Calendar folder added to Postman collection with requests for both endpoints, example responses for 200/404/422/401, and test scripts validating envelope shape. |
Rules Applied
Architecture
- Strict layer separation: Route → Middleware → Controller → FormRequest → Service → Repository → Model (no layer skipping)
- Controller methods are 5–15 lines; all logic delegated to
CalendarService - Repository pattern with an interface (
EventRepositoryInterface) bound in AppServiceProvider - Module is self-contained under
app/Modules/Calendar/with its own Controllers, Services, Repositories, DTOs, Enums, Exceptions, Resources, and routes - Shared model (
MoodleCalendarEvent) lives inapp/Shared/Models/as a read-only Moodle integration model - API versioned under
/api/v1/; kebab-case URIs; plural nouns (/events) - Standard response envelope:
{ success, message, data, meta, errors, code }
Shared Database
MoodleCalendarEventextendsMoodleModel— write operations throw exceptions (read-only guard enforced at model level)- Table name set to bare
'event';DB_TABLE_PREFIXapplied automatically by Laravel - No migrations — the
mdl_eventtable is Moodle-owned and never altered - Subqueries use table aliases (
ue,e,c,gm) to remain prefix-agnostic - No mass-assignment on Moodle-sourced data; explicit column selection via Eloquent
Coding Style
- PHP 8.3 features:
enumfor event types and error codes,final readonlyfor DTOs, constructor promotion throughout,match-style typed casts - All classes are
finalby default - All properties and parameters have explicit types; no
mixed - Strict comparison (
===,!==) for sentinel value checks in Resource - Laravel helpers:
Carbon::parse(),Carbon::createFromTimestamp(),$request->filled(),$request->integer(),Builder::when(),Builder::tap() - PHPDoc on every class and public method with
@throwsdeclared where applicable - Named arguments used where clarity requires (e.g.,
eventId:,studentId:)
Security
- All endpoints protected by
auth:sanctum+moodle.activemiddleware — no public access - All input validation in
ListEventsRequestFormRequest — no inline controller validation - Student data isolation enforced at query level: the scope ensures a student can only ever see their own personal events, site events, and events from enrolled courses
- Foreign-key IDs validated via subquery existence checks (not client-trusted)
- No raw SQL; all queries use Eloquent query builder with parameter binding
- 404 response on inaccessible events (not 403) to prevent event-ID enumeration
Testing
- TDD workflow followed: tests written before implementation (Red → Green → Refactor)
- Feature tests use
RefreshDatabasewith real SQLite schemas mirroring Moodle tables - AAA pattern (Arrange / Act / Assert) in every test method
- All test methods prefixed
test_it_with descriptive names stating expected behavior - No mocking of Eloquent or Laravel internals;
Http::fake()andQueue::fake()for external concerns - 100% happy-path + all validation branches + all auth failures covered
- Unit tests isolate
CalendarServiceusing mockedEventRepositoryInterface
API Design
- HTTP 200 for successful GET, 404 for not found, 422 for validation errors, 401 for unauthenticated
- Pagination metadata in
metaenvelope field (current_page,per_page,total) - Error responses carry machine-readable
codefield (e.g.,4001) for client-side handling - Route names follow
api.v1.calendar.events.*dot-notation convention - Both endpoints documented in
docs/openapi.yamlanddocs/postman_collection.json