[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-ff4a0a03-ea12-40e9-b250-2bd93dc73e78":3,"$f0Y3mpp-hZAmUKgT3IY-ktcrPooSW-XQlRJxSL7fvFuU":42},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":33},"ff4a0a03-ea12-40e9-b250-2bd93dc73e78","moodle-external-api-development","这项技能指导您创建符合Moodle LMS外部API框架和编码标准的自定义外部Web服务API。","cat_coding_backend","mod_coding","sickn33,coding","---\nname: moodle-external-api-development\ndescription: \"This skill guides you through creating custom external web service APIs for Moodle LMS, following Moodle's external API framework and coding standards.\"\nrisk: unknown\nsource: community\ndate_added: \"2026-02-27\"\n---\n\n# Moodle External API Development\n\nThis skill guides you through creating custom external web service APIs for Moodle LMS, following Moodle's external API framework and coding standards.\n\n## When to Use This Skill\n\n- Creating custom web services for Moodle plugins\n- Implementing REST\u002FAJAX endpoints for course management\n- Building APIs for quiz operations, user tracking, or reporting\n- Exposing Moodle functionality to external applications\n- Developing mobile app backends using Moodle\n\n## Core Architecture Pattern\n\nMoodle external APIs follow a strict three-method pattern:\n\n1. **`execute_parameters()`** - Defines input parameter structure\n2. **`execute()`** - Contains business logic\n3. **`execute_returns()`** - Defines return structure\n\n## Step-by-Step Implementation\n\n### Step 1: Create the External API Class File\n\n**Location**: `\u002Flocal\u002Fyourplugin\u002Fclasses\u002Fexternal\u002Fyour_api_name.php`\n\n```php\n\u003C?php\nnamespace local_yourplugin\\external;\n\ndefined('MOODLE_INTERNAL') || die();\nrequire_once(\"$CFG->libdir\u002Fexternallib.php\");\n\nuse external_api;\nuse external_function_parameters;\nuse external_single_structure;\nuse external_value;\n\nclass your_api_name extends external_api {\n    \n    \u002F\u002F Three required methods will go here\n    \n}\n```\n\n**Key Points**:\n- Class must extend `external_api`\n- Namespace follows: `local_pluginname\\external` or `mod_modname\\external`\n- Include the security check: `defined('MOODLE_INTERNAL') || die();`\n- Require externallib.php for base classes\n\n### Step 2: Define Input Parameters\n\n```php\npublic static function execute_parameters() {\n    return new external_function_parameters([\n        'userid' => new external_value(PARAM_INT, 'User ID', VALUE_REQUIRED),\n        'courseid' => new external_value(PARAM_INT, 'Course ID', VALUE_REQUIRED),\n        'options' => new external_single_structure([\n            'includedetails' => new external_value(PARAM_BOOL, 'Include details', VALUE_DEFAULT, false),\n            'limit' => new external_value(PARAM_INT, 'Result limit', VALUE_DEFAULT, 10)\n        ], 'Options', VALUE_OPTIONAL)\n    ]);\n}\n```\n\n**Common Parameter Types**:\n- `PARAM_INT` - Integers\n- `PARAM_TEXT` - Plain text (HTML stripped)\n- `PARAM_RAW` - Raw text (no cleaning)\n- `PARAM_BOOL` - Boolean values\n- `PARAM_FLOAT` - Floating point numbers\n- `PARAM_ALPHANUMEXT` - Alphanumeric with extended chars\n\n**Structures**:\n- `external_value` - Single value\n- `external_single_structure` - Object with named fields\n- `external_multiple_structure` - Array of items\n\n**Value Flags**:\n- `VALUE_REQUIRED` - Parameter must be provided\n- `VALUE_OPTIONAL` - Parameter is optional\n- `VALUE_DEFAULT, defaultvalue` - Optional with default\n\n### Step 3: Implement Business Logic\n\n```php\npublic static function execute($userid, $courseid, $options = []) {\n    global $DB, $USER;\n\n    \u002F\u002F 1. Validate parameters\n    $params = self::validate_parameters(self::execute_parameters(), [\n        'userid' => $userid,\n        'courseid' => $courseid,\n        'options' => $options\n    ]);\n\n    \u002F\u002F 2. Check permissions\u002Fcapabilities\n    $context = \\context_course::instance($params['courseid']);\n    self::validate_context($context);\n    require_capability('moodle\u002Fcourse:view', $context);\n\n    \u002F\u002F 3. Verify user access\n    if ($params['userid'] != $USER->id) {\n        require_capability('moodle\u002Fcourse:viewhiddenactivities', $context);\n    }\n\n    \u002F\u002F 4. Database operations\n    $sql = \"SELECT id, name, timecreated\n            FROM {your_table}\n            WHERE userid = :userid\n              AND courseid = :courseid\n            LIMIT :limit\";\n    \n    $records = $DB->get_records_sql($sql, [\n        'userid' => $params['userid'],\n        'courseid' => $params['courseid'],\n        'limit' => $params['options']['limit']\n    ]);\n\n    \u002F\u002F 5. Process and return data\n    $results = [];\n    foreach ($records as $record) {\n        $results[] = [\n            'id' => $record->id,\n            'name' => $record->name,\n            'timestamp' => $record->timecreated\n        ];\n    }\n\n    return [\n        'items' => $results,\n        'count' => count($results)\n    ];\n}\n```\n\n**Critical Steps**:\n1. **Always validate parameters** using `validate_parameters()`\n2. **Check context** using `validate_context()`\n3. **Verify capabilities** using `require_capability()`\n4. **Use parameterized queries** to prevent SQL injection\n5. **Return structured data** matching return definition\n\n### Step 4: Define Return Structure\n\n```php\npublic static function execute_returns() {\n    return new external_single_structure([\n        'items' => new external_multiple_structure(\n            new external_single_structure([\n                'id' => new external_value(PARAM_INT, 'Item ID'),\n                'name' => new external_value(PARAM_TEXT, 'Item name'),\n                'timestamp' => new external_value(PARAM_INT, 'Creation time')\n            ])\n        ),\n        'count' => new external_value(PARAM_INT, 'Total items')\n    ]);\n}\n```\n\n**Return Structure Rules**:\n- Must match exactly what `execute()` returns\n- Use appropriate parameter types\n- Document each field with description\n- Nested structures allowed\n\n### Step 5: Register the Service\n\n**Location**: `\u002Flocal\u002Fyourplugin\u002Fdb\u002Fservices.php`\n\n```php\n\u003C?php\ndefined('MOODLE_INTERNAL') || die();\n\n$functions = [\n    'local_yourplugin_your_api_name' => [\n        'classname'   => 'local_yourplugin\\external\\your_api_name',\n        'methodname'  => 'execute',\n        'classpath'   => 'local\u002Fyourplugin\u002Fclasses\u002Fexternal\u002Fyour_api_name.php',\n        'description' => 'Brief description of what this API does',\n        'type'        => 'read',  \u002F\u002F or 'write'\n        'ajax'        => true,\n        'capabilities'=> 'moodle\u002Fcourse:view', \u002F\u002F comma-separated if multiple\n        'services'    => [MOODLE_OFFICIAL_MOBILE_SERVICE] \u002F\u002F Optional\n    ],\n];\n\n$services = [\n    'Your Plugin Web Service' => [\n        'functions' => [\n            'local_yourplugin_your_api_name'\n        ],\n        'restrictedusers' => 0,\n        'enabled' => 1\n    ]\n];\n```\n\n**Service Registration Keys**:\n- `classname` - Full namespaced class name\n- `methodname` - Always 'execute'\n- `type` - 'read' (SELECT) or 'write' (INSERT\u002FUPDATE\u002FDELETE)\n- `ajax` - Set true for AJAX\u002FREST access\n- `capabilities` - Required Moodle capabilities\n- `services` - Optional service bundles\n\n### Step 6: Implement Error Handling & Logging\n\n```php\nprivate static function log_debug($message) {\n    global $CFG;\n    $logdir = $CFG->dataroot . '\u002Flocal_yourplugin';\n    if (!file_exists($logdir)) {\n        mkdir($logdir, 0777, true);\n    }\n    $debuglog = $logdir . '\u002Fapi_debug.log';\n    $timestamp = date('Y-m-d H:i:s');\n    file_put_contents($debuglog, \"[$timestamp] $message\\n\", FILE_APPEND | LOCK_EX);\n}\n\npublic static function execute($userid, $courseid) {\n    global $DB;\n\n    try {\n        self::log_debug(\"API called: userid=$userid, courseid=$courseid\");\n        \n        \u002F\u002F Validate parameters\n        $params = self::validate_parameters(self::execute_parameters(), [\n            'userid' => $userid,\n            'courseid' => $courseid\n        ]);\n\n        \u002F\u002F Your logic here\n        \n        self::log_debug(\"API completed successfully\");\n        return $result;\n\n    } catch (\\invalid_parameter_exception $e) {\n        self::log_debug(\"Parameter validation failed: \" . $e->getMessage());\n        throw $e;\n    } catch (\\moodle_exception $e) {\n        self::log_debug(\"Moodle exception: \" . $e->getMessage());\n        throw $e;\n    } catch (\\Exception $e) {\n        \u002F\u002F Log detailed error info\n        $lastsql = method_exists($DB, 'get_last_sql') ? $DB->get_last_sql() : '[N\u002FA]';\n        self::log_debug(\"Fatal error: \" . $e->getMessage());\n        self::log_debug(\"Last SQL: \" . $lastsql);\n        self::log_debug(\"Stack trace: \" . $e->getTraceAsString());\n        throw $e;\n    }\n}\n```\n\n**Error Handling Best Practices**:\n- Wrap logic in try-catch blocks\n- Log errors with timestamps and context\n- Capture SQL queries on database errors\n- Preserve stack traces for debugging\n- Re-throw exceptions after logging\n\n## Advanced Patterns\n\n### Complex Database Operations\n\n```php\n\u002F\u002F Transaction example\n$transaction = $DB->start_delegated_transaction();\n\ntry {\n    \u002F\u002F Insert record\n    $recordid = $DB->insert_record('your_table', $dataobject);\n    \n    \u002F\u002F Update related records\n    $DB->set_field('another_table', 'status', 1, ['recordid' => $recordid]);\n    \n    \u002F\u002F Commit transaction\n    $transaction->allow_commit();\n} catch (\\Exception $e) {\n    $transaction->rollback($e);\n    throw $e;\n}\n```\n\n### Working with Course Modules\n\n```php\n\u002F\u002F Create course module\n$moduleid = $DB->get_field('modules', 'id', ['name' => 'quiz'], MUST_EXIST);\n\n$cm = new \\stdClass();\n$cm->course = $courseid;\n$cm->module = $moduleid;\n$cm->instance = 0; \u002F\u002F Will be updated after activity creation\n$cm->visible = 1;\n$cm->groupmode = 0;\n$cmid = add_course_module($cm);\n\n\u002F\u002F Create activity instance (e.g., quiz)\n$quiz = new \\stdClass();\n$quiz->course = $courseid;\n$quiz->name = 'My Quiz';\n$quiz->coursemodule = $cmid;\n\u002F\u002F ... other quiz fields ...\n\n$quizid = quiz_add_instance($quiz, null);\n\n\u002F\u002F Update course module with instance ID\n$DB->set_field('course_modules', 'instance', $quizid, ['id' => $cmid]);\ncourse_add_cm_to_section($courseid, $cmid, 0);\n```\n\n### Access Restrictions (Groups\u002FAvailability)\n\n```php\n\u002F\u002F Restrict activity to specific user via group\n$groupname = 'activity_' . $activityid . '_user_' . $userid;\n\n\u002F\u002F Create or get group\nif (!$groupid = $DB->get_field('groups', 'id', ['courseid' => $courseid, 'name' => $groupname])) {\n    $groupdata = (object)[\n        'courseid' => $courseid,\n        'name' => $groupname,\n        'timecreated' => time(),\n        'timemodified' => time()\n    ];\n    $groupid = $DB->insert_record('groups', $groupdata);\n}\n\n\u002F\u002F Add user to group\nif (!$DB->record_exists('groups_members', ['groupid' => $groupid, 'userid' => $userid])) {\n    $DB->insert_record('groups_members', (object)[\n        'groupid' => $groupid,\n        'userid' => $userid,\n        'timeadded' => time()\n    ]);\n}\n\n\u002F\u002F Set availability condition\n$restriction = [\n    'op' => '&',\n    'show' => false,\n    'c' => [\n        [\n            'type' => 'group',\n            'id' => $groupid\n        ]\n    ],\n    'showc' => [false]\n];\n\n$DB->set_field('course_modules', 'availability', json_encode($restriction), ['id' => $cmid]);\n```\n\n### Random Question Selection with Tags\n\n```php\nprivate static function get_random_questions($categoryid, $tagname, $limit) {\n    global $DB;\n    \n    $sql = \"SELECT q.id\n            FROM {question} q\n            INNER JOIN {question_versions} qv ON qv.questionid = q.id\n            INNER JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid\n            INNER JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid\n            JOIN {tag_instance} ti ON ti.itemid = q.id\n            JOIN {tag} t ON t.id = ti.tagid\n            WHERE LOWER(t.name) = :tagname\n              AND qc.id = :categoryid\n              AND ti.itemtype = 'question'\n              AND q.qtype = 'multichoice'\";\n    \n    $qids = $DB->get_fieldset_sql($sql, [\n        'categoryid' => $categoryid,\n        'tagname' => strtolower($tagname)\n    ]);\n    \n    shuffle($qids);\n    return array_slice($qids, 0, $limit);\n}\n```\n\n## Testing Your API\n\n### 1. Via Moodle Web Services Test Client\n\n1. Enable web services: **Site administration > Advanced features**\n2. Enable REST protocol: **Site administration > Plugins > Web services > Manage protocols**\n3. Create service: **Site administration > Server > Web services > External services**\n4. Test function: **Site administration > Development > Web service test client**\n\n### 2. Via curl\n\n```bash\n# Get token first\ncurl -X POST \"https:\u002F\u002Fyourmoodle.com\u002Flogin\u002Ftoken.php\" \\\n  -d \"username=admin\" \\\n  -d \"password=yourpassword\" \\\n  -d \"service=moodle_mobile_app\"\n\n# Call your API\ncurl -X POST \"https:\u002F\u002Fyourmoodle.com\u002Fwebservice\u002Frest\u002Fserver.php\" \\\n  -d \"wstoken=YOUR_TOKEN\" \\\n  -d \"wsfunction=local_yourplugin_your_api_name\" \\\n  -d \"moodlewsrestformat=json\" \\\n  -d \"userid=2\" \\\n  -d \"courseid=3\"\n```\n\n### 3. Via JavaScript (AJAX)\n\n```javascript\nrequire(['core\u002Fajax'], function(ajax) {\n    var promises = ajax.call([{\n        methodname: 'local_yourplugin_your_api_name',\n        args: {\n            userid: 2,\n            courseid: 3\n        }\n    }]);\n\n    promises[0].done(function(response) {\n        console.log('Success:', response);\n    }).fail(function(error) {\n        console.error('Error:', error);\n    });\n});\n```\n\n## Common Pitfalls & Solutions\n\n### 1. \"Function not found\" Error\n**Solution**: \n- Purge caches: **Site administration > Development > Purge all caches**\n- Verify function name in services.php matches exactly\n- Check namespace and class name are correct\n\n### 2. \"Invalid parameter value detected\"\n**Solution**:\n- Ensure parameter types match between definition and usage\n- Check required vs optional parameters\n- Validate nested structure definitions\n\n### 3. SQL Injection Vulnerabilities\n**Solution**:\n- Always use placeholder parameters (`:paramname`)\n- Never concatenate user input into SQL strings\n- Use Moodle's database methods: `get_record()`, `get_records()`, etc.\n\n### 4. Permission Denied Errors\n**Solution**:\n- Call `self::validate_context($context)` early in execute()\n- Check required capabilities match user's permissions\n- Verify user has role assignments in the context\n\n### 5. Transaction Deadlocks\n**Solution**:\n- Keep transactions short\n- Always commit or rollback in finally blocks\n- Avoid nested transactions\n\n## Debugging Checklist\n\n- [ ] Check Moodle debug mode: **Site administration > Development > Debugging**\n- [ ] Review web services logs: **Site administration > Reports > Logs**\n- [ ] Check custom log files in `$CFG->dataroot\u002Flocal_yourplugin\u002F`\n- [ ] Verify database queries using `$DB->set_debug(true)`\n- [ ] Test with admin user to rule out permission issues\n- [ ] Clear browser cache and Moodle caches\n- [ ] Check PHP error logs on server\n\n## Plugin Structure Checklist\n\n```\nlocal\u002Fyourplugin\u002F\n├── version.php                 # Plugin version and metadata\n├── db\u002F\n│   ├── services.php           # External service definitions\n│   └── access.php             # Capability definitions (optional)\n├── classes\u002F\n│   └── external\u002F\n│       ├── your_api_name.php  # External API implementation\n│       └── another_api.php    # Additional APIs\n├── lang\u002F\n│   └── en\u002F\n│       └── local_yourplugin.php  # Language strings\n└── tests\u002F\n    └── external_test.php      # Unit tests (optional but recommended)\n```\n\n## Examples from Real Implementation\n\n### Simple Read API (Get Quiz Attempts)\n\n```php\n\u003C?php\nnamespace local_userlog\\external;\n\ndefined('MOODLE_INTERNAL') || die();\nrequire_once(\"$CFG->libdir\u002Fexternallib.php\");\n\nuse external_api;\nuse external_function_parameters;\nuse external_single_structure;\nuse external_value;\n\nclass get_quiz_attempts extends external_api {\n    public static function execute_parameters() {\n        return new external_function_parameters([\n            'userid' => new external_value(PARAM_INT, 'User ID'),\n            'courseid' => new external_value(PARAM_INT, 'Course ID')\n        ]);\n    }\n\n    public static function execute($userid, $courseid) {\n        global $DB;\n\n        self::validate_parameters(self::execute_parameters(), [\n            'userid' => $userid,\n            'courseid' => $courseid\n        ]);\n\n        $sql = \"SELECT COUNT(*) AS quiz_attempts\n                FROM {quiz_attempts} qa\n                JOIN {quiz} q ON qa.quiz = q.id\n                WHERE qa.userid = :userid AND q.course = :courseid\";\n\n        $attempts = $DB->get_field_sql($sql, [\n            'userid' => $userid,\n            'courseid' => $courseid\n        ]);\n\n        return ['quiz_attempts' => (int)$attempts];\n    }\n\n    public static function execute_returns() {\n        return new external_single_structure([\n            'quiz_attempts' => new external_value(PARAM_INT, 'Total number of quiz attempts')\n        ]);\n    }\n}\n```\n\n### Complex Write API (Create Quiz from Categories)\n\nSee attached `create_quiz_from_categories.php` for a comprehensive example including:\n- Multiple database insertions\n- Course module creation\n- Quiz instance configuration\n- Random question selection with tags\n- Group-based access restrictions\n- Extensive error logging\n- Transaction management\n\n## Quick Reference: Common Moodle Tables\n\n| Table | Purpose |\n|-------|---------|\n| `{user}` | User accounts |\n| `{course}` | Courses |\n| `{course_modules}` | Activity instances in courses |\n| `{modules}` | Available activity types (quiz, forum, etc.) |\n| `{quiz}` | Quiz configurations |\n| `{quiz_attempts}` | Quiz attempt records |\n| `{question}` | Question bank |\n| `{question_categories}` | Question categories |\n| `{grade_items}` | Gradebook items |\n| `{grade_grades}` | Student grades |\n| `{groups}` | Course groups |\n| `{groups_members}` | Group memberships |\n| `{logstore_standard_log}` | Activity logs |\n\n## Additional Resources\n\n- [Moodle External API Documentation](https:\u002F\u002Fmoodledev.io\u002Fdocs\u002F5.2\u002Fapis\u002Fsubsystems\u002Fexternal\u002Ffunctions)\n- [Moodle Coding Style](https:\u002F\u002Fmoodledev.io\u002Fgeneral\u002Fdevelopment\u002Fpolicies\u002Fcodingstyle)\n- [Moodle Database API](https:\u002F\u002Fmoodledev.io\u002Fdocs\u002F5.2\u002Fapis\u002Fcore\u002Fdml)\n- [Web Services API Documentation](https:\u002F\u002Fmoodledev.io\u002Fdocs\u002F5.2\u002Fapis\u002Fsubsystems\u002Fexternal)\n\n## Guidelines\n\n- Always validate input parameters using `validate_parameters()`\n- Check user context and capabilities before operations\n- Use parameterized SQL queries (never string concatenation)\n- Implement comprehensive error handling and logging\n- Follow Moodle naming conventions (lowercase, underscores)\n- Document all parameters and return values clearly\n- Test with different user roles and permissions\n- Consider transaction safety for write operations\n- Purge caches after service registration changes\n- Keep API methods focused and single-purpose\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,166,460,"2026-05-16 13:29:39",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"编程开发","coding","mdi-code-braces","代码生成、调试、审查，提升开发效率",2,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":25,"skillCount":32,"createdAt":26},"后端开发","backend","mdi-server","API、数据库、服务端架构",296,[34],{"id":35,"skillId":4,"version":36,"fileName":37,"fileSize":38,"filePath":39,"fileHash":40,"manifest":41,"createdAt":19},"4656390e-90bc-4b49-a220-befd0e491a38","1.0.0","moodle-external-api-development.zip",6410,"uploads\u002Fskills\u002Fff4a0a03-ea12-40e9-b250-2bd93dc73e78\u002Fmoodle-external-api-development.zip","c891ba95436b684e21fca7d3ad5d39cdd364c0004053928ea24ee4b979344f90","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":18535}]",{"code":43,"message":44,"data":45},200,"success",{"items":46,"stats":47,"page":50},[],{"averageRating":48,"totalRatings":48,"ratingCounts":49},0,[48,48,48,48,48],{"limit":51,"offset":48,"hasMore":52,"nextOffset":51,"ratedOnly":16},15,false]