Moodle's AMD modules load JavaScript asynchronously and isolate dependencies using RequireJS. This guide covers module structure, injecting Moodle services, localization, and building production-ready minified output using Grunt.
SOP: Writing and Building AMD Modules in Moodle
This guide will walk you through creating, localizing, and building asynchronous module definition (AMD) JavaScript modules in Moodle, as well as how to pull in core Moodle services (like the notification system) from your AMD code.
1. What is AMD in Moodle?
Moodle uses RequireJS to load JavaScript modules asynchronously. An AMD module is a JavaScript file that:
- Declares its dependencies via a
define([...], function(...) { … }) call. - Exports one or more functions (often a single
init entry point). - Can be minified and concatenated into production-ready code.
By using AMD, modules only load when needed and avoid polluting the global namespace.
2. Folder Layout
Inside your plugin (e.g. mod/assessmentform/), you should have:
mod/assessmentform/
├── amd/
│ ├── src/ ← Your unbuilt source files
│ │ └── survey_init.js
│ └── build/ ← Moodle’s build step will place minified files here
│ └── survey_init.min.js
└── view.php ← Calls your AMD module via js_call_amd()
amd/src/Place your human-readable modules here.amd/build/After running the build, minified code will live here. Commit the build outputs so your plugin works without requiring Node on the production server.
3. Writing Your AMD Module
In amd/src/survey_init.js, you might have:
define([
'jquery',
'core/notification',
'core/str', // for translations
'survey-core',
'survey-js-ui'
], function($, notification, str, Survey) {
return {
init: function(args) {
// 1. Localization
var texts = args.str || {};
// str.get_string('key','mod_yourplugin') can also be used.
// 2. Load and render your SurveyJS
$.getJSON(args.jsonUrl).done(function(def) {
var survey = new Survey.Model(def);
survey.render(document.getElementById(args.containerId;
survey.onComplete.add(function(sender) {
// Build payload…
$.ajax({
url: args.postUrl,
method: 'POST',
data: JSON.stringify({ /* … */ }),
contentType: 'application/json',
dataType: 'json',
}).done(function(response) {
notification.addNotification({
message: texts.responsesavedsuccess || 'Saved successfully!',
type: 'success'
});
// redirect …
}).fail(function() {
notification.addNotification({
message: texts.responsefailed || 'Save failed.',
type: 'error'
});
});
});
}).fail(function() {
$('#'+args.containerId)
.html('<div class="alert alert-danger">Error loading form.</div>');
});
}
};
});
Key points
define([...], function(...) { … })List the module IDs you depend on. Moodle’s loader will map these to the right files.core/notificationExposes notification.addNotification({message, type}).core/strOffers str.get_string(key, component) for on-the-fly translations.js_call_amd() in PHPPass an associative array of arguments from PHP into your JS entry-point.
4. Injecting Moodle Services
Just list them in your dependency array:
define([
'jquery',
'core/notification',
'core/str',
'core/ajax',
// …your other libraries…
], function($, notification, str, Ajax) {
// You now have `notification`, `str` and `Ajax.call()` available.
});
core/ajaxUse Moodle’s AJAX wrapper:Ajax.call([{ methodname: 'mod_yourplugin_do_something', args: { foo: 1 }}])[0].then(function(result){ … }).catch(function(err){ notification.exception(err); });
5. Localization
- In your language file (
lang/en/mod_assessmentform.php): $string['responsesavedsuccess'] = 'Assessment saved successfully for {$a->fullname}.';$string['responsefailed'] = 'Error saving assessment.';- Pass them into JS when you call your AMD module:
$PAGE->requires->js_call_amd( 'mod_assessmentform/survey_init', 'init', [ 'str' => [ 'responsesavedsuccess' => get_string('responsesavedsuccess','mod_assessmentform'), 'responsefailed' => get_string('responsefailed','mod_assessmentform'), ], // …other args… ]);- In JS, access
args.str.responsesavedsuccess.
6. Building (Minifying) Your AMD
Moodle provides a Grunt setup out of the box. At your plugin root:
- Install tools (one-time per project):
npm install --global grunt-clinpm install- Build all AMD modules (including yours):
grunt amd- This reads every
amd/src/*.js in all plugins, minifies them, and writes to amd/build/*.min.js. - You can also target just your plugin:
grunt amd:mod_assessmentform- Commit the contents of
amd/build/ into your plugin’s repository.
If you prefer a standalone build, you can drop a Gruntfile.js at your plugin root that only references your AMD folder. Moodle’s core Gruntfile already handles this; there’s no extra setup needed.
7. Summary Checklist
- Create
amd/src/yourmodule.js with a define([...], ...). - Load it in PHP with:
$PAGE->requires->js_call_amd('mod_yourplugin/yourmodule','init',[$args]);- Use injected services:
core/notification → notification.addNotification(...)core/str → str.get_string(...)core/ajax → Ajax.call(...)- Build with
npm install && grunt amd and commit amd/build/*.min.js. - Test on your dev site, then deploy.
With this workflow in place, you can rapidly iterate your JS in amd/src/ and then generate production-ready, minified AMD modules via Moodle’s standard Grunt toolchain.
Legacy Totara and Moodle checkouts requiring PHP 7.4 run into problems when the host's default PHP is newer. This guide shows how to create a dedicated PHP launcher directory and bypass Totara's PATH reset behavior so PHPUnit uses the correct interpreter.
Running the correct version of PHP and PHPUnit for a specific Moodle release is a prerequisite for reliable plugin tests. This guide covers setting up a parallel PHP 7.4 environment alongside a newer system PHP, and configuring PHPUnit to use it — working around the PATH reset issue in Totara's PHPUnit bootstrap.
Use this when a legacy Totara/Moodle checkout needs PHPUnit under PHP 7.4, but the host default php binary is a newer version and Totara's PHPUnit bootstrap keeps resolving nested php calls to the wrong interpreter.
Scope
This applies when all of the following are true:
- the application requires PHP 7.4 for test tooling
- `/usr/bin/php` points to a newer version
- Totara's `test/phpunit/phpunit.php` bootstrap is used
- Composer dependency install is blocked by a newer Composer or audit defaults
Why the normal PATH trick fails
Totara's PHPUnit wrapper resets PATH using dirname(PHP_BINARY). If you start it with /usr/bin/php7.4, it prepends /usr/bin again, and nested php calls still resolve to /usr/bin/php instead of PHP 7.4.
Recommended approach
- Create a dedicated launcher directory with a copied `php` binary for PHP 7.4.
- Skip Totara's Composer bootstrap if necessary and install PHPUnit dependencies manually with a Composer version that still supports the locked package set.
- Use Totara's lower-level `server/admin/tool/phpunit/cli/util.php` entrypoint directly, not `test/phpunit/phpunit.php init`, if the wrapper still leaks to the system PHP.
Example
Assumptions:
- PHP 7.4 binary: `/usr/bin/php7.4`
- project root: `/path/to/project/totara`
Create the launcher:
mkdir -p /tmp/php74bin
cp /usr/bin/php7.4 /tmp/php74bin/php
/tmp/php74bin/php -r 'echo PHP_BINARY, PHP_EOL;'
Expected output:
/tmp/php74bin/php
If the Totara checkout expects totara/config.php, provide the instance bridge temporarily:
ln -sf /path/to/instance/config.php /path/to/project/totara/config.php
Prepare Composer manually when Totara's init step cannot:
cd /path/to/project/totara
curl -L https://getcomposer.org/download/2.6.6/composer.phar -o composer.phar
/tmp/php74bin/php composer.phar --version
/tmp/php74bin/php composer.phar -d test/phpunit update phpunit/phpunit brianium/paratest -W
Provide a PHPUnit config if the instance config does not define phpunit_* settings:
cat > /tmp/project-phpunit-config.php <<'PHP'
<?php
$CFG = new stdClass();
$CFG->dataroot = '/tmp/project_phpunit_data';
$CFG->dbtype = 'pgsql';
$CFG->dblibrary = 'native';
$CFG->dbhost = 'localhost';
$CFG->dbname = 'project_db';
$CFG->dbuser = 'dbuser';
$CFG->dbpass = 'dbpass';
$CFG->prefix = 'phpu_';
$CFG->dboptions = [
'dbpersist' => false,
'dbsocket' => false,
'dbport' => '',
];
PHP
ln -sf /tmp/project-phpunit-config.php /path/to/project/totara/test/phpunit/config.php
Initialize the isolated PHPUnit site directly:
cd /path/to/project/totara
/tmp/php74bin/php -d max_input_vars=5000 server/admin/tool/phpunit/cli/util.php --install
/tmp/php74bin/php -d max_input_vars=5000 server/admin/tool/phpunit/cli/util.php --buildconfig
Run a targeted test:
cd /path/to/project/totara
/tmp/php74bin/php -d max_input_vars=5000 server/admin/tool/phpunit/cli/util.php --run --filter local_multitenant_delete_capability_testcase server/local/multitenant/tests/delete_capability_test.php
Cleanup:
rm -f /path/to/project/totara/config.php
rm -f /path/to/project/totara/test/phpunit/config.php
Notes
- Prefer a project-local `composer.phar` instead of changing the system Composer.
- If Composer still blocks old locked packages, either use an older Composer release or repair the PHPUnit lockfile under PHP 7.4.
- Do not use a shell wrapper script for `php`; in this environment `PHP_BINARY` still resolved to the original binary path. A copied PHP 7.4 binary worked.
- The `test/phpunit/phpunit.php init` wrapper may still leak to the host default `php`. If that happens, use `server/admin/tool/phpunit/cli/util.php` directly.
Testing that Moodle plugins correctly post data to remote endpoints requires a local test server to capture requests. This guide shows how to set up a simple PHP endpoint, configure the plugin, and inspect the actual HTTP payloads being sent.
SOP: Testing Moodle Plugins That Transmit Data to a Remote Endpoint
Purpose
To verify that a Moodle plugin correctly sends data (e.g., grades) to a remote web service (typically via HTTP POST) when specific events are triggered, such as grading an assignment.
Prerequisites
- Moodle instance with the plugin installed and enabled
- Access to Moodle server (for grading & logs)
- PHP CLI available (7.4+)
- A terminal:
- Linux/macOS: Bash or Zsh
- Windows: PowerShell or CMD
- Optional: curl or Postman for manual POSTs
- Step-by-Step Procedure
- Step 1: Set Up a Fake Local Endpoint
- UNIX-like systems (Linux/macOS)
- Create a test directory:
mkdir -p ~/php/wstest cd ~/php/wstest- Create a file called
index.php:<?php ob_start();echo "=== REQUEST METHOD: " . $_SERVER['REQUEST_METHOD'] . " ===\n";foreach (getallheaders() as $name => $value) { echo "$name: $value\n";}echo "\n";$input = file_get_contents('php://input');echo "=== BODY ===\n";echo $input . "\n";$json = json_decode($input, true);if ($json !== null) { echo "\n=== PARSED JSON ===\n"; print_r($json);}file_put_contents('php://stdout', "=== LOG FROM index.php ===\n" . ob_get_contents() . "\n");ob_end_flush();- Start the PHP test server:
php -S localhost:8000 - Keep this terminal open — you'll see incoming requests printed here.
- 🪟 Windows (for testers)
- Install PHP
- Download from https://windows.php.net/
- Extract to
C:\php - Add
C:\php to your PATH - Create a test script
- Create folder:
C:\php\wstest - Create file:
index.php (same contents as above) - Start server in CMD or PowerShell:
cd C:\php\wstest php -S localhost:8000- Step 2: Point Moodle to the Local Endpoint
- In the plugin settings, set the endpoint to your local test server:
http://localhost:8000 - Ensure your plugin's event observer is correctly registered. Example (your plugin may differ):
'callback' => '\local_yourplugin\observer::graded' - Confirm that your observer method exists and is properly namespaced. Example method signature:
public static function graded($eventdata) { // your handling code}- Step 3: Trigger the Event
- Grade an assignment or perform any action that triggers the event
- Watch your terminal window
Example terminal output:
=== LOG FROM index.php ====== REQUEST METHOD: POST ===Content-Type: application/json=== BODY ==={"Cijfer":"77.00000","Cijfertype":"point","OpdrachtID":"15030","GebruikerID":"145404"}=== PARSED JSON ===Array( [Cijfer] => 77.00000 [Cijfertype] => point [OpdrachtID] => 15030 [GebruikerID] => 145404)- Step 4: Debug If Nothing Appears
- Check that:
- The plugin is enabled
- The correct Moodle event is triggered
- The callback matches your observer class and method
- Add debug logging with: Example:
error_log("Triggered observer::graded()"); - Check web server logs
- Ensure your
curl call includes: Example:CURLOPT_RETURNTRANSFER => true - Success Criteria
- Endpoint receives a POST request
- Data includes expected keys and values
- No errors are logged in Moodle or PHP logs
- Cleanup
- Press
Ctrl+C to stop the test server - Revert endpoint configuration if needed
Building AMD modules in Moodle plugins using esbuild avoids Grunt's complexity while maintaining full compatibility. This guide covers setup, configuration, and the complete build-to-production workflow.
Standard Operating Procedure: Using esbuild to Build AMD Modules in a Moodle Plugin
Purpose
To compile modern JavaScript source files in amd/src/ into AMD-compatible .min.js files in amd/build/ using esbuild, in a way that's fully Moodle-compliant but avoids Grunt.
Directory Structure
Your Moodle plugin should contain:
mod/yourplugin/
amd/
src/
editor.js
runtime.js
build/ (created automatically by esbuild)
Each source file should begin with a valid AMD define() block:
// amd/src/editor.js
define(['jquery'], function($) {
return {
init: function(cfg) {
console.log('Hello from editor.js', cfg);
}
};
});
Step 1: Initialize NPM (once)
From your plugin root:
npm init -y
This creates a package.json file.
Step 2: Install esbuild
npm install --save-dev esbuild
This installs esbuild locally into your plugin.
Step 3: Create build.js file
Create a file named build.js in the plugin root:
touch build.js
Paste in:
// build.js
const esbuild = require('esbuild');
esbuild.build({
entryPoints: ['amd/src/editor.js', 'amd/src/runtime.js'],
outdir: 'amd/build',
outExtension: { '.js': '.min.js' },format: 'iife', // Moodle-compatible format
bundle: false,
minify: true,target: ['es2015']}).then) => {
console.log(' JS build complete.');
}).catcherr) => {
console.error(' Build failed:', err);
process.exit(1);
});
Step 4: Run the build
From your plugin root:
node build.js
You should see:
JS build complete.
Then verify:
ls amd/build/
You should see:
editor.min.js
runtime.min.js
Optional: Add NPM build script
Edit package.json to add:
"scripts": {
"build": "node build.js"
}
Now you can build with:
npm run build
TL;DR Summary
npm init -ynpm install --save-dev esbuild- Create
build.js with AMD-compatible config - Run
node build.js or npm run build - Moodle will load your AMD modules from
amd/build/*.min.js
You’re building a Moodle plugin that calls a service on your local machine — a REST API, a microservice, an AI backend, a webhook handler. You send the request through Moodle’s built-in cURL wrapper. Nothing comes back. No connection error, just silence, or a vague failure in your plugin’s response.
The cause: Moodle blocks outbound cURL requests to localhost (127.0.0.1, ::1) by default. This is intentional — it guards against Server-Side Request Forgery (SSRF), where an attacker tricks the server into fetching internal resources. On production that protection is essential. On a local dev machine it just gets in the way.
This guide shows how to lift that restriction.
What you’ll see
With developer debugging enabled, Moodle logs a message from corefilescurl_security_helper indicating the URL was blocked. Without debugging, the cURL call silently returns false or an empty response with no further detail.
Fix: admin UI (recommended)
Go to Site administration > General > Security > HTTP security.
Step 1 — cURL blocked hosts list: clear this field entirely. When empty, Moodle skips the reserved-address check that blocks localhost.
Step 2 — cURL allowed ports list: confirm ports 80 and 443 are listed, then add the port your local service uses (e.g., 8000). If this list contains any entries at all, Moodle blocks every port not listed.
Step 3: click Save changes, then go to Site administration > Development > Purge all caches.
Fix: config.php (alternative)
If you can’t access the admin UI, or want a scripted setup, add these lines to config.php after the require_once(__DIR__ . '/lib/setup.php'); line:
$CFG->curlsecurityblockedhosts = ''; // empty = localhost not blocked
$CFG->curlsecurityallowedport = '80,443,8000'; // add your dev port here
Then purge caches.
Verify
Retry the cURL call from your plugin. If it still fails, enable developer debugging (Site administration > Development > Debugging, set to DEVELOPER) and check the output for messages from curl_security_helper.
Don’t carry this into production
An empty blocked hosts list disables SSRF protection for all local addresses. On a production server, the list should contain at minimum:
127.0.0.0/8
::1/128
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
Only remove entries if you have a specific, trusted internal service and understand the exposure.
Moodle's backup and restore API is powerful but complex. This guide covers the required context setup, database operations, and workflows for programmatic backups and targeted course/section restores, including critical pitfalls around controller cleanup.
This guide aims to help developers programmatically interact with Moodle's robust backup and restore (B&R) system. While powerful, the API can be complex, and certain scenarios, like targeted section restores, require precise handling.
This guide is based on the Moodle 4.1 backup & restore API.
I. General Principles & Best Practices
- Moodle Environment is Key:
- Configuration: Always include Moodle's main
config.php at the beginning of your script. - CLI Context: If the script is intended for command-line execution, define
define('CLI_SCRIPT', true); before including config.php. - Required Libraries: Include essential Moodle libraries. Common ones for B&R include:
$CFG->libdir . '/clilib.php' (for CLI scripts)$CFG->libdir . '/backup/util/includes/backup_includes.php'$CFG->libdir . '/backup/util/includes/restore_includes.php'$CFG->dirroot . '/course/lib.php' (for course/section operations)- User Context:
- Most B&R operations require a valid Moodle user context, typically an administrator, to ensure appropriate permissions.
Fetch an admin user using get_admin():global $USER; // Ensure $USER is global if not already
$USER = get_admin();
- Error Handling & Logging:
- Exception Handling: Moodle's B&R API can throw various exceptions (e.g.,
moodle_exception, backup_exception, restore_controller_exception). Wrap B&R operations in try...catch blocks to handle potential errors gracefully. - Detailed Logging: Implement a robust logging mechanism to track the script's progress, key variable states, and any errors encountered. This is invaluable for debugging complex B&R workflows. For CLI scripts,
cli_writeln() can be used for console output, supplemented by logging to a file. - Resource Management – The
destroy() Method: - CRITICAL PITFALL:
backup_controller and restore_controller objects, along with their associated backup_plan and restore_plan objects, create complex internal structures. They may hold database connections, temporary file references, and significant amounts of data in memory. - Best Practice: ALWAYS call the
$controller->destroy() method after you are completely finished with the controller object. This is typically done in a finally block to ensure cleanup even if errors occur. The destroy() method on the controller will also handle the destruction of its associated plan. - Consequences of Neglect: Failure to call
destroy() can lead to memory leaks, PHP timeouts, database connection exhaustion, and persistent temporary data (e.g., "existing temptables found" errors when Moodle attempts its own cleanup later). - Temporary Directories:
- Moodle's Temp Space: Moodle B&R operations use a temporary directory, typically located at
$CFG->dataroot . '/temp/backup/'. - Path Relativity (Restore): When providing a path to extracted backup contents for a
restore_controller, this path must be relative to Moodle's main backup temporary directory (i.e., $CFG->dataroot/temp/backup/). - Creating Temp Dirs: Use Moodle's
make_backup_temp_directory() function to create uniquely named subdirectories within this temporary space. This function returns an absolute path. - Cleanup:
- Moodle's controller
destroy() methods are responsible for cleaning up Moodle's internal temporary files and database records related to that specific B&R operation. - Your script is responsible for cleaning up any additional temporary directories or files it creates (e.g., a directory where you manually copy an MBZ file or extract its contents before passing a relative path to the
restore_controller). Use fulldelete() for this. - Database Context (If Working with Multiple Databases):
- If your script interacts with a separate database for sourcing backup data (e.g., a dedicated backup Moodle instance's database), ensure you manage the global
$DB object context correctly.
Switch to the backup database connection before performing operations against it, and restore the original live Moodle database connection immediately afterward.global $DB;
$original_live_db_connection = $DB;
// Assume $backup_db_conn is a Moodle database object for your backup DB
$DB = $backup_db_conn;
// ... perform operations against the backup database ...
$DB = $original_live_db_connection; // Switch back to live Moodle DB
- Moodle Version Awareness:
- The B&R API, like other Moodle APIs, can evolve between Moodle versions. Code written for one version might require adjustments for another. Always test thoroughly against your target Moodle version.
- Consult the Source Code:
- When API behavior is unclear or documentation is sparse, the Moodle source code (primarily in
backup/controller/, backup/moodle2/, and related task/plan files) is the definitive reference. PHPDoc comments within the code can also be very helpful. - Iterative Development & Debugging:
- Programmatic B&R can be complex. Start with simpler operations and incrementally add complexity. Use systematic debugging techniques and the logging you've implemented.
II. Key Components of the B&R API
- Controllers (
backup_controller, restore_controller): - These are the central orchestrators for backup and restore operations.
- Instantiation Examples:
new backup_controller($type, $id, $format, $interactive, $mode, $userid)new restore_controller($tempdir_relative_path, $courseid, $interactive, $mode, $userid, $target)- Common Parameters:
$type (Backup): The scope of the backup, e.g., backup::TYPE_1COURSE, backup::TYPE_1SECTION.$id (Backup): The Moodle ID of the item to be backed up (e.g., course ID, section ID from the source database).$format: The backup format, typically backup::FORMAT_MOODLE.$interactive: For scripts, this should be backup::INTERACTIVE_NO.$mode: The purpose/context of the operation, e.g., backup::MODE_GENERAL.$userid: The Moodle user ID of the user performing the operation.$tempdir_relative_path (Restore): The path to the directory containing the extracted backup contents, relative to $CFG->dataroot/temp/backup/.$courseid (Restore): The Moodle ID of the target course for the restore operation.$target (Restore): Specifies how the restore interacts with the target course, e.g., backup::TARGET_NEW_COURSE, backup::TARGET_CURRENT_ADDING.- Typical Lifecycle: Instantiate Controller -> Get Plan -> (Optionally Adjust Plan Settings/Tasks) -> (For Restore: Execute Prechecks) -> Execute Plan -> (Optionally Get Results) -> Destroy Controller.
- Plans (
backup_plan, restore_plan): - The detailed blueprint of the B&R operation, generated by its controller.
- Contains:
- Settings: High-level configuration options for the entire operation (e.g., whether to include users, logs, files). Accessed via
$plan->get_setting('setting_name'). - Tasks: An ordered list of individual operations that will be performed (e.g., restore course structure, restore a specific section's content, restore an activity module). Accessed via
$plan->get_tasks(). - The plan is obtained from the controller using
$controller->get_plan(). - Tasks (e.g.,
backup_section_task, restore_section_task, restore_activity_task): - Represent discrete units of work within a plan. Each task is responsible for a specific part of the B&R process.
- Tasks can have their own settings, specific to that unit of work (e.g., whether to include user data for a particular activity).
- Importance for Targeted Operations: For specific operations like restoring a single section into a particular place, understanding and correctly configuring the relevant task (e.g.,
restore_section_task) is crucial. - Settings (
backup_setting): - Objects that represent configurable options within the B&R process.
- They exist at both the overall plan level and within individual tasks.
- Access settings using
$plan->get_setting('name') or $task->get_setting('name'). - Set their values using
$setting_object->set_value(...). - Challenge: Discovering the exact name of a setting and determining whether it's a plan-level or task-level setting often requires inspecting Moodle source code (e.g., the
define_settings() methods in task classes or how settings are added in plan builder classes). - Backup Types & Restore Targets (Constants):
- These constants, defined in
backup/util/defines.php (which is usually included via backup_includes.php), dictate the scope and destination of B&R operations. - Backup Types (Scope Examples):
backup::TYPE_1COURSE: Backs up an entire course.backup::TYPE_1SECTION: Backs up a single section from a course.backup::TYPE_1ACTIVITY: Backs up a single activity module.- Restore Targets (Destination Examples):
backup::TARGET_NEW_COURSE: Restores the backup into a brand new Moodle course.backup::TARGET_CURRENT_ADDING: Adds content from the backup into the course specified by $courseid (passed to restore_controller), merging with any existing content.backup::TARGET_CURRENT_DELETING: Deletes existing content from the target $courseid before restoring content from the backup.- (Others like
TARGET_EXISTING_ADDING, TARGET_EXISTING_DELETING are for restoring into a different existing course, selected during UI workflows or configured programmatically). - Key Interaction: The interplay between the backup
TYPE_... and the restore TARGET_... significantly influences how settings and tasks behave. For instance, restoring a TYPE_1SECTION backup using TARGET_CURRENT_ADDING requires careful targeting of the restore_section_task to place the content correctly.
III. Core Operations: Step-by-Step with Examples
A. Programmatically Backing Up a Single Section
This example demonstrates backing up a specific section from a Moodle course.
// Assumed variables:
// $source_db_connection: Moodle DB connection object for the source database (if different from live).
// $USER: An admin user object.
// $source_course_id: The ID of the course containing the section.
// $source_section_id: The ID of the section record to back up (from the source DB).
global $DB; // Live Moodle DB
$original_live_db = $DB;
$copied_mbz_path = null;
$backup_controller = null;
$script_controlled_mbz_temp_dir = null;
try {
// Switch DB context if source is different
if (isset($source_db_connection) && $source_db_connection !== $original_live_db) {
$DB = $source_db_connection;
cli_writeln("Switched DB context to source for backup.");
}
cli_writeln("Backing up section ID {$source_section_id} from course {$source_course_id}");
$backup_controller = new backup_controller(
backup::TYPE_1SECTION,
$source_section_id,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_GENERAL,
$USER->id
);
$backup_plan = $backup_controller->get_plan();
// Configure general plan settings (e.g., exclude user data)
$general_plan_settings = $backup_plan->get_settings();
if (isset($general_plan_settings['users']) && $general_plan_settings['users'] instanceof backup_setting) {
$general_plan_settings['users']->set_value(0);
}
// Add other general settings as needed (e.g., role_assignments, grade_histories to 0)
// Configure settings for the specific section task (e.g., ensure activities and files are included)
$backup_tasks = $backup_plan->get_tasks();
foreach ($backup_tasks as $task) {
if ($task instanceof backup_section_task && $task->get_sectionid() == $source_section_id) {
// Ensure activities and files within this section are backed up
if (method_exists($task, 'get_setting' { // Good practice to check
$activities_setting = $task->get_setting('activities');
if ($activities_setting instanceof backup_setting) {
$activities_setting->set_value(1); // 1 for include
}
$files_setting = $task->get_setting('files');
if ($files_setting instanceof backup_setting) {
$files_setting->set_value(1); // 1 for include
}
}
break; // Found the relevant section task
}
}
$backup_controller->execute_plan();
$backup_status = $backup_controller->get_status();
if ($backup_status != backup::STNorthwind UniversityS_FINISHED_OK && $backup_status != backup::STNorthwind UniversityS_FINISHED_WARN) {
throw new moodle_exception("Backup failed. Controller status: " . $backup_status);
}
$backup_results = $backup_controller->get_results();
$moodle_managed_backup_file = $backup_results['backup_destination'] ?? null;
if (!$moodle_managed_backup_file instanceof stored_file) {
throw new moodle_exception('Backup destination file (stored_file object) not found in backup results.');
}
// Copy the Moodle-managed backup file to a script-controlled temporary location
$script_controlled_mbz_temp_dir = make_backup_temp_directory('my_script_section_backup_' . $source_section_id . '_' . uniqid(;
$copied_mbz_path = $script_controlled_mbz_temp_dir . DIRECTORY_SEPARATOR . $moodle_managed_backup_file->get_filename();
if (!$moodle_managed_backup_file->copy_content_to($copied_mbz_path {
throw new moodle_exception('Failed to copy MBZ file to script-controlled location: ' . $copied_mbz_path);
}
cli_writeln("Section backup MBZ successfully created at: " . $copied_mbz_path);
} catch (Exception $e) {
cli_writeln("ERROR during backup operation: " . $e->getMessage(;
if ($e instanceof moodle_exception && !empty($e->debuginfo {
cli_writeln("Moodle Exception Debug Info: " . $e->debuginfo);
}
// Further error handling or re-throwing as appropriate
} finally {
// Restore original DB context if it was changed
if (isset($source_db_connection) && $source_db_connection !== $original_live_db && $DB !== $original_live_db) {
$DB = $original_live_db;
cli_writeln("Restored DB context to live Moodle DB.");
}
// Destroy the backup controller to clean up Moodle's internal temp files and DB records
if ($backup_controller instanceof backup_controller) {
try {
$backup_controller->destroy();
} catch (Exception $e_destroy) {
cli_writeln("Warning: Exception during backup_controller destroy: " . $e_destroy->getMessage(;
}
}
// The $copied_mbz_path is now available for the restore step.
// Cleanup of $script_controlled_mbz_temp_dir should happen after $copied_mbz_path is no longer needed.
}
// The $copied_mbz_path variable now holds the path to the generated .mbz file.
// Remember to clean up $script_controlled_mbz_temp_dir later using fulldelete().
B. Programmatically Restoring a Single Section into a Specific Location in an Existing Course
This scenario requires creating an empty placeholder section in the target course first, then instructing the restore process to populate this specific placeholder.
// Assumed variables:
// $target_live_course_id: ID of the live Moodle course to restore into.
// $target_insertion_section_number: The desired section *number* for the new content.
// $USER: An admin user object.
// $path_to_section_mbz: Absolute path to the .mbz file created in the backup step.
// $original_section_backup_details: (Optional) An object/array holding metadata (name, summary, visibility)
// from the original section in the backup, to apply after restore.
global $DB; // Live Moodle DB
$restore_controller = null;
$script_controlled_extraction_dir = null; // For script-managed extraction path (absolute)
$placeholder_section_id_in_live_course = null;
try {
// 1. Create a placeholder section in the live target course
cli_writeln("Creating placeholder section at position {$target_insertion_section_number} in course {$target_live_course_id}...");
$placeholder_section_record = course_create_section($target_live_course_id, $target_insertion_section_number);
if (!$placeholder_section_record || !isset($placeholder_section_record->id {
throw new moodle_exception("Failed to create placeholder section in target course.");
}
$placeholder_section_id_in_live_course = $placeholder_section_record->id;
cli_writeln("Placeholder section created in live course: ID {$placeholder_section_id_in_live_course}, Number {$placeholder_section_record->section}");
// 2. Extract the .mbz file to a Moodle temporary directory (for restore_controller)
// The path passed to restore_controller must be relative to $CFG->dataroot/temp/backup/
$extraction_dir_relative_name = 'my_script_restore_extraction_' . uniqid();
$script_controlled_extraction_dir = make_backup_temp_directory($extraction_dir_relative_name); // Gets absolute path
if (!$script_controlled_extraction_dir) {
throw new moodle_exception("Could not create script-controlled extraction directory.");
}
$packer = get_file_packer('application/vnd.moodle.backup'); // Standard Moodle MBZ packer
if (!$packer) {
throw new moodle_exception('Could not get file packer for MBZ.');
}
if (!$packer->extract_to_pathname($path_to_section_mbz, $script_controlled_extraction_dir {
$packer_errors = $packer->get_errors();
throw new moodle_exception('Failed to extract .mbz file: ' . implode('; ', $packer_errors;
}
cli_writeln("MBZ successfully extracted to: " . $script_controlled_extraction_dir . " (Relative path for controller: " . $extraction_dir_relative_name . ")");
// Ensure moodle_backup.xml exists in the extraction
if (!file_exists($script_controlled_extraction_dir . DIRECTORY_SEPARATOR . 'moodle_backup.xml' {
throw new moodle_exception('moodle_backup.xml not found in extraction directory.');
}
// 3. Instantiate the restore_controller
$restore_controller = new restore_controller(
$extraction_dir_relative_name, // Relative path to extracted content
$target_live_course_id, // Target course ID
backup::INTERACTIVE_NO,
backup::MODE_GENERAL,
$USER->id,
backup::TARGET_CURRENT_ADDING // Adding content to the current (target) course
);
// 4. (Optional but recommended) Execute prechecks
if ($restore_controller->get_status() == backup::STNorthwind UniversityS_NEED_PRECHECK) {
cli_writeln("Executing restore prechecks...");
if (!$restore_controller->execute_precheck( {
// Handle precheck failures/warnings from $restore_controller->get_precheck_results()
// For example, log them and decide whether to proceed.
$precheck_results = $restore_controller->get_precheck_results();
cli_writeln("Restore prechecks indicated issues: " . var_export($precheck_results, true;
// Depending on severity, you might throw an exception here.
}
cli_writeln("Restore prechecks completed. Status: " . $restore_controller->get_status(;
}
// 5. Get the restore plan
$restore_plan = $restore_controller->get_plan();
if (!$restore_plan) {
throw new moodle_exception("Failed to retrieve restore plan from controller.");
}
// 6. CRITICAL: Configure the restore_section_task to target the placeholder
// This is essential for TYPE_1SECTION restores into a specific slot.
if ($restore_controller->get_type() == backup::TYPE_1SECTION) {
$restore_tasks = $restore_plan->get_tasks();
$section_task_configured_for_target = false;
foreach ($restore_tasks as $task) {
if ($task instanceof restore_section_task) {
// This task will process the single section from the backup.
// We instruct it to use our placeholder section's ID as its target in the live course.
cli_writeln("Configuring restore_section_task (Task Name: {$task->get_name()}) to target live section ID: {$placeholder_section_id_in_live_course}");
$task->set_sectionid($placeholder_section_id_in_live_course);
$section_task_configured_for_target = true;
break; // Assuming only one restore_section_task for a TYPE_1SECTION backup
}
}
if (!$section_task_configured_for_target) {
throw new moodle_exception("Could not find or configure the restore_section_task within the plan for TYPE_1SECTION restore.");
}
} else {
// This script's specific targeting logic is primarily for TYPE_1SECTION.
// Handling other backup types (e.g., TYPE_1COURSE) would require different plan/task configuration.
cli_writeln("WARNING: The backup type is not TYPE_1SECTION (actual: {$restore_controller->get_type()}). The specific section targeting logic used here may not apply or work as expected.");
}
// 7. Adjust other general plan settings (e.g., exclude user data, roles)
$general_restore_plan_settings = $restore_plan->get_settings();
if (is_array($general_restore_plan_settings {
foreach ($general_restore_plan_settings as $setting) {
if ($setting instanceof backup_setting) {
if (in_array($setting->get_name(), ['users', 'role_assignments', 'grade_histories', 'course_module_completion'] {
$setting->set_value(0); // 0 for exclude
}
}
}
}
// 8. Execute the restore plan
if ($restore_controller->get_status() != backup::STNorthwind UniversityS_AWAITING) {
throw new moodle_exception("Restore controller is not in AWAITING state before execute_plan. Current status: " . $restore_controller->get_status(;
}
cli_writeln("Executing restore plan...");
$restore_controller->execute_plan();
$restore_status = $restore_controller->get_status();
if ($restore_status != backup::STNorthwind UniversityS_FINISHED_OK && $restore_status != backup::STNorthwind UniversityS_FINISHED_WARN) {
throw new moodle_exception("Restore plan execution failed. Controller status: " . $restore_status);
}
cli_writeln("Restore plan execution finished with status: " . $restore_status);
// 9. (Optional but recommended) Update metadata of the now-populated section
// The restore_section_structure_step populates the section. This step ensures
// metadata like name, summary, visibility exactly matches the source backup object.
$final_live_section_obj = $DB->get_record('course_sections', ['id' => $placeholder_section_id_in_live_course]);
if ($final_live_section_obj && isset($original_section_backup_details {
$update_params = new stdClass();
$update_params->id = $final_live_section_obj->id;
$update_params->name = $original_section_backup_details->name;
$update_params->summary = $original_section_backup_details->summary;
$update_params->summaryformat = $original_section_backup_details->summaryformat;
$update_params->visible = $original_section_backup_details->visible;
$update_params->availability = $original_section_backup_details->availability; // If applicable
$DB->update_record('course_sections', $update_params);
cli_writeln("Metadata (name, summary, etc.) updated for restored section ID {$final_live_section_obj->id}.");
} else if (!$final_live_section_obj) {
cli_writeln("WARNING: Could not re-fetch section ID {$placeholder_section_id_in_live_course} after restore to update metadata. It might not have been correctly populated.");
}
cli_writeln("Section content successfully restored into live section ID {$placeholder_section_id_in_live_course}.");
// 10. Rebuild the course cache for the target course
cli_writeln("Rebuilding course cache for course ID {$target_live_course_id}...");
rebuild_course_cache($target_live_course_id, true);
cli_writeln("Course cache rebuilt.");
} catch (Exception $e) {
cli_writeln("ERROR during restore operation: " . $e->getMessage(;
if ($e instanceof moodle_exception && !empty($e->debuginfo {
cli_writeln("Moodle Exception Debug Info: " . $e->debuginfo);
}
// Further error handling
} finally {
// Destroy the restore controller
if ($restore_controller instanceof restore_controller) {
try {
$restore_controller->destroy();
} catch (Exception $e_destroy) {
cli_writeln("Warning: Exception during restore_controller destroy: " . $e_destroy->getMessage(;
}
}
// Clean up the script-controlled extraction directory
if ($script_controlled_extraction_dir && is_dir($script_controlled_extraction_dir {
fulldelete($script_controlled_extraction_dir);
cli_writeln("Cleaned up script-controlled extraction directory: " . $script_controlled_extraction_dir);
}
// The original .mbz file ($path_to_section_mbz) might also need cleanup if it was temporary.
}
IV. Stumbling Blocks & Pitfalls (Summary)
- Targeting
TYPE_1SECTION Restores into Existing Courses: - The Challenge: Ensuring the single section's content from the backup is placed into a specific, pre-determined slot (placeholder section) within the target course.
- Solution: Create an empty placeholder section using
course_create_section(). Then, during the restore process, find the restore_section_task in the restore_plan and call its set_sectionid() method, passing the ID of your placeholder section. This directs the task to populate that specific section record. - Distinguishing Plan-Level vs. Task-Level Settings:
- Settings can apply globally to the plan or be specific to individual tasks. Knowing where a setting resides is crucial for configuring it correctly.
- Identifying Correct Setting Names:
- Setting names are not always obvious. Inspecting
define_settings() methods in task classes or related Moodle source code is often necessary. - Essential
destroy() Calls: - Always call
$controller->destroy() to prevent resource leaks and cleanup issues. - Correct Paths for
restore_controller: - The
$tempdir parameter for new restore_controller(...) must be the path to the directory containing the extracted backup contents, relative to $CFG->dataroot/temp/backup/. It is not the path to the .mbz file itself.
V. Debugging Tips
- Comprehensive Logging: Log controller statuses, plan details (settings and tasks), key variable values, and any error messages.
- Inspect Objects: Use
var_dump() or print_r() on controller, plan, task, and setting objects to understand their structure and current state (e.g., $plan->get_settings(), $task->get_settings()). - Moodle Developer Debugging: Enable developer-level debugging in Moodle's site administration to get more verbose error messages and stack traces from Moodle itself.
- Server and Moodle Error Logs: Check the web server's error log and Moodle's configured error log file for PHP errors or other Moodle-specific error details.
- Incremental Testing: Start with the simplest possible B&R operation and gradually add complexity to isolate where issues arise.
backup/util/defines.php: Refer to this file (usually included via backup_includes.php) for the definitions of all backup:: constants.- Examine
moodle_backup.xml: This file, found within an extracted MBZ, describes the structure and contents of the backup. Reviewing it can provide insights into relevant settings and tasks.
PsySH provides an interactive REPL for Moodle development, similar to `rails console`. This guide shows how to set it up project-locally with a custom init script, giving you direct access to Moodle’s database and API without polluting the repository.
This is a guide for Moodle developers who want to install and use PsySH on a per-project basis, using an init.php file to load Moodle and helper logic — without polluting the Moodle Git repo.
Installing and Using PsySH REPL for Moodle Development (Project-Scoped)
Overview
This guide helps Moodle developers:
- Set up an interactive REPL (like
rails console or irb) using PsySH
- Run it within a Moodle context (with
$CFG, $DB, $USER, etc.)
- Keep all files outside the Git-tracked Moodle directory
- Ensure correct PHP version is used per project
1. Requirements
- PHP CLI (matching your project version, e.g.
php7.4)
- Composer (any global version)
- Local Moodle install (e.g. in
~/php/icm/public_html/)
2. Install PsySH per project
Create a tools/ directory next to your Moodle root:
cd ~/php/icm
mkdir tools && cd tools
php7.4 /usr/bin/composer require psy/psysh:^0.11
This installs a project-local version of PsySH compatible with PHP 7.4. (Assumption: your webroot is something like ~/php/icm/public_html)
3. Create init.php to bootstrap Moodle and helpers
Inside ~/php/icm/tools/init.php, add:
<?php
// Load Moodle
define('CLI_SCRIPT', true);
require(__DIR__ . '/../public_html/config.php');
// Simulate a logged-in admin user
$USER = get_admin();
// Setup typical Moodle page context
$PAGE = new moodle_page();
$PAGE->set_context(context_system::instance(;
$PAGE->set_url('/');
$PAGE->set_pagelayout('admin');
$COURSE = get_site();
$SITE = $COURSE;
// Optional helpers
function uname(int $id): string {
global $DB;
$user = $DB->get_record('user', ['id' => $id], '*', MUST_EXIST);
return fullname($user);
}
echo "[Moodle REPL ready — user={$USER->username}]\n";
This gives you access to the full Moodle environment in the REPL.
4. Create a launch script: moodlerepl
Inside ~/php/icm/tools/, create:
touch moodlerepl
chmod +x moodlerepl
With the contents:
#!/usr/bin/env bash
DIR="$(cd "$(dirname "$0")" && pwd)"
PHPBIN="php7.4" # Change if needed
"$PHPBIN" "$DIR/vendor/bin/psysh" "$DIR/init.php"
Now you can just run:
./moodlerepl
5. Example REPL session
$ ./moodlerepl
[Moodle REPL ready — user=admin]
Psy Shell v0.12.7 (PHP 7.4.33 — cli)
>>> $DB->get_record('user', ['id' => 2]);
>>> uname(2);
6. Keep everything out of Git
Add this to your .gitignore if needed:
tools/
Or selectively ignore:
tools/vendor/
tools/init.php
tools/moodlerepl