Composer plugin package detain/myadmin-vps-module — VPS provisioning, lifecycle management, slice-based scaling, and SOAP/REST API for the MyAdmin billing platform.
composer install # install deps including phpunit
composer test # run all tests (config: phpunit.xml.dist)
composer test:unit # run unit tests only- Plugin class:
src/Plugin.php—Detain\MyAdminVps\Plugin· registers hooks, API endpoints, settings, addon handlers - API functions:
src/api.php— procedural functions loaded on-demand viafunction_requirements() - CLI utility:
bin/check_vlan_ips.php— validates VLAN IP ranges againstvps_ips/qs_ipstables - Tests:
tests/ApiFunctionsTest.php·tests/PluginTest.php· bootstrap stubs intests/bootstrap.php - Autoload:
Detain\MyAdminVps\→src/·Detain\MyAdminVps\Tests\→tests/ - CI/CD:
.github/— containsworkflows/with automated test and deployment pipelines - IDE config:
.idea/— JetBrains project files includinginspectionProfiles/,deployment.xml,encodings.xml
Plugin::getHooks() returns hook map consumed by MyAdmin's EventDispatcher:
public static function getHooks() {
return [
'api.register' => [__CLASS__, 'apiRegister'],
'function.requirements' => [__CLASS__, 'getRequirements'],
self::$module.'.load_processing' => [__CLASS__, 'loadProcessing'],
self::$module.'.load_addons' => [__CLASS__, 'getAddon'],
self::$module.'.settings' => [__CLASS__, 'getSettings'],
];
}Plugin::getRequirements() lazy-loads src/api.php functions:
public static function getRequirements(GenericEvent $event) {
$loader = $event->getSubject();
$loader->add_requirement('api_validate_buy_vps', 'src/api.php');
$loader->add_requirement('api_buy_vps', 'src/api.php');
}All public API functions follow validate → place flow:
function api_buy_vps($os, $slices, $platform, $controlpanel, $period,
$location, $version, $hostname, $coupon, $rootpass,
$comment = '', $ipv6only = false) {
$custid = get_custid($GLOBALS['tf']->session->account_id, 'vps');
function_requirements('validate_buy_vps');
$validation = validate_buy_vps($custid, ...);
if ($validation['continue'] === true) {
function_requirements('place_buy_vps');
$order_response = place_buy_vps(...);
$return['status'] = 'ok';
$return['invoices'] = implode(',', $order_response['real_iids']);
} else {
$return['status'] = 'error';
$return['status_text'] = implode("\n", $validation['errors']);
}
return $return;
}Return arrays always include: status, status_text, invoices, cost.
Closures passed to setEnable/setReactivate/setDisable/setTerminate:
->setTerminate(function ($service) {
$serviceInfo = $service->getServiceInfo();
$settings = get_module_settings(self::$module); // PREFIX, TABLE, TBLNAME
$db = get_module_db(self::$module);
$db->query("update {$settings['TABLE']} set ...", __LINE__, __FILE__);
myadmin_log(self::$module, 'info', 'message', __LINE__, __FILE__, self::$module, $id);
$GLOBALS['tf']->history->add(self::$module.'queue', $id, 'destroy', '', $custid);
})Key constants used throughout: PREFIX = 'vps' · TABLE = 'vps' · TBLNAME = 'VPS' · TITLE_FIELD = 'vps_hostname' · TITLE_FIELD2 = 'vps_ip'
Access pattern: $settings['PREFIX'].'_id' → vps_id, $settings['TABLE'] → vps.
api_register_array('buy_vps_result_status', [
'status' => 'string', 'status_text' => 'string',
'invoices' => 'string', 'cost' => 'float'
]);
api_register('api_buy_vps', ['os'=>'string',...], ['return'=>'buy_vps_result_status'], 'desc');- Config:
phpunit.xml.dist· Bootstrap:tests/bootstrap.php - Use
ReflectionFunctionto assert parameter names/counts without calling functions tests/bootstrap.phpstubs:myadmin_log,function_requirements,get_module_db,get_module_settings,run_event,validate_buy_vps,place_buy_vps,$GLOBALS['tf']- Tests extend
PHPUnit\Framework\TestCaseunder namespaceDetain\MyAdminVps\Tests
- Tabs for indentation (enforced by
.scrutinizer.yml) camelCasefor parameters and propertiesUPPERCASEfor constants- Always pass
__LINE__, __FILE__as last args to$db->query()andmyadmin_log() - Admin-only operations: gate with
$GLOBALS['tf']->ima != 'admin' - Cast user-supplied server IDs:
$server = (int)$server - IP validation: use
validIp($ip)before acting on IP strings - Before committing:
caliber refresh && git add CLAUDE.md .claude/
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.