moodle-external-api-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMoodle External API Development
Moodle 外部API开发
This skill guides you through creating custom external web service APIs for Moodle LMS, following Moodle's external API framework and coding standards.
本技能将指导您遵循Moodle的外部API框架和编码标准,为Moodle LMS创建自定义外部Web服务API。
When to Use This Skill
适用场景
- Creating custom web services for Moodle plugins
- Implementing REST/AJAX endpoints for course management
- Building APIs for quiz operations, user tracking, or reporting
- Exposing Moodle functionality to external applications
- Developing mobile app backends using Moodle
- 为Moodle插件创建自定义Web服务
- 实现课程管理的REST/AJAX端点
- 构建用于测验操作、用户跟踪或报表的API
- 向外部应用暴露Moodle功能
- 使用Moodle开发移动应用后端
Core Architecture Pattern
核心架构模式
Moodle external APIs follow a strict three-method pattern:
- - Defines input parameter structure
execute_parameters() - - Contains business logic
execute() - - Defines return structure
execute_returns()
Moodle外部API遵循严格的三步方法模式:
- - 定义输入参数结构
execute_parameters() - - 包含业务逻辑
execute() - - 定义返回结构
execute_returns()
Step-by-Step Implementation
分步实现
Step 1: Create the External API Class File
步骤1:创建外部API类文件
Location:
/local/yourplugin/classes/external/your_api_name.phpphp
<?php
namespace local_yourplugin\external;
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/externallib.php");
use external_api;
use external_function_parameters;
use external_single_structure;
use external_value;
class your_api_name extends external_api {
// Three required methods will go here
}Key Points:
- Class must extend
external_api - Namespace follows: or
local_pluginname\externalmod_modname\external - Include the security check:
defined('MOODLE_INTERNAL') || die(); - Require externallib.php for base classes
位置:
/local/yourplugin/classes/external/your_api_name.phpphp
<?php
namespace local_yourplugin\external;
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/externallib.php");
use external_api;
use external_function_parameters;
use external_single_structure;
use external_value;
class your_api_name extends external_api {
// Three required methods will go here
}关键点:
- 类必须继承
external_api - 命名空间遵循:或
local_pluginname\externalmod_modname\external - 包含安全检查:
defined('MOODLE_INTERNAL') || die(); - 引入externallib.php以使用基础类
Step 2: Define Input Parameters
步骤2:定义输入参数
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_OPTIONAL)
]);
}Common Parameter Types:
- - Integers
PARAM_INT - - Plain text (HTML stripped)
PARAM_TEXT - - Raw text (no cleaning)
PARAM_RAW - - Boolean values
PARAM_BOOL - - Floating point numbers
PARAM_FLOAT - - Alphanumeric with extended chars
PARAM_ALPHANUMEXT
Structures:
- - Single value
external_value - - Object with named fields
external_single_structure - - Array of items
external_multiple_structure
Value Flags:
- - Parameter must be provided
VALUE_REQUIRED - - Parameter is optional
VALUE_OPTIONAL - - Optional with default
VALUE_DEFAULT, defaultvalue
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_OPTIONAL)
]);
}常见参数类型:
- - 整数
PARAM_INT - - 纯文本(HTML会被剥离)
PARAM_TEXT - - 原始文本(不做清理)
PARAM_RAW - - 布尔值
PARAM_BOOL - - 浮点数
PARAM_FLOAT - - 带扩展字符的字母数字
PARAM_ALPHANUMEXT
结构类型:
- - 单个值
external_value - - 带命名字段的对象
external_single_structure - - 项目数组
external_multiple_structure
值标记:
- - 必须提供该参数
VALUE_REQUIRED - - 参数为可选
VALUE_OPTIONAL - - 可选且带默认值
VALUE_DEFAULT, defaultvalue
Step 3: Implement Business Logic
步骤3:实现业务逻辑
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('moodle/course:viewhiddenactivities', $context);
}
// 4. Database operations
$sql = "SELECT id, name, timecreated
FROM {your_table}
WHERE userid = :userid
AND courseid = :courseid
LIMIT :limit";
$records = $DB->get_records_sql($sql, [
'userid' => $params['userid'],
'courseid' => $params['courseid'],
'limit' => $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:
- Always validate parameters using
validate_parameters() - Check context using
validate_context() - Verify capabilities using
require_capability() - Use parameterized queries to prevent SQL injection
- Return structured data matching return definition
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('moodle/course:viewhiddenactivities', $context);
}
// 4. Database operations
$sql = "SELECT id, name, timecreated
FROM {your_table}
WHERE userid = :userid
AND courseid = :courseid
LIMIT :limit";
$records = $DB->get_records_sql($sql, [
'userid' => $params['userid'],
'courseid' => $params['courseid'],
'limit' => $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)
];
}关键步骤:
- 始终使用验证参数
validate_parameters() - 使用检查上下文
validate_context() - 使用验证权限
require_capability() - 使用参数化查询防止SQL注入
- 返回与定义匹配的结构化数据
Step 4: Define Return Structure
步骤4:定义返回结构
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 returns
execute() - Use appropriate parameter types
- Document each field with description
- Nested structures allowed
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')
]);
}返回结构规则:
- 必须与返回的内容完全匹配
execute() - 使用合适的参数类型
- 为每个字段添加描述文档
- 允许嵌套结构
Step 5: Register the Service
步骤5:注册服务
Location:
/local/yourplugin/db/services.phpphp
<?php
defined('MOODLE_INTERNAL') || die();
$functions = [
'local_yourplugin_your_api_name' => [
'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' => 0,
'enabled' => 1
]
];Service Registration Keys:
- - Full namespaced class name
classname - - Always 'execute'
methodname - - 'read' (SELECT) or 'write' (INSERT/UPDATE/DELETE)
type - - Set true for AJAX/REST access
ajax - - Required Moodle capabilities
capabilities - - Optional service bundles
services
位置:
/local/yourplugin/db/services.phpphp
<?php
defined('MOODLE_INTERNAL') || die();
$functions = [
'local_yourplugin_your_api_name' => [
'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' => 0,
'enabled' => 1
]
];服务注册关键字:
- - 完整的带命名空间的类名
classname - - 始终为'execute'
methodname - - 'read'(查询)或'write'(增删改)
type - - 设置为true以支持AJAX/REST访问
ajax - - 所需的Moodle权限(多个用逗号分隔)
capabilities - - 可选的服务捆绑包
services
Step 6: Implement Error Handling & Logging
步骤6:实现错误处理与日志
php
private static function log_debug($message) {
global $CFG;
$logdir = $CFG->dataroot . '/local_yourplugin';
if (!file_exists($logdir)) {
mkdir($logdir, 0777, true);
}
$debuglog = $logdir . '/api_debug.log';
$timestamp = date('Y-m-d H:i:s');
file_put_contents($debuglog, "[$timestamp] $message\n", FILE_APPEND | LOCK_EX);
}
public static function execute($userid, $courseid) {
global $DB;
try {
self::log_debug("API called: userid=$userid, courseid=$courseid");
// Validate parameters
$params = self::validate_parameters(self::execute_parameters(), [
'userid' => $userid,
'courseid' => $courseid
]);
// Your logic here
self::log_debug("API completed successfully");
return $result;
} catch (\invalid_parameter_exception $e) {
self::log_debug("Parameter validation failed: " . $e->getMessage());
throw $e;
} catch (\moodle_exception $e) {
self::log_debug("Moodle exception: " . $e->getMessage());
throw $e;
} catch (\Exception $e) {
// Log detailed error info
$lastsql = method_exists($DB, 'get_last_sql') ? $DB->get_last_sql() : '[N/A]';
self::log_debug("Fatal error: " . $e->getMessage());
self::log_debug("Last SQL: " . $lastsql);
self::log_debug("Stack trace: " . $e->getTraceAsString());
throw $e;
}
}Error Handling Best Practices:
- Wrap logic in try-catch blocks
- Log errors with timestamps and context
- Capture SQL queries on database errors
- Preserve stack traces for debugging
- Re-throw exceptions after logging
php
private static function log_debug($message) {
global $CFG;
$logdir = $CFG->dataroot . '/local_yourplugin';
if (!file_exists($logdir)) {
mkdir($logdir, 0777, true);
}
$debuglog = $logdir . '/api_debug.log';
$timestamp = date('Y-m-d H:i:s');
file_put_contents($debuglog, "[$timestamp] $message\n", FILE_APPEND | LOCK_EX);
}
public static function execute($userid, $courseid) {
global $DB;
try {
self::log_debug("API called: userid=$userid, courseid=$courseid");
// Validate parameters
$params = self::validate_parameters(self::execute_parameters(), [
'userid' => $userid,
'courseid' => $courseid
]);
// Your logic here
self::log_debug("API completed successfully");
return $result;
} catch (\invalid_parameter_exception $e) {
self::log_debug("Parameter validation failed: " . $e->getMessage());
throw $e;
} catch (\moodle_exception $e) {
self::log_debug("Moodle exception: " . $e->getMessage());
throw $e;
} catch (\Exception $e) {
// Log detailed error info
$lastsql = method_exists($DB, 'get_last_sql') ? $DB->get_last_sql() : '[N/A]';
self::log_debug("Fatal error: " . $e->getMessage());
self::log_debug("Last SQL: " . $lastsql);
self::log_debug("Stack trace: " . $e->getTraceAsString());
throw $e;
}
}错误处理最佳实践:
- 将逻辑包裹在try-catch块中
- 记录带时间戳和上下文的错误
- 数据库错误时捕获SQL查询
- 保留堆栈跟踪用于调试
- 记录后重新抛出异常
Advanced Patterns
高级模式
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;
}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);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]);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);
}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
API测试
1. Via Moodle Web Services Test Client
1. 通过Moodle Web服务测试客户端
- Enable web services: Site administration > Advanced features
- Enable REST protocol: Site administration > Plugins > Web services > Manage protocols
- Create service: Site administration > Server > Web services > External services
- Test function: Site administration > Development > Web service test client
- 启用Web服务:站点管理 > 高级功能
- 启用REST协议:站点管理 > 插件 > Web服务 > 管理协议
- 创建服务:站点管理 > 服务器 > Web服务 > 外部服务
- 测试功能:站点管理 > 开发 > Web服务测试客户端
2. Via curl
2. 通过curl
bash
undefinedbash
undefinedGet token first
Get token first
curl -X POST "https://yourmoodle.com/login/token.php"
-d "username=admin"
-d "password=yourpassword"
-d "service=moodle_mobile_app"
-d "username=admin"
-d "password=yourpassword"
-d "service=moodle_mobile_app"
curl -X POST "https://yourmoodle.com/login/token.php"
-d "username=admin"
-d "password=yourpassword"
-d "service=moodle_mobile_app"
-d "username=admin"
-d "password=yourpassword"
-d "service=moodle_mobile_app"
Call your API
Call your API
curl -X POST "https://yourmoodle.com/webservice/rest/server.php"
-d "wstoken=YOUR_TOKEN"
-d "wsfunction=local_yourplugin_your_api_name"
-d "moodlewsrestformat=json"
-d "userid=2"
-d "courseid=3"
-d "wstoken=YOUR_TOKEN"
-d "wsfunction=local_yourplugin_your_api_name"
-d "moodlewsrestformat=json"
-d "userid=2"
-d "courseid=3"
undefinedcurl -X POST "https://yourmoodle.com/webservice/rest/server.php"
-d "wstoken=YOUR_TOKEN"
-d "wsfunction=local_yourplugin_your_api_name"
-d "moodlewsrestformat=json"
-d "userid=2"
-d "courseid=3"
-d "wstoken=YOUR_TOKEN"
-d "wsfunction=local_yourplugin_your_api_name"
-d "moodlewsrestformat=json"
-d "userid=2"
-d "courseid=3"
undefined3. Via JavaScript (AJAX)
3. 通过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);
});
});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
1. "Function not found" 错误
Solution:
- Purge caches: Site administration > Development > Purge all caches
- Verify function name in services.php matches exactly
- Check namespace and class name are correct
解决方案:
- 清除缓存:站点管理 > 开发 > 清除所有缓存
- 验证services.php中的函数名是否完全匹配
- 检查命名空间和类名是否正确
2. "Invalid parameter value detected"
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
3. SQL注入漏洞
Solution:
- Always use placeholder parameters ()
:paramname - Never concatenate user input into SQL strings
- Use Moodle's database methods: ,
get_record(), etc.get_records()
解决方案:
- 始终使用占位符参数()
:paramname - 绝不要将用户输入拼接进SQL字符串
- 使用Moodle的数据库方法:、
get_record()等get_records()
4. Permission Denied Errors
4. 权限拒绝错误
Solution:
- Call early in execute()
self::validate_context($context) - Check required capabilities match user's permissions
- Verify user has role assignments in the context
解决方案:
- 在execute()早期调用
self::validate_context($context) - 检查所需权限是否与用户权限匹配
- 验证用户在上下文中的角色分配
5. Transaction Deadlocks
5. 事务死锁
Solution:
- Keep transactions short
- Always commit or rollback in finally blocks
- Avoid nested transactions
解决方案:
- 保持事务简短
- 始终在finally块中提交或回滚
- 避免嵌套事务
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/ - Verify database queries using
$DB->set_debug(true) - Test with admin user to rule out permission issues
- Clear browser cache and Moodle caches
- Check PHP error logs on server
- 检查Moodle调试模式:站点管理 > 开发 > 调试
- 查看Web服务日志:站点管理 > 报表 > 日志
- 检查中的自定义日志文件
$CFG->dataroot/local_yourplugin/ - 使用验证数据库查询
$DB->set_debug(true) - 使用管理员用户测试以排除权限问题
- 清除浏览器缓存和Moodle缓存
- 检查服务器上的PHP错误日志
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)local/yourplugin/
├── version.php # 插件版本和元数据
├── db/
│ ├── services.php # 外部服务定义
│ └── access.php # 权限定义(可选)
├── classes/
│ └── external/
│ ├── your_api_name.php # 外部API实现
│ └── another_api.php # 其他API
├── lang/
│ └── en/
│ └── local_yourplugin.php # 语言字符串
└── tests/
└── external_test.php # 单元测试(可选但推荐)Examples from Real Implementation
真实实现示例
Simple Read API (Get Quiz Attempts)
简单读取API(获取测验尝试次数)
php
<?php
namespace local_userlog\external;
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/externallib.php");
use external_api;
use external_function_parameters;
use external_single_structure;
use external_value;
class get_quiz_attempts extends external_api {
public static function execute_parameters() {
return new external_function_parameters([
'userid' => new external_value(PARAM_INT, 'User ID'),
'courseid' => new external_value(PARAM_INT, 'Course ID')
]);
}
public static function execute($userid, $courseid) {
global $DB;
self::validate_parameters(self::execute_parameters(), [
'userid' => $userid,
'courseid' => $courseid
]);
$sql = "SELECT COUNT(*) AS quiz_attempts
FROM {quiz_attempts} qa
JOIN {quiz} q ON qa.quiz = q.id
WHERE qa.userid = :userid AND q.course = :courseid";
$attempts = $DB->get_field_sql($sql, [
'userid' => $userid,
'courseid' => $courseid
]);
return ['quiz_attempts' => (int)$attempts];
}
public static function execute_returns() {
return new external_single_structure([
'quiz_attempts' => new external_value(PARAM_INT, 'Total number of quiz attempts')
]);
}
}php
<?php
namespace local_userlog\external;
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/externallib.php");
use external_api;
use external_function_parameters;
use external_single_structure;
use external_value;
class get_quiz_attempts extends external_api {
public static function execute_parameters() {
return new external_function_parameters([
'userid' => new external_value(PARAM_INT, 'User ID'),
'courseid' => new external_value(PARAM_INT, 'Course ID')
]);
}
public static function execute($userid, $courseid) {
global $DB;
self::validate_parameters(self::execute_parameters(), [
'userid' => $userid,
'courseid' => $courseid
]);
$sql = "SELECT COUNT(*) AS quiz_attempts
FROM {quiz_attempts} qa
JOIN {quiz} q ON qa.quiz = q.id
WHERE qa.userid = :userid AND q.course = :courseid";
$attempts = $DB->get_field_sql($sql, [
'userid' => $userid,
'courseid' => $courseid
]);
return ['quiz_attempts' => (int)$attempts];
}
public static function execute_returns() {
return new external_single_structure([
'quiz_attempts' => new external_value(PARAM_INT, 'Total number of quiz attempts')
]);
}
}Complex Write API (Create Quiz from Categories)
复杂写入API(从分类创建测验)
See attached for a comprehensive example including:
create_quiz_from_categories.php- Multiple database insertions
- Course module creation
- Quiz instance configuration
- Random question selection with tags
- Group-based access restrictions
- Extensive error logging
- Transaction management
请查看附件获取完整示例,包括:
create_quiz_from_categories.php- 多数据库插入
- 课程模块创建
- 测验实例配置
- 带标签的随机题目选择
- 基于分组的访问限制
- 详细错误日志
- 事务管理
Quick Reference: Common Moodle Tables
快速参考:常见Moodle表
| Table | Purpose |
|---|---|
| User accounts |
| Courses |
| Activity instances in courses |
| Available activity types (quiz, forum, etc.) |
| Quiz configurations |
| Quiz attempt records |
| Question bank |
| Question categories |
| Gradebook items |
| Student grades |
| Course groups |
| Group memberships |
| Activity logs |
| 表名 | 用途 |
|---|---|
| 用户账户 |
| 课程 |
| 课程中的活动实例 |
| 可用活动类型(测验、论坛等) |
| 测验配置 |
| 测验尝试记录 |
| 题库 |
| 题目分类 |
| 成绩册项目 |
| 学生成绩 |
| 课程分组 |
| 分组成员 |
| 活动日志 |
Additional Resources
额外资源
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
- 始终使用验证输入参数
validate_parameters() - 操作前检查用户上下文和权限
- 使用参数化SQL查询(绝不要字符串拼接)
- 实现全面的错误处理和日志
- 遵循Moodle命名约定(小写、下划线)
- 清晰记录所有参数和返回值
- 使用不同用户角色和权限进行测试
- 写入操作考虑事务安全性
- 服务注册变更后清除缓存
- 保持API方法聚焦于单一功能