# Moodle External API Development - Detailed Guide > This file contains the detailed procedure and reference material extracted from `SKILL.md` for focused loading. The root skill defines activation, examples, safety constraints, and limitations. ## Core Architecture Pattern Moodle external APIs follow a strict three-method pattern: 1. **`execute_parameters()`** - Defines input parameter structure 2. **`execute()`** - Contains business logic 3. **`execute_returns()`** - Defines return structure ## Step-by-Step Implementation ### Step 1: Create the External API Class File **Location**: `/local/yourplugin/classes/external/your_api_name.php` ```php libdir/externallib.php"); use external_api; use external_function_parameters; use external_single_structure; use external_value; use external_multiple_structure; class your_api_name extends external_api { // Three required methods will go here } ``` **Key Points**: - Class must extend `external_api` - Namespace follows: `local_pluginname\external` or `mod_modname\external` - Include the security check: `defined('MOODLE_INTERNAL') || die();` - Require externallib.php for base classes ### Step 2: Define Input Parameters ```php public static function execute_parameters() { return new external_function_parameters([ 'userid' => new external_value(PARAM_INT, 'User ID', VALUE_REQUIRED), 'courseid' => new external_value(PARAM_INT, 'Course ID', VALUE_REQUIRED), 'options' => new external_single_structure([ 'includedetails' => new external_value(PARAM_BOOL, 'Include details', VALUE_DEFAULT, false), 'limit' => new external_value(PARAM_INT, 'Result limit', VALUE_DEFAULT, 10) ], 'Options', VALUE_DEFAULT, []) ]); } ``` **Common Parameter Types**: - `PARAM_INT` - Integers - `PARAM_TEXT` - Plain text (HTML stripped) - `PARAM_RAW` - Raw text (no cleaning) - `PARAM_BOOL` - Boolean values - `PARAM_FLOAT` - Floating point numbers - `PARAM_ALPHANUMEXT` - Alphanumeric with extended chars **Structures**: - `external_value` - Single value - `external_single_structure` - Object with named fields - `external_multiple_structure` - Array of items **Value Flags**: - `VALUE_REQUIRED` - Parameter must be provided - `VALUE_OPTIONAL` - Parameter is optional - `VALUE_DEFAULT, defaultvalue` - Optional with default ### Step 3: Implement Business Logic ```php public static function execute($userid, $courseid, $options = []) { global $DB, $USER; // 1. Validate parameters $params = self::validate_parameters(self::execute_parameters(), [ 'userid' => $userid, 'courseid' => $courseid, 'options' => $options ]); // 2. Check permissions/capabilities $context = \context_course::instance($params['courseid']); self::validate_context($context); require_capability('moodle/course:view', $context); // 3. Verify user access if ($params['userid'] != $USER->id) { require_capability('local_yourplugin:viewotheruserdata', $context); // Define this capability in db/access.php; do not substitute viewhiddenactivities. } // 4. Database operations $sql = "SELECT id, name, timecreated FROM {your_table} WHERE userid = :userid AND courseid = :courseid ORDER BY id"; $records = $DB->get_records_sql($sql, [ 'userid' => $params['userid'], 'courseid' => $params['courseid'] ], 0, max(1, min(100, $params['options']['limit']))); // 5. Process and return data $results = []; foreach ($records as $record) { $results[] = [ 'id' => $record->id, 'name' => $record->name, 'timestamp' => $record->timecreated ]; } return [ 'items' => $results, 'count' => count($results) ]; } ``` **Critical Steps**: 1. **Always validate parameters** using `validate_parameters()` 2. **Check context** using `validate_context()` 3. **Verify capabilities** using `require_capability()` 4. **Use parameterized queries** to prevent SQL injection 5. **Return structured data** matching return definition ### Step 4: Define Return Structure ```php public static function execute_returns() { return new external_single_structure([ 'items' => new external_multiple_structure( new external_single_structure([ 'id' => new external_value(PARAM_INT, 'Item ID'), 'name' => new external_value(PARAM_TEXT, 'Item name'), 'timestamp' => new external_value(PARAM_INT, 'Creation time') ]) ), 'count' => new external_value(PARAM_INT, 'Total items') ]); } ``` **Return Structure Rules**: - Must match exactly what `execute()` returns - Use appropriate parameter types - Document each field with description - Nested structures allowed ### Step 5: Register the Service **Location**: `/local/yourplugin/db/services.php` ```php [ 'classname' => 'local_yourplugin\external\your_api_name', 'methodname' => 'execute', 'classpath' => 'local/yourplugin/classes/external/your_api_name.php', 'description' => 'Brief description of what this API does', 'type' => 'read', // or 'write' 'ajax' => true, 'capabilities'=> 'moodle/course:view', // comma-separated if multiple 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] // Optional ], ]; $services = [ 'Your Plugin Web Service' => [ 'functions' => [ 'local_yourplugin_your_api_name' ], 'restrictedusers' => 1, 'enabled' => 0 // Enable only after assigning approved least-privilege users. ] ]; ``` **Service Registration Keys**: - `classname` - Full namespaced class name - `methodname` - Always 'execute' - `type` - 'read' (SELECT) or 'write' (INSERT/UPDATE/DELETE) - `ajax` - Set true for AJAX/REST access - `capabilities` - Administrative metadata only; execute() must enforce context-specific capability checks - `services` - Optional service bundles ### Step 6: Implement Error Handling & Logging Use the configured Moodle logging/event API with a documented data-minimization policy. Do not create mode-0777 debug directories or log arbitrary SQL, raw exceptions, access tokens, learner identifiers or stack arguments. Return a sanitized error while preserving restricted diagnostic event IDs. Logging is not a substitute for authorization and the sample does not supply a logger backend. ## Advanced Patterns The course-module/group/question SQL below targets historical internals and omits cache rebuilds, events, current question-version visibility and some required fields. Use `add_moduleinfo` and supported groups/question-bank APIs for the chosen Moodle release; do not copy direct inserts into a live site. Bound requested limits and add schema-version/role/group visibility tests. Filtering by tag alone must not expose hidden/old question versions. [External API security](https://moodledev.io/docs/5.2/apis/subsystems/external/security) and [DML pagination](https://moodledev.io/docs/4.5/apis/core/dml) define the core boundaries. ### Complex Database Operations ```php // Transaction example $transaction = $DB->start_delegated_transaction(); try { // Insert record $recordid = $DB->insert_record('your_table', $dataobject); // Update related records $DB->set_field('another_table', 'status', 1, ['recordid' => $recordid]); // Commit transaction $transaction->allow_commit(); } catch (\Exception $e) { $transaction->rollback($e); throw $e; } ``` ### Working with Course Modules ```php // Create course module $moduleid = $DB->get_field('modules', 'id', ['name' => 'quiz'], MUST_EXIST); $cm = new \stdClass(); $cm->course = $courseid; $cm->module = $moduleid; $cm->instance = 0; // Will be updated after activity creation $cm->visible = 1; $cm->groupmode = 0; $cmid = add_course_module($cm); // Create activity instance (e.g., quiz) $quiz = new \stdClass(); $quiz->course = $courseid; $quiz->name = 'My Quiz'; $quiz->coursemodule = $cmid; // ... other quiz fields ... $quizid = quiz_add_instance($quiz, null); // Update course module with instance ID $DB->set_field('course_modules', 'instance', $quizid, ['id' => $cmid]); course_add_cm_to_section($courseid, $cmid, 0); ``` ### Access Restrictions (Groups/Availability) ```php // Restrict activity to specific user via group $groupname = 'activity_' . $activityid . '_user_' . $userid; // Create or get group if (!$groupid = $DB->get_field('groups', 'id', ['courseid' => $courseid, 'name' => $groupname])) { $groupdata = (object)[ 'courseid' => $courseid, 'name' => $groupname, 'timecreated' => time(), 'timemodified' => time() ]; $groupid = $DB->insert_record('groups', $groupdata); } // Add user to group if (!$DB->record_exists('groups_members', ['groupid' => $groupid, 'userid' => $userid])) { $DB->insert_record('groups_members', (object)[ 'groupid' => $groupid, 'userid' => $userid, 'timeadded' => time() ]); } // Set availability condition $restriction = [ 'op' => '&', 'show' => false, 'c' => [ [ 'type' => 'group', 'id' => $groupid ] ], 'showc' => [false] ]; $DB->set_field('course_modules', 'availability', json_encode($restriction), ['id' => $cmid]); ``` ### Random Question Selection with Tags ```php private static function get_random_questions($categoryid, $tagname, $limit) { global $DB; $sql = "SELECT q.id FROM {question} q INNER JOIN {question_versions} qv ON qv.questionid = q.id INNER JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid INNER JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid JOIN {tag_instance} ti ON ti.itemid = q.id JOIN {tag} t ON t.id = ti.tagid WHERE LOWER(t.name) = :tagname AND qc.id = :categoryid AND ti.itemtype = 'question' AND q.qtype = 'multichoice'"; $qids = $DB->get_fieldset_sql($sql, [ 'categoryid' => $categoryid, 'tagname' => strtolower($tagname) ]); shuffle($qids); return array_slice($qids, 0, $limit); } ``` ## Testing Your API ### 1. Via Moodle Web Services Test Client 1. Enable web services: **Site administration > Advanced features** 2. Enable REST protocol: **Site administration > Plugins > Web services > Manage protocols** 3. Create service: **Site administration > Server > Web services > External services** 4. Test function: **Site administration > Development > Web service test client** ### 2. Via a private REST client Provision an approved least-privilege test-service token through Moodle's supported admin flow. Send it in the documented REST POST body from a secret-aware client, not in command-line arguments or URLs; never use a production admin password to mint mobile-service credentials. Disable redirects, validate TLS and bound time, input/output and request count. Inspect only redacted result fields. ### 3. Via JavaScript (AJAX) ```javascript require(['core/ajax'], function(ajax) { var promises = ajax.call([{ methodname: 'local_yourplugin_your_api_name', args: { userid: 2, courseid: 3 } }]); promises[0].done(function(response) { console.log('Success:', response); }).fail(function(error) { console.error('Error:', error); }); }); ``` ## Common Pitfalls & Solutions ### 1. "Function not found" Error **Solution**: - Purge caches: **Site administration > Development > Purge all caches** - Verify function name in services.php matches exactly - Check namespace and class name are correct ### 2. "Invalid parameter value detected" **Solution**: - Ensure parameter types match between definition and usage - Check required vs optional parameters - Validate nested structure definitions ### 3. SQL Injection Vulnerabilities **Solution**: - Always use placeholder parameters (`:paramname`) - Never concatenate user input into SQL strings - Use Moodle's database methods: `get_record()`, `get_records()`, etc. ### 4. Permission Denied Errors **Solution**: - Call `self::validate_context($context)` early in execute() - Check required capabilities match user's permissions - Verify user has role assignments in the context ### 5. Transaction Deadlocks **Solution**: - Keep transactions short - Commit only on successful completion; rollback on exceptions using the delegated transaction API, never commit unconditionally in finally - Avoid nested transactions ## Debugging Checklist - [ ] Check Moodle debug mode: **Site administration > Development > Debugging** - [ ] Review web services logs: **Site administration > Reports > Logs** - [ ] Check custom log files in `$CFG->dataroot/local_yourplugin/` - [ ] Use redacted database diagnostics in isolated development only; raw SQL debugging may expose data - [ ] Test expected denial for guest/unenrolled/cross-user roles as well as intended permitted roles - [ ] Clear browser cache and Moodle caches - [ ] Check PHP error logs on server ## Plugin Structure Checklist ``` local/yourplugin/ ├── version.php # Plugin version and metadata ├── db/ │ ├── services.php # External service definitions │ └── access.php # Capability definitions (optional) ├── classes/ │ └── external/ │ ├── your_api_name.php # External API implementation │ └── another_api.php # Additional APIs ├── lang/ │ └── en/ │ └── local_yourplugin.php # Language strings └── tests/ └── external_test.php # Unit tests (optional but recommended) ``` ## Quick Reference: Common Moodle Tables | Table | Purpose | |-------|---------| | `{user}` | User accounts | | `{course}` | Courses | | `{course_modules}` | Activity instances in courses | | `{modules}` | Available activity types (quiz, forum, etc.) | | `{quiz}` | Quiz configurations | | `{quiz_attempts}` | Quiz attempt records | | `{question}` | Question bank | | `{question_categories}` | Question categories | | `{grade_items}` | Gradebook items | | `{grade_grades}` | Student grades | | `{groups}` | Course groups | | `{groups_members}` | Group memberships | | `{logstore_standard_log}` | Activity logs | ## Additional Resources - [Moodle External API Documentation](https://moodledev.io/docs/5.2/apis/subsystems/external/functions) - [Moodle Coding Style](https://moodledev.io/general/development/policies/codingstyle) - [Moodle Database API](https://moodledev.io/docs/5.2/apis/core/dml) - [Web Services API Documentation](https://moodledev.io/docs/5.2/apis/subsystems/external) ## Guidelines - Always validate input parameters using `validate_parameters()` - Check user context and capabilities before operations - Use parameterized SQL queries (never string concatenation) - Implement comprehensive error handling and logging - Follow Moodle naming conventions (lowercase, underscores) - Document all parameters and return values clearly - Test with different user roles and permissions - Consider transaction safety for write operations - Purge caches after service registration changes - Keep API methods focused and single-purpose