solitweb/laravel-directadmin
Laravel integration for the DirectAdmin control panel API. Manage hosting accounts, domains, email, databases and more from your Laravel app with a clean, fluent interface. Includes config publishing, easy credentials setup and support for common DirectAdmin API commands.
Install and wire it up
composer require solitweb/laravel-directadmin
On Laravel 5.5+ the service provider and DirectAdmin facade are auto-discovered. On older 5.x apps, register manually:
// config/app.php
'providers' => [
Solitweb\DirectAdmin\DirectAdminServiceProvider::class,
],
'aliases' => [
'DirectAdmin' => Solitweb\DirectAdmin\Facades\DirectAdmin::class,
],
Publish the config file — this is the first thing you should read before writing any code:
php artisan vendor:publish --provider="Solitweb\DirectAdmin\DirectAdminServiceProvider"
You'll end up with config/directadmin.php (host, port, username, password/login key, HTTPS toggle). Populate it from .env:
DIRECTADMIN_HOST=panel.example.com
DIRECTADMIN_PORT=2222
DIRECTADMIN_USERNAME=reseller_account
DIRECTADMIN_PASSWORD=secret
DIRECTADMIN_HTTPS=true
Where to look first
config/directadmin.php — the auth pair here is the single most common source of "it just returns an error."DirectAdmin class behind the facade — this is a thin wrapper, so the method list is your API surface. Skim its methods before designing anything./CMD_API_*) — the package mirrors those endpoints, so when a method feels "missing," it's usually just not wrapped and you'll fall back to the generic request method.First use case: provision a hosting account
The canonical use case is a signup flow: a customer pays, and you create their DirectAdmin user + domain + mailbox in one go.
use Solitweb\DirectAdmin\Facades\DirectAdmin;
$result = DirectAdmin::createUser([
'username' => 'acme',
'email' => '[email protected]',
'password' => $plainPassword,
'domain' => 'acme.test',
'package' => 'default',
]);
if (! $result) {
// throw, roll back the order, notify ops
}
Prove this works against a staging panel with a throwaway account before wiring it into your order pipeline — the response handling is the part you want to learn early.
Wrap the facade, don't sprinkle it
The facade is convenient but exposes DirectAdmin's raw semantics (URL-encoded strings, error=0/1, numeric result codes). Put it behind your own service so the rest of the app speaks your domain language:
class HostingProvisioner
{
public function provision(Order $order): HostingAccount
{
$response = DirectAdmin::createUser([...]);
if (! $this->succeeded($response)) {
throw new ProvisioningFailed($this->message($response));
}
return HostingAccount::create([...]);
}
private function succeeded($response): bool
{
// DirectAdmin returns error=0 on success for most endpoints
return (string) ($response['error'] ?? '1') === '0';
}
}
Wrap this in an interface (HostingProvider) so tests and future migrations to cPanel/Plesk don't ripple through controllers.
Always provision inside a queued job
DirectAdmin calls are synchronous HTTP requests to a remote panel — they can take seconds and they fail. Never call them in an HTTP request that a user is staring at.
class ProvisionHostingAccount implements ShouldQueue
{
public $tries = 3;
public $backoff = [30, 120, 300];
public function handle(HostingProvisioner $provisioner)
{
$provisioner->provision($this->order);
}
public function failed(Throwable $e)
{
// mark order as needs_attention, ping ops
}
}
This gives you retries, backoff, and a failed() hook for the "customer paid but no account" scenario, which is the one that actually hurts.
Compensating rollback
DirectAdmin has no transactions. If user creation succeeds and mailbox creation fails, you must undo manually. Record each successful step and reverse them in catch:
$steps = [];
try {
DirectAdmin::createUser($userData); $steps[] = 'user';
DirectAdmin::createDatabase($dbData); $steps[] = 'db';
} catch (Throwable $e) {
foreach (array_reverse($steps) as $step) {
match ($step) {
'user' => DirectAdmin::deleteUser($username),
'db' => DirectAdmin::deleteDatabase($dbName),
};
}
throw $e;
}
Per-request credentials for multi-server setups
If you host across several DirectAdmin boxes, the single global config is a limitation. Either build a small factory that constructs the underlying client with per-server credentials (the wrapper class is instantiable directly), or keep one panel for now and note the constraint. Don't fight the config file — instantiate the class yourself where you need a second panel.
Idempotency
createUser on an existing username typically fails. Before provisioning, call the "list/show user" endpoint, or treat the "already exists" error as success. This makes your job retries safe.
Testing
There's no built-in fake, so bind a stub in the container:
$this->mock(HostingProvisioner::class, function ($mock) {
$mock->shouldReceive('provision')->once()->andReturn(new HostingAccount);
});
Keep a thin seam so tests never touch the panel.
It is not a REST client. DirectAdmin answers with URL-encoded query strings (error=0&text=...&details=...), not JSON. The wrapper parses these into arrays/objects, but the semantics leak through: success is error=0, and details is the human-readable message. Read text/details before showing anything to ops.
Error codes are numeric strings. 0 is success; 1 is generic failure; various codes signal specific conditions (auth, package missing, user exists). Log the raw response — the message is often the only clue. Build a small map of the codes you see in practice.
HTTPS + self-signed certs. Panels frequently use self-signed certs on port 2222. The wrapper will fail SSL verification. Either install a valid cert (preferred), or configure the client to skip verification for this host only. Don't globally disable TLS verification in your app.
Port 2222 must be reachable from your app server. This is the #1 ops issue after credentials. If calls work locally but hang in production, it's a firewall/security-group rule, not a code bug. DirectAdmin also supports a custom endpoint — check the config.
Use a login key, not the password. DirectAdmin supports API login keys scoped to specific commands. Create a key with only the CMD_API_* commands you need and use it for DIRECTADMIN_PASSWORD. Rotating a key is painless; rotating the reseller password is not.
Config caching bites. If you change panel credentials in .env, run php artisan config:clear. A stale bootstrap/cache/config.php will silently keep the old host, which looks like "the package is broken."
Special characters in credentials. Passwords with &, +, or =, or spaces, break form-encoded auth. Either quote them in .env (DIRECTADMIN_PASSWORD="p&ss") or just use a login key with an alphanumeric token.
No rate limiting, no built-in queue. Bulk operations (migrating 500 accounts) will slam the panel. Throttle in your job ($this->release(2) or RateLimited middleware) and set a sane retry_after.
It's a thin wrapper, not a full SDK. Methods map roughly to CMD_API_* endpoints; missing features are best handled by constructing a raw request against the panel rather than forking the package. If you find yourself needing five unwrapped endpoints, subclass the DirectAdmin class and bind your subclass in a service provider — that's the cleanest extension point.
Laravel 5 in the name. Check the package's compatibility before relying on it in a Laravel 9/10/11 app. If it targets illuminate/support ^5.x, you'll either need --ignore-platform-reqs (risky) or to treat the package as a reference implementation and copy the ~200-line client into your own codebase. Given the small size, the latter is often the pragmatic move.
Don't leak panel errors to end users. DirectAdmin's details can contain server paths and internal usernames. Map to a friendly message ("We couldn't set up your hosting; our team has been notified") and log the raw response with the order ID attached.
How can I help you explore Laravel packages today?