# Eklenti Modülü Yazma

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

Eklenti, sabit bir görevi olmayan modül tipidir. Bir ayar formu, bir admin sayfası, bir müşteri sayfası ve bir kanca dosyası alır; oradan çekirdeğe dokunmadan ürünün her yerine uzanır.

## Genel Bakış

Diğer her modül tipi tanımlı bir soruyu yanıtlar. Sunucu modülü hesap sağlar, ödeme modülü para tahsil eder, servis sağlayıcı modülü alan adı kaydeder. Eklenti belirli bir soruyu yanıtlamaz; en geniş ve en çok yanlış kullanılan tip olmasının sebebi budur.

`coremio/modules/Addons` altındaki dokuz modülün ortak yanı yalnızca taban sınıftır. Bir talep asistanı, canlı sohbet, kimlik doğrulama, iki muhasebe köprüsü, virüs tarayıcı, çeviri aracı, lisans yöneticisi ve kum havuzu örneği.

> **Eklenti modülü, ürün ek hizmeti değildir**
> 
> Adlar çakışıyor. **Ürün ek hizmeti** bir faturalama kavramıdır: müşterinin bir hizmetin yanında satın aldığı, satın alma akışıyla faturalanan ve etkinleştirilen bir ekstradır. **Eklenti modülü** ise kurduğunuz bir eklentidir. Bu makalede birincisinden hiç söz edilmez.

- **AddonModule**: Taban sınıf: yapılandırma, dil, şifreleme, ayar kaydetme yolu, aç/kapa anahtarı. Hiçbiri soyut ya da zorunlu değildir.
- **hooks.php**: Erişimin geldiği yer. Sınıfın yanında duran, her istekte yüklenen ve eklentiyi çekirdek akışlarına sokan dinleyicileri kaydeden dosya.
- **adminArea()**: Admin panelde isteğe bağlı bir sayfa; eklentinin kendi adresi altında, hiçbir rota kaydı gerekmeden yönlendirilir.
- **clientArea()**: Müşteri panelinde isteğe bağlı bir sayfa; menü girdisi ve isteğe bağlı kısa adresle birlikte.
- **use_ metotları**: İstek köprüsü. Adı `use_` ile başlayan bir metot panelin kendi dağıtıcısı üzerinden çağrılabilir; başka hiçbiri çağrılamaz.

## Ö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); bu makale yalnız eklentiye özgü kısımları anlatır.
- Neyi genişlettiğinizi bilin: yeni bir ekran için admin sayfası, davranış değişikliği için var olan bir kanca gerekir.
- İzleyiciye erken karar verin. Yönetim amaçlı bir eklenti müşteri sayfası açmamalıdır; bu, istek köprüsünü kimliği doğrulanmamış çağıranlara da açar.

## Yapı

```bash
coremio/modules/Addons/Acme/
├── Acme.php        sınıf: extends AddonModule
├── config.php      künye, durum, erişim yetkileri, kaydedilmiş ayarlar
├── hooks.php       isteğe bağlı: her istekte kaydedilen dinleyiciler
├── logo.png
├── lang/en.php     $this->lang, künye bloğu dahil
├── views/          $this->view() ile render edilen şablonlar
│   ├── index.php       admin genel bakışı
│   └── client.php      müşteri sayfası
└── src/            kendi yardımcı sınıflarınız, düz new ile kurulur
```

```php
return [
    'created_at' => 1561714288,
    'meta' => [
        'name'         => 'Acme',
        'version'      => '1.0',
        'author'       => 'Your name goes here',
        'opening-type' => 'normal',

        // Müşteri paneli menü simgesi. 'font' simgenin bir sınıf, 'image' bir yol ya da URL olduğunu söyler.
        'icon_type'    => 'font',
        'icon'         => 'bi bi-puzzle',

        // İsteğe bağlı kısa adres: sayfa /addon/Acme yanında /{slug} adresinden de açılır.
        'slug'         => 'acme',
    ],
    'show_on_adminArea'  => true,     // admin sayfasını bas
    'show_on_clientArea' => true,     // müşteri sayfasını ve menü girdisini bas
    'status'             => true,     // açık; panel anahtarı buraya geri yazar
    'access_ps'          => [],       // onu açmasına izin verilen admin yetkileri
    'settings'           => [],       // ayar formunun yazdığı, kodunuzun okuduğu
];
```

| Erişim | Nasıl | Bunu yapan bir modül |
| --- | --- | --- |
| Admin panelde bir sayfa | `adminArea()` artı bir görünüm | Ekranı olan her eklenti |
| Müşteri panelinde bir sayfa | `clientArea()` artı yapılandırma bayrağı | Kum havuzu örneği |
| Çekirdek ekranına işaretleme enjekte etme | Bir `ui:` kancası | Yapay zekâ asistanı, talep yanıt düzenleyicisinde |
| Zamanlanmış iş | `register:cronjobs` artı bir kuyruk işleyici sınıfı | Muhasebe köprüsü, fatura durumunu yoklarken |
| Çekirdek olayına tepki verme | Bir `action:` kancası | Muhasebe köprüsü, fatura resmileştiğinde |
| Kendi API uçları | `filter:api.routes` artı işleyici metotlar | Canlı sohbet aracı |
| Ek admin menü girdisi | `register:admin.menu` | Sohbet ve virüs tarayıcı |
| Çekirdek controller'a ek operation | `register:admin.operations` | Virüs tarayıcı ve muhasebe köprüsü |

## Adım Adım

### Sınıfı Yazın

İşi yapıcı üstlenir. Adı sınıftan türetir, dizini ve genel URL'i çözer, yapılandırmayı ve dil dosyasını yükler. `$this->admin` ile `$this->user` alanlarını da oturumdakinden doldurur.

1. Sınıfa dizinle birebir aynı adı verin. Taban adı yansımayla türetir; uyuşmazlık, kurduğu her yolu bozar.
2. Ayar formu için `fields()` tanımlayın; formu diğer modül tipleriyle aynı alan motoru kurar.
3. Kurulum bir şey yapacaksa `enable()` tanımlayın: tablo oluşturmak, satır tohumlamak, bir önkoşulu denetlemek.

### Ayar Formunu Verin

Formu siz kurmazsınız, kaydetme yolunu da yazmazsınız. `fields()` tanımları döndürür, panel onları gösterir ve taban sınıf değerleri yapılandırma dosyanızdaki `settings` altına yazar.

1. `fields()` tanım haritasını döndürür; her kayıt mevcut değerini `$this->config['settings']` içinden okur.
2. `save_fields()` isteğe bağlıdır: gönderilen değerleri alır, doğrular ve saklanacak diziyi döndürür. Sırları burada şifreleyin.
3. `settings_notice()` isteğe bağlıdır: HTML döndürün, tüm alanların üstünde bir bant olarak görünür.

### Sayfaları Ekleyin

İki sayfa metodu da aynı tanımı döndürür: bir başlık, kırıntı yolu ve içerik. Yönlendirme otomatiktir, kaydedilecek bir şey yoktur. Alt sayfalar bir görünüm dosyasını adlandıran istek parametresiyle sürülür; dosya yoksa geri düşülür.

### Çekirdeğe Uzanın

Tipi işe yarar kılan kısım budur, disiplin de burada. Dinleyicileri `hooks.php` içine koyun ve eklentinin açık olmasına bağlayın. Bir kanca noktası modülünüzün adını asla bilmemelidir.

1. Yapılandırmayı dosyanın başında ucuza yükleyin ve dinleyicileri bir durum denetimine sarın. Kuyruk işleyicilerini kaydetmek bilinçli istisnadır ve denetimin dışında kalır.
2. Dinleyicileri kaydedin. İşaretleme üreten her şey bir closure içinde dursun ki onu göstermeyecek sayfa için hiçbir şey kurulmasın.
3. İhtiyacınız olan nokta yoksa, çekirdeği yerinde düzenlemek yerine onu düzgün biçimde açın. Jenerik bir kanca artı koşulunuzun `hooks.php` içinde durması desteklenen biçimdir.

## Referans

### Taban Sınıfın Verdikleri

```php
class AddonModule
{
    public string|bool $error     = '';   // eski nesil; yeni kod bunun yerine fırlatır
    public array       $config    = [];   // config.php, ayrıştırılmış hali
    public array       $lang      = [];   // etkin dil için lang/{lang}.php
    public string      $area_link = '';   // eklentinin kendi sayfa adresi
    public string      $_name     = '';   // dizin ve sınıf adı
    public array       $user      = [];   // oturum açmış müşteri, varsa
    public array       $admin     = [];   // oturum açmış yönetici, varsa
    public string      $url;              // modül dizininin genel URL'i
    public string      $dir;              // modül dizininin dosya sistemi yolu

    protected string $cryptKey = 'system';

    public function __construct();
    protected function view($file = '', $variables = []): string;
    public function privileges();
    public function save_settings($pFields, $accessPs): bool;
    public function change_addon_status($arg = '');
    public function save_config($data = []): bool;
    protected function encode_str(string $str = '', string $key = ''): string;
    protected function decode_str(string $str = '', string $key = ''): string;
    public function use_default_settings($formElements = null);
    public function isEnabled();
}
```

- **view($file, $variables)**: Modül dizininizdeki `views/{$file}` dosyasını, verilen değişkenler açılmış halde yükler. Dosya adını uzantısıyla geçin.
- **save_config(array $data): bool**: Yapılandırma dosyasının tamamını değiştirir; okuyun, gerekeni değiştirin ve sonucu yazın. Yazma yönetilen dosya yazıcısı üzerinden gider, derlenmiş dosya önbelleğini o halleder.
- **encode_str(), decode_str()**: `$cryptKey` ile adlandırılan, sisteme bağlı anahtarla şifreler ve çözer; varsayılan sistem anahtarıdır. Boş boş kalır; başarısız çözme şifreli metni değil boş dize döndürür.
- **isEnabled()**: Durum bayrağını yapılandırmadan okur. Her kanca dinleyicisi bir şey yapmadan önce buna bakmalıdır.
- **privileges()**: Tam yetki listesi; operatörün eklentiyi hangi rollerin açabileceğini seçmesine izin veren bir ayar ekranı için.
- **use_default_settings($formElements)**: Standart ayar ekranını alanlarınızın etrafına kurar: durum anahtarı, yetki seçici ve kaydet butonu hazır gelir.

### İsteğe Bağlı Metotlar ve Ne Zaman Koştukları

Bunların hiçbiri taban sınıfta yoktur. Her biri `method_exists()` ile yoklanır ve yoksa atlanır; yani sözleşme imzanın kendisidir.

```php
// Ayar formu.
public function fields(): array;
public function save_fields($fields = []): array|bool;   // saklanacak diziyi döndürün ya da fırlatın
public function settings_notice(): string;               // alanların üstünde HTML bant
public function edit_settings_tab(\WISECP\Components\Tab $tab): void;   // ayar sayfasına bir sekme ekler

// Kurulum yaşam döngüsü. false döndürmek durum değişikliğini iptal eder.
public function enable(): bool;
public function disable(): bool;
public function uninstall(): bool;

// Sayfalar. Her biri bir sayfa tanımı döndürür.
public function adminArea(): array;
public function clientArea(): array;
public function main(): string;      // oturum açmamış ziyaretçiler için genel bir sayfa

// İstek köprüsü: yalnız use_ ön ekli bir metoda erişilebilir.
public function use_sample_method(): array|string;
```

| Metot | Ne zaman çalışır | false döndürmek ne demek |
| --- | --- | --- |
| fields | Ayar ekranı açılırken ve kayıtta bir kez daha | geçerli değil |
| save_fields | Ayarlar kaydedilirken, hiçbir şey yazılmadan önce | Hata özelliğindeki mesajla iptal |
| enable | Operatör eklentiyi açtığında | Kapalı kalır |
| disable | Operatör eklentiyi kapattığında | Açık kalır |
| uninstall | Eklenti kaldırılırken | Kaldırma reddedilir |
| adminArea | Yönetici eklenti sayfasını açtığında | geçerli değil |
| clientArea | Oturum açmış bir müşteri eklenti sayfasını açtığında | geçerli değil |
| main | Bir ziyaretçi genel sayfayı açtığında | geçerli değil |

### Sayfa Tanımı

```php
return [
    'page_title'  => 'Acme',
    'breadcrumbs' => [
        ['link' => $this->area_link, 'title' => 'Acme'],
        ['link' => '', 'title' => 'Reports'],       // boş bir link geçerli sayfayı işaretler
    ],

    // Yalnız admin. Sayfa başlığının yanına basılan düğmeler.
    'page_title_buttons' => [
        [
            'outerHTML'  => '',                     // doluysa aşağıdaki üç anahtarı devre dışı bırakır
            'element'    => 'button',
            'attributes' => ['class' => 'btn btn-primary', 'onclick' => "acmeRefresh();"],
            'content'    => 'Refresh',
        ],
    ],

    'content' => $this->view('index.php', $variables),
];
```

### İstek Köprüsü

Bir eklentiye iki dağıtıcı ulaşır: biri admin oturumunun arkasında, biri genel sitede. İkisi de aynı kuralı uygular: istenen adın başına `use_` eklenir ve başka hiçbir şey çağrılabilir değildir.

```php
$method = (string) Filter::init("REQUEST/method", "route");

// Boşluk, tire ve nokta alt çizgiye normalleştirilir, sonra ön ek eklenir.
$method = "use_" . str_replace([' ', '-', '.'], '_', $method);

if (!method_exists($instance, $method))
    throw new Exception("Module does not have a method named {$method}.");

$result = $instance->$method();

// Yanlış değerli bir dönüş başarısızlık sayılır; başarıda asla boş dizi döndürmeyin.
if (!$result) throw new Exception($instance->error ?: "Unknown error");
```

- **admin köprüsü**: Admin oturumunun ve eklenti yetkisinin arkasında. Yalnız yöneticiye açık bir eklentinin kullandığı köprü budur.
- **website köprüsü**: Yalnız web yüzü olan bir eklentiye açıktır. Müşteri sayfası olan bir eklenti oturum açmış müşteri ister; yalnız genel sayfası olan istemez.
- **web yüzü yoksa website köprüsü de yok**: Ne müşteri sayfası ne genel sayfası olan bir eklentiye website dağıtıcısından erişilemez; metotları hiçbir zaman kimliği doğrulanmamış bir istekle karşılaşmaz.
- **yanlış değerli dönüş hatadır**: Boş olmayan bir dizi ya da boş olmayan bir dize döndürün. `true`, `[]` ve `''` üçü de başarısızlık okunur ve exception'a döner.
- **argüman yok**: Metot argüman almaz. İsteği kendiniz, girdi filtresi üzerinden, tıpkı bir operation gibi okursunuz.

### Eklentilerin Gerçekten Kullandığı Kanca Noktaları

- **register:cronjobs**: Kuyruk işleyici sınıflarını kaydeder. Her sistemde çalışır, çünkü kapalı bir eklentiye zaten iş gönderilmez. Kaydı dinleyicinin içinde yapın; dönüş yoksayılır.
- **filter:api.routes**: Eklentinin kendi uçlarını ekler. Önce izleyici argümanını denetleyin, sonra kendi metotlarınıza işaret eden rota kayıtlarını ekleyin.
- **register:admin.menu**: Admin gezinmesine bir girdi ekler; bir eklentinin operatörün bulabileceği bir şeye dönüşme yolu budur.
- **register:admin.operations**: Çekirdek bir controller'a ek operation bağlar; böylece eklenti, sahibi olmadığı bir ekrandaki isteği yanıtlayabilir.
- **action: kancaları**: Olan bir şeye tepki verir. Bu, bir fatura resmileştiğinde çalışır; muhasebe köprüsünün başladığı yer orasıdır.
- **ui: kancaları**: Çekirdek ekranına işaretleme enjekte eder. Talep detay sayfası birkaç tane taşır; asistan ile tarayıcının orada belirmesinin yolu budur.
- **action:module.addon_settings_saved**: Herhangi bir eklentinin ayarları yazıldıktan sonra, modül adı ve yeni yapılandırmayla çalışır. Dönüş yoksayılır.

## Örnek

Küçük ama eksiksiz bir eklenti: ayarlar, onu işe yarar kılan kanca dosyası ve ayarları geri okuyan kod. Kimsenin okumadığı bir ayar bu tipteki en yaygın kusurdur.

```php
namespace WISECP\Modules\Addons;

use AddonModule;
use Exception;
use Filter;

class Acme extends AddonModule
{
    public string $version = '1.0';

    public function fields(): array
    {
        $settings = $this->config['settings'] ?? [];

        return [
            'api_key' => [
                'name'        => $this->lang['api-key'],
                'description' => $this->lang['api-key-desc'],
                'type'        => 'password',
                'wrap_width'  => 100,

                // Saklanan değeri yalnız maske olarak gösterin; gerçek anahtar şifreli kalır.
                'value'       => ($settings['api_key'] ?? '') !== '' ? '********' : '',
            ],
            'notify' => [
                'name'       => $this->lang['notify'],
                'type'       => 'switch',
                'wrap_width' => 100,

                // Anahtar alanı 'value' değil 'checked' okur.
                'checked'    => (int) ($settings['notify'] ?? 0) === 1,
            ],
            'threshold' => [
                'name'         => $this->lang['threshold'],
                'type'         => 'text',
                'wrap_width'   => 100,
                'value'        => $settings['threshold'] ?? '100',

                // Yalnız üstteki anahtar açıkken görünür.
                'parent'       => 'notify',
                'parentEffect' => 'hide',
            ],
        ];
    }

    public function save_fields($fields = []): array|bool
    {
        // Maske "değişmedi" demektir: saklanan değer neyse korunur.
        if (($fields['api_key'] ?? '') === '********')
            $fields['api_key'] = $this->config['settings']['api_key'] ?? '';
        elseif (($fields['api_key'] ?? '') !== '')
            $fields['api_key'] = $this->encode_str($fields['api_key']);

        if ((int) ($fields['notify'] ?? 0) === 1 && (int) ($fields['threshold'] ?? 0) <= 0)
            throw new Exception($this->lang['err-threshold']);

        return $fields;
    }

    public function enable(): bool
    {
        // Kurulumun gerektirdiği ne varsa. false döndürmek eklentiyi kapalı bırakır.
        return true;
    }

    public function adminArea(): array
    {
        $action = Filter::init("REQUEST/action", "route") ?: 'index';
        if (!is_file($this->dir . 'views' . DS . $action . '.php')) $action = 'index';

        return [
            'page_title'  => $this->lang['meta']['name'],
            'breadcrumbs' => [['link' => '', 'title' => $this->lang['meta']['name']]],
            'content'     => $this->view($action . '.php', [
                'link'    => $this->area_link,
                'name'    => $this->lang['meta']['name'],
                'version' => $this->config['meta']['version'],
            ]),
        ];
    }

    // ?operation=use_addon_method&method=refresh ile erişilir
    public function use_refresh(): array
    {
        $id = (int) Filter::init("POST/id", "rnumbers");
        if (!$id) throw new Exception($this->lang['err-id-required']);

        // Başarıda asla boş dizi döndürmeyin: köprü yanlış değeri başarısızlık okur.
        return ['status' => 'successful', 'id' => $id];
    }
}
```

```php
<?php

// Ucuz yükleme: sınıfı dahil etmeden yalnız yapılandırma. Bu dosya istisnasız
// her istekte koşar; eklenti kapalıyken neredeyse hiçbir maliyeti olmamalı.
Modules::Load('Addons', 'Acme', true);
$acme_config = Modules::Config('Addons', 'Acme') ?: [];

// Kuyruk işleyicileri durum denetiminin DIŞINDA, her sistemde kaydedilir: kuyrukta
// bekleyen bir iş, eklenti kapatıldıktan sonra da işleyici sınıfını bulabilmeli.
// Kapalı olmak yalnız bu tipte yeni iş gönderilmemesi demektir.
Hook::add('register:cronjobs', 1, function () {
    require_once __DIR__ . DS . 'cronjobs' . DS . 'AcmeSync.php';
    CronJobQueue::register(AcmeSync::TYPE, AcmeSync::class);
});

if ($acme_config['status'] ?? false) {

    // Bir çekirdek olayına tepki verin. Örnek dinleyicinin dışında değil içinde kurulur,
    // böylece hiç tetiklenmeyen bir eklenti bunun bedelini de hiç ödemez.
    Hook::add('action:invoice.formalized', 1, function ($invoice, $userId = 0) {
        $m = Modules::getInstance('Addons', 'Acme');
        if (!$m) return;
        $m->queue_invoice((int) ($invoice['id'] ?? 0));
    });

    // Çekirdek ekranına işaretleme enjekte edin.
    Hook::add('ui:admin.tickets_detail.bottom', 1, function ($ticket) {
        $m = Modules::getInstance('Addons', 'Acme');

        return $m ? $m->ticket_panel($ticket) : '';
    });
}
```

```php
public function queue_invoice(int $invoiceId): void
{
    if (!$invoiceId) return;

    $settings = $this->config['settings'] ?? [];

    // Asla empty(): saklanan bir "0" yok sayılır ve anahtarı sessizce açık gösterir.
    if ((int) ($settings['notify'] ?? 0) !== 1) return;

    // Yalnız kullanım anında çözün, asla bir özelliğin içine değil.
    $apiKey = $this->decode_str($settings['api_key'] ?? '');
    if ($apiKey === '') throw new Exception($this->lang['err-not-configured']);

    $threshold = (int) ($settings['threshold'] ?? 0);
    // ... faturayı sağlayıcıya teslim edin
}
```

## Tuzaklar

> **Kanca dosyası her istekte çalışır, sizi yoksayanlar dahil**
> 
> Yapılandırmayı sınıfı dahil etmeyen hafif çağrıyla yükleyin. Örneği en üstte bir kez değil her dinleyicinin içinde kurun: dosya kapsamında API istemcisi kuran bir eklenti her sayfayı yavaşlatır.

> **save_config() dosyanın tamamını değiştirir**
> 
> Birleştirme yapmaz. Mevcut yapılandırmayı okuyun, anahtarlarınızı değiştirin, diziyi eksiksiz geri verin. Yalnız değiştirdiğinizi geçmek meta bloğunu, durum işaretini ve yetki listesini düşürür.

> **Müşteri sayfası website köprüsünü de açar**
> 
> Yalnız admin sayfası olan bir eklentiye genel dağıtıcıdan erişilemez. Müşteri sayfası ya da genel sayfa eklemek bunu `use_` metotlarının tamamı için değiştirir, yalnız açmak istedikleriniz için değil.

> **Köprü metodu yanlış değerli bir şey döndürmemeli**
> 
> Dağıtıcı yanlış değerli her dönüşü başarısızlık sayar ve exception fırlatır. Bildirecek bir şeyi olmayan metot bile boş olmayan bir yük döndürür, örneğin bir durum anahtarı.

> **Çekirdek modülünüzün adını asla bilmemeli**
> 
> İhtiyacınız olan davranışa erişilemiyorsa jenerik bir kanca noktası açın ve koşulunuzu kendi kanca dosyanıza yazın. Modülünüzün adını anan bir çekirdek dosyası, bir sonraki güncellemenin üzerine yazacağı bir değişikliktir.

> **Fırlatın; hata özelliğini kum havuzundan kopyalamayın**
> 
> Örnek modül hâlâ hata özelliğine atama yapıp false döndüren eski deseni gösteriyor. Önceki neslin kalıntısıdır; üretim eklentileri bunun yerine fırlatır.

## İlgili Makaleler

- [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi)
- [Admin Sayfası Ekleme](https://dev.wisecp.com/tr/admin-sayfasi-ekleme)
- [Modülden Kanca Kaydetme](https://dev.wisecp.com/tr/modulden-kanca-kaydetme)
- [API Ucu Açma](https://dev.wisecp.com/tr/api-ucu-acma)
- [Zamanlanmış Görev Ekleme](https://dev.wisecp.com/tr/zamanlanmis-gorev-ekleme)
- [Çekirdeğe Dokunmadan Çalışma](https://dev.wisecp.com/tr/cekirdege-dokunmadan-calisma)
