# Müşteri Paneli Köprüsü

https://dev.wisecp.com/tr/musteri-paneli-koprusu

Müşterinin tarayıcısından eklenti modülünüzün bir metodunu çağırırsınız. Operation zaten var: kendinize ait controller, rota ya da uç kaydetmezsiniz.

## Genel Bakış

Müşteri panelindeki bir eklenti sayfası, modülünüzün ürettiği HTML'dir. Sonra bir şey yapması gerekir: tercih kaydetme, durum çekme, iş başlatma. Website tarafında hazır bağlanmış tek bir operation var ve adı `use_` ile başlayan her metodu çağırır.

Admin panelde de aynı köprü var, tools controller'ının arkasında. Fark, kimin geçmesine izin verildiğidir; bu makalenin güvenlik kısmı da o farktır.

## Ön Koşullar

- Yapılandırmasında `status` açık olan bir Addon modülü; kapalı bir eklenti köprü için görünmezdir.
- Bir web yüzü: müşteri paneli tercihi (`show_on_clientArea` artı `clientArea()`) ya da public bir `main()` sayfası. Biri yoksa köprü her çağrıyı reddeder.
- `coremio/modules/Addons/SampleAddon`, iki yüzün de çalışan bir gösterimi.

## Yapı

- **controllers/website/addon.php**: Website yüzü: `/addon/{Ad}` adresini çözer, sayfayı üretir ve köprü operation'ını dağıtır.
- **operations/ClientAddon.php**: `use_addon_method` operation'ını ve eklentinizin web yüzü olup olmadığına karar veren kapıyı taşıyan trait.
- **operations/AdminTools.php**: Admin ikizi: aynı operation adı ve önek kuralı, yönetici oturumunun arkasında.
- **AddonModule**: Taban sınıfınız. `$area_link`, `$error`, `$config`, `$lang`, `$dir`, `$url` ve `view()` verir.

```text
tarayıcı POST /addon/MyAddon   operation=use_addon_method & method=save-preference
   |
   +-- addon controller  ->  addon_ctx()     etkin yapılandırma + modül örneği + web yüzü?
   +-- ClientAddon       ->  yüz müşteri paneliyse üye oturumu gerekir
   +-- ad normalize      ->  'save-preference'  use_save_preference'a dönüşür
   +-- method_exists     ->  yoksa reddedilir
   +-- $module->use_save_preference()          HİÇBİR argüman verilmeden çağrılır
   +-- falsy dönüş       ->  Exception($module->error)
   '-- dizi ya da dize   ->  JSON gövdesi
```

## Adım Adım

### Eklentiye Bir Web Yüzü Verin

1. `config.php` içinde `show_on_clientArea` değerini açın ve `['page_title' => …, 'breadcrumbs' => …, 'content' => …]` döndüren bir `clientArea()` metodu ekleyin. Bu, `/addon/{Ad}` adresini müşteri sayfası yapar ve giriş yapmış bir üye ister.
2. Ya da public bir sayfa için `main()` metodu ekleyin; o sayfaya hiçbir oturum garantisi verilmez.
3. Güzel bir adres için `meta.slug` ayarlayın; controller `$area_link` değerini ona göre yazar.

### use_ Metodunu Yazın

1. Modül sınıfında `use_{bir_şey}` olarak adlandırın; başka hiçbir şey çağrılabilir değildir ve dağıtım katmanındaki sınırın tamamı bu önektir.
2. Parametre almayın. Girdiyi tıpkı bir operation gibi kendiniz `Filter::init("POST/…")` ile okuyun.
3. Boş olmayan bir dizi ya da dize döndürün; falsy dönüş başarısızlık sayılır ve `$this->error` taşıyan bir exception fırlatır.
4. Gerçek bir hata için fırlatın: çağıran taraf bunu standart hata zarfına çevirir.

### Sayfadan Çağırın

1. Controller'ın doğru adrese yönelttiği `$this->area_link` adresine POST edin. Yolu elle yazmayın.
2. `operation=use_addon_method` ve `method={öneksiz ad}` gönderin, metodunuzun okuduğu diğer alanlarla birlikte.
3. Website'te `X-Requested-With` başlığıyla düz `fetch`, admin panelde `WcpRequest` kullanın.

## Referans

### İki Uç

| Yüz | Adres | Kim geçer |
| --- | --- | --- |
| müşteri paneli | `/addon/{Ad}` ya da yapılandırılmış slug | giriş yapmış bir üye, eklenti tercih ettiyse |
| public sayfa | `/addon/{Ad}` | **herkes**, oturumsuz |
| eski takma ad | `/addon/{Ad}/client` | müşteri paneliyle aynı; tercih yoksa 404 |
| admin panel | tools eklenti adresi | tools yetkisi olan bir yönetici |
| website'ten çağrılan salt-admin eklenti | yukarıdakilerden herhangi biri | kimse: operation `Addon not found.` fırlatır |

> **Web yüzü kontrolü gerçek bir güvenlik sınırıdır**
> 
> O kontrol yokken salt-admin bir eklentinin `use_*` metotlarına website'ten oturumsuz ulaşılabiliyordu. Ayarlar eziliyor, talep verisi okunuyor, ücretli API kredisi yakılıyordu. Kapıyı dış duvar sayın, tek duvar değil.

### Operation ve Ad Kuralı

```php
public function use_addon_method(Operation $operation): bool;
```

```php
$method = (string) Filter::init("REQUEST/method", "route");        // a-zA-Z0-9 - _ . korunur
$method = "use_" . str_replace([' ', '-', '.'], '_', $method);     // boşluk, tire, nokta -> alt çizgi

if ($method === "use_" || !method_exists($module, $method))
    throw new \Exception("Undefined addon method.");

$result = $module->{$method}();                                     // argüman YOK
if (!$result) throw new \Exception((string) (($module->error ?? '') ?: 'An error occurred'));

return $operation->output($result);
```

Yani `save-preference`, `save.preference` ve `save_preference` üçü de `use_save_preference()` metoduna ulaşır. `route` filtresi en başta çalışır ve yalnız `a-zA-Z0-9`, tire, alt çizgi ile noktayı korur; bu yüzden bir ad başka bir sınıfa kaçamaz.

### Metodunuz Ne Döndürmeli

```php
public function use_sample_method(): array|string;
```

| Döndürdüğünüz | Müşterinin aldığı | Ne için |
| --- | --- | --- |
| boş olmayan dizi | o dizi, JSON olarak | olağan durum; standart anahtarları koruyun |
| boş olmayan dize | dize, olduğu gibi yazılır | DOM'a yerleştirilecek hazır HTML parçası |
| `[]`, `''`, `false` ya da `null` | `$this->error` değerinden kurulan `{"status":"error","message":"…"}` | hiçbir şey: eski bir yol, buna göre tasarlamayın |
| fırlatılan exception | `{"status":"error","message":"mesajınız"}` | her gerçek hata |

Hedeflenecek zarf panelin geri kalanıyla aynıdır:

```php
return [
    'status'  => 'successful',
    'message' => $this->lang['saved'] ?? 'Saved.',
    'data'    => ['preference' => $value, 'updated_at' => time()],
];
```

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

```php
class AddonModule
{
    public string|bool $error = '';        // metodunuz yanlış değer döndürünce köprü bunu okur
    public array  $config    = [];         // kaydedilmiş ayarlarla birleştirilmiş config.php
    public array  $lang      = [];         // lang/{selected}.php
    public string $area_link = '';         // POST edilecek adres; her yüze göre YENİDEN YAZILIR
    public string $_name     = '';         // dizin adı
    public array  $user      = [];         // oturum açmış üye, varsa
    public array  $admin     = [];         // oturum açmış yönetici, varsa
    public string $url       = CORE_FOLDER . DS . MODULES_FOLDER . DS . 'Addons' . DS;
    public string $dir;                    // modül dizininin dosya sistemi yolu
    // Kurucu $url sonuna {Name} ekleyip onu public bir URL'e çözer; $dir, $config, $lang,
    // $user ve $admin alanlarını doldurur ve $area_link değerini o anki yüze yöneltir.

    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;
    public function use_default_settings($formElements = null);
    public function isEnabled();
}
```

> **area_link sabit tek bir değer değildir**
> 
> Panelde tools eklenti adresini gösterir. Website'te eklenti controller'ı onu public adresle değiştirir. Yol kurmak yerine onu gösterin.

### Çekirdek Verisi Okuma

```php
// $groupAction, rota kayıt defterindeki "Group/Action" değeridir; müşteri kayıt defteri için
// başına "client:" ekleyin, o durumda $input ayrıca owner_id taşımalıdır.
public static function internal(string $groupAction, array $input = [], array $query = []): array;
```

```php
use WISECP\Api\Kernel;

// Süreç içinde: HTTP yok, kimlik doğrulama yok, hız sınırı yok; ama dış API'nin döndürdüğü
// zarfın AYNISI. $input önce yol parametrelerini, sonra gövdeyi doldurur.
$resp    = Kernel::internal('Tickets/GetTicketMessages', ['id' => $ticketId], ['limit' => 50]);
$replies = $resp['data'] ?? [];
```

Bunu doğrudan yardımcı sınıf çağırmaya tercih edin: kaynak katmanı satırları çözülmüş, izin listesinden geçmiş ve normalize edilmiş verir.

## Örnek

Tek bir müşteri tercihini saklayan bir müşteri sayfası.

```php
<?php
namespace WISECP\Modules\Addons\MyAddon;

use Exception;
use Filter;
use Language;
use User;
use UserManager;

class MyAddon extends \AddonModule
{
    /** config.php içinde show_on_clientArea true olduğunda /addon/MyAddon adresinde basılır. */
    public function clientArea(): array
    {
        $member = UserManager::LoginData('member');

        return [
            'page_title'  => $this->lang['meta']['name'] ?? 'My Addon',
            'breadcrumbs' => [['link' => '', 'title' => $this->lang['meta']['name'] ?? 'My Addon']],
            'content'     => $this->view('client.php', [
                'link'       => $this->area_link,                                  // buraya POST edin
                'preference' => (string) (User::getInfo((int) $member['id'], ['my_addon_pref'])['my_addon_pref'] ?? ''),
            ]),
        ];
    }

    /**
     * method=save-preference ile erişilir. Parametresizdir: girdisini tıpkı bir operation
     * gibi kendisi okur ve hesabı, isteğin gönderdiği hiçbir şeye güvenmek yerine
     * oturumdan yeniden okur.
     *
     * @throws Exception
     */
    public function use_save_preference(): array
    {
        $member = UserManager::LoginData('member');
        if (!$member) throw new Exception(Language::gc('website/index/addon-login-required') ?: 'Login required.');

        $value = Filter::init('POST/preference', 'route');
        if ($value === '' || !in_array($value, ['daily', 'weekly', 'never'], true))
            throw new Exception($this->lang['err-bad-preference'] ?? 'Choose one of the offered options.');

        User::AddInfo((int) $member['id'], ['my_addon_pref' => $value]);

        return [
            'status'  => 'successful',
            'message' => $this->lang['saved'] ?? 'Saved.',
            'data'    => ['preference' => $value],
        ];
    }
}
```

```html
<select id="ma-pref" class="form-select">
    <option value="daily">Daily</option>
    <option value="weekly">Weekly</option>
    <option value="never">Never</option>
</select>
<button type="button" id="ma-save" class="btn btn-primary mt-2">Save</button>

<script>
(function () {
    var link = ;
    var btn  = document.getElementById('ma-save');
    var sel  = document.getElementById('ma-pref');
    if (!btn || !sel) return;

    btn.addEventListener('click', function () {
        btn.disabled = true;

        var body = new URLSearchParams({
            operation:  'use_addon_method',
            method:     'save-preference',   // tire sorun değil, use_save_preference olur
            preference: sel.value
        });

        fetch(link, {
            method:  'POST',
            headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
            body:    body
        })
        .then(function (r) { return r.json(); })
        .then(function (r) {
            if (r.status === 'successful') window.WCPTheme && window.WCPTheme.toast(r.message, 'success');
            else window.WCPTheme && window.WCPTheme.toast(r.message, 'error');
        })
        .finally(function () { btn.disabled = false; });
    });
})();
</script>
```

Aynı çağrının admin sayfasından yapılışı:

```javascript
WcpRequest(AREA_LINK, {
    method: 'POST',
    data:   { operation: 'use_addon_method', method: 'save-preference', preference: 'weekly' },
    button: runBtn,
    buttonLoader: window.saving_loader,
    done: function (response) {           // `done` yazmazsanız standart işleme uygulanır
        output.textContent = JSON.stringify(response, null, 2);
    }
});
```

## Tuzaklar

> **Public sayfa tüm use_ metotlarını da public yapar**
> 
> Web yüzü `main()` olan bir eklentiye, eski modüllerle uyum gereği tasarım olarak oturumsuz erişilir. Her metot için "bunu kim çağırabilmeli" diye sorun ve o kontrolü metoda yazın.

> **İstekle gelen hesap kimliğine güvenmeyin**
> 
> Köprü ziyaretçiyi doğrular, kaydı değil. Müşteri kimliğini gövdeden okuyan bir metot, isteyene başka bir müşterinin verisini verir. Hesabı üye oturumundan okuyun.

> **Boş ama başarılı bir sonuç hata olarak okunur**
> 
> Haklı olarak boş dönen bir sorgudan sonra `[]` döndürmek, hata özelliğinde ne varsa onu taşıyan bir hata zarfı üretir; çoğu zaman boş bir mesaj. Veri alanı boş liste olan bir dizi döndürün.

> **Ayar operation'ı üzerinden kaydetmek durumu ve erişim yetkilerini ezer**
> 
> Standart ayar kaydı, alanlarınızla birlikte durum işaretini ve erişim yetkisi listesini de yazar; onu yeniden kullanan özel bir kaydetme eklentinizi kapatabilir. Özel kaydetmeye ayrı bir `use_` metodu verin.

> **Kapalı eklenti hiçbir şey yanıtlamaz**
> 
> Bağlam çözücü örneği kurmadan önce etkinlik işaretine bakar, bu yüzden kapalı bir eklentiye yapılan çağrı metot hatası değil `Addon not found.` ile düşer. Önce durum işaretini kontrol edin.

## İlgili Makaleler

- [Eklenti Modülü Yazma](https://dev.wisecp.com/tr/eklenti-modulu-yazma)
- [API Ucu Açma](https://dev.wisecp.com/tr/api-ucu-acma)
- [Admin Sayfası Ekleme](https://dev.wisecp.com/tr/admin-sayfasi-ekleme)
- [Operation'lar](https://dev.wisecp.com/tr/operationlar)
- [Kullanıcı Girdisini Filtreleme](https://dev.wisecp.com/tr/kullanici-girdisini-filtreleme)
- [Müşteri Paneli](https://dev.wisecp.com/tr/musteri-paneli)
