What breaks when you upgrade Moodle, and how to rescue old plugins
Moodle core upgrades are rarely the problem. Old custom plugins, removed features and server requirements are. A practical list, a risk score and a rescue plan.
Moodle core upgrades well. Moodle HQ tests the upgrade path from each supported starting point, and a clean site with only standard plugins usually moves from one version to the next in an afternoon.
Real sites are not clean. They have the attendance plugin someone installed in 2019, a custom report a contractor wrote for the HR team, a theme with a few lines changed in core “just for now”, and an Atto button the language department depends on. That is what breaks.
This post is the list we work through when a client asks us to get them from Moodle 4.x to 5.3.
Score your own upgrade first
Tick what applies to your site. The score is a rough guide from our own upgrade work, not a formula. It is useful for one thing: deciding whether this is a weekend task or a project.
Upgrade risk, Moodle 4.x to 5.3
- Server on PHP 8.2 or older
- Database below the 5.3 minimum
- Site on Moodle 4.3 or older
- Oracle database
- Atto or Atto plugins in use
- Theme based on Classic
- Unmaintained custom plugins
- Edited core files
- MNet, Chat, Survey or CAS login in use
- No staging site
- Git based upgrade from 5.0 or older
The server comes first
Moodle 5.3 needs PHP 8.3 or 8.4 and a database at PostgreSQL 17, MySQL 8.4, MariaDB 11.4 or SQL Server 2019 (5.3 release notes). If your server is older than that, the Moodle upgrade can’t start.
Do the PHP step first, while still on 4.5. Moodle 4.5 lists PHP 8.3 as supported, so you can upgrade PHP, let it run for a week, and only then touch Moodle. Splitting the work this way means that when something breaks, you know which change broke it.
PHP upgrades are where old plugin code shows up first. Code written for PHP 7.x that worked on 8.1 often fails on 8.3 because of things like dynamic properties (deprecated in 8.2), changed function signatures, or stricter type handling. The site keeps running but the logs fill with deprecation warnings, and some pages throw errors.
Features removed from core
Each of these is in a release note, and each one stops something on a real site:
| Removed | Release | What breaks |
|---|---|---|
| Atto editor | 5.0 | Custom Atto buttons and every Atto plugin |
| Chat, Survey | 5.0 | Courses that use them, unless installed as add-on plugins |
| MNet, CAS auth | 5.0 | Single sign on between Moodle sites, CAS logins |
| Oracle support | 5.0 | The whole site, until the database is migrated |
Legacy subplugins.php | 5.0 | Plugins with sub-plugins that never moved to subplugins.json |
| Activities block, subsection pages | 5.2 | Course layouts that used them |
| Deprecated code from 4.4 and earlier | 5.2 | Plugins that call those functions |
| Classic theme | 5.3 | Custom themes built on Classic |
Sources: release notes for 5.0, 5.2 and 5.3.
The Atto one deserves a note. Teachers don’t know what editor they use. They know “the button that inserts the Arabic keyboard” or “the button that adds our template box”. When Atto goes, the button goes. Rebuilding it for TinyMCE is a rewrite, not a port: different plugin type, different JavaScript API.
The /public move in 5.1
Moodle 5.1 moved all web-accessible files into a public folder (upgrade guide). The web server’s document root has to point there, and the upgrade won’t run until it does.
If you upgrade by replacing code from a zip, you place plugins in the new layout as you go. If you upgrade through git, your existing additional plugins stay in their old folders and must be moved by hand into the matching place under public/. The guide says so plainly. Deployment scripts, backup scripts and any server rules that reference paths also need checking.
Custom plugins: the real blocker
Here is the pattern we see most. The site has a few dozen additional plugins. Most are from the plugins directory (now Moodle Marketplace) and have releases for the new version. Three or four are custom, written for this organisation, by someone who is no longer around. Those three or four decide the upgrade date.
They fail in predictable ways:
- Calls to removed functions. Moodle deprecates a function, keeps it for a few releases with a warning, then removes it. Plugins that ignored the warnings stop working on the release that removes it.
- Old JavaScript. YUI modules and old AMD patterns. Moodle is moving to ES modules and React, and it advises against new AMD code (frontend guide).
- Templates tied to Bootstrap 4. Moodle 5.0 moved Boost to Bootstrap 5 with a compatibility layer. Markup that relied on Bootstrap 4 classes mostly renders, but things like modals and dropdowns can behave differently.
- No privacy provider. The plugin stores user data but does not implement the Privacy API, so data export and deletion requests silently skip it. That is a GDPR problem before it is an upgrade problem.
- Broken upgrade steps.
db/upgrade.phpwithout savepoints, or schema changes that were applied by hand on production and never written down. - Core hacks. A file in
/libor/courseedited to make the plugin work. Every upgrade overwrites it.
How we rescue an old plugin
The rescue process is the same for most plugins, and none of it is guesswork:
- Get the plugin running on the target version in a clean environment. Fresh Moodle 5.3, PHP 8.3, the plugin, and the debug level at developer. Every warning is a to-do item.
- Run
moodle-plugin-ci.phplint,phpcs,validate,savepointsandmustachegive a full list of structural problems in minutes. On a legacy plugin the first run is usually long. - Write tests for what the plugin does today, before changing anything. A few PHPUnit tests on the core logic and one Behat scenario per main screen. Then the fixes can’t silently change behaviour.
- Fix in order: fatal errors, removed APIs, deprecations, then style.
- Add what was missing: privacy provider, backup and restore if it stores course data, capability checks that were skipped.
- Check the upgrade path on a copy of production data, not just a fresh install. The upgrade step that worked on an empty table can time out on a table with two million rows.
- Decide: fix or rewrite. If more than half the code has to change, a clean rewrite with the same database tables is often faster and leaves you with something maintainable.
AI agents speed up steps 2 to 5 a lot. They are fast at mechanical replacements, writing test scaffolding and drafting a privacy provider from the table definitions. Steps 1, 6 and 7 still need someone who has done it many times. Knowing that a renamed capability will lock every teacher out of a report is not something you find by reading the diff.
A realistic timeline
For a typical organisation on Moodle 4.5 with three custom plugins:
- Week 1: PHP and database upgrades on 4.5, inventory of plugins, staging site.
- Week 1 to 2: custom plugins fixed or rewritten, in parallel with the server work.
- Week 2: staging upgrade to 5.3, testing with real teachers.
- Week 3: production upgrade in a planned window.
Most of the risk sits in the custom plugins. Take them off the critical path early and the rest is routine.
Sources
- Moodle HQ, release notes for 5.0, 5.1, 5.2 and 5.3.
- MoodleDocs, Upgrading (5.3).
- Moodle HQ, Frontend development guide (5.2).
- Moodle HQ, Privacy API.
- Moodle HQ, moodle-plugin-ci CLI reference.
- MoodleDocs, Plugins FAQ on Moodle Marketplace.
Need a Moodle plugin built or rescued?
Tell the scoping assistant what you need. You get a fixed price and a timeline in a few minutes, and an engineer reviews it within one business day.
Get a fixed quote