C04 — Access Restrictions Logic
5W Overview
Parses and evaluates Moodle's availability JSON tree stored in mdl_course_modules.availability against the current student's state, then filters or annotates module responses. Supports six condition types — date, completion, grade, group, grouping, and profile — with full recursive boolean operator logic (&, |, !&, !|) and NOT propagation.
Runs synchronously at request time, triggered on every call to the module list endpoint (GET /api/v1/courses/{id}/sections/{id}/modules) and the module show endpoint (GET /api/v1/courses/{id}/modules/{id}). No background jobs. No writes to any table. Pure read-only evaluation.
Moodle supports conditional access rules per course module. Without this feature the student portal exposes all modules unconditionally, violating instructor configuration. Modules with unmet conditions must be either hidden entirely (preventing information leak) or shown as locked with a human-readable reason — exactly mirroring Moodle's native student course view behaviour.
New service-only module at app/Modules/Availability/ — no routes, controllers, or form requests. Contains Conditions, DTOs, Enums, Contracts, and Services. Integrated into the existing Course module via injection into SectionController and ModuleService. Six new read-only shared models added to app/Shared/Models/.
1 — Parsing: AvailabilityService::evaluate() receives the raw JSON string, decodes it with JSON_THROW_ON_ERROR, and delegates to evaluateNode(). 2 — Tree traversal: Each node is either a leaf condition (has type key) or an operator subtree (has op + c keys). Leaf conditions are resolved via ConditionFactory by type string. Unknown types return AvailabilityResult::hidden() as a secure default. 3 — Logic flags: computeLogicFlags(op, not) derives [$innerNot, $andMode] via XOR logic, correctly propagating NOT through nested subtrees. 4 — AND mode: All children must pass; showc[i]=false on any failing child hides the whole module. 5 — OR mode: First passing child wins; show=false on the node hides when all fail. 6 — Results: Three states — available(), locked(reason), hidden(). Hidden modules are excluded from lists and return 404 on direct access (no info leak). Locked modules return 423 with the reason string.
Sequence Diagram — Module List with Availability
Flowchart — Module Show Availability Check
Files Changed
Rules Applied
🏗 Architecture
- New
Availabilitymodule is fully self-contained — service layer only, no routes or controllers — following the modular architecture mandate. - No layer skipping:
SectionController(HTTP layer) callsAvailabilityService(service layer);ModuleServiceorchestrates across services within the same layer. - All Moodle tables accessed via dedicated read-only Eloquent models extending
MoodleReadModel. No writes to any Moodle table. - DTOs (
AvailabilityContext,AvailabilityResult) used to pass structured data across layer boundaries. - Constructor injection throughout — no
app()orresolve()calls in services or condition classes. ConditionFactoryandAvailabilityServicebound as singletons inAppServiceProvider.- Controllers remain thin — all evaluation logic delegated to
AvailabilityService; controller only handles exception-to-response mapping. - API errors use established
ApiResource::error()envelope format with HTTP 423 for locked, 404 for hidden.
🔒 Security
- Hidden modules return 404 on direct access — identical to "not found", preventing enumeration of restricted content.
- Unknown condition types default to
hidden()— fail-closed posture; never assume available when we cannot interpret a condition. - All DB access for availability goes through read-only Eloquent models — no risk of accidental writes.
- Locked module 423 response exposes only the human-readable reason string — no internal IDs, DB details, or stack traces.
- JSON decoded with
JSON_THROW_ON_ERROR— malformed JSON throws a catchable exception rather than silently producing wrong availability state. - No raw SQL — all queries use Eloquent query builder with parameter binding.
💻 Coding Style
- All new classes are
final. DTOs arefinal readonlywith constructor-promoted typed properties. - Enums used for all fixed value sets:
AvailabilityOperator,CompletionExpectation,ProfileOperator,CourseErrorCode. No magic strings or integers. Carbonused for all time operations inDateCondition— not rawtime()orDateTime.Str::contains,Str::startsWith,Str::endsWithused inProfileCondition— Laravel helpers over raw PHP string functions.Collection::filter()andCollection::values()used inapplyAvailability()— notarray_filter.- Every class and public method has a PHPDoc block.
@throwsdeclared on all throwing methods. - Explicit return types on all methods. Typed properties on all classes. Strict comparison (
===,!==) throughout. - Named arguments used for clarity on multi-parameter constructors (e.g.
AvailabilityContext,AvailabilityResult).
🧪 Testing
- TDD workflow followed: failing tests written per condition type before implementation.
- All tests follow AAA (Arrange → Act → Assert) and are named with
test_it_prefix describing expected behaviour. - Unit tests for
AvailabilityServicecover all 4 operators, allshowc/showcombinations, nested subtrees, and unknown type blocking (12 test methods). - One test class per condition (6 files): happy path, failure path, NOT flag, reason string.
- DB-touching unit tests use
RefreshDatabasetrait and hit the real test database (no Eloquent mocking). - Feature tests verify full HTTP request cycle: correct status codes (200, 404, 423), envelope shape, and
available/available_reasonfield presence. - Tests are independent — each seeds its own data; no shared mutable state across test methods.
- Controllers not directly unit tested — covered by feature tests per testing rules.