Server Extensions

Build a provisioning integration that creates, suspends, unsuspends and terminates services on an external platform.

A server extension connects WemX to a platform such as a game panel, a hypervisor or a hosting control panel. WemX calls it whenever an order needs to be provisioned or changes state.

Minimal example

extensions/Servers/Acme/Server.php
<?php

namespace Extensions\Servers\Acme;

use App\Extensions\Foundation\ServerExtension;
use App\Models\Order;
use App\Models\Package;
use App\Models\ServerConnection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class Server extends ServerExtension
{
    protected string $id = 'server-acme';
    protected string $name = 'Acme Cloud';
    protected string $description = 'Provision Acme Cloud instances.';
    protected string $type = 'Server';
    protected string $version = '1.0.0';
    protected array $wemxVersions = ['*'];
    protected array $authors = [['name' => 'You', 'email' => 'you@example.com']];

    /** Fields for the server connection form (credentials). */
    public function setConfig(): array
    {
        return [
            ['key' => 'url', 'name' => 'API URL', 'description' => 'e.g. https://api.acme.cloud', 'type' => 'url', 'rules' => ['required', 'url']],
            ['key' => 'api_key', 'name' => 'API Key', 'type' => 'password', 'rules' => ['required', 'string']],
        ];
    }

    /** Fields for each package's "Server" tab. */
    public function setPackageConfig(Package $package, ServerConnection $connection): array
    {
        return [
            ['key' => 'plan', 'name' => 'Plan', 'type' => 'select', 'options' => ['small' => 'Small', 'large' => 'Large'], 'rules' => ['required']],
            ['key' => 'memory', 'name' => 'Memory (MB)', 'type' => 'number', 'default_value' => 1024, 'rules' => ['required', 'integer'], 'is_configurable' => true],
            ['key' => 'region', 'name' => 'Region', 'type' => 'text', 'default_value' => 'eu', 'rules' => ['required'], 'is_configurable' => true],
        ];
    }

    /** Optional: verify credentials when the connection is saved and every 5 minutes. */
    public function testConnection(array $config): bool
    {
        Http::withToken($config['api_key'])->get($config['url'].'/ping')->throw();

        return true;
    }

    public function create(Order $order, ServerConnection $connection)
    {
        $response = $this->api($connection)->post('/instances', [
            'plan' => $order->option('plan'),
            'memory' => $order->option('memory'),   // customer's choice if configurable, else package value
            'region' => $order->option('region'),
            'owner_email' => $order->user->email,
        ])->throw()->json();

        $order->update(['external_id' => $response['id']]);

        $order->createExternalUser([
            'external_id' => $response['user_id'],
            'username' => $response['username'],
            'password' => $response['password'],   // stored encrypted, shown to the customer
        ]);
    }

    public function suspend(Order $order, ServerConnection $connection)
    {
        $this->api($connection)->post("/instances/{$order->external_id}/suspend")->throw();
    }

    public function unsuspend(Order $order, ServerConnection $connection)
    {
        $this->api($connection)->post("/instances/{$order->external_id}/unsuspend")->throw();
    }

    public function terminate(Order $order, ServerConnection $connection)
    {
        $this->api($connection)->delete("/instances/{$order->external_id}")->throw();
    }

    private function api(ServerConnection $connection)
    {
        return Http::baseUrl($connection->config['url'])
            ->withToken($connection->config['api_key'])
            ->acceptJson();
    }
}

Required methods

MethodPurpose
setConfig(): arrayFields for the server connection form. Values are stored encrypted in $connection->config.
setPackageConfig(Package $package, ServerConnection $connection): arrayFields for the package's Server tab. Values are stored in $package->data. You can call the platform's API here to build dynamic option lists, such as available nodes or images.
create(Order $order, ServerConnection $connection)Provision the service.
suspend(…) / unsuspend(…) / terminate(…)Change the service's state.

Throw an exception on failure. The provisioning job catches it, stores it in the order's Incident logs, fires the matching error event, and for create sets the order to failed. Staff can retry from the admin panel once the cause is fixed. Don't swallow errors: if a method returns normally, WemX treats the action as successful.

Config field schema

Both setConfig() and setPackageConfig() return a list of fields:

KeyRequiredDescription
key✓Storage key. Dot notation is allowed (e.g. environment.VERSION).
name✓Label shown to the admin.
type✓text, number, password, email, url, textarea or select.
rules✓Laravel validation rules (array or string).
descriptionHelp text.
optionsfor selectvalue => label array.
default_valuePre-filled value.
colBootstrap column width on the package form, e.g. col-6.
is_configurablePackage fields only. If true, admins can offer this field to customers as a configurable option.

Reading values in your methods

$connection->config['api_key'];        // server connection credentials
$order->package->data('plan');         // package server setting
$order->option('memory');              // configurable option chosen by the customer,
                                       // falling back to the package setting
$order->option('environment');         // all keys under "environment.*" as a nested array
$order->user;                          // the customer
$order->external_id;                   // your remote ID, saved during create()

Customer accounts on the platform

Many platforms need a user account per customer. Use the helpers on Order:

if (! $order->hasExternalUser()) {
    $order->createExternalUser([
        'external_id' => $remoteUserId,
        'username' => $username,
        'password' => $password,   // encrypted at rest
        'data' => ['panel_url' => $url],
    ]);
}

$account = $order->getExternalUser();   // ServerAccount for this order, else the user's account on this extension

A ServerAccount is reused across a customer's orders on the same extension, so create the remote user only once.

Optional capabilities

Implement these methods to unlock more features. WemX detects them automatically:

MethodUnlocks
testConnection(array $config)The Test Connection button, health monitoring every 5 minutes, alerts and Prevent Purchasing. Return true, or throw (or return false) on failure.
upgradeOrDowngrade(Order $order, PackagePrice $old, PackagePrice $new, ServerConnection $connection)The admin Upgrade/Downgrade action. Resize the service to the new package.
changePassword(Order $order, string $newPassword)Lets customers change their panel password from the order page.
eventAddToCart(Package $package, array $configOptions)Runs before a package is added to the cart. Throw to block it, for example when a region is out of capacity.

Showing information to customers and staff

Use UI elements to add views to the order page, for example a Login to panel button or live server stats:

public function elements(): array
{
    return [
        ['element' => 'client-order-top-view', 'view' => 'server-acme::client.order-panel'],
        ['element' => 'admin-order-sidebar-view', 'view' => 'server-acme::admin.order-info'],
    ];
}

These views receive $order.

Emails

Send an email to the customer with $user->email(). For a one-off message, pass the subject and lines directly:

$order->user->email([
    'subject' => 'Your Acme instance is ready',
    'lines' => [
        "Your instance for order #{$order->id} has been created.",
        "Username: **{$username}**",
    ],
    'button' => ['text' => 'Open panel', 'url' => $panelUrl],
    'mailable_type' => \App\Models\Order::class,
    'mailable_id' => $order->id,
]);

To make the email editable by admins under Emails → Templates, register a template definition from your extension's service provider, then send it by identifier:

// in a service provider returned from providers()
public function boot(): void
{
    // Template identifiers contain dots, so merge the array instead of using config('a.b.c')
    config(['email_events' => array_merge(config('email_events', []), [
        'server.acme.created' => [
            'name' => 'Acme instance created',
            'group' => 'Servers',
            'description' => 'Sent when an Acme instance is provisioned.',
            'subject' => 'Your {{package_name}} instance is ready',
            'body' => "Your instance has been created.\nUsername: **{{username}}**",
            'button_text' => 'Open panel',
            'placeholders' => ['package_name' => 'Package name', 'username' => 'Panel username'],
        ],
    ])]);
}

// in create()
$order->user->email([
    'identifier' => 'server.acme.created',
    'variables' => ['package_name' => $order->package->name, 'username' => $username],
    'button' => ['url' => $panelUrl],
]);

Testing your extension

  1. Run php artisan extension:discover and enable it under Admin → Servers → Installed Servers.
  2. Create a server connection and test it.
  3. Create a package on that connection and fill in its Server tab.
  4. Buy it with the Sandbox gateway, and watch storage/logs and the order's Incident logs.
  5. Use the order's admin actions (suspend, unsuspend, terminate) to check the other methods.

On this page