Student Me
✓ Implemented
📋
5W Overview
What
GET /api/v1/students/mereturns 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/meHTTP request - Requires a valid Sanctum Bearer token — fired post-authentication, not at login
- Eager-loads
infoData.fieldonly 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 modelapp/Shared/Models/MoodleUserInfoData.php— new read-only modelapp/Shared/Models/MoodleUser.php— extended withinfoData()relationshipapp/Providers/AppServiceProvider.php— singleton bindingroutes/api.php— Student module routes loaded here
How
- Sanctum resolves the authenticated
MoodleUserfrom the Bearer token via$request->user() StudentProfileService::getProfile()callsloadMissing('infoData.field')— two SQL queries via Eloquent eager-loadingMoodleUserhas-manyMoodleUserInfoData(viauserid); each data entry belongs-toMoodleUserInfoField(viafieldid)StudentResourcemapsinfoDatacollection to theprofile_fieldsarray, 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_fieldandmdldf_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 }
(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
📏
Rules Applied
Architecture
- All business logic placed in
StudentProfileService;MeControlleris 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 inapp/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 StudentProfileServicebound as singleton inAppServiceProvider
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
MeControllerfor$serviceproperty - First-class callable syntax used in
StudentResource(fn(MoodleUserInfoData $data): array =>with typed parameter) - Collection's
->map()->values()->all()chain used — no rawarray_map - PHPDoc on every class and every public method, with
@throwswhere 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:sanctummiddleware — no public endpoint - Students can only retrieve their own profile —
$request->user()scopes the query to the authenticated user's ID automatically MoodleUser::$hiddenalready hidespasswordandsecret— 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()throwsLogicExceptionpreventing 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
RefreshDatabasetrait used in feature tests — each test gets a clean slate- Feature tests use
Schema::createto 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_onlyconfirms two users cannot see each other's data