mcpField
M
moodle-external-api-development

moodle-external-api-development

github.com/davila7開発33.1k177 回インストール今日更新
リポジトリ

moodle-external-api-development について

Moodle LMS プラグイン開発者向けに、外部 Web サービス API の実装方法を案内するスキルです。API クラスの構成、サービス登録、アクセス検証、安全なデータベース操作、テストとデバッグを扱います。

主な機能

  1. 01三つのメソッドによる API クラス構成
  2. 02入力・戻り値の構造定義
  3. 03サービス登録とアクセス制御
  4. 04パラメーター検証と安全なクエリ
  5. 05エラー処理・ログ・トランザクション
  6. 06REST・AJAX・curl によるテスト

活用シーン

  1. 01Moodle プラグインの Web サービス作成
  2. 02コース管理やクイズの API 開発
  3. 03外部アプリケーションへの Moodle 機能公開
  4. 04API のパラメーターや権限エラーのデバッグ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/davila7/claude-code-templates --skill moodle-external-api-development

インストール後、Claude Code で/moodle-external-api-developmentを入力して読み込まれたか確認します。

詳細

Moodle External API 開発

このスキルは、Moodle の外部 API フレームワークとコーディング標準に従って、Moodle LMS 向けのカスタム外部 Web サービス API を作成する手順を案内します。

このスキルを使用する場面

  • Moodle プラグイン向けのカスタム Web サービスを作成する場合
  • コース管理用の REST/AJAX エンドポイントを実装する場合
  • クイズ操作、ユーザー追跡、レポート作成向けの API を構築する場合
  • Moodle の機能を外部アプリケーションに公開する場合
  • Moodle を使用してモバイルアプリのバックエンドを開発する場合

コアアーキテクチャパターン

Moodle の外部 API は、厳格な 3 メソッドパターンに従います。

  1. execute_parameters() - 入力パラメーターの構造を定義します
  2. execute() - ビジネスロジックを含みます
  3. 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. 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)
    ];
}

重要な手順:

  1. 必ず validate_parameters() を使用してパラメーターを検証します
  2. validate_context() を使用してコンテキストを確認します
  3. require_capability() を使用して権限を確認します
  4. SQL インジェクションを防ぐため、パラメーター化されたクエリを使用します
  5. 戻り値の定義に一致する構造化データを返します
ステップ 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");
        
        // 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 クエリを取得します
  • デバッグのためにスタックトレースを保持します
  • ログ記録後に例外を再スローします

応用パターン

複雑なデータベース操作
// 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;
}
コースモジュールの操作
// 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);
アクセス制限(グループ/利用可能条件)
// 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]);
タグを使用したランダムな問題選択
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 サービステストクライアントを使用する
  1. Web サービスを有効にします: サイト管理 > 高度な機能
  2. REST プロトコルを有効にします: サイト管理 > プラグイン > Web サービス > プロトコルの管理
  3. サービスを作成します: サイト管理 > サーバー > Web サービス > 外部サービス
  4. 関数をテストします: サイト管理 > 開発 > Web サービステストクライアント
2. curl を使用する
# Get token first
curl -X POST "https://yourmoodle.com/login/token.php" \
  -d "username=admin" \
  -d "password=yourpassword" \
  -d "service=moodle_mobile_app"

# 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"
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() の early 段階で self::validate_context($context) を呼び出す
  • 必要なケイパビリティがユーザーの権限と一致していることを確認する
  • ユーザーがそのコンテキストでロールの割り当てを受けていることを確認する
5. トランザクションのデッドロック

解決策:

  • トランザクションは短く保つ
  • finally ブロックで必ず commit または rollback を行う
  • ネストされたトランザクションを避ける

デバッグチェックリスト

  • Moodle のデバッグモードを確認する:サイト管理 > 開発 > デバッグ
  • ウェブサービスのログを確認する:サイト管理 > レポート > ログ
  • $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}アクティビティログ

追加リソース

ガイドライン

  • 入力パラメーターは必ず validate_parameters() を使用して検証する
  • 操作の前にユーザーのコンテキストとケイパビリティを確認する
  • パラメーター化された SQL クエリを使用する(文字列連結は行わない)
  • 包括的なエラー処理とロギングを実装する
  • Moodle の命名規則に従う(小文字、アンダースコア区切り)
  • すべてのパラメーターと戻り値を明確に文書化する
  • さまざまなユーザーロールと権限でテストする
  • 書き込み操作ではトランザクションの安全性を考慮する
  • サービス登録を変更した後はキャッシュを消去する
  • API メソッドは焦点を絞り、単一の目的に限定する

コメント