generated from MokoConsulting/Template-Joomla
fix(installer): add MokoMenuRegistry for shared admin menu #23
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGIT.Workflow
|
||||
# INGROUP: MokoCLI.Automation
|
||||
# VERSION: 01.00.38
|
||||
# VERSION: 01.00.39
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
DEFGROUP: Template-Joomla
|
||||
INGROUP: Template-Joomla.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
|
||||
VERSION: 01.00.38
|
||||
VERSION: 01.00.39
|
||||
PATH: ./CODE_OF_CONDUCT.md
|
||||
BRIEF: Community expectations and enforcement guidelines
|
||||
NOTE: Adapted with attribution from the Contributor Covenant v2.1
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
DEFGROUP: mokoconsulting-tech.Template-Joomla
|
||||
INGROUP: MokoStandards.Governance
|
||||
REPO: https://github.com/mokoconsulting-tech/Template-Joomla
|
||||
VERSION: 01.00.38
|
||||
VERSION: 01.00.39
|
||||
PATH: /GOVERNANCE.md
|
||||
BRIEF: Project governance rules, roles, and decision process for Template-Joomla
|
||||
-->
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
|
||||
INGROUP: Template-Joomla.Documentation
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 01.00.38
|
||||
VERSION: 01.00.39
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuiteSight
|
||||
* @subpackage com_mokosuitesight
|
||||
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @license GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Moko\Component\MokoSuiteSight\Administrator\Installer;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
\defined('_JEXEC') or die;
|
||||
|
||||
/**
|
||||
* Self-contained MokoSuite admin-menu registry helper.
|
||||
*
|
||||
* Each MokoSuite extension calls register()/unregister() from its install script
|
||||
* to publish its admin menu items into `#__mokosuite_menu`, which the
|
||||
* mod_mokosuiteclient_menu module reads and renders. The helper is deliberately
|
||||
* dependency-free and self-healing (it creates the table if missing) so an
|
||||
* extension can install BEFORE the Client hub that owns the table.
|
||||
*
|
||||
* This file is the canonical contract: copy it verbatim into each MokoSuite
|
||||
* extension, changing only the namespace to match that extension. Do NOT add
|
||||
* cross-extension dependencies here.
|
||||
*
|
||||
* @since 02.59.07
|
||||
*/
|
||||
final class MokoMenuRegistry
|
||||
{
|
||||
/**
|
||||
* Idempotently (re)register one extension's admin menu items.
|
||||
*
|
||||
* Each $items entry is an associative array:
|
||||
* - title (string, required) language key or literal title
|
||||
* - link (string) index.php?option=... route ('' for a bare group)
|
||||
* - icon (string) a VERIFIED icon-* glyph (Atum renders many blank)
|
||||
* - parent (string) owner element of the group this nests under;
|
||||
* '' means this row IS a top-level group
|
||||
* - element (string) component this item routes into (for active-state)
|
||||
* - access (int) Joomla view access level (default 1)
|
||||
* - ordering (int) sort within its level (default 0)
|
||||
* - menu_group (string) reserved sidebar section (default 'suite')
|
||||
* - params (string|null) reserved JSON
|
||||
*
|
||||
* @param string $owner Registering extension element, e.g. 'com_mokosuitehq'.
|
||||
* @param array $items The menu rows to publish for this owner.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register(string $owner, array $items): void
|
||||
{
|
||||
try {
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
if (!self::ensureTable($db)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete-then-insert on owner = fully idempotent across reinstall/upgrade.
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuite_menu'))
|
||||
->where($db->quoteName('owner') . ' = ' . $db->quote($owner))
|
||||
)->execute();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$row = (object) [
|
||||
'owner' => $owner,
|
||||
'element' => (string) ($item['element'] ?? ''),
|
||||
'parent' => (string) ($item['parent'] ?? ''),
|
||||
'title' => (string) ($item['title'] ?? ''),
|
||||
'link' => (string) ($item['link'] ?? ''),
|
||||
'icon' => (string) ($item['icon'] ?? 'icon-puzzle-piece'),
|
||||
'menu_group' => (string) ($item['menu_group'] ?? 'suite'),
|
||||
'access' => (int) ($item['access'] ?? 1),
|
||||
'ordering' => (int) ($item['ordering'] ?? 0),
|
||||
'published' => (int) ($item['published'] ?? 1),
|
||||
'params' => $item['params'] ?? null,
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokosuite_menu', $row);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Best-effort: a registry failure must never block the install.
|
||||
Log::add(
|
||||
'MokoMenuRegistry::register(' . $owner . ') failed: ' . $e->getMessage(),
|
||||
Log::ERROR,
|
||||
'com_mokosuitesight'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all admin menu rows owned by an extension. Call on uninstall.
|
||||
*
|
||||
* @param string $owner The registering extension element.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function unregister(string $owner): void
|
||||
{
|
||||
try {
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
if (!self::tableExists($db)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuite_menu'))
|
||||
->where($db->quoteName('owner') . ' = ' . $db->quote($owner))
|
||||
)->execute();
|
||||
} catch (\Throwable $e) {
|
||||
Log::add(
|
||||
'MokoMenuRegistry::unregister(' . $owner . ') failed: ' . $e->getMessage(),
|
||||
Log::ERROR,
|
||||
'com_mokosuitesight'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the registry table if it does not exist yet. This lets a sibling
|
||||
* extension install before the Client hub that ships the table in its schema.
|
||||
*
|
||||
* @param DatabaseInterface $db The database driver.
|
||||
*
|
||||
* @return boolean True if the table exists (or was created).
|
||||
*/
|
||||
private static function ensureTable(DatabaseInterface $db): bool
|
||||
{
|
||||
if (self::tableExists($db)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$db->setQuery(
|
||||
'CREATE TABLE IF NOT EXISTS ' . $db->quoteName('#__mokosuite_menu') . ' ('
|
||||
. '`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,'
|
||||
. "`owner` VARCHAR(75) NOT NULL DEFAULT '',"
|
||||
. "`element` VARCHAR(75) NOT NULL DEFAULT '',"
|
||||
. "`parent` VARCHAR(75) NOT NULL DEFAULT '',"
|
||||
. "`title` VARCHAR(255) NOT NULL DEFAULT '',"
|
||||
. "`link` VARCHAR(1024) NOT NULL DEFAULT '',"
|
||||
. "`icon` VARCHAR(75) NOT NULL DEFAULT 'icon-puzzle-piece',"
|
||||
. "`menu_group` VARCHAR(50) NOT NULL DEFAULT 'suite',"
|
||||
. '`access` INT UNSIGNED NOT NULL DEFAULT 1,'
|
||||
. '`ordering` INT NOT NULL DEFAULT 0,'
|
||||
. '`published` TINYINT(1) NOT NULL DEFAULT 1,'
|
||||
. '`params` TEXT NULL,'
|
||||
. 'PRIMARY KEY (`id`),'
|
||||
. 'UNIQUE KEY `idx_owner_link` (`owner`, `link`(191)),'
|
||||
. 'KEY `idx_owner` (`owner`),'
|
||||
. 'KEY `idx_parent` (`parent`),'
|
||||
. 'KEY `idx_published_ordering` (`published`, `ordering`)'
|
||||
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
||||
)->execute();
|
||||
|
||||
return self::tableExists($db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the #__mokosuite_menu table currently exists.
|
||||
*
|
||||
* @param DatabaseInterface $db The database driver.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private static function tableExists(DatabaseInterface $db): bool
|
||||
{
|
||||
return \in_array($db->replacePrefix('#__mokosuite_menu'), $db->getTableList(), true);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,9 @@
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<version>01.00.38</version>
|
||||
<version>01.00.39</version>
|
||||
<namespace path="src">Moko\Component\MokoSuiteSight</namespace>
|
||||
<scriptfile>script.php</scriptfile>
|
||||
<administration>
|
||||
<files folder="admin"><folder>services</folder><folder>src</folder><folder>tmpl</folder></files>
|
||||
<menu>COM_MOKOSUITESIGHT</menu>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MOKOSUITESIGHT
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Installer\InstallerAdapter;
|
||||
|
||||
class Com_mokosuitesightInstallerScript
|
||||
{
|
||||
public function postflight(string $type, InstallerAdapter $adapter): void
|
||||
{
|
||||
if ($type === 'install' || $type === 'update' || $type === 'discover_install')
|
||||
{
|
||||
$this->registerAdminMenu();
|
||||
}
|
||||
}
|
||||
|
||||
public function uninstall(InstallerAdapter $adapter): void
|
||||
{
|
||||
$this->unregisterAdminMenu();
|
||||
}
|
||||
|
||||
private const MENU_OWNER = 'com_mokosuitesight';
|
||||
|
||||
private function registerAdminMenu(): void
|
||||
{
|
||||
$registry = $this->loadMenuRegistry();
|
||||
|
||||
if ($registry !== null)
|
||||
{
|
||||
$registry::register(self::MENU_OWNER, $this->getMenuItems());
|
||||
}
|
||||
}
|
||||
|
||||
private function unregisterAdminMenu(): void
|
||||
{
|
||||
$registry = $this->loadMenuRegistry();
|
||||
|
||||
if ($registry !== null)
|
||||
{
|
||||
$registry::unregister(self::MENU_OWNER);
|
||||
}
|
||||
}
|
||||
|
||||
private function loadMenuRegistry(): ?string
|
||||
{
|
||||
$class = '\\Moko\\Component\\MokoSuiteSight\\Administrator\\Installer\\MokoMenuRegistry';
|
||||
|
||||
if (!class_exists($class))
|
||||
{
|
||||
$path = JPATH_ADMINISTRATOR . '/components/com_mokosuitesight/src/Installer/MokoMenuRegistry.php';
|
||||
|
||||
if (is_file($path))
|
||||
{
|
||||
require_once $path;
|
||||
}
|
||||
}
|
||||
|
||||
return class_exists($class) ? $class : null;
|
||||
}
|
||||
|
||||
private function getMenuItems(): array
|
||||
{
|
||||
$owner = self::MENU_OWNER;
|
||||
|
||||
return [
|
||||
['element' => $owner, 'parent' => '', 'title' => 'COM_MOKOSUITESIGHT_SHORT', 'link' => 'index.php?option=com_mokosuitesight', 'icon' => 'icon-eye', 'ordering' => 91],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>01.00.38</version>
|
||||
<version>01.00.39</version>
|
||||
<php_minimum>8.3</php_minimum>
|
||||
<description>PLG_SYSTEM_MOKOSUITESIGHT_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoSuiteSight</namespace>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<name>Web Services - MokoSuite Insight</name>
|
||||
<element>mokosuitesight</element>
|
||||
<author>Moko Consulting</author>
|
||||
<version>01.00.38</version>
|
||||
<version>01.00.39</version>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<namespace path="src">Moko\Plugin\WebServices\MokoSuiteInsight</namespace>
|
||||
<files><folder>src</folder><folder>services</folder></files>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuite Insight</name>
|
||||
<packagename>mokosuitesight</packagename>
|
||||
<version>01.00.38</version>
|
||||
<version>01.00.39</version>
|
||||
<creationDate>2026-06-21</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user