Moodle Backup and Restore API for Developers
A developer-focused guide to Moodle backup and restore automation, including controller lifecycle, temp directories, and common restore pitfalls.
A Developer’s Guide
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
-
User Context:
-
Most B&R operations require a valid Moodle user context, typically an administrator, to ensure appropriate permissions.
-
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)
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
-
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.
-
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().
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::STATUS_FINISHED_OK &&
$backup_status != backup::STATUS_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::STATUS_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::STATUS_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::STATUS_FINISHED_OK &&
$restore_status != backup::STATUS_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.
-
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.
-
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.
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(.
backup/util/defines.php: Refer to this file (usually
included via backup_includes.php) for the definitions of all backup::
constants.
Solin helps teams automate Moodle data operations, migrations, and recovery workflows without breaking production systems. Need help? Contact us.
Contact us