feat: auto-migrate from MokoCassiopeia on install — copy params, styles, set default
Some checks failed
Repo Health / Access control (push) Successful in 1s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 3s

This commit is contained in:
2026-04-21 17:36:13 +00:00
parent 67f6b61bf2
commit a581da7bdc

View File

@@ -1,6 +1,6 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
@@ -12,8 +12,9 @@
* Joomla calls the methods in this class automatically during template
* install, update, and uninstall via the <scriptfile> element in
* templateDetails.xml.
* Joomla 5 and 6 compatible — uses the InstallerScriptInterface when
* available, falls back to the legacy class-based approach otherwise.
*
* On first install, detects MokoCassiopeia and migrates template styles,
* parameters, menu assignments, and user files automatically.
*/
defined('_JEXEC') or die;
@@ -24,33 +25,19 @@ use Joomla\CMS\Log\Log;
class Tpl_MokoonyxInstallerScript
{
/**
* Minimum PHP version required by this template.
*/
private const MIN_PHP = '8.1.0';
/**
* Minimum Joomla version required by this template.
*/
private const MIN_JOOMLA = '4.4.0';
/**
* Called before install/update/uninstall.
*
* @param string $type install, update, discover_install, or uninstall.
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool True to proceed, false to abort.
*/
private const OLD_NAME = 'mokocassiopeia';
private const NEW_NAME = 'mokoonyx';
private const OLD_DISPLAY = 'MokoCassiopeia';
private const NEW_DISPLAY = 'MokoOnyx';
public function preflight(string $type, InstallerAdapter $parent): bool
{
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
Factory::getApplication()->enqueueMessage(
sprintf(
'MokoOnyx requires PHP %s or later. You are running PHP %s.',
self::MIN_PHP,
PHP_VERSION
),
sprintf('MokoOnyx requires PHP %s or later. You are running PHP %s.', self::MIN_PHP, PHP_VERSION),
'error'
);
return false;
@@ -58,11 +45,7 @@ class Tpl_MokoonyxInstallerScript
if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) {
Factory::getApplication()->enqueueMessage(
sprintf(
'MokoOnyx requires Joomla %s or later. You are running Joomla %s.',
self::MIN_JOOMLA,
JVERSION
),
sprintf('MokoOnyx requires Joomla %s or later. You are running Joomla %s.', self::MIN_JOOMLA, JVERSION),
'error'
);
return false;
@@ -71,37 +54,17 @@ class Tpl_MokoonyxInstallerScript
return true;
}
/**
* Called after a successful install.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function install(InstallerAdapter $parent): bool
{
$this->logMessage('MokoOnyx template installed.');
return true;
}
/**
* Called after a successful update.
*
* This is where the CSS variable sync runs — it detects variables that
* were added in the new version and injects them into the user's custom
* palette files without overwriting existing values.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function update(InstallerAdapter $parent): bool
{
$this->logMessage('MokoOnyx template updated.');
// Run CSS variable sync to inject any new variables into user's custom palettes.
$synced = $this->syncCustomVariables($parent);
if ($synced > 0) {
Factory::getApplication()->enqueueMessage(
sprintf(
@@ -116,47 +79,165 @@ class Tpl_MokoonyxInstallerScript
return true;
}
/**
* Called after a successful uninstall.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function uninstall(InstallerAdapter $parent): bool
{
$this->logMessage('MokoOnyx template uninstalled.');
return true;
}
/**
* Called after install/update completes (regardless of type).
*
* @param string $type install, update, or discover_install.
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function postflight(string $type, InstallerAdapter $parent): bool
{
// On install: migrate from MokoCassiopeia if it exists
if ($type === 'install') {
$this->migrateFromCassiopeia();
}
return true;
}
/**
* Run the CSS variable sync utility.
*
* Loads sync_custom_vars.php from the template directory and calls
* MokoCssVarSync::run() to detect and inject missing variables.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return int Number of variables added across all files.
* Detect MokoCassiopeia and migrate styles, params, and user files to MokoOnyx.
*/
private function migrateFromCassiopeia(): void
{
$db = Factory::getDbo();
// Check if MokoCassiopeia has any template styles
$query = $db->getQuery(true)
->select('*')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
->where($db->quoteName('client_id') . ' = 0');
$oldStyles = $db->setQuery($query)->loadObjectList();
if (empty($oldStyles)) {
$this->logMessage('No MokoCassiopeia styles found — fresh install.');
return;
}
$this->logMessage('MokoCassiopeia detected — migrating ' . count($oldStyles) . ' style(s).');
// 1. Copy template styles with params
foreach ($oldStyles as $oldStyle) {
$newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title);
$newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle);
// Check if MokoOnyx already has a style with this title
$check = $db->getQuery(true)
->select('COUNT(*)')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
->where($db->quoteName('title') . ' = ' . $db->quote($newTitle));
if ((int) $db->setQuery($check)->loadResult() > 0) {
// Update existing MokoOnyx style with MokoCassiopeia's params
$params = is_string($oldStyle->params)
? str_replace(self::OLD_NAME, self::NEW_NAME, $oldStyle->params)
: $oldStyle->params;
$update = $db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('params') . ' = ' . $db->quote($params))
->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
->where($db->quoteName('title') . ' = ' . $db->quote($newTitle));
$db->setQuery($update)->execute();
$this->logMessage("Updated existing MokoOnyx style: {$newTitle}");
continue;
}
// Create new MokoOnyx style from MokoCassiopeia style
$newStyle = clone $oldStyle;
unset($newStyle->id);
$newStyle->template = self::NEW_NAME;
$newStyle->title = $newTitle;
$newStyle->home = 0; // Don't set as default yet
if (is_string($newStyle->params)) {
$newStyle->params = str_replace(self::OLD_NAME, self::NEW_NAME, $newStyle->params);
}
$db->insertObject('#__template_styles', $newStyle, 'id');
$newId = $newStyle->id;
// If the old style was the default, make the new one default
if ($oldStyle->home == 1) {
$db->setQuery(
$db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 1')
->where('id = ' . (int) $newId)
)->execute();
$db->setQuery(
$db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 0')
->where('id = ' . (int) $oldStyle->id)
)->execute();
$this->logMessage('Set MokoOnyx as default site template.');
}
$this->logMessage("Migrated style: {$oldStyle->title}{$newTitle}");
}
// 2. Copy user files (custom themes, user.css, user.js)
$this->copyUserFiles();
// 3. Notify admin
Factory::getApplication()->enqueueMessage(
'<strong>MokoOnyx has been installed as a replacement for MokoCassiopeia.</strong><br>'
. 'Your template settings and custom files have been migrated automatically. '
. 'MokoOnyx is now your active site template. '
. 'You can safely uninstall MokoCassiopeia from Extensions &rarr; Manage.',
'success'
);
}
/**
* Copy user-specific files from MokoCassiopeia media to MokoOnyx media.
*/
private function copyUserFiles(): void
{
$oldMedia = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME;
$newMedia = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
if (!is_dir($oldMedia) || !is_dir($newMedia)) {
return;
}
$userFiles = [
'css/theme/light.custom.css',
'css/theme/dark.custom.css',
'css/theme/light.custom.min.css',
'css/theme/dark.custom.min.css',
'css/user.css',
'css/user.min.css',
'js/user.js',
'js/user.min.js',
];
$copied = 0;
foreach ($userFiles as $relPath) {
$src = $oldMedia . '/' . $relPath;
$dst = $newMedia . '/' . $relPath;
if (is_file($src) && !is_file($dst)) {
$dstDir = dirname($dst);
if (!is_dir($dstDir)) {
mkdir($dstDir, 0755, true);
}
copy($src, $dst);
$copied++;
}
}
if ($copied > 0) {
$this->logMessage("Copied {$copied} user file(s) from MokoCassiopeia.");
}
}
private function syncCustomVariables(InstallerAdapter $parent): int
{
$templateDir = $parent->getParent()->getPath('source');
// The sync script lives alongside this script in the template root.
$syncScript = $templateDir . '/sync_custom_vars.php';
if (!is_file($syncScript)) {
@@ -172,20 +253,13 @@ class Tpl_MokoonyxInstallerScript
}
try {
$joomlaRoot = JPATH_ROOT;
$results = MokoCssVarSync::run($joomlaRoot);
$results = MokoCssVarSync::run(JPATH_ROOT);
$totalAdded = 0;
foreach ($results as $filePath => $result) {
$totalAdded += count($result['added']);
if (!empty($result['added'])) {
$this->logMessage(
sprintf(
'CSS sync: added %d variable(s) to %s',
count($result['added']),
basename($filePath)
)
);
$this->logMessage(sprintf('CSS sync: added %d variable(s) to %s', count($result['added']), basename($filePath)));
}
}
@@ -196,12 +270,6 @@ class Tpl_MokoonyxInstallerScript
}
}
/**
* Log a message to Joomla's log system.
*
* @param string $message The log message.
* @param string $priority Log priority (info, warning, error).
*/
private function logMessage(string $message, string $priority = 'info'): void
{
$priorities = [