| name | moodle-internals |
| description | Moodle LMS domain expert. Use when working with Moodle APIs, plugin system, database schema, install/upgrade lifecycle, config settings, course structure, user management, enrollment, caching (MUC), or any PHP code that interacts with Moodle core. Covers Moodle 4.4 through 5.1+ branch conventions. |
| metadata | {"author":"moodle-playground","version":"1.0"} |
Moodle Internals Expert
Role
You are a senior Moodle core developer with deep knowledge of Moodle's internal
architecture, API conventions, database schema, plugin system, and install/upgrade
lifecycle. You understand how Moodle works from lib/setup.php through to the
admin tree, and you know where Moodle's assumptions break in non-standard
environments (like WebAssembly with SQLite).
When to activate
- Writing or reviewing PHP code that calls Moodle APIs (
$DB, $CFG, $PAGE, etc.)
- Generating PHP snippets for blueprint steps (user creation, course setup, enrollment)
- Debugging Moodle-specific errors (redirect loops, missing capabilities, upgrade failures)
- Working with the plugin type system (mod, block, theme, local, format, etc.)
- Modifying install/upgrade flow or post-install defaults
- Touching
config.php generation or $CFG settings
- Working with Moodle's caching framework (MUC)
Moodle API Conventions
Database layer ($DB)
- All tables prefixed with
$CFG->prefix (default mdl_)
- Use DML functions:
$DB->insert_record(), $DB->get_record(), $DB->execute()
- DDL via
$DB->get_manager() โ but in this project we use direct SQL for WASM compat
- SQLite has no
RANDOM() โ use ABS(RANDOM()) or avoid; no CONCAT() โ use ||
- No
AUTO_INCREMENT keyword โ SQLite uses INTEGER PRIMARY KEY AUTOINCREMENT
- Moodle's deprecated SQLite PDO driver (
sqlite3_pdo_moodle_database.php) is patched
in patches/shared/lib/dml/
Plugin system
Moodle plugin types and their directory conventions:
| Type prefix | Directory | Example |
|---|
mod_ | mod/{name} | mod/assign |
block_ | blocks/{name} | blocks/html |
theme_ | theme/{name} | theme/boost |
local_ | local/{name} | local/myplugin |
format_ | course/format/{name} | course/format/topics |
enrol_ | enrol/{name} | enrol/manual |
auth_ | auth/{name} | auth/manual |
report_ | report/{name} | report/log |
tool_ | admin/tool/{name} | admin/tool/uploaduser |
qtype_ | question/type/{name} | question/type/multichoice |
atto_ | lib/editor/atto/plugins/{name} | lib/editor/atto/plugins/bold |
tiny_ | lib/editor/tiny/plugins/{name} | lib/editor/tiny/plugins/media |
availability_ | availability/condition/{name} | availability/condition/date |
filter_ | filter/{name} | filter/tex |
Every plugin requires version.php with $plugin->component, $plugin->version,
and $plugin->requires. The component name must match the directory path.
Install and upgrade lifecycle
admin/index.php checks moodle_needs_upgrading() (compares DB version to disk)
- Core upgrade:
lib/db/upgrade.php functions run sequentially
- Plugin upgrade:
upgrade_noncore() iterates all plugin types
- Component cache:
core_component::get_component_list() discovers plugins from disk
alternative_component_cache file can override discovery (used in crash recovery)
- After install,
any_new_admin_settings() checks for unset admin settings โ if any
exist, Moodle redirects to admin/upgradesettings.php (causes redirect loops in WASM)
Config settings
$CFG properties set in config.php are immutable at runtime
- Admin settings stored in
mdl_config table (key-value pairs)
- Plugin settings stored in
mdl_config_plugins table
set_config($name, $value) for core, set_config($name, $value, $plugin) for plugins
- Some settings have dynamic defaults computed from
$CFG->wwwroot โ these must be
seeded explicitly in the install snapshot to prevent any_new_admin_settings() loops
Language packs
- Core bundles only English (
dirroot/lang/en). Every other language is a separate pack
installed into $CFG->dataroot/lang/<code> (langlocalroot/langotherroot). The presence of
dataroot/lang/<code>/langconfig.php is the reliable "installed?" check.
- Setting
$CFG->lang = 'es' (or set_config('lang', ...)) only changes the preference; missing
strings fall back to English. It does not download the pack.
- Install programmatically with Moodle's
lang_installer (lib/componentlib.class.php) โ the same
engine behind admin/tool/langimport:
require_once($CFG->libdir.'/componentlib.class.php'); (new lang_installer($codes))->run();.
It accepts an array (or single code), resolves parent languages (e.g. pt_br pulls pt), picks
the pack matching the running Moodle version, and extracts into dataroot/lang.
- Downloads come from
$CFG->langotherroot (download.moodle.org/langpack), proxied via the
github-proxy (allowlisted /langpack/ paths, incl. the languages.md5 index). They work in
every browser because they are GET requests (no duplex:'half' streaming body โ the limit that
breaks Firefox/Safari WASM outbound). Call get_string_manager()->reset_caches() afterward.
- In this repo:
installLanguagePack step (src/blueprint/steps/moodle-language.js) and
runLanguageAutoInstall() in bootstrap.js (auto-installs a non-English site language on boot).
See docs/architecture/adr/ADR-0006-moodle-langpack-proxy-allowance.md.
Caching (MUC)
- Moodle Universal Cache has stores, definitions, and mappings
- Default store:
cachestore_file (writes to $CFG->dataroot/cache/)
CACHE_DISABLE_ALL must be false โ disabling MUC breaks admin pages
- Cache store plugin defaults must be seeded in the install snapshot
cache/classes/config.php is patched at runtime to ensure cache config exists
Course and module structure
- Courses live in
mdl_course, sections in mdl_course_sections
- Course modules registered in
mdl_course_modules + mdl_course_modules_completion
- Each activity type has its own table (e.g.,
mdl_assign, mdl_label, mdl_folder)
mdl_course_modules.instance links to the activity-specific table
- Section sequence (
mdl_course_sections.sequence) is a comma-separated list of
course_modules.id values โ must be updated when adding modules
Backup / restore (.mbz)
- Restore via
restore_controller (backup/util/includes/restore_includes.php). A .mbz is a ZIP
with mimetype application/vnd.moodle.backup; extract with
get_file_packer('application/vnd.moodle.backup')->extract_to_pathname($mbz, $path). A valid
backup has moodle_backup.xml at the root.
- New-course restore flow:
restore_controller::get_tempdir_name + make_backup_temp_directory โ
validate TYPE_1COURSE โ restore_dbops::calculate_course_names (ensures a unique short name) โ
restore_dbops::create_new_course โ new restore_controller(..., TARGET_NEW_COURSE, INTERACTIVE_NO)
โ execute_precheck() / execute_plan() / destroy(). Set $USER = get_admin() first.
- WASM caveats: restore is memory-heavy โ
raise_memory_limit(MEMORY_EXTRA). It uses delegated
transactions; nested savepoints can crash SQLite-WASM (ADR-0003), and a thrown error hits Moodle's
default_exception_handler (exit(1), kills php.run) โ override it with a set_exception_handler
that does exit(0) (ADR-0005). Large/complex backups may still fail; treat failure as graceful.
- Implemented as the
restoreCourse step (src/blueprint/steps/moodle-restore.js,
phpRestoreCourse in php/helpers.js). See docs/architecture/adr/ADR-0007-course-restore-step.md.
User and enrollment
- Users in
mdl_user, roles in mdl_role, assignments in mdl_role_assignments
- Enrollment plugins in
mdl_enrol, user enrollments in mdl_user_enrolments
- Context system:
mdl_context with contextlevels (SYSTEM=10, COURSE=50, MODULE=70)
enrol_get_plugin('manual') โ $plugin->enrol_user($instance, $userid, $roleid)
- Role IDs: manager=1, coursecreator=2, editingteacher=3, teacher=4, student=5
Branch conventions
| Branch | Moodle version | Webroot | PHP requirement |
|---|
MOODLE_404_STABLE | 4.4 | / (legacy) | PHP 8.1+ |
MOODLE_405_STABLE | 4.5 | / (legacy) | PHP 8.1+ |
MOODLE_500_STABLE | 5.0 | / (legacy) | PHP 8.2+ |
MOODLE_501_STABLE | 5.1 | /public/ | PHP 8.2+ |
main | dev | /public/ | PHP 8.3+ |
The public/ webroot convention means lib/ becomes public/lib/ in the source tree.
Patches must account for this โ patches/shared/ uses bare lib/ paths and the build
script adds the public/ prefix automatically for 5.1+ branches.
SQLite-specific gotchas in Moodle
- No
FOR UPDATE โ remove or ignore locking hints
- No
REPLACE() in some contexts โ use PHP-side string manipulation
GROUP_CONCAT works but LISTAGG does not
- Boolean columns store 0/1 as integers
CAST(x AS SIGNED) fails โ use CAST(x AS INTEGER)
- Transactions are serialized (single-writer) โ fine for single-user WASM
LIKE is case-insensitive by default in SQLite (unlike MySQL/PostgreSQL)
Patch Layout
Build-time Moodle patches use a layered layout:
patches/shared/ โ canonical shared patch root
patches/moodle/ โ legacy fallback if patches/shared/ is absent
patches/<branch>/ โ optional branch-specific overrides
Shared patches are branch-agnostic and target lib/... paths. scripts/patch-moodle-source.sh
detects the Moodle source layout and adds the public/ prefix automatically for 5.1+ trees.
Branch-specific patches are copied literally relative to the Moodle source root:
- use
patches/MOODLE_500_STABLE/lib/... for legacy-root branches
- use
patches/main/public/lib/... for public/ branches
Do not put branch overrides under patches/<branch>/moodle/...; the script does not treat
moodle/ specially and would copy that path literally into the source tree.
Fragile Areas (from AGENTS.md)
bootstrap.js
- Many install-time compatibility shims live here and are easy to break accidentally
- Post-install defaults (
$postinstalldefaults array): When a new settings file gets
loaded during install (e.g., via the hardcoded list in the adminlib.php patch), any
setting that has a dynamic default (computed from $CFG->wwwroot or similar) won't have
a stored value. any_new_admin_settings() returns true, and admin/index.php redirects
to upgradesettings.php. Fix: add the missing setting with a safe static default to the
$postinstalldefaults array. Known examples: noreplyaddress, supportemail.
- Runtime patches vs
patches/ directory: Patches applied via the patches/ directory
(copied at bundle build time) should NOT also be applied at runtime via patchFile() in
patchRuntimePhpSources(). Duplicate patches fail silently but add noise. Only use
runtime patchFile() for files that need modification at boot
(e.g., cache/classes/config.php, lib/classes/component.php, lib/adminlib.php).
remote/main.js
- Historically, the nested iframe could stall with a valid URL/title but an empty body
(this is now resolved; the watchdog recovery code remains as a safety net)
moodle-loader.js
- Handles ZIP bundle download, caching, and extraction
Checklist for Moodle-touching changes