Overview

📝

What

Allows a student to edit only their bio via PATCH /api/v1/students/me. The profile picture is explicitly immutable — any attempt to change picture or imagealt is rejected with a 422. The bio is persisted to Moodle's user.description column through a dedicated, narrowly-scoped write model.

When

Triggered by an authenticated student sending a PATCH request. Rate-limited to 10 requests per minute (sensitive write endpoint). The StudentProfileUpdated event is dispatched synchronously; the Moodle forwarding listener runs on the queue (async) so it never blocks the HTTP response.

💡

Why

PM requested "edit profile" functionality for students, explicitly scoped to the bio field. Writing to Moodle's core user table is forbidden by default per the architecture rules — this is a PM-approved exception using a dedicated write model with explicit column enumeration, ensuring no other column (especially picture) can be touched.

📁

Where

Lives in the Student module (app/Modules/Student/). The write model is in Shared (app/Shared/Models/MoodleUserProfile.php) since it touches Moodle's core user table. The event, service, repository, DTO, request, and controller all reside within the Student module boundary.

How

The request flows through the standard architecture layers: UpdateProfileRequest validates input (requires bio, prohibits picture/imagealt) → UpdateProfileController (invokable, thin) delegates to StudentProfileService::updateProfile() → the service calls ProfileWriteRepository::updateBio() which delegates to MoodleUserProfile::updateBio() — an explicit query that touches exactly description, descriptionformat, and timemodified via an enumerated column update (no mass assignment). The service then dispatches StudentProfileUpdated (maps to Moodle's \core\event\user_updated), reloads the user model, and returns a StudentProfileDTO. The controller wraps it in the existing StudentResource.

Sequence Diagram

sequenceDiagram
    participant B as Browser
    participant R as Route / Middleware
    participant RQ as UpdateProfileRequest
    participant C as UpdateProfileController
    participant S as StudentProfileService
    participant RP as ProfileWriteRepository
    participant M as MoodleUserProfile
    participant DB as MySQL (user table)
    participant E as Event Dispatcher
    participant L as ForwardEventToMoodle
    participant MO as Moodle Plugin (REST)

    B->>R: PATCH /api/v1/students/me
    R->>R: auth:sanctum + moodle.active + throttle:10,1
    R->>RQ: Validate input
    alt bio missing / too long
        RQ-->>B: 422 Validation Error
    end
    alt picture or imagealt present
        RQ-->>B: 422 Picture editing not allowed
    end
    RQ->>C: toDTO() produces UpdateProfileDTO
    C->>S: updateProfile(dto, user)
    S->>RP: updateBio(studentId, bio)
    RP->>M: updateBio(userId, bio)
    M->>DB: UPDATE user SET description, descriptionformat, timemodified WHERE id = ?
    DB-->>M: OK
    S->>E: dispatch(StudentProfileUpdated)
    E->>L: handle(event) [queued]
    L->>MO: POST /event (user_updated) [async]
    S->>DB: user refresh
    DB-->>S: Fresh user row
    S-->>C: StudentProfileDTO
    C-->>B: 200 JSON envelope with updated profile
      

Flowchart

flowchart TD
    A[PATCH /api/v1/students/me] --> B{Authenticated?}
    B -- No --> C[401 Unauthenticated]
    B -- Yes --> D{Rate limit OK?}
    D -- No --> E[429 Too Many Requests]
    D -- Yes --> F{Validate input}
    F -- bio missing --> G[422 bio is required]
    F -- bio exceeds 65535 chars --> H[422 bio too long]
    F -- picture or imagealt present --> I[422 picture not allowed]
    F -- Valid --> J[UpdateProfileController]
    J --> K[StudentProfileService.updateProfile]
    K --> L[ProfileWriteRepository.updateBio]
    L --> M[MoodleUserProfile.updateBio]
    M --> N[UPDATE user SET description, descriptionformat, timemodified]
    N --> O[Dispatch StudentProfileUpdated]
    O --> P[ForwardEventToMoodle queued]
    P --> Q[Refresh user model]
    Q --> R[Return StudentProfileDTO]
    R --> S[200 OK with StudentResource envelope]

    style C fill:#ef4444,color:#fff
    style E fill:#f59e0b,color:#000
    style G fill:#ef4444,color:#fff
    style H fill:#ef4444,color:#fff
    style I fill:#ef4444,color:#fff
    style S fill:#10b981,color:#000
      

Files Changed

File PathLayerDescription
app/Shared/Models/MoodleUserProfile.php Model Dedicated writable model for Moodle's user table. Exposes a single updateBio() method that touches exactly description, descriptionformat, timemodified. No mass assignment.
app/Modules/Student/Repositories/ProfileWriteRepositoryInterface.php Repository Contract for the narrow write path into the student's bio. Single method: updateBio(int, string).
app/Modules/Student/Repositories/ProfileWriteRepository.php Repository Concrete implementation — delegates to MoodleUserProfile::updateBio().
app/Modules/Student/DTOs/UpdateProfileDTO.php DTO final readonly DTO carrying studentId and bio from the request to the service layer.
app/Modules/Student/Requests/UpdateProfileRequest.php Request FormRequest validating bio (required, string, max 65535) and prohibiting picture/imagealt. Includes toDTO() factory method.
app/Modules/Student/Controllers/UpdateProfileController.php Controller Invokable controller — calls service, returns StudentResource envelope. Thin: 6 lines of logic.
app/Modules/Student/Services/StudentProfileService.php Service Modified — added updateProfile() method and ProfileWriteRepositoryInterface dependency. Persists bio, dispatches event, reloads and returns profile.
app/Modules/Student/Events/StudentProfileUpdated.php Event Domain event extending BaseEvent. Maps to Moodle's \core\event\user_updated. CRUD=Update, EduLevel=Other, ContextLevel=User.
app/Modules/Student/routes.php Route Added PATCH me route with throttle:10,1 middleware, named api.v1.students.me.update.
app/Providers/AppServiceProvider.php Config Added binding: ProfileWriteRepositoryInterface → ProfileWriteRepository.
tests/Feature/Student/UpdateProfileTest.php Test 8 feature tests: success path, persistence verification, 401 unauthenticated, 422 (missing bio, max length, prohibited picture), picture immutability, event dispatch.
tests/Unit/Modules/Student/Services/StudentProfileServiceTest.php Test Modified — added ProfileWriteRepositoryInterface mock as constructor dependency to match updated service signature.
docs/openapi.yaml Docs Added PATCH /students/me endpoint spec with request/response schemas and error codes.
docs/postman_collection.json Docs Added Postman request with example response and envelope test script.

Rules Applied