Help us improve
Share bugs, ideas, or general feedback.
From moodle-dev
Upgrades Moodle plugins across major versions: replaces deprecated APIs (print_error, add_to_log, formslib), migrates to Moodle 4.x/5.x conventions (Hooks, PSR-4, /public doc-root, Routing Engine), and bumps PHP 8.1–8.4 compatibility with upgrade.txt notes.
npx claudepluginhub saadrahman01/claude-moodle-dev --plugin moodle-devHow this skill is triggered — by the user, by Claude, or both
Slash command
/moodle-dev:moodle-upgrade-migrationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Moodle deprecates aggressively but rarely removes. Code from Moodle 2.x often still runs in 4.5 — but uses APIs that emit warnings, fail PHP 8 strict checks, or block plugin directory acceptance. This skill catalogs the migration path from each generation.
Guides Moodle plugin development: version.php, DB install/upgrade, capabilities, web services, PSR-4 autoloading, hooks, settings, privacy provider, and coding standards.
Step-by-step PHP version upgrade playbook covering 8.0 through 8.4+ with automated tooling. Guides users through auditing, running Rector, fixing deprecations, and testing.
Guides creation of custom external web service APIs for Moodle LMS using the external API framework and PHP coding standards. For plugins, REST/AJAX endpoints, quizzes, and mobile backends.
Share bugs, ideas, or general feedback.
Moodle deprecates aggressively but rarely removes. Code from Moodle 2.x often still runs in 4.5 — but uses APIs that emit warnings, fail PHP 8 strict checks, or block plugin directory acceptance. This skill catalogs the migration path from each generation.
$plugin->requires bump)$plugin->requires to your minimum target Moodle versionupgrade.txt to document changesphpcs --standard=moodle + visual smoke test$plugin->version + add db/upgrade.php step if schema changed| Old | New | Since |
|---|---|---|
print_error('errkey', 'comp') | throw new \moodle_exception('errkey', 'comp') | 3.7 |
add_to_log() | Trigger an event class | 2.7 |
notify('text', 'notifyproblem') | \core\notification::error('text') | 3.0 |
redirect($url, 'msg', 0) | redirect($url, 'msg', null, \core\notification::INFO) | 3.0 |
external_api (bare) | \core_external\external_api | 4.2 |
external_function_parameters etc. | \core_external\... | 4.2 |
$mform->setHelpButton() | $mform->addHelpButton() | 2.0 |
Magic callbacks <comp>_extend_navigation | Hooks API \core\hook\... (where applicable) | 4.4 |
\core\session\manager::write_close() | Same — but check if needed | — |
mtrace() in non-CLI | Use debugging() or proper logger | — |
cleardoubleslashes | Use clean_param($url, PARAM_URL) | — |
addslashes() for DB | Use $DB->... placeholders | 2.0 |
mysql_* functions | $DB always | 2.0 |
userdate($t, '', false) (no timezone) | Pass timezone or use core_date::get_user_timezone_object() | 3.2 |
create_function() | Closures function() { } | PHP 7.2 removed |
each($array) | foreach | PHP 7.2 removed |
assert($string) | Real expression | PHP 7.2+ |
(unset) cast | Plain unset() | PHP 7.2+ |
\Mustache_Engine direct | $OUTPUT->render_from_template | — |
cm_info::get_modinfo second arg | API change in 3.4 | 3.4 |
format_text no context | Always pass ['context' => $context] | 2.0 |
\html_writer::nonempty_tag | Use html_writer::tag with check | 3.5 |
\stdClass w/o : \stdClass return type in PHP 8.1+ | Add return types | 8.1 |
each(), create_function() removed'string' . null warns — explicit cast(string $x = null) deprecated → (?string $x = null)Returning by reference from a void function#[\AllowDynamicProperties] if you set undeclared propertiestempnam, parse_url minor signature changes${...} string interpolation deprecated → {$...}utf8_encode/utf8_decode deprecated#[\Override] attribute encouragedDate_Create_From_Format strict-nessfunction f(string $x = null) → function f(?string $x = null)E_STRICT constant removed (was already a no-op)trigger_error(..., E_USER_ERROR) deprecated — throw an exception insteadfputcsv() / fgetcsv() / str_getcsv() default $escape deprecated — pass '' explicitly to opt out of legacy escapexml_set_*_handler string-callable form deprecated — pass [$obj, 'method'] or first-class callablemysqli_kill(), mysqli_refresh(), mysqli_ping() deprecated — irrelevant to Moodle ($DB layer), but flag in custom codeDatePeriod ISO8601 string constructor deprecated → DatePeriod::createFromISO8601String()mb_trim(), mb_ltrim(), mb_rtrim() added — prefer over manual regex trimming for multibyteClean removed; use Boostfrontpage, mydashboard, incourse, course may need overridesOld:
function local_example_extend_navigation(global_navigation $nav) { /* ... */ }
New (where supported):
// db/hooks.php
$callbacks = [
[
'hook' => \core\hook\navigation\primary_extend::class,
'callback' => '\local_example\hooks\navigation::extend_primary',
],
];
// classes/hooks/navigation.php
namespace local_example\hooks;
class navigation {
public static function extend_primary(\core\hook\navigation\primary_extend $hook): void {
$hook->get_primary_view()->add(/* ... */);
}
}
Magic callbacks still work in 4.4+; Hooks API is preferred for new code, required for some new extension points.
// Pre-4.2
class get_items extends external_api { /* ... */ }
// 4.2+
use core_external\external_api;
use core_external\external_function_parameters;
class get_items extends external_api { /* ... */ }
Compatibility shim: 4.2+ aliases bare names to namespaced for back-compat — but new code should use namespaced.
format_<name> plugins migrated from format_base to \core_courseformat\base:
// classes/output/courseformat/content.php
namespace format_yourname\output\courseformat;
class content extends \core_courseformat\output\local\content { /* ... */ }
3.x format plugins need full rewrite for 4.0+.
.sr-only → .visually-hidden.float-left → .float-start.ml-* → .ms-*data-toggle → data-bs-toggle\core_ai)max_input_vars ≥ 5000./public document root. Web server must point at <moodleroot>/public. Plugins still live above (/local/..., /mod/...) — installer relocates; manual moves needed for in-place upgrades.file_encode_url() deprecated → use moodle_url::make_pluginfile_url()mod/quiz/UPGRADING.md)course/changenumsections.php page removedUPGRADING.md (5.1 replaces upgrade.txt for API change notes).core/modal_factory, core/modal_registry (AMD) — use core/modal directlylib/deprecatedlib.php from ≤ 4.4 removedsite_info, mobile_config, choice_results.Always check /public/lib/upgrade.txt (path changed in 5.1) and per-component UPGRADING.md for the target version.
<plugin>/upgrade.txt:
This files describes API changes in /local/example/*,
information provided here is intended especially for developers.
=== 1.5.0 ===
* The deprecated function local_example_old_thing() has been removed. Use local_example_new_thing() instead.
* New \local_example\hooks\foo class for the navigation hook (Moodle 4.4+).
=== 1.4.0 ===
* New web service local_example_export_csv.
Mirrors Moodle's own lib/upgrade.txt. Plugin directory reviewers read it.
<?php
defined('MOODLE_INTERNAL') || die();
function xmldb_local_example_upgrade($oldversion) {
global $DB;
$dbman = $DB->get_manager();
if ($oldversion < 2024010100) {
$table = new xmldb_table('local_example_items');
$field = new xmldb_field('status', XMLDB_TYPE_INTEGER, '4', null,
XMLDB_NOTNULL, null, '0', 'name');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_plugin_savepoint(true, 2024010100, 'local', 'example');
}
if ($oldversion < 2024050100) {
// Drop legacy index
$table = new xmldb_table('local_example_items');
$index = new xmldb_index('legacy_idx', XMLDB_INDEX_NOTUNIQUE, ['userid']);
if ($dbman->index_exists($table, $index)) {
$dbman->drop_index($table, $index);
}
upgrade_plugin_savepoint(true, 2024050100, 'local', 'example');
}
if ($oldversion < 2025010100) {
// Data migration — chunked to avoid memory blow
$rs = $DB->get_recordset_select('local_example_items', 'metadata IS NULL');
foreach ($rs as $r) {
$r->metadata = json_encode([]);
$DB->update_record('local_example_items', $r);
}
$rs->close();
upgrade_plugin_savepoint(true, 2025010100, 'local', 'example');
}
return true;
}
Each if block targets one historical version. Never delete past blocks — sites may upgrade through multiple versions.
For plugins supporting multiple Moodle versions:
if (class_exists('\core_external\external_api')) {
class_alias('\core_external\external_api', '\local_example\compat\external_api');
} else {
class_alias('\external_api', '\local_example\compat\external_api');
}
Or guard with \core\plugin_manager::get_remote_plugin_info and moodle_major_version():
if (version_compare(moodle_major_version(), '4.4', '>=')) {
// Hooks API path
} else {
// Magic callback fallback
}
| Mistake | Fix |
|---|---|
Bumping $plugin->requires without testing on min version | Test against $plugin->requires Moodle release |
Removing db/upgrade.php blocks | Keep all historical blocks |
Skipping upgrade_plugin_savepoint | Required — marks step as done |
Adding new field without field_exists guard | Use guard in case site is mid-upgrade |
Forgetting upgrade.txt | Plugin directory reviewers expect it |
| Removing deprecated function used by other plugins | Mark deprecated for one major, then remove |
Using PHP 8.2 features without bumping $plugin->requires Moodle | Match Moodle's PHP min |
print_error left after migration | Replace with throw new moodle_exception |
external_api namespace mismatch breaking 4.1 | Use compatibility alias |
# Find deprecated API usage
phpcs --standard=moodle --report=summary local/example
moodle-cs/standards/Moodle/...
# Find PHP version issues
phpcompatinfo analyser:run local/example
phpstan analyse local/example --level=5