PHP plugin selling DirectAdmin licenses as a VPS addon in MyAdmin billing.
- Namespace:
Detain\MyAdminVpsDirectadmin→src/ - Test Namespace:
Detain\MyAdminVpsDirectadmin\Tests→tests/ - Module:
vps· Type:addon - Key deps:
symfony/event-dispatcher ^5.0
composer install
vendor/bin/phpunit tests/ -v
vendor/bin/phpunit tests/ --coverage-clover coverage.xml --whitelist src/Config: phpunit.xml.dist
CI/CD: .github/ contains workflows/tests.yml for automated testing pipelines. .idea/ contains IDE configuration including inspectionProfiles/Project_Default.xml, deployment.xml, and encodings.xml.
Plugin class: src/Plugin.php → Detain\MyAdminVpsDirectadmin\Plugin
Addon function: src/vps_add_directadmin.php → vps_add_directadmin()
Hooks registered in Plugin::getHooks():
function.requirements→getRequirements()— registersvps_add_directadminpage requirementvps.load_addons→getAddon()— registersAddonHandlerwith costVPS_DA_COSTvps.settings→getSettings()— addsvps_da_costtext setting to admin panel
Hook registration pattern:
public static function getHooks()
{
return [
'function.requirements' => [__CLASS__, 'getRequirements'],
self::$module.'.load_addons' => [__CLASS__, 'getAddon'],
self::$module.'.settings' => [__CLASS__, 'getSettings'],
];
}Lifecycle:
doEnable(\ServiceHandler, $repeatInvoiceId)— callsactivate_directadmin()afterfunction_requirements('activate_directadmin')doDisable(\ServiceHandler, $repeatInvoiceId)— callsdeactivate_directadmin(), sends admin email via\MyAdmin\Mail()->adminMail()usingadmin/vps_da_canceled.tpl
MyAdmin helpers:
get_module_settings(self::$module)→PREFIX,TABLE,TBLNAMEmyadmin_log($module, 'info', $msg, __LINE__, __FILE__, $module, $serviceId)function_requirements('name')— lazy-loads function/class filesrun_event('parse_service_extra', $extra, $module)— hook dispatchvps_get_password($id, $custid)— get VPS root passworddirectadmin_get_best_type($module, $type, $serviceInfo, $serviceExtra)
License functions: require_once __DIR__.'/../../../../include/licenses/license.functions.inc.php'
- Tabs for indentation (enforced in
.scrutinizer.yml) - camelCase properties and parameters;
UPPERCASEconstants (e.g.VPS_DA_COST) - All logging via
myadmin_log()with__LINE__, __FILE__ - Load external functions via
function_requirements(), not directrequire_onceexcept license functions - Static class methods for all plugin handlers; constructor is empty
tests/PluginTest.php— plugin class structuretests/VpsAddDirectadminTest.php— source-reading tests onsrc/vps_add_directadmin.php- Pattern:
file_get_contents()+assertStringContains/assertMatchesRegularExpressionon raw PHP source - PHPUnit 9
vendor/bin/phpunit tests/PluginTest.php -v
vendor/bin/phpunit tests/VpsAddDirectadminTest.php -vThis 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 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.