Migrate Plugin from Matomo 5.X to Matomo 6
Migrate Plugin from Matomo 5.X to Matomo 6
This migration guide covers the changes required in order to make a plugin compatible with Matomo 6. A list of all changes in Matomo 6 can be found in the Changelog.
Create a new branch
We recommend creating a new branch for your plugin that supports Matomo 6. For example 6.x-dev. This way you will be able to make changes to your plugin for Matomo 5 and Matomo 6 and release independent versions for each of them. You can still publish updates to your plugin that supports Matomo 5 once you have published an update for a version that supports Matomo 6.
Adjust the required Matomo version
For your plugin to be executed in Matomo 6 you first need to show it is compatible with Matomo 6 in your plugin.json file:
- specify that your plugin requires Matomo 6 (the requirement for Matomo 5 used to be e.g.
"matomo": ">=5.0.0-b1,<6.0.0-b1"). - we also recommend increasing your plugin's major version number e.g. from
5.1.9to6.0.0.
The plugin.json would look like this:
"version": "6.0.0",
"require": {
"matomo": ">=6.0.0-b1,<7.0.0-b1"
},
The -b1 suffix on the lower bound matters: a plain >=6.0.0 sorts above 6.0.0-b1, so your plugin would be disabled against a Matomo 6 beta or release candidate.
It's not allowed to support multiple major Matomo versions such as Matomo 5 and Matomo 6: "matomo": ">=5.0.0-b1,<7.0.0-b1". In this case you would receive an error email and the release would not be published.
Required PHP and database versions
Matomo 6 raises the minimum requirements:
- PHP 8.1.0 (was 7.2.5)
- MySQL 8.0 or MariaDB 10.6 (was 5.5)
If your plugin declares its own php requirement in plugin.json or composer.json, raise it to at least 8.1.0. Support for TiDB has been dropped.
Removed PHP APIs
| Removed | Replacement |
|---|---|
Piwik\Archive::getBlob() |
one of the Piwik\Archive::getDataTable*() methods |
Piwik\Archive::clearStaticCache() |
none — it was already a no-op |
Piwik\ArchiveProcessor\Parameters::setIsPartialArchive() |
none — remove the call, see below |
Piwik\Db\Adapter::getDefaultPortForAdapter() |
Piwik\Db\Schema::getDefaultPortForSchema() |
Piwik\Db\AdapterInterface::getDefaultPort() and its Mysqli / Pdo\Mysql implementations |
Piwik\Db\Schema::getDefaultPortForSchema() |
Piwik\Url::saveCORSHostnameInConfig() |
none — it was no longer in use |
Piwik\Plugin\Report::getThirdLeveltableDimension() |
Piwik\Plugin\Report::getNthLevelTableDimension(2) |
Piwik\Db::optimizeTables() |
Piwik\Db\Schema::getInstance()->optimizeTables() |
Piwik\Db::isOptimizeInnoDBSupported() |
Piwik\Db\Schema::getInstance()->isOptimizeInnoDBSupported() |
Piwik\Db\TransactionLevel::setUncommitted() |
Piwik\Db\TransactionLevel::setTransactionLevelForNonLockingReads() |
Piwik\API\Request::isTokenAuthProvidedSecurely() |
none |
Piwik\Plugins\Overlay\API::getExcludedQueryParameters() |
the SitesManager.getExcludedQueryParameters API method |
Note that getDefaultPort() also exists as an instance method on Piwik\Db\SchemaInterface, which is not removed. Check the receiver before changing a call.
An Archiver that marked its archive as partial no longer needs to do anything: Parameters::isPartialArchive() is now derived from the requested report, so the setIsPartialArchive(true) call (usually in the archiver's constructor) should simply be deleted. See Archiving.
The SEO plugin has been removed
The whole SEO plugin is gone, including its widget, the SEO.getRank API method and Piwik\Plugins\SEO\* classes. Matomo 6 deactivates and uninstalls it on upgrade. If your plugin referenced it, that reference has to be removed.
TrackingSpamPrevention is now bundled
TrackingSpamPrevention ships with core, is activated by default and can no longer be uninstalled. It is no longer distributed on the Marketplace, and Piwik\Plugin\Manager::isPluginBundledWithCore('TrackingSpamPrevention') now returns true.
Removed HTTP API methods
| Removed | Replacement |
|---|---|
API.getSettings |
none. Integrations reading key/value pairs over the REST API must move to another mechanism; the [APISettings] section of config/global.ini.php is gone and any entries in a local config.ini.php become inert |
SitesManager.setGlobalExcludedQueryParameters |
SitesManager.setGlobalQueryParamExclusion |
Overlay.getExcludedQueryParameters |
SitesManager.getExcludedQueryParameters |
SEO.getRank |
none |
Remember to search your templates, JavaScript, tests, fixtures and expected files, not just your PHP.
Removed global functions
The polyfills in libs/upgradephp/upgrade.php have been removed, as every supported PHP version provides them natively.
| Removed | Replacement |
|---|---|
_glob() |
native glob(). It returns false on error, so callers expecting an array need glob(…) ?: [] |
safe_serialize(), _safe_serialize() |
native serialize() |
_parse_ini_file() |
Piwik\Config, or Matomo\Ini\IniReader for other INI files |
the fallbacks for mysqli_set_charset(), file_get_contents(), utf8_encode(), utf8_decode(), fnmatch(), the Error class and PHP_INT_SIZE / PHP_INT_MAX |
the native functions and constants |
the gzopen() alias to gzopen64() |
none — Piwik\Unzip falls back to PclZip |
safe_unserialize() is kept, but it is stricter than native unserialize(): it rejects R: reference tokens and reads a resource back as the integer 0. Swapping safe_serialize() for serialize() is therefore only a drop-in replacement for values that contain no objects, references or resources.
glob(), fnmatch() and file_get_contents() also moved from the recommended to the required functions in the system check. Matomo now refuses to start when one of them is listed in the disable_functions php.ini directive instead of emulating it.
Removed console commands and scripts
The development console commands git:commit, git:pull and git:push have been removed. Use git directly.
The archiving script ./misc/cron/archive.sh has been removed. Use the core:archive console command instead.
Interface changes
Piwik\Log\LoggerInterfacefollows psr/log 3, solog(),debug(),info(),notice(),warning(),error(),critical(),alert()andemergency()all require a: voidreturn type. Plugins that obtain the logger through dependency injection or extendPiwik\Log\Loggerare not affected.
Dependency upgrades
Several bundled libraries received a major upgrade. These surface as fatal errors rather than deprecation notices, so they are worth checking even if your plugin looks unaffected.
- Monolog 1 → 3: every custom handler, formatter and processor now receives a
Monolog\LogRecordinstead ofarray $record, and records are immutable.protected function write(array $record)becomesprotected function write(LogRecord $record): void, and mutating a record becomesreturn $record->with(message: $new);. Core's handlers underplugins/Monolog/are the reference implementations. - PHP-DI 6 → 7:
Piwik\Container\Container::get(),make()andinjectOn()gained native types, so an overriding class must match them. Annotation-based injection no longer exists —@Injectdocblocks must become explicit container configuration or PHP attributes. - Symfony 5.4 → 6.4 (console, event-dispatcher, process, monolog-bridge):
Piwik\Plugin\ConsoleCommand::addOption()andaddArgument()returnstaticandgetHelper()returnsmixed. Only a plugin overriding them is affected. - matomo/matomo-php-tracker 3 → 4: typed setters. Use
setUserId(null)instead ofsetUserId(false), pass a string name tosetCustomVariable()rather than an array, and declare: stringon an overriddengetBaseUrl(). - Others:
geoip2/geoip22 → 3,matomo/decompress2 → 3,psr/log1 → 3,wikimedia/less.php3 → 5,twig/twignow^3.11.3. For tests, PHPUnit 8.5 → 9 (assertRegExp()becomesassertMatchesRegularExpression()) and PHPStan 1.12 → 2.
Controllers returning JSON
Controller actions that return JSON should now carry the #[Piwik\Http\JsonResponse] attribute and declare a string return type:
use Piwik\Http\JsonResponse;
#[JsonResponse]
public function myControllerMethod(): string
{
return json_encode($result);
}
An action using the attribute must return the JSON string, must not send the Content-Type header itself, and must not emit output or call exit/die before returning.
The attribute is not inherited. If your plugin extends a core controller and overrides an action that core has annotated, you must re-declare #[JsonResponse] and declare a compatible string return type. Affected core actions include Dashboard::getAllDashboards() and getDashboardLayout(), CoreHome::markNotificationAsRead(), and several in SitesManager, Goals, CoreAdminHome, Marketplace, GeoIp2, CoreUpdater and MobileMessaging.
See Controllers for details.
Vue build moved to Vite
Matomo 6 builds plugin Vue libraries with Vite instead of the Vue CLI, and requires Node 24. The vue:build command is unchanged, but a few things around it are:
vue:buildno longer emits the unminifiedplugins/<Plugin>/vue/dist/<Plugin>.umd.js. Only<Plugin>.umd.min.jswas ever loaded by Matomo. Delete the committedvue/dist/<Plugin>.umd.jsfrom your repository and add/vue/dist/*.umd.jsto your.gitignore.- ESLint is no longer run as part of
vue:build. Runnpm run eslintseparately. - The
--clear-webpack-cacheoption is now--clear-cache, and clearsnode_modules/.vite.
The build is stricter about TypeScript than the previous toolchain. vue:build exits with status 0 even when it reports TypeScript errors, so read its output. The recurring fixes are:
- type-only re-exports need
export type { X }rather thanexport { X }; - interfaces referenced in emitted declarations must be exported (
interface State→export interface State); - explicit coercion where a type is now checked:
translate('X', count)→translate('X', String(count)),:content-title="i"→:content-title="String(i)"; - null guards:
alert.siteName→alert.siteName || '',logs?.length < 1→(logs?.length || 0) < 1,$sanitize(x)→$sanitize(x || ''); - typed array props:
type: Array→type: Array as PropType<Alert[]>, plus thePropTypeimport; - drop
this.in templates::title="this.triggers[id]"→:title="triggers[id]"; - drop the
.tssuffix from import specifiers:from '../types.ts'→from '../types'.
Some CoreHome exports are now type-only (SiteRef, WidgetType, WidgetContainerType, GroupedWidgetsType); importing them as values fails. tslib is a new external provided by the CoreVue polyfill.
Jest replaced with Vitest
Vue component tests now run on Vitest. npm test is TZ=UTC vitest run. In your specs, replace jest.mock, jest.fn and jest.spyOn with vi.mock, vi.fn and vi.spyOn, drop the Jest-only { virtual: true } mock option, and replace the require-after-mock pattern with an ESM import using vi.hoisted.
Theming and Less changes
Registering a removed stylesheet in getStylesheetFiles() breaks stylesheet merging with The ui asset with 'href' = … is not readable, so check these first:
plugins/Morpheus/stylesheets/base/mode-colors.lesshas been removed together with@color-mode-blackand@color-mode-white. Use the.inDarkMode()mixin frombase/mixins.lessor a@theme-color-*variable.plugins/Login/stylesheets/variables.lesshas been removed together with@login-section-background.
Removed Less variables:
- the
Piwik-era aliases@color-black-piwik,@color-blue-piwik,@color-red-piwikand@color-green-piwik— use the@color-*-matomoequivalents; - the third-party brand colours
@color-orange-brand(#f57c00),@color-green-brandSocial(#009874),@color-blue-brandSocial(#3b5998),@color-blue-brandSocialLight(#1c87bd) and@color-blue-brandSocialVeryLight(#00aced) — use the literal value; - the unused tokens
@color-gray-light,@color-gray-bright,@color-gray-400,@color-jetstream,@color-silver-l14,@color-silver-l50,@color-silver-l70and@color-silver-l98.
Variables that were only used inside a single stylesheet have been inlined or renamed to private @_-prefixed names and are no longer visible to other stylesheets: @top-menu-nav-color, the four @color-period-selector*, the eight @add-widget-*, and @calendarHeaderBackground, @calendarHeaderColor, @calendarCurrentStateHover and @calendarBorder.
Themes should note that @theme-color-new-brand / ThemeStyles::$colorNewBrand has been removed, and its teal is now the value of @theme-color-brand / $colorBrand — which previously held green. That green moved to the new @theme-color-success / $colorSuccess, which is deliberately kept independent of the brand colour so success states stay green when a theme overrides the brand. If your theme overrode colorNewBrand, move that override to colorBrand, not to colorSuccess. Note that these are [light, dark] pairs where $colorNewBrand was a plain string; a bare string still works but gives both modes the same colour. A theme reading themeStyles.getPropertyValue('colorNewBrand') breaks silently rather than erroring.
Finally, @theme-color-widget-background and a group of @theme-color-menu-contrast-* and @theme-color-widget-* variables are deprecated and will be removed in Matomo 7. They keep working in Matomo 6; see the Changelog for the full list.
JavaScript changes
- The jQuery UI widget
$.fn.liveWidget(piwik.liveWidget) has been removed together withplugins/Live/javascripts/live.js. Use theLive.AutoRefreshWidgetVue component.
Behaviour changes
These do not produce fatal errors, so they typically show up as unexpected results or failing tests.
- Request parameters are no longer trimmed.
Piwik\API\Request::getRequestArrayFromString()used to applytrim((string) $value)to every non-array parameter. Whitespace in values such as alabelor a segment operand is now preserved, and scalars keep their type. As a consequence, a boolean passed throughPiwik\API\Request::processRequest()no longer arrives as'1'/'', so a parameter read withCommon::getRequestVar($name, $default, 'string')orRequest::getStringParameter()falls back to its default. Pass a string, or read it withRequest::getBoolParameter(). See Request parameters. Annotations.add,Annotations.saveandAnnotations.deletenow requireWritepermission. Previouslyaddrequired onlyView, and an annotation's author could modify or delete it withView.UsersManager.addCapabilitiesandremoveCapabilitiesare now gated behind theenable_users_adminsetting.- Report rows gained
{metric}_percent_of_totalcolumns. If you parse CSV or TSV output by column position, this changes the header and column count — passpercent_of_total=0to keep the previous output. See the API reference. - One Click Update is HTTPS-only. The "retry over HTTP" fallback and the
httpsrequest parameter of theCoreUpdater.oneClickUpdateaction have been removed, along with the$httpsparameter ofPiwik\Plugins\CoreUpdater\Updater::updatePiwik()andgetArchiveUrl().
Tests on CI
If you use the GitHub test action, update the PHP versions it runs against. You can use the matomo6_min_php and matomo6_max_php aliases instead of literal versions so they follow the supported range:
$ ./console generate:test-action --plugin="MyPlugin" --php-versions="matomo6_min_php,matomo6_max_php"
Set node-version: '24' on every job that specifies one, and add a database matrix covering MySQL 8.0 and MariaDB 10.6. More details are in the GitHub tests guide.
Summary
In this guide we have seen which steps to take to migrate your Matomo plugin to be compatible with our latest Matomo 6. If you need further help with converting your plugin to Matomo 6, head over to the Matomo developers community forums.
Once you've adjusted your plugin, don't forget to release a new version!