-
Notifications
You must be signed in to change notification settings - Fork 2
Converting‐a‐Module‐with‐mTools
This tutorial explains how to convert an existing XOOPS module so it uses
mtools as a shared helper layer instead of carrying a private
class/Common/ copy. It uses the quotes module as the practical reference
implementation.
The goal is not to make every module depend on every mTools helper. The goal is to move stable, reusable helper behavior into one shared module while each consumer module keeps ownership of its own domain model, handlers, templates, blocks, language files, and visual design.
mtools is the shared-helper host. A consumer module may use:
-
XoopsModules\Mtools\Common\...for stable shared helpers. -
XoopsModules\Mtools\Bootstrapfor runtime dependency checks. -
XoopsModules\Mtools\Module\Dependencyfor lower-level module dependency checks.
Do not consume classes from XoopsModules\Mtools\Lab\... unless you accept an
experimental API. Do not extend module-local mTools classes unless they are
explicitly documented in docs/architecture.md.
A converted module still owns:
- its own namespace, for example
XoopsModules\Quotes - its own
xoops_version.php - its own handlers and objects
- its own install/update/uninstall hooks
- its own admin pages
- its own templates and block templates
- its own CSS and UI decisions
- its own upload paths, SQL schema, and test data
In quotes, mTools provides shared helper behavior, but the quote/category/
author models, handlers, templates, blocks, and public UI remain in the Quotes
module.
Pick one module and audit its local class/Common/ folder.
Classify each local helper:
- Shared helper: generic XOOPS support logic that belongs in mTools.
- Module-specific helper: logic tied to this module's tables, templates, or workflows. Keep it in the module.
-
Unsafe helper: code that reads
$_POST, redirects, echoes output, writes files, or touches the database at file scope. Do not move this directly into mTools. Split it into a pure class plus a controller/admin endpoint first.
The best first migration is usually Utility extends Mtools\Common\SysUtility, because it removes the old copied utility base
without changing the module's domain behavior.
Add mtools to the consumer module manifest:
$modversion = [
// ...
'min_modules' => ['mtools' => '1.1.0'],
// ...
];Quotes example:
'min_modules' => ['mtools' => '1.1.0'],This documents the dependency for XOOPS and for humans reading the module. It is not enough by itself; install and runtime checks are still required.
Create a bootstrap.php file in the consumer module.
Quotes uses:
<?php declare(strict_types=1);
require_once __DIR__ . '/preloads/autoloader.php';
if (defined('XOOPS_ROOT_PATH')) {
$mtoolsBootstrap = XOOPS_ROOT_PATH . '/modules/mtools/bootstrap.php';
if (is_file($mtoolsBootstrap)) {
require_once $mtoolsBootstrap;
}
}
require_once __DIR__ . '/include/mtools_dependency.php';The important rules:
- The consumer registers its own namespace.
- The consumer loads the public mTools bootstrap.
- The consumer does not require
modules/mtools/preloads/autoloader.phpdirectly. - Class files should not perform
require_onceside effects.
Create include/mtools_dependency.php in the consumer module.
Quotes uses:
<?php declare(strict_types=1);
use XoopsModules\Mtools;
if (!function_exists('quotes_mtools_dependency_error')) {
function quotes_mtools_dependency_error(): string
{
if (!class_exists(Mtools\Bootstrap::class)) {
return 'The mtools module files are missing. Install mtools before installing or running Quotes.';
}
$status = Mtools\Bootstrap::checkRuntime('1.0.0', '1.1.0');
return $status['ok'] ? '' : Mtools\Bootstrap::statusMessage($status);
}
}For another module, rename the function:
function mymodule_mtools_dependency_error(): stringUse the same minimum API and module version unless the mTools architecture document says your helper requires a newer version.
Public and admin entry points should load the module bootstrap before using classes that extend mTools helpers.
Quotes public bootstrap path:
require __DIR__ . '/bootstrap.php';
$mtoolsDependencyError = quotes_mtools_dependency_error();
if ('' !== $mtoolsDependencyError) {
redirect_header(XOOPS_URL, 3, $mtoolsDependencyError);
exit;
}Quotes admin bootstrap follows the same pattern and redirects with a clear message if mTools is missing, inactive, or too old.
Do not let the first symptom be:
Class "XoopsModules\Mtools\Common\SysUtility" not found
Fail early with a message that tells the site admin what to install or update.
Install and update hooks run in contexts where preload order is not enough. They must load the consumer bootstrap and check the dependency before using mTools classes.
Quotes install hook:
require dirname(__DIR__) . '/bootstrap.php';
function xoops_module_pre_install_quotes(\XoopsModule $module)
{
$mtoolsDependencyError = quotes_mtools_dependency_error();
if ('' !== $mtoolsDependencyError) {
$module->setErrors($mtoolsDependencyError);
return false;
}
$utility = new \XoopsModules\Quotes\Utility();
$xoopsSuccess = $utility::checkVerXoops($module);
$phpSuccess = $utility::checkVerPhp($module);
return $xoopsSuccess && $phpSuccess;
}Quotes update hook does the same before it creates Utility or
Mtools\Common\Configurator.
This is the point that prevents broken partial installs.
The consumer should expose its own module class name while inheriting stable mTools behavior.
Quotes class/Utility.php:
<?php declare(strict_types=1);
namespace XoopsModules\Quotes;
use XoopsModules\Mtools;
class Utility extends Mtools\Common\SysUtility
{
// Add only module-specific methods here.
}This gives existing Quotes code a stable local name:
$utility = new Utility();
$utility::checkVerPhp($module);while the implementation comes from:
XoopsModules\Mtools\Common\SysUtilityDo not keep a copied class/Common/SysUtility.php just to avoid changing one
use statement. The thin adapter is the bridge.
Quotes uses Configurator for module configuration paths and install-time
folder setup:
$configurator = new \XoopsModules\Mtools\Common\Configurator($helper->path());
foreach ($configurator->uploadFolders as $folder) {
$utility::prepareFolder($folder);
}Use this pattern only for helpers that are documented as stable shared API.
If a helper needs the consuming module's dirname, path, template name, or URL,
pass that context explicitly. A shared helper must not infer that it is running
inside mtools.
Do not move module templates into mTools just because the module consumes mTools helpers.
Quotes keeps:
modules/quotes/templates/
modules/quotes/templates/blocks/
modules/quotes/assets/css/style.css
This is intentional. Quote cards, author photos, category visuals, block layouts, and dark-theme behavior are Quotes UI concerns.
mTools may eventually provide shared design tokens or rendering conventions, but the consumer owns its final presentation.
Blocks can run in side-column contexts where the full module page bootstrap may not have run the way you expect.
Quotes block files start defensively:
require_once dirname(__DIR__) . '/bootstrap.php';
use XoopsModules\Quotes\Helper;
if (!class_exists(Helper::class) || '' !== quotes_mtools_dependency_error()) {
return [];
}For your module, use the same idea:
- require the consumer bootstrap
- check the dependency
- return an empty block or a clear admin-visible message if the dependency is not satisfied
Do not let a side block fatal the whole page.
After the module compiles and runs with mTools:
- Search for references to the old namespace.
- Replace local common-class imports with
XoopsModules\Mtools\Common\...only when the helper is stable and documented. - Keep thin module adapters where existing module code expects local class names.
- Delete only the unused local helper files.
- Do not delete module-specific helpers.
Useful searches:
rg "class/Common|Common\\\\" modules/mymodule
rg "require_once .*mtools/preloads" modules/mymodule
rg "extends .*Common" modules/mymodule/classNo consumer class file should require mTools internals directly.
At minimum, verify these paths:
- module install fails clearly when mTools is missing or inactive
- module install succeeds when mTools is installed and active
- module update performs the same dependency check
- public index page loads
- public list/detail pages load
- admin dashboard loads
- admin blocks page saves
- blocks render without fatal errors
- PHP lint passes for edited files
For Quotes, the important smoke checks are:
php -l htdocs/modules/quotes/bootstrap.php
php -l htdocs/modules/quotes/include/mtools_dependency.php
php -l htdocs/modules/quotes/class/Utility.php
php -l htdocs/modules/quotes/include/oninstall.php
php -l htdocs/modules/quotes/include/onupdate.phpAnd representative URLs:
/modules/quotes/
/modules/quotes/quote.php
/modules/quotes/category.php
/modules/quotes/author.php
/modules/quotes/admin/index.php
/modules/quotes/admin/blocksadmin.php
Wrong:
require_once XOOPS_ROOT_PATH . '/modules/mtools/preloads/autoloader.php';inside class/Utility.php.
Right:
require __DIR__ . '/bootstrap.php';from entry points and install/update hooks.
Only Common\... plus the documented bootstrap/dependency classes are for
consumers. Check docs/architecture.md before adopting a helper.
If the code renders a quote card, category grid, author image, or module block, it belongs in the consumer module. mTools should not become a dumping ground for every template fragment.
Module CSS should use variables and theme-aware selectors where needed. Quotes owns its dark-theme card colors because Quotes owns the quote-card UI.
Never promote helpers that process posted paths, call chmod() from raw input,
redirect at file scope, or perform filesystem writes during autoload. Split
those behaviors before sharing them.
-
xoops_version.phpdeclares'min_modules' => ['mtools' => '1.1.0']. - Consumer
bootstrap.phploads the consumer autoloader and public mTools bootstrap. - Consumer has one dependency helper, e.g.
mymodule_mtools_dependency_error(). - Public/admin entry points check the dependency before using mTools-based classes.
- Install and update hooks check the dependency and return
falsewith$module->setErrors()on failure. - Thin local adapters replace copied common base classes.
- No class file requires
mtools/preloads/autoloader.php. - Only documented
Mtools\Common\...helpers are consumed. - Templates, blocks, CSS, language files, and domain handlers remain in the consumer module.
- Old local
class/Common/files are removed only after references are gone. - Public pages, admin pages, install/update hooks, and blocks are smoke tested.
- Add dependency metadata and bootstrap files.
- Add install/update dependency checks.
- Convert
Utilityto extendMtools\Common\SysUtility. - Convert one additional stable helper, such as
Configurator. - Run public/admin/block smoke checks.
- Remove unused local
class/Common/files. - Only then consider adopting more mTools helpers.
Small, verified conversions are better than a broad rewrite. Quotes is the model: it consumes mTools for shared infrastructure, while still acting like a normal, self-contained XOOPS module for its own data and UI.