# Ürün Modülü Yazma

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

Ürün modülü, hosting hesabı da alan adı da olmayan her şeyi sağlar: lisans, abonelik, sertifika. Arkasında sunucu kaydı yoktur.

## Genel Bakış

Hosting, sunucu ya da alan adı olmayan bir hizmet Product tipine çözülür: `special` ve `software` hizmetleri ve SSL sertifikaları.

Yedi Product modülünün dördü SSL ürünüdür ve SSL'in genel tabanın altında kendi taban sınıfı vardır.

- **ProductModule**: Genel taban: modül trait'i, müşteri paneli sözleşmesi, hizmet içe aktarıcısı. Soyut değildir.
- **SslProductModule**: Soyut: sertifika panosu, ürün alanları, doğrulama ayrıştırması, yedi müşteri aksiyonu.
- **Services::module_type()**: hosting ve server Servers'a, domain Registrars'a, geri kalanı Product'a gider.
- **Services::run_module()**: Aynı tek kapı; yazmadığınız fiil atlanır.

## Ön Koşullar

- [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi) ve [Modül Yapılandırması](https://dev.wisecp.com/tr/modul-yapilandirmasi).
- Sertifikalar SSL tabanını, diğer her şey genel tabanı genişletir.
- Sağlayıcı API kimlik bilgileri, `$this->config`'ten okunur.
- Modüle bağlı `special` ya da `software` tipinde bir ürün.

## Yapı

Sunucu dışında her modül tipiyle aynı biçim.

```bash
coremio/modules/Product/Acme/
├── Acme.php            extends ProductModule, sertifikalar için SslProductModule
├── ApiClient.php       kendi HTTP sarmalayıcınız
├── config.php          künye ve ayarlar formu
├── logo.png
├── lang/en.php
└── pages/
    ├── configuration.php   admin panelindeki modül ayarları ekranı
    └── dashboard.php       yönetim yüzeyi; varlığı müşteri sekmesini açar
```

| Fark | Sunucu modülü | Ürün modülü |
| --- | --- | --- |
| Kimlik bilgileri | Sunucu kaydı, taban sınıf çözer | Modül yapılandırması, siz çözersiniz |
| Bağlantı testi | Sunucu formunda `test_connect()` | `controller_test_connection()` |
| Ayarlar ekranı | Yapılandırma alanlarından üretilir | `page_configuration()` + kaydetme controller'ı |
| Kaynak limitleri | `get_limit()`, taban sınıf çözer | Ürünün modül verisinden |
| Müşteri araçları | Araç kataloğu ve paylaşılan şablonlar | Pano sayfanız + çağrılabilir aksiyonlar |
| Müşteri sekmesi | Pano varsa her zaman | İsteğe bağlı: `pages/dashboard.php` ekleyin |

## Adım Adım

### Taban Sınıfı Seçin

SSL tabanı soyuttur: yazılacak yedi metot, karşılığında devralınan sertifika ekranları.

```php
namespace WISECP\Modules\Product;

use Exception;
use ProductModule;

// Bir lisans, bir abonelik, bir uygulama kiracısı.
class Acme extends ProductModule
{
    public function __construct()
    {
        parent::__construct();       // zorunlu: initModule('Product') çağrısını koşar
    }
}
```

```php
namespace WISECP\Modules\Product;

use SslProductModule;

class AcmeSSL extends SslProductModule
{
    // Yedi soyut metot yazılmak zorundadır; yüzeyin geri kalanı devralınır.
    protected function initApi(): void { /* ... */ }
    public function fetchRemoteStatus(): array { return []; }
    protected function sslProductOptions(): array { return []; }
    protected function apiReissue(string $csr, string $dcv_method, string $approver_email): array|bool { return true; }
    protected function apiResendValidation(string $domain = ''): array|bool { return true; }
    protected function apiRevalidate(string $domain = ''): array|bool { return true; }
    protected function apiChangeValidationMethod(string $domain, string $method, string $approver = 'admin'): array|bool { return true; }
}
```

### Ayarlar Ekranı

Ayarlar sayfası modülün kendisine aittir. `controller_` ön ekli metotlar o sayfadan erişilebilir; tire alt çizgiye çevrilerek adla dağıtılır.

1. `page_configuration()` ekranı kurar, çoğunlukla form oluşturucuyla.
2. `controller_save()` gönderilen alanları `save_config()` ile yazar. Her sırrı önce şifreleyin.
3. `controller_test_connection()` kimlik bilgilerini kanıtlar.

### Ürün Alanları

`product_configuration()`, yöneticinin ürün başına dolduracağı alan tanımlarını döndürür. Seçtikleri ürünün modül verisi olur ve sağlamaya `$this->options['creation_info']` olarak ulaşır.

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

Biçim olarak sunucu modülüyle aynıdır: seçimleri oku, sağlayıcıyı çağır, saklanacağı teslim et.

1. API istemcisini yapıcıda değil, ihtiyaç duyulan metodun içinde tembelce kurun.
2. `create()` metodunu yeniden denemeye dayanıklı yazın: kuyruk onu tekrar çalıştırır, o yüzden var olan hesabı denetleyin ve kimlik bilgilerini erken kalıcılaştırın.

### Müşteri Ekranı

Varsayılan olarak müşteriye hiçbir şey açılmaz; ayrıntılar [Ürün Modülü Müşteri Yönetimi](https://dev.wisecp.com/tr/urun-modulu-musteri-yonetimi) makalesindedir.

## Referans

### Genel Taban

```php
class ProductModule
{
    public bool   $client_area = false;              // yalnız müşteri panelinde basılırken true
    public string $area_link   = '';                 // hizmet kimliğini taşıyan müşteri controller bağlantısı
    public array  $client_callable_methods = [];     // müşterinin koşabileceği handle_* adları
    public array  $client_readonly_methods = [];     // GET üzerinden sunulan alt küme, CSRF jetonu yok

    public function __construct();                   // initModule('Product') çağırır
    public function get_page($page_file = '', $vars = []): string;
    public function use_controller($param = '');
    public function service_management_page(): string;
    public function has_client_management(): bool;
    public function client_overview_data(): array;
    public function client_quick_actions(int $limit = 8): array;
    protected function import_service(array $data): int;
}
```

- **use_controller($param)**: Tireleri alt çizgiye çevirip `controller_{param}` metoduna dağıtır. Metot yoksa hiçbir şey dönmez; bilinmeyen sayfa sessizce düşer.
- **get_page($page_file, $vars)**: `pages/` dizininizden bir şablon yükler, `$module` enjekte eder; şablon yoksa paylaşılan özel ürün şablonlarına düşer.
- **has_client_management()**: `pages/dashboard.php` varsa ya da `page_dashboard()` tanımlıysa true; yalnız yönetici için `false` döndürecek şekilde ezin.
- **import_service(array $data): int**: Sağlayıcıda zaten var olan bir hesap için hizmet satırı oluşturur. Kimliği döndürür, sahip ya da ürün eksikse 0.

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

Hiçbiri tabanda yoktur ve hiçbiri soyut değildir; çekirdek her birini `method_exists()` ile yoklar, yani sözleşme imzanın kendisidir.

```php
// Ayarlar ekranı. use_controller() üzerinden erişilir.
public function page_configuration(): string;
public function controller_save(): array;
public function controller_test_connection(): array;

// Ürün ve hizmet formları. İki save metodu da değerlerini 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. Dönüş tipinin burada array|bool olduğuna dikkat edin, sunucu tipinden geniştir.
public function create(): array|bool;
public function renew(): array|bool;
public function suspend(): array|bool;
public function unsuspend(): array|bool;
public function cancel(): array|bool;
public function upgrade(): array|bool;
public function change_password(string $password): bool;

// Eklentiler; biçim olarak sunucu tipiyle aynı.
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;

// Canlı durum ve admin panosu.
public function fetchRemoteStatus(): array;
public function getDashboardData(): array;

// Metrik faturalama.
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;

// Müşteri aksiyonları. Buradaki ad, bildirilen adın başına handle_ eklenmiş halidir.
public function handle_reset_usage(): array;
```

> **Bu tipte upgrade() argüman almaz**
> 
> Sunucu modülü yeni ürünü `upgrade(array $new_product = [])` olarak alır. Ürün modülleri `upgrade(): array|bool` yazar ve yeni durumu hizmetten okur.

### SSL Sözleşmesi

```php
abstract protected function initApi(): void;
abstract public function fetchRemoteStatus(): array;
abstract protected function sslProductOptions(): array;
abstract protected function apiReissue(string $csr, string $dcv_method, string $approver_email): array|bool;
abstract protected function apiResendValidation(string $domain = ''): array|bool;
abstract protected function apiRevalidate(string $domain = ''): array|bool;
abstract protected function apiChangeValidationMethod(string $domain, string $method, string $approver = 'admin'): array|bool;
```

```php
// Askıya alma bir sertifika için anlamsızdır, bu yüzden ikisi de sizin yerinize yanıtlanır.
public function suspend(): array|bool;
public function unsuspend(): array|bool;

// Ürün alanları: sertifika açılır listesi artı dahil SAN sayısı.
public function product_configuration(array $data = []): array;
public function save_product_configuration(array &$values): void;

// Yedi müşteri aksiyonu; her biri sahiplik, CSRF ve aktif hizmet denetimiyle korunmuş.
public function handle_reissue(): array;
public function handle_resend_validation(): array;
public function handle_revalidate(): array;
public function handle_change_validation_method(): array;
public function handle_add_san(): array;
public function handle_remove_san(): array;
public function handle_download_certificate(): string;

// Sertifika durumu çevresindeki yardımcılar.
public function stagedSans(): array;
public function certificateId(): string;
public static function collectExpiringServices(string $module, int $maxDays = 30): array;
```

- **client_callable_methods**: Yedi aksiyon adının hepsi tabanca doldurulur; yeniden bildirmeyin.
- **client_readonly_methods**: Yalnız `download_certificate`: GET ile dosya akıtır, token ve POST istemez.
- **dcv_methods**: `email`, `http`, `https`, `dns`. Başka her şey e-postaya normalleşir.
- **paylaşılan sertifika metinleri**: Kurulumda `$this->lang` içine birleştirilir; çakışmada kendi dosyanız kazanır.
- **fetchRemoteStatus() biçimi**: Okunanlar: `status`, `domain`, `ssl_type`, `sans`, `sans_included`, `sans_addon`, `sans_max`, `issued_at`, `expires_at`, `validation_method`, `approver_email`, `dcv_file`, `dcv_dns`, `serial_number`, `signature_algo`, `key_size`, `issuer`, `crt_code` ve `ca_code`.

### import_service() Anahtarları

- **owner_id, product_id**: İkisi de zorunlu tam sayı; eksik ya da bilinmeyen biri hiçbir şey yazmadan 0 döndürür.
- **cycle**: `monthly` gibi bir döngü anahtarı: dönemi, süreyi ve fiyatı çözer, fiyatı önce alıcının para biriminde arar.
- **period, period_time, amount, amount_cid**: Açık ezmeler: `period` döngü çözümünü, `amount` fiyat aramasını atlar.
- **options**: Hizmet seçeneklerine birleşir; `established` zorla true olur ve ürünün modül verisi `creation_info` içine gider.
- **name, status, cdate, duedate, renewaldate**: Ad ürün adına, durum `active` değerine, üç tarih şimdiye düşer.

## Örnek

Bir lisans modülü: ayar kaydı, sağlama, sonucun geri okunması.

```php
public function controller_save(): array
{
    $endpoint = Filter::init("POST/api_endpoint", "hclear");
    if (!$endpoint) throw new Exception($this->lang['err-endpoint-required']);

    $this->save_config([
        'api_endpoint' => $endpoint,

        // Geçirgen: başka her filtre anahtarı güçlü kılan şeyi siler.
        'api_key'      => $this->encode_str(Filter::init("POST/api_key", "password")),
        'mode'         => Filter::init("POST/mode", "letters"),
    ]);

    return ['status' => 'successful'];
}

public function controller_test_connection(): array
{
    $this->initApi();
    $this->api->call('ping');

    return ['status' => 'successful', 'message' => $this->lang['connection-ok']];
}

private function initApi(): void
{
    // Tembel: örnek, ağa hiç ulaşmayan bağlamlarda da kurulur.
    if (isset($this->api)) return;

    include_once __DIR__ . DS . 'ApiClient.php';
    $this->api = new ApiClient(
        $this->config['settings']['api_endpoint'] ?? '',
        $this->decode_str($this->config['settings']['api_key'] ?? ''),
    );
}
```

```php
public function create(): array|bool
{
    // Yöneticinin üründe yapılandırdığı; sipariş anındaki kopya tercih edilir.
    $module_data = ($this->options['creation_info'] ?? []) ?: ($this->product['module_data'] ?? []);

    $plan  = $module_data['plan'] ?? 'starter';
    $seats = (int) ($module_data['seats'] ?? 1);

    // Eklentiler taban hakkın üzerine eklenir; gereksinimler alıcının yazdıklarıdır.
    $seats += (int) ($this->addon_params['extra_seats'] ?? 0);
    $company = $this->requirement_params['company_name'] ?? '';

    $this->initApi();

    // Yeniden deneme güvenliği: kuyruk bu metodu bir hatadan sonra tekrar koşar.
    $existing = $this->options['config']['id'] ?? '';
    if ($existing) return true;

    $result = $this->api->call('licences', [
        'plan'    => $plan,
        'seats'   => $seats,
        'company' => $company,
        'email'   => $this->user['email'] ?? '',
    ], 'POST');

    return [
        'config' => [
            'id'  => $result['licence_id'] ?? '',
            'key' => $this->encode_str($result['licence_key'] ?? ''),
        ],
        'login' => [
            'username' => $result['username'] ?? '',
            'password' => $this->encode_str($result['password'] ?? ''),
        ],
    ];
}
```

```php
// Sonraki her fiil, create() metodunun döndürdüğünden başlar. Çekirdek bu koşmadan
// önce 'config' ve 'login' anahtarlarını hizmet seçeneklerine birleştirdi.
public function cancel(): array|bool
{
    $licenceId = $this->options['config']['id'] ?? '';
    if (!$licenceId) return true;             // hiçbir zaman bir şey sağlanmadı

    $this->initApi();
    $this->api->call('licences/' . $licenceId, [], 'DELETE');

    return true;
}

// Admin panosu ve müşteri özeti için canlı sağlayıcı durumu.
public function fetchRemoteStatus(): array
{
    $licenceId = $this->options['config']['id'] ?? '';
    if (!$licenceId) return [];

    $this->initApi();
    $remote = $this->api->call('licences/' . $licenceId);

    return [
        'status'     => $remote['state'] ?? 'unknown',
        'seats_used' => (int) ($remote['seats_used'] ?? 0),

        // Döndürmeden önce biçimlendirin: müşteri paneli bu değerleri olduğu gibi basar.
        'expires_at' => DateManager::format(Config::get("options/date-format"), $remote['expires'] ?? ''),
    ];
}
```

## Tuzaklar

> **Bir sunucu modülünü kopyalayıp sunucu kısımlarını silmeyin**
> 
> `upgrade()` farklı imzaya sahiptir, çözümlenmiş limit yardımcısı ve araç kataloğu yoktur. Bir Product kum havuzu örneğinden başlayın.

> **API istemcisini tembelce kurun, asla yapıcıda değil**
> 
> Örnek, sağlayıcıyı çağırmayan sayfalarda da kurulur.

> **Modül yapılandırmasındaki sır şifrelenmelidir**
> 
> Bunu sizin yerinize yapan bir sunucu kaydı yok: kaydetmeden önce şifreleyin, kullanmadan önce çözün, gönderilen değeri geçirgen filtreyle okuyun.

> **Hatayı fırlatarak bildirin**
> 
> Bir hata dizesi atayıp `false` döndürmek kalıntıdır. Çevrilmiş bir exception fırlatın; kuyruk mesajı kaydeder ve yeniden dener.

> **Tarihleri ve sayıları döndürmeden önce biçimlendirin**
> 
> Müşteri paneli değerleri verildiği gibi gösterir, ham zaman damgası dahil.

## İlgili Makaleler

- [Ürün Modülü Müşteri Yönetimi](https://dev.wisecp.com/tr/urun-modulu-musteri-yonetimi)
- [Sunucu Modülü Yazma](https://dev.wisecp.com/tr/sunucu-modulu-yazma)
- [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)
- [Admin Form Oluşturucu](https://dev.wisecp.com/tr/admin-form-olusturucu)
