📋 5W Overview
🎯
What
  • GET /api/v1/students/me returns the authenticated student's full profile
  • Includes core Moodle user data: id, username, firstname, lastname, email
  • Includes all institution-defined custom profile fields with full metadata: shortname, name, datatype, description, value, dataformat
  • Read-only — no writes to any Moodle table
When
  • Triggered on every GET /api/v1/students/me HTTP request
  • Requires a valid Sanctum Bearer token — fired post-authentication, not at login
  • Eager-loads infoData.field only once per request (loadMissing)
  • Request lifecycle: Middleware → Controller → Service → Eloquent → Resource → Response
💡
Why
  • The login response exposes only a minimal token — students need their full profile for the portal UI
  • Moodle custom fields are institution-specific (student ID, department, etc.) and must be surfaced as first-class profile data
  • Frontend needs field metadata (datatype, description) to render type-aware UI controls
  • Separates profile concerns from auth — follows the single-responsibility principle per module
📁
Where
  • app/Modules/Student/ — new Student module (Controller, Service, Resource, routes)
  • app/Shared/Models/MoodleUserInfoField.php — new read-only model
  • app/Shared/Models/MoodleUserInfoData.php — new read-only model
  • app/Shared/Models/MoodleUser.php — extended with infoData() relationship
  • app/Providers/AppServiceProvider.php — singleton binding
  • routes/api.php — Student module routes loaded here
⚙️
How
  • Sanctum resolves the authenticated MoodleUser from the Bearer token via $request->user()
  • StudentProfileService::getProfile() calls loadMissing('infoData.field') — two SQL queries via Eloquent eager-loading
  • MoodleUser has-many MoodleUserInfoData (via userid); each data entry belongs-to MoodleUserInfoField (via fieldid)
  • StudentResource maps infoData collection to the profile_fields array, flattening the nested relation
  • Response wrapped in the standard API envelope via ApiResource
👤
Who
  • Students — call the endpoint with a valid Bearer token to fetch their own profile
  • Our team — built the Student module and extended the Shared models
  • Moodle — owns mdldf_user_info_field and mdldf_user_info_data; we access them read-only
  • No admin, teacher, or parent interaction — student-only endpoint
🔁 Sequence Diagram
sequenceDiagram participant B as Browser participant MW as Sanctum Middleware participant C as MeController participant S as StudentProfileService participant U as MoodleUser participant DB1 as mdldf_user_info_data participant DB2 as mdldf_user_info_field participant R as StudentResource B->>+MW: GET /api/v1/students/me
(Authorization: Bearer <token>) MW->>MW: Validate Sanctum token alt Token invalid / missing MW-->>B: 401 Unauthenticated end MW->>+C: Request + resolved MoodleUser C->>+S: getProfile(user) S->>+U: loadMissing('infoData.field') U->>+DB1: SELECT * FROM user_info_data
WHERE userid = ? DB1-->>-U: MoodleUserInfoData[] U->>+DB2: SELECT * FROM user_info_field
WHERE id IN (fieldid list) DB2-->>-U: MoodleUserInfoField[] U-->>-S: MoodleUser (infoData + field loaded) S-->>-C: MoodleUser C->>+R: new StudentResource(user) R->>R: toArray() → map infoData
into profile_fields R-->>-C: JsonResponse (API envelope) C-->>-B: 200 OK { success, message, data }
🔀 Flowchart
flowchart TD A([Client: GET /api/v1/students/me]) --> B{Bearer token present?} B -- No --> C[401 Unauthenticated]:::error B -- Yes --> D{Sanctum validates token} D -- Invalid / Expired --> C D -- Valid --> E[Resolve MoodleUser from token] E --> F[MeController.__invoke] F --> G[StudentProfileService.getProfile] G --> H[MoodleUser.loadMissing infoData.field] H --> I[(SELECT user_info_data WHERE userid = ?)] I --> J{Has custom field data?} J -- Yes --> K[(SELECT user_info_field WHERE id IN fieldids)] K --> L[Return MoodleUser with loaded relations] J -- No --> M[Return MoodleUser with empty infoData collection] L --> N[StudentResource.toArray] M --> N N --> O[Map infoData → profile_fields array] O --> P[Wrap in ApiResource envelope] P --> Q([200 OK: success + data + profile_fields]):::success classDef error fill:#ef4444,color:#fff,stroke:#ef4444 classDef success fill:#10b981,color:#fff,stroke:#10b981
📂 Files Changed
File Path Layer Description
app/Modules/Student/Controllers/MeController.php Controller Single-action invokable controller for GET /api/v1/students/me. Resolves the authenticated user, delegates to StudentProfileService, and returns StudentResource.
app/Modules/Student/Services/StudentProfileService.php Service Contains all business logic for profile retrieval. Calls loadMissing('infoData.field') on the user model and returns the enriched MoodleUser.
app/Modules/Student/Resources/StudentResource.php Resource Extends ApiResource. Maps MoodleUser core fields and flattens the infoData.field nested relation into the profile_fields array in the API envelope.
app/Modules/Student/routes.php Routing Defines the Student module route: GET v1/students/me under auth:sanctum middleware, named api.v1.students.me.
app/Shared/Models/MoodleUserInfoField.php Model New read-only Eloquent model for mdldf_user_info_field. Stores institution-defined custom profile field definitions. Inherits write guards from MoodleModel.
app/Shared/Models/MoodleUserInfoData.php Model New read-only Eloquent model for mdldf_user_info_data. Per-user field values. Defines field(): BelongsTo to MoodleUserInfoField via fieldid.
app/Shared/Models/MoodleUser.php Model Extended with infoData(): HasMany relationship to MoodleUserInfoData via userid foreign key, enabling eager-loading of custom field data.
routes/api.php Routing Updated to load app/Modules/Student/routes.php under the api middleware group, wiring the Student module into the application.
app/Providers/AppServiceProvider.php Provider Registers StudentProfileService as a singleton binding ($this->app->singleton(StudentProfileService::class)) in the service container.
tests/Unit/Shared/Models/MoodleUserInfoFieldTest.php Test Unit tests verifying table name, timestamps disabled, read-only enforcement (throws LogicException on save()), and MoodleModel inheritance.
tests/Unit/Shared/Models/MoodleUserInfoDataTest.php Test Unit tests verifying table name, timestamps disabled, read-only enforcement, MoodleModel inheritance, and the field(): BelongsTo relationship to MoodleUserInfoField.
tests/Unit/Shared/Models/MoodleUserTest.php Test Extended with test_it_has_many_info_data — verifies the new HasMany relationship returns the correct related model (MoodleUserInfoData).
tests/Unit/Modules/Student/Services/StudentProfileServiceTest.php Test Unit tests for StudentProfileService::getProfile(): verifies it returns the same user instance with infoData relation loaded, and handles the empty-fields case.
tests/Feature/Student/MeTest.php Test Full HTTP integration tests: 401 unauthenticated, 200 with custom fields, full field metadata shape, empty fields, API envelope structure, and profile isolation (user only sees their own data).
📏 Rules Applied
Architecture
  • All business logic placed in StudentProfileService; MeController is thin (≤10 lines, delegates immediately)
  • New Student module is self-contained: Controller, Service, Resource, and routes.php all under app/Modules/Student/
  • Shared models (MoodleUserInfoField, MoodleUserInfoData) placed in app/Shared/Models/ for cross-module reuse
  • Module routes loaded by routes/api.php — each module owns its route file
  • Moodle tables accessed through dedicated read-only Eloquent models only — no raw SQL, no Query Builder on Moodle tables
  • All API routes versioned under /api/v1/
  • Constructor injection used throughout — no app() helper calls in service or controller
  • StudentProfileService bound as singleton in AppServiceProvider
Coding Style
  • declare(strict_types=1) in every PHP file
  • All classes declared final — no open inheritance unless explicitly justified
  • Every public method has an explicit return type declaration
  • Constructor promotion used in MeController for $service property
  • First-class callable syntax used in StudentResource (fn(MoodleUserInfoData $data): array => with typed parameter)
  • Collection's ->map()->values()->all() chain used — no raw array_map
  • PHPDoc on every class and every public method, with @throws where applicable
  • Single quotes for plain strings, double quotes only for interpolation
  • No magic strings — table names set as explicit properties on models
Security
  • Every endpoint protected by auth:sanctum middleware — no public endpoint
  • Students can only retrieve their own profile — $request->user() scopes the query to the authenticated user's ID automatically
  • MoodleUser::$hidden already hides password and secret — never exposed in responses
  • No FormRequest needed (GET with no user-controlled parameters beyond the token) — no input injection vector
  • Moodle table write guards enforced at model level — save() throws LogicException preventing any accidental writes
  • No sensitive Moodle-internal data (admin fields, other users' data) exposed in the response
Testing
  • TDD workflow followed — unit tests written before implementation for all shared models and the service
  • All test methods prefixed with test_it_ and describe expected behavior, not implementation details
  • AAA pattern (Arrange → Act → Assert) in every test method
  • RefreshDatabase trait used in feature tests — each test gets a clean slate
  • Feature tests use Schema::create to mirror Moodle table structure in the test database
  • Moodle HTTP event API mocked with Http::fake() in feature tests — external calls never made in test environment
  • Queue::fake() used to prevent background job execution during tests
  • One concern per test — happy path, empty fields, metadata shape, and auth failure are separate test methods
  • Isolation verified: test_it_returns_own_profile_only confirms two users cannot see each other's data