Composer plugin (detain/myadmin-webhosting-module) for the MyAdmin control panel. Manages shared hosting lifecycle — provisioning, suspension, reactivation, termination — across ISPconfig and ISPmanager.
composer install # install deps including phpunit
vendor/bin/phpunit # run all tests (phpunit.xml.dist)
php -l src/api.php # syntax-check api file
php -l src/Plugin.php # syntax-check plugin class- Plugin entry:
src/Plugin.php· namespaceDetain\MyAdminWebhosting\· registers hooks viagetHooks() - API functions:
src/api.php· procedural, no namespace · exposesapi_place_buy_websiteandapi_validate_buy_website - Tests:
tests/ApiTest.php·tests/PluginTest.php· bootstrap attests/bootstrap.php - Autoload: PSR-4
Detain\MyAdminWebhosting\→src/· test PSR-4Detain\MyAdminWebhosting\Tests\→tests/ - CI/CD:
.github/workflows automate test runs and code quality checks on push and pull requests - IDE Config:
.idea/contains JetBrains project settings includinginspectionProfiles/,deployment.xml, andencodings.xml
| Constant | Value |
|---|---|
TABLE |
'websites' |
PREFIX |
'website' |
TBLNAME |
'Websites' |
TITLE_FIELD |
'website_hostname' |
TITLE_FIELD2 |
'website_username' |
src/Plugin.php registers four hooks in getHooks():
public static function getHooks() {
return [
'api.register' => [__CLASS__, 'apiRegister'],
'function.requirements' => [__CLASS__, 'getRequirements'],
self::$module.'.load_processing' => [__CLASS__, 'loadProcessing'],
self::$module.'.settings' => [__CLASS__, 'getSettings'],
];
}getRequirements() maps function names to src/api.php via add_requirement():
$loader->add_requirement('api_place_buy_website', 'src/api.php');Both functions in src/api.php follow this structure:
get_custid($GLOBALS['tf']->session->account_id, 'vps')to resolve customerfunction_requirements('validate_buy_website')to lazy-load helpers- Destructure result array from
validate_buy_website() - Return
$return['status']='ok'|'error'and$return['status_text']
Callbacks set on $service in src/Plugin.php::loadProcessing():
setEnable— set{PREFIX}_status='active', calladmin_email_website_pending_setup()setReactivate— set status active, fetchemail/admin/website_reactivated.tplviaTFSmarty, send via\MyAdmin\Mail::adminMail()setDisable— empty stubsetTerminate— dispatchwebhosting.terminatesubevent; on exception sendadmin/website_connect_error.tpl; on success callsetServerStatus('deleted')->save()
getSettings() in src/Plugin.php uses:
add_dropdown_setting()foroutofstock_webhosting*flags and limited package enableadd_text_setting()for numeric config (limits, demo days)add_master_checkbox_setting()/add_master_text_setting()/add_master_label()for per-server controls- Wrap setting default with
defined('CONST') ? CONST : fallbackpattern
tests/bootstrap.phpdefinesPRORATE_BILLINGconstant before autoloadtests/ApiTest.phpreadssrc/api.phpas a string and asserts on structure (no execution)- Assert function signatures, docblocks,
$return['status']keys,function_requirements()calls phpunit.xml.distat repo root configures test suite
- Tabs for indentation (see
.scrutinizer.ymluse_tabs: true) - camelCase for parameters and properties
src/api.phpmust have no namespace declarationsrc/api.phpmust define exactly the functions registered ingetRequirements()- Log via
myadmin_log('webhosting', $level, $message, __LINE__, __FILE__, self::$module, $id) - DB queries use
get_module_db(self::$module)— never PDO
This package is on the shared contract harness from detain/myadmin-plugin-installer.
tests/ContractTest.php is generated — run composer myadmin:scaffold-tests (add
--force --write to re-emit it), never hand-edit it.
The harness executes the plugin: it defines the bare constants the class body references
and then calls getHooks(), getSettings(), getMenu(), apiRegister() and — for
type=service packages — the activate/deactivate/change-ip/queue handlers, for real.
So do not write reflection-only tests for the plugin class. Asserting a handler exists, is public, is static and takes one parameter passes whether or not the handler works; three production bugs in this fleet were sitting behind assertions of exactly that shape. Older guidance in this repo that says those methods must not be called predates the harness.
The harness is additive: it runs alongside this package's existing tests, and nothing is
deleted to make room for it. Run the whole suite, never --filter ContractTest alone — the
contract class primes constants and calls register_module(), neither of which can be undone.
See the plugin-contract-tests skill for the full workflow, and docs/testing-harness.md in
the installer.
IMPORTANT: Before every git commit, you MUST ensure Caliber syncs agent configs with the latest code changes.
First, check if the pre-commit hook is already installed:
grep -q "caliber" .git/hooks/pre-commit 2>/dev/null && echo "hook-active" || echo "no-hook"- If hook-active: the hook handles sync automatically — just commit normally. Tell the user: "Caliber will sync your agent configs automatically via the pre-commit hook."
- If no-hook: run Caliber manually before committing:
- Tell the user: "Caliber: Syncing agent configs with your latest changes..."
- Run:
caliber refresh && git add CLAUDE.md .claude/ .cursor/ .cursorrules .github/copilot-instructions.md .github/instructions/ AGENTS.md CALIBER_LEARNINGS.md .agents/ .opencode/ 2>/dev/null - After it completes, briefly tell the user what Caliber updated. Then proceed with the commit.
If caliber is not found, tell the user: "This project uses Caliber for agent config sync. Run /setup-caliber to get set up."
Read CALIBER_LEARNINGS.md for patterns and anti-patterns learned from previous sessions.
These are auto-extracted from real tool usage — treat them as project-specific rules.
This project uses Caliber to keep AI agent configs in sync across Claude Code, Cursor, Copilot, and Codex.
Configs update automatically before each commit via caliber refresh.
If the pre-commit hook is not set up, run /setup-caliber to configure everything automatically.