# Sunucu Modülü Yazma

https://dev.wisecp.com/tr/sunucu-modulu-yazma

Sunucu modülü, ödenmiş bir siparişi hosting panelinde ya da bulut sağlayıcısında gerçek bir hesaba dönüştürür. Çekirdek yaşam döngüsü metotlarınızı adıyla çağırır; döndürdüğünüz şey hizmetin üzerine yazılır.

## Genel Bakış

Servers en büyük modül tipidir: 50 modül, üçü paylaşımlı hosting, özel makine ve sanallaştırma için kum havuzu örneği.

Sınıfınız `ServerModule`'ü genişletir, o da `ModuleBaseTrait`'i içeri alır. İkisi birlikte sunucuyu, hizmeti, ürünü, alıcıyı, çözümlenmiş limitleri, eklenti cevaplarını ve araç düzeneğini zaten tutar.

Çekirdek sınıfınızı hiç kurmaz: örneği fabrikadan çözer ve fiili adıyla çağırır. Yazmadığınız bir fiil atlanır; tipin isteğe-bağlılık mekanizması budur.

- **Services::run_module()**: Tek kapı: örneği kurar, takma adları çözer, metodu çalıştırır, sonucu uygular.
- **Services::instance_module()**: Modül tipini hizmetten seçer, sonra fabrikayı çağırıp hizmeti ve siparişi doldurur. hosting ve server Servers'a gider.
- **Modules::getInstance()**: Kanonik fabrika: yapılandırma, dil, örnek önbelleği. Bir modül için `new` asla kullanılmaz.
- **ModuleQueue**: Yeniden deneyen arka plan işçisi: aynı kapı, sonra aksiyonun gerektirdiği durum.

## Ön Koşullar

- Önce [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi) ve [Modül Yapılandırması](https://dev.wisecp.com/tr/modul-yapilandirmasi); bu makale yalnızca Servers tipini anlatır.
- API erişimi olan bir sağlayıcı hesabı ve bağlantı testini geçen bir sunucu kaydı.
- O sunucuya bağlı bir test ürünü; yoksa `create()` okuyacak bir plan bulamaz.
- Hata, `false` döndürerek değil fırlatılarak bildirilir.

## Yapı

Her modül için tek bir dizin, içindeki sınıfla birebir aynı adda.

```bash
coremio/modules/Servers/Acme/
├── Acme.php            sınıf: extends ServerModule
├── ApiClient.php       kendi HTTP sarmalayıcınız, düz new, WISECP modülü değil
├── config.php          künye, sunucu form alanları, desteklenen kartlar ve araçlar
├── logo.png            modül seçicide görünür
├── lang/en.php         $this->lang, dil başına bir dosya
└── pages/              get_page() ile render edilen isteğe bağlı şablonlar
```

```php
namespace WISECP\Modules\Servers;

use Exception;
use Language;
use ServerModule;

class Acme extends ServerModule
{
    private ApiClient $api;

    // set_server() tarafından çağrılır; onu hem yapıcı hem set_service() koşar,
    // $this->server dolduktan ve sırları çözüldükten sonra.
    protected function define_server_info(array $server = []): void
    {
        include_once __DIR__ . DS . 'ApiClient.php';
        $this->api = new ApiClient($server);
    }
}
```

| Ekran | Sınıfınızdaki giriş noktası | Nereden ulaşılır |
| --- | --- | --- |
| Sunucu ayar formu | `config.php` alanları, ardından `test_connect()` | Admin, Sunucular, Sunucu Yönet |
| Ürün ayarları | `product_configuration()`, `save_product_configuration()` | Admin, ürün detayı, Modül sekmesi |
| Sağlama | `create()`, `suspend()`, `unsuspend()`, `cancel()` | Sipariş onayı, admin aksiyonları, kuyruk |
| Müşteri paneli panosu | `dashboard_data()` | Müşteri panelindeki hizmet detayı |
| Müşteri paneli araçları | `tool_data()`, `tool_action()` | Araç kenar çubuğu, araçlar makalesine bakın |
| Metrik faturalama | `metrics_usage()` ya da `metrics_usage_bulk()` | Kullanım toplama cron'u |
| İçe aktarma | `list()` | Admin, panelden hesap içe aktarma |

## Adım Adım

### Katmanları Bu Sırayla Yazın

Yaşam döngüsüyle başlamak boşa iştir. `create()`, alıcının seçimlerini hizmet seçeneklerinden okur ve o seçenekler ancak ürün formu tanımlıysa oluşur.

1. `define_server_info()`, ardından `test_connect()`: sunucuyu ekleyin, kimlik bilgilerini kanıtlayın.
2. `product_configuration()` ve `save_product_configuration()`, artı plan listesini dolduran çağrılabilir metot.
3. `create()`, sonra `suspend()`, `unsuspend()`, `cancel()`.
4. `change_password()` ve `upgrade()`.
5. `list()`, `dashboard_data()`, tek oturum açma metotları, metrikler.
6. `configure_features()` ve araçlar.

Her katmanı bir sonrakine geçmeden canlı sağlayıcıya karşı doğrulayın: sonda çıkan ilk hatanın altı olası nedeni olur.

### Sağlayıcıya Bağlanma

Kimlik bilgileri sunucu kaydından gelir. Hangi alanların görüneceğine `config.php` karar verir; taban sınıf onları çözülmüş teslim eder.

```php
return [
    'name'                => 'Acme Cloud',

    // 'hosting' = alan adı tabanlı hesaplar, 'server' = VPS ya da özel makineler.
    'type'                => 'hosting',

    // Uzun bir API jetonu parola alanına değil erişim anahtarı alanına yazılır:
    // servers.password bir varchar'dır ve şifrelenmiş uzun jeton oraya sığmaz.
    'use-access-hash'     => true,
    'require-access-hash' => true,

    'use-test-connection' => true,
    'use-port'            => true,
    'not-secure-port'     => 2082,
    'secure-port'         => 2083,

    // Ek alanlar $server['fields'] içine düşer. 'crypt' değeri şifreli saklar.
    'fields' => [
        'region'    => ['type' => 'text', 'name' => '{lang.region}', 'col_class' => 'col-md-6'],
        'sub_token' => ['type' => 'password', 'name' => '{lang.sub_token}', 'crypt' => true],
    ],

    // Hesabı sağlayıcı tarafında hangi hizmet seçeneğinin tanımladığı.
    'service-relationship' => 'domain',
];
```

> **Parola zaten çözülmüş gelir**
> 
> Taban sınıf `$server['password']` değerini tek bir noktada çözer; `define_server_info()` metoduna her zaman düz metin ulaşır. Yeniden çözerseniz API bozulmuş bir sır alır.

### Ürün Alanları ve Plan Açılır Listesi

`product_configuration()` bir alan tanım haritası döndürür. Yöneticinin seçtiği şey ürünün modül verisi olarak kaydedilir ve sağlama kodunuza `$this->options['creation_info']` içinde ulaşır.

Seçenek değeri olarak planın **adını** yazın, sağlayıcının sayısal kimliğini değil; adı kimliğe `create()` içinde çevirin.

### Yaşam Döngüsü Gövdesi

Her sağlama fiili aynı üç vuruşu izler: alıcının seçtiğini oku, sağlayıcıyı çağır, saklanacağı döndür. Kalıcılaştırmanın yolu dizi döndürmektir.

1. Planı `creation_info` içinden, limitleri `get_limit()` ile, eklenti cevaplarını `addon_params`, sipariş formu cevaplarını `requirement_params` içinden okuyun.
2. Sağlayıcıyı çağırın. API hatasında istemcinin fırlatmasına izin verin ya da çevrilmiş bir mesajla kendiniz fırlatın.
3. `['config' => [...]]` döndürün; çekirdek bunu hizmet seçeneklerine birleştirir ve sonraki her fiil hesabı oradan okur.

## Referans

### Yaşam Döngüsü İmzaları

Hiçbiri taban sınıfta soyut bildirilmez. Çekirdek her birini `method_exists()` ile yoklar ve olmayanı atlar; sözleşme imzanın kendisidir.

```php
// Hazırlık. define_server_info, diğer her fiilden önce set_server() içinden koşar.
protected function define_server_info(array $server = []): void;
public function test_connect(): array|bool;
public function configure_features(): void;

// Ürün ve hizmet formları. İki save metodu da değerleri REFERANSLA alır.
public function product_configuration(array $data = []): array;
public function save_product_configuration(array &$values): void;
public function service_configuration(): array;
public function save_service_configuration(array &$values): void;

// Sağlama.
public function create(): array|bool;
public function suspend(): bool;
public function unsuspend(): bool;
public function cancel(): bool;
public function renew(): bool;
public function upgrade(array $new_product = []): bool;
public function change_password(string $password): bool;
public function change_limits(array $limits): bool;
public function reset_limits(): array|bool;

// Eklentiler. $addon, hizmetin eklenti tablosundaki bir satırdır.
public function addon_create(array $addon = []): array|bool;
public function addon_suspend(array $addon = []): array|bool;
public function addon_unsuspend(array $addon = []): array|bool;
public function addon_cancel(array $addon = []): array|bool;
public function addon_upgrade(array $addon, array $new_addon): array|bool;

// Müşteri paneli.
public function dashboard_data(): array;
public function tool_data(string $tool, string $action = 'index', array $params = []): array;
public function tool_action(string $tool, string $action, array $data = []): array;
public function sso_panel_login(): string;
public function sso_root_panel_login(): string;

// Metrik faturalama ve içe aktarma.
public function metrics_usage(): array;
public function metrics_usage_bulk(array $services = []): array;
public function metric_enable(array $metric): void;
public function metric_disable(array $metric): void;
public function list(bool $rCount = false, array $filters = [], array $orders = [], int $start = 0, int $end = -1): array|int;
```

| Metot | Zorunlu | Ne zaman çağrılır | Sonraki durum |
| --- | --- | --- | --- |
| define_server_info | evet | Her örnekleme | yok |
| test_connect | evet | Sunucu formunda Bağlantıyı Test Et | yok |
| product_configuration | evet | Ürün detayı, Modül sekmesi | yok |
| create | evet | Sipariş onaylandığında ya da adminde Yeniden Oluştur | active |
| suspend | evet | Vadesi geçen fatura ya da elle askıya alma | suspended |
| unsuspend | evet | Ödeme alındığında ya da elle geri açma | active |
| cancel | evet | İptal işlendiğinde | cancelled |
| change_password | evet | Müşteri ya da yönetici hesap parolasını değiştirdiğinde | değişmez |
| upgrade | evet | Aynı modülde kalan plan değişikliğinde | değişmez |
| renew | isteğe bağlı | Yenileme faturası ödendiğinde, vade zaten ilerlemişken | değişmez |
| change_limits, reset_limits | isteğe bağlı | Yalnız sağlayıcı hesap başına limit ezmeye izin veriyorsa | değişmez |
| addon_* | isteğe bağlı | Hizmetteki bir eklenti durum değiştirdiğinde | yalnız eklenti |
| list | isteğe bağlı | Yönetici Hesapları İçe Aktar ekranını açar; o ekranı görünür kılan şey bu metottur | yok |
| metric_enable, metric_disable | isteğe bağlı | Yalnız metrik faturalama artı sağlayıcı tarafı limit ezme varsa | yok |

### create() Ne Döndürebilir

Sade bir başarı için `true`, aksi halde bir dizi döndürün. Dizinin her anahtarı bir talimat sayılır; ayrılmış olanlar, üzerine yazmak yerine hizmet seçeneklerine özyinelemeli olarak birleştirilir.

- **config**: Hizmet seçeneklerinde `config` altına birleştirilir. Hesap kimliği burada yaşar: `user`, şifrelenmiş `password`, `home_dir`, sağlayıcı tarafındaki kimlik.
- **login**: `login` altına birleştirilir. Müşteri panelinin gösterdiği ya da tek oturum açmanın kullandığı kimlik bilgileri.
- **creation_info**: `creation_info` altına, yani ürün formunun yazdığı torbaya birleştirilir. Gerçekten neyin sağlandığını kaydetmek için kullanın.
- **options**: Seçenek köküne birleştirilir. Yukarıdaki dört anahtarın kapsamadığı her şey için kaçış kapısı.
- **status**: Kuyruk tarafından tüketilir, hiç saklanmaz. `'inprocess'` ya da `'waiting'`, eşzamansız sağlama biterken hizmeti aktif durumdan uzak tutar.
- **diğer her anahtar**: Seçenek köküne olduğu gibi yazılır. `hostname`, `ip` ya da `ftp_info` iç içe koyulmadan böyle saklanır.

### Elinizde Hazır Duran Durum

Hepsi metodunuz koşmadan önce, yapıcı ve `set_service()` tarafından doldurulur. Hiçbiri için sorgu atmayın.

- **$this->server**: Sunucu satırı: ip, hostname, kullanıcı adı, çözülmüş parola, erişim anahtarı ve yapılandırmanızdan gelen `fields`.
- **$this->service, $this->product, $this->user, $this->order**: Hizmet satırı, ürünü, satın alan müşteri ve kaynak sipariş; hepsi düz dizi olarak.
- **$this->options ve save_options()**: Hizmet seçenekleri canlı bir dizi olarak. Değiştirin, sonra metot ortasında kalıcılaştırmak için `$this->save_options()` çağırın; sonda dizi döndürmek de aynı işi yapar.
- **get_limit(string $key): mixed**: Çözümlenmiş tek bir kaynak limiti; hizmet seviyesi ürün seviyesini ezer. Anahtarlar: `disk_limit`, `bandwidth_limit`, `email_limit`, `database_limit`, `addons_limit`, `subdomain_limit`, `ftp_limit`, `park_limit`, `max_email_per_hour`.
- **$this->addon_params, $this->addon_params_by_id**: Aktif eklentilerin birleşik toplamları ve aynı değerlerin eklenti satırı kimliğine göre ayrılmış hali. Anahtarlar `addon-params` altında bildirilenlerdir.
- **$this->requirement_params**: Alıcının sipariş formunda verdiği cevaplar; `requirement-params` altında bildirilen adlarla anahtarlanır.
- **encode_str(), decode_str()**: Bir sırrı seçeneklere yazmadan önce şifreleyin, sağlayıcıya göndermeden önce çözün. Panel parolası hiçbir zaman açık saklanmaz.
- **username_generator(string|int|null $domain): string**: Statik. Alan adından panele uygun bir kullanıcı adı türetir. Boş alan adı boş kullanıcı adı verir, sağlayıcı da onu reddeder.

### Çağrının Çekirdek Tarafı

```php
public static function run_module(int|array $service, string $action, array $params = []): mixed;
public static function instance_module(int|array $service): ?object;
```

```php
Services::run_module($id, 'create');                       // argüman yok
Services::run_module($id, 'change_password', [$password]); // tek konumsal dize
Services::run_module($service, 'upgrade', [$newProduct]);  // yeni ürün dizisi
Services::run_module($service, 'addon_create', [$addon]);  // tek eklenti satırı
```

- **null döner**: Sınıfınızda böyle bir metot yok. Kuyruk "modül metodu bulunamadı" kaydı düşer ve aksiyon sessizce başarılı sayılmak yerine başarısız olur.
- **false döner**: Modül reddetti. Kuyruk öğeyi başarısız işaretler ve yeniden dener.
- **terminate, cancel'a çözülür**: `terminate()` yoksa çekirdek önce `cancel()`, sonra `cancelled()` dener. `cancel()` yazarsanız iki yol da çalışır.
- **gate:service.module_action**: Metodunuzdan önce çalışır. Boş olmayan bir dize döndüren dinleyici aksiyonu o mesajla veto eder.
- **filter:service.module_result**: Sonuç uygulanmadan önce referansla çalışır; dinleyici neyin saklanacağını yeniden yazabilir.
- **action:service.module_ran**: Sonuç uygulandıktan sonra çalışır: hizmet, örnek, aksiyon, sonuç ve hata ile. Dönüş kullanılmaz.

## Örnek

Eksiksiz bir sağlama metodu ve hemen ardından dönüş değerini geri okuyan çekirdek kodu. İki yarı birbirine aittir: döndürdüğünüz anahtarlar çekirdeğin birleştirdiği anahtarlardır ve sonraki fiillerinizin okuduğu her şey aynı torbadan çıkar.

```php
public function create(): array|bool
{
    $domain = $this->options['domain'] ?? '';
    if (!$domain) throw new Exception($this->lang['error-domain-required']);

    // Yeniden sağlama: yeni bir kimlik üretmek yerine önceki koşudakini yeniden kullanın.
    $username = $this->options['config']['user'] ?? '';
    if (!$username) $username = self::username_generator($domain);

    $password = ($this->options['config']['password'] ?? '') !== ''
        ? $this->decode_str($this->options['config']['password'])
        : Utility::generate_hash(12);

    $creation = $this->options['creation_info'] ?? [];

    $parameters = [
        'username'  => $username,
        'password'  => $password,
        'domain'    => $domain,

        // Seçenek değerleri plan ADINI taşır, bu yüzden onu bu sunucuda çözün.
        'plan_id'   => $this->resolve_plan_id($creation['plan'] ?? ''),

        'disk'      => $this->get_limit('disk_limit'),
        'bandwidth' => $this->get_limit('bandwidth_limit'),

        // Anahtar alanı "0" ya da "1" dizesi gönderir; empty("0") true'dur, onun yerine cast edin.
        'shell'     => (int) ($creation['shell_access'] ?? 0) === 1,
    ];

    // Sipariş formu cevapları ve eklenti toplamları; config.php'de bildirilen adlarla anahtarlanır.
    foreach ($this->requirement_params as $key => $value) $parameters[$key] = $value;
    foreach ($this->addon_params as $key => $value) $parameters[$key] = $value;

    // Yeniden deneme güvenliği: kuyruk bu metodu bir hatadan sonra tekrar koşar.
    if (!$this->api->account_exists($username))
        $this->api->call('accounts', $parameters, 'POST');

    return [
        'config' => [
            'user'     => $username,
            'password' => $this->encode_str($password),
            'home_dir' => '/home/' . $username,
        ],
        // Ayrılmış bir anahtar değil, bu yüzden seçenek köküne yazılır.
        'ip' => $this->server['ip'] ?? '',
    ];
}
```

```php
// Services::apply_module_result, sizi ilgilendiren kısma indirgenmiş hali.
if (is_array($result)) {
    if (isset($result['config']) && is_array($result['config']))
        $options['config'] = array_replace_recursive($options['config'] ?? [], $result['config']);

    // Ayrılmış kümenin dışında kalan her şey seçenek köküne düşer.
    $reserved = ['config', 'login', 'creation_info', 'options', 'status'];
    foreach ($result as $rKey => $rVal)
        if (!in_array($rKey, $reserved, true)) $options[$rKey] = $rVal;
}

// Sonra ModuleQueue yeni hizmet durumuna karar verir.
$moduleStatus = is_array($result) ? ($result['status'] ?? null) : null;
$targetStatus = $moduleStatus ?: match ($action) {
    'create', 'unsuspend', 'register' => 'active',
    'suspend'                          => 'suspended',
    'cancel'                           => 'cancelled',
    default                            => null,
};

// Ve sonraki her fiilin okuduğu şey budur:
$username = $this->options['config']['user'] ?? '';
$password = $this->decode_str($this->options['config']['password'] ?? '');
```

## Tuzaklar

> **Hatayı fırlatarak bildirin, hata dizesiyle false döndürerek değil**
> 
> Hata özelliğine atama yapıp `false` döndürmek önceki neslin kalıntısıdır. cPanel 41 yerde fırlatıyor, hiçbir yerde atamıyor. Çevrilmiş bir exception fırlatın; çağıran taraf mesajı gösterir.

> **Kuyruk create() metodunu iki kez koşacak**
> 
> Hesap oluştuktan sonraki bir hata baştan yeniden denenir ve kopyayı reddeden bir sağlayıcı bundan sonra hep başarısız olur. Önce hesabı denetleyin ve kimlik bilgilerini elinize geçer geçmez `save_options()` ile yazın ki ikinci koşu onları bulabilsin.

> **Plan adını saklayın, sağlayıcı kimliğini değil**
> 
> Yük dengeli bir sunucu grubunda sipariş yeri olan makineye düşer ve aynı plan orada farklı bir sayısal kimlik taşır. Sabit olan addır: adı kaydedin, kimliğe çağrı anında çevirin.

> **Anahtar değerini asla empty() ile sınamayın**
> 
> Onay ve anahtar alanları `"0"` ve `"1"` dizelerini gönderir; `empty("0")` ise true'dur. `(int) ($data['key'] ?? 0) === 1` yazın; bu, o biçimlerin hepsinde aynı davranır.

> **Sunucu parolasını kendiniz çözmeyin**
> 
> Taban sınıf bunu tek bir yerde zaten yaptı; böylece bağlantı testi akışı ile sağlama akışı aynı davranır. İkinci bir çözme düz metin yolunu bozar ve hata ancak gerçek bir sağlayıcıya karşı ortaya çıkar.

## İlgili Makaleler

- [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi)
- [Modül Yapılandırması](https://dev.wisecp.com/tr/modul-yapilandirmasi)
- [Modül Yaşam Döngüsü](https://dev.wisecp.com/tr/modul-yasam-dongusu)
- [Sunucu Modülü Araçları](https://dev.wisecp.com/tr/sunucu-modulu-araclari)
- [Ürün Modülü Yazma](https://dev.wisecp.com/tr/urun-modulu-yazma)
- [Alan Yardımcıları](https://dev.wisecp.com/tr/alan-yardimcilari)
