moodle-external-api-development
Moodle LMSのexternal web service APIをカスタム開発する際に、MoodleのExternal APIフレームワークおよびコーディング標準に従って実装を進められるようガイドするスキルです。
description の原文を見る
This skill guides you through creating custom external web service APIs for Moodle LMS, following Moodle's external API framework and coding standards.
SKILL.md 本文
Moodle External API Development
このスキルは、Moodle の外部 API フレームワークとコーディング標準に従って、Moodle LMS 用のカスタム外部 Web サービス API を作成する方法を説明します。
このスキルの使用時機
- Moodle プラグイン用のカスタム Web サービス作成
- コース管理用の REST/AJAX エンドポイント実装
- クイズ操作、ユーザートラッキング、またはレポート用 API 構築
- 外部アプリケーションへの Moodle 機能公開
- Moodle を使用したモバイルアプリバックエンド開発
コアアーキテクチャパターン
Moodle 外部 API は厳密な 3 メソッドパターンに従います:
execute_parameters()- 入力パラメータ構造を定義execute()- ビジネスロジックを含むexecute_returns()- 戻り値の構造を定義
ステップバイステップ実装
ステップ 1: 外部 API クラスファイルを作成する
場所: /local/yourplugin/classes/external/your_api_name.php
<?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\externalまたはmod_modname\external - セキュリティチェックを含める:
defined('MOODLE_INTERNAL') || die(); - 基底クラスのために externallib.php を必須化する
ステップ 2: 入力パラメータを定義する
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- 整数PARAM_TEXT- プレーンテキスト(HTML は削除)PARAM_RAW- 生のテキスト(クリーニングなし)PARAM_BOOL- ブール値PARAM_FLOAT- 浮動小数点数PARAM_ALPHANUMEXT- 拡張文字付き英数字
構造:
external_value- 単一値external_single_structure- 名前付きフィールドを持つオブジェクトexternal_multiple_structure- アイテムの配列
値フラグ:
VALUE_REQUIRED- パラメータは必須VALUE_OPTIONAL- パラメータはオプションVALUE_DEFAULT, defaultvalue- デフォルト値付きオプション
ステップ 3: ビジネスロジックを実装する
public static function execute($userid, $courseid, $options = []) {
global $DB, $USER;
// 1. パラメータを検証
$params = self::validate_parameters(self::execute_parameters(), [
'userid' => $userid,
'courseid' => $courseid,
'options' => $options
]);
// 2. 権限・機能をチェック
$context = \context_course::instance($params['courseid']);
self::validate_context($context);
require_capability('moodle/course:view', $context);
// 3. ユーザーアクセスを確認
if ($params['userid'] != $USER->id) {
require_capability('moodle/course:viewhiddenactivities', $context);
}
// 4. データベース操作
$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. データを処理して返す
$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 インジェクション防止のためパラメータ化クエリを使用
- 戻り値定義に一致する構造化データを返す
ステップ 4: 戻り値構造を定義する
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()が返すものと正確に一致する必要がある- 適切なパラメータタイプを使用
- 各フィールドを説明文書化
- ネストされた構造は許可
ステップ 5: サービスを登録する
場所: /local/yourplugin/db/services.php
<?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- フルネームスペース付きクラス名methodname- 常に 'execute'type- 'read' (SELECT) または 'write' (INSERT/UPDATE/DELETE)ajax- AJAX/REST アクセス用に true を設定capabilities- 必須の Moodle 機能services- オプションのサービスバンドル
ステップ 6: エラーハンドリングとログを実装する
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");
// パラメータを検証
$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) {
// 詳細なエラー情報をログ
$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 クエリをキャプチャ
- デバッグ用にスタックトレースを保持
- ログ後に例外を再スロー
高度なパターン
複雑なデータベース操作
// トランザクション例
$transaction = $DB->start_delegated_transaction();
try {
// レコードを挿入
$recordid = $DB->insert_record('your_table', $dataobject);
// 関連レコードを更新
$DB->set_field('another_table', 'status', 1, ['recordid' => $recordid]);
// トランザクションをコミット
$transaction->allow_commit();
} catch (\Exception $e) {
$transaction->rollback($e);
throw $e;
}
コースモジュールを使用する
// コースモジュールを作成
$moduleid = $DB->get_field('modules', 'id', ['name' => 'quiz'], MUST_EXIST);
$cm = new \stdClass();
$cm->course = $courseid;
$cm->module = $moduleid;
$cm->instance = 0; // アクティビティ作成後に更新される
$cm->visible = 1;
$cm->groupmode = 0;
$cmid = add_course_module($cm);
// アクティビティインスタンスを作成(例:クイズ)
$quiz = new \stdClass();
$quiz->course = $courseid;
$quiz->name = 'My Quiz';
$quiz->coursemodule = $cmid;
// ... other quiz fields ...
$quizid = quiz_add_instance($quiz, null);
// インスタンス ID でコースモジュールを更新
$DB->set_field('course_modules', 'instance', $quizid, ['id' => $cmid]);
course_add_cm_to_section($courseid, $cmid, 0);
アクセス制限(グループ/利用可能性)
// アクティビティをグループ経由で特定ユーザーに制限
$groupname = 'activity_' . $activityid . '_user_' . $userid;
// グループを作成または取得
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);
}
// ユーザーをグループに追加
if (!$DB->record_exists('groups_members', ['groupid' => $groupid, 'userid' => $userid])) {
$DB->insert_record('groups_members', (object)[
'groupid' => $groupid,
'userid' => $userid,
'timeadded' => time()
]);
}
// 利用可能性条件を設定
$restriction = [
'op' => '&',
'show' => false,
'c' => [
[
'type' => 'group',
'id' => $groupid
]
],
'showc' => [false]
];
$DB->set_field('course_modules', 'availability', json_encode($restriction), ['id' => $cmid]);
タグを使用したランダム質問選択
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);
}
API のテスト
1. Moodle Web Services テストクライアント経由
- Web サービスを有効化:サイト管理 > 詳細機能
- REST プロトコルを有効化:サイト管理 > プラグイン > Web サービス > プロトコル管理
- サービスを作成:サイト管理 > サーバー > Web サービス > 外部サービス
- テスト関数:サイト管理 > 開発 > Web サービステストクライアント
2. curl 経由
# まずトークンを取得
curl -X POST "https://yourmoodle.com/login/token.php" \
-d "username=admin" \
-d "password=yourpassword" \
-d "service=moodle_mobile_app"
# 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"
3. JavaScript(AJAX)経由
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);
});
});
よくある落とし穴と解決策
1. 「Function not found」エラー
解決策:
- キャッシュをパージ:サイト管理 > 開発 > すべてのキャッシュをパージ
- services.php の関数名が正確に一致することを確認
- 名前空間とクラス名が正しいことを確認
2. 「Invalid parameter value detected」
解決策:
- パラメータタイプが定義と使用で一致することを確認
- 必須パラメータとオプションパラメータをチェック
- ネストされた構造定義を検証
3. SQL インジェクション脆弱性
解決策:
- 常にプレースホルダーパラメータを使用(
:paramname) - ユーザー入力を SQL 文字列に連結しない
- Moodle データベースメソッドを使用:
get_record()、get_records()など
4. 権限拒否エラー
解決策:
execute()の最初でself::validate_context($context)を呼び出す- 必須機能がユーザーの権限と一致することを確認
- ユーザーがコンテキスト内にロール割り当てを持つことを確認
5. トランザクションデッドロック
解決策:
- トランザクションを短く保つ
- finally ブロックで常にコミットまたはロールバック
- ネストされたトランザクションを避ける
デバッグチェックリスト
- Moodle デバッグモードをチェック:サイト管理 > 開発 > デバッグ
- Web サービスログを確認:サイト管理 > レポート > ログ
-
$CFG->dataroot/local_yourplugin/のカスタムログファイルをチェック -
$DB->set_debug(true)を使用してデータベースクエリを確認 - 権限問題を除外するため管理ユーザーでテスト
- ブラウザキャッシュと Moodle キャッシュをクリア
- サーバー上の PHP エラーログをチェック
プラグイン構造チェックリスト
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)
実装の実例
シンプルな読み取り API(クイズの試行を取得)
<?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')
]);
}
}
複雑な書き込み API(カテゴリからクイズを作成)
添付の create_quiz_from_categories.php を参照してください。以下を含む包括的な例が示されています:
- 複数のデータベース挿入
- コースモジュール作成
- クイズインスタンス設定
- タグを使用したランダム質問選択
- グループベースのアクセス制限
- 広範なエラーログ
- トランザクション管理
クイックリファレンス:一般的な Moodle テーブル
| テーブル | 目的 |
|---|---|
{user} | ユーザーアカウント |
{course} | コース |
{course_modules} | コース内のアクティビティインスタンス |
{modules} | 利用可能なアクティビティタイプ(quiz、forum など) |
{quiz} | クイズ設定 |
{quiz_attempts} | クイズ試行レコード |
{question} | 質問バンク |
{question_categories} | 質問カテゴリ |
{grade_items} | 成績表アイテム |
{grade_grades} | 学生成績 |
{groups} | コースグループ |
{groups_members} | グループメンバーシップ |
{logstore_standard_log} | アクティビティログ |
追加リソース
- Moodle External API Documentation
- Moodle Coding Style
- Moodle Database API
- Web Services API Documentation
ガイドライン
validate_parameters()を使用して常に入力パラメータを検証- 操作前にユーザーコンテキストと機能をチェック
- パラメータ化 SQL クエリを使用(文字列連結を使用しない)
- 包括的なエラーハンドリングとログを実装
- Moodle 命名規則に従う(小文字、アンダースコア)
- すべてのパラメータと戻り値を明確に文書化
- 異なるユーザーロールと権限でテスト
- 書き込み操作のトランザクション安全性を検討
- サービス登録変更後にキャッシュをパージ
- API メソッドを焦点を絞った単一目的で保つ
制限事項
- このスキルは、タスクが上記の説明範囲と明確に一致する場合にのみ使用してください。
- 出力を、環境固有の検証、テスト、または専門家による確認の代替として扱わないでください。
- 必須の入力、権限、安全境界、または成功基準が不明な場合は、立ち止まって明確化を求めてください。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- sickn33
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/sickn33/antigravity-awesome-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。