# Özel Operation Ekleme

https://dev.wisecp.com/tr/ozel-operation-ekleme

Rota ve hata biçimi icat etmeden, veri değiştirip JSON yanıt veren bir POST ucu ekleyin.

## Genel Bakış

Operation, veri değiştiren ve JSON ile yanıt veren bir metottur. Var olan bir controller'a `operation` parametresi gönderirsiniz: rota kaydı yoktur. Controller adı çözer, yetkileri kontrol eder, fırlattığınızı hataya çevirir.

Operation'lar trait'lerde yaşar ve controller'a `use` ile eklenir. Metot, controller'ın modeline ve yardımcılarına erişir.

## Ön Koşullar

- Genişlettiğiniz sayfaya zaten yanıt veren bir controller, yönetici ya da müşteri tarafında.
- Operatör panelinde bir yetki anahtarı; boş tanımı oturum açan herkes çağırabilir.
- Bir çağıran: `status`, `message` ve `redirect` anahtarlarınızı panelin istek yardımcısı işler.

## Yapı

| Parça | Nerede | Ne taşır |
| --- | --- | --- |
| Trait | `coremio/operations`, ad alanı `WISECP\operations` | Operation başına bir public metot |
| Controller | `coremio/controllers`, yönetici ya da website | Trait için `use` ve tanım dizisi |
| Çağıran | Bir şablon ya da kendi JavaScript'iniz | `operation` ve alanlarınızı gönderir, JSON'u okur |

## Adım Adım

### Metodu Yazın

1. Bir trait'e public metot ekleyin: tek parametresi operation nesnesi, dönüşü `bool`.
2. Metot yazıyorsa ilk satırda demo korumasını çağırın.
3. Girdileri filtre yardımcısıyla okuyun, ham istek dizilerine dokunmayın; parola kendi temizleyicisini ister.
4. Doğrulamayı fırlatarak yapın; kurulacak bir hata dönüş değeri yoktur.
5. Yanıtı, after kancasının değiştirebileceği bir değişkenden çıktı yardımcısıyla döndürün.

### Tanımlayın

1. Trait'i içe aktarın ve controller sınıfında `use` ile ekleyin.
2. Gönderilen adla anahtarlayıp yapıcıdaki tanım dizisine ekleyin.
3. Sayfanın kullandığı yetkileri verin; kontrol metottan önce çalışır.
4. Giriş metodunun dağıtıcıya devrettiğini doğrulayın; müşteri controller'ı bunu açıkça yapar.

### Çağırın

1. Controller'ın kendi adresine, `operation` adınıza ayarlı olarak gönderin.
2. İsteği asenkron çağrı olarak gönderin; yönetici operation'ı öyle görünmeyeni reddeder.
3. Standart anahtarlar: mesaj bildirim olur, yönlendirme gider, `reload` yeniler.
4. Geri çağırmaları yalnız bu anahtarların yapamadığı iş için kullanın.

## Referans

### Operation Nesnesi

```php
class Operation
{
    public ?string $name = '';          // gönderilen operation adı
    public static ?string $last = '';   // istek başına kurulan son ad

    // Dağıtıcı kurar: $properties, tanım dizisindeki kayıttır.
    public function __construct($name = '', $properties = []);

    // Tanıtım sisteminde fırlatır. Yazan her operation'ın ilk satırı.
    public function demo(): void;

    // Kancayı ateşler ve cevapları normalize eder. 'before' ve 'after', iki genel
    // admin-operation filtresinin kısaltmasıdır; başka bir dize olduğu gibi kullanılır.
    // Dönüş: [] ya da ['overwrite' => array] ya da ['output' => mixed].
    public function hook(string $name = '', array $vars = []): array;

    // Yanıtı gönderir. Dizi, JSON başlığı kurularak JSON'a çevrilir; başka her şey
    // olduğu gibi basılır. Her zaman true döner, yani `return $op->output(...)`
    // bool dönüş tipini karşılar.
    public static function output($response): bool;

    public static function name(): ?string;
    public function assertContext(): void;
    public static function resolveContext(): bool;
}
```

- **$operation->demo()**: Sadece yazan operation'larda; okuma yapan bir operation bunu taşımamalı.
- **Operation::output()**: JSON içerik tipini kendisi kurar ve yanıtı API yanıt filtresinden geçirir.
- **$operation->hook()**: Controller adını, operation adını ve değişkenlerinizi geçirir. Hata durumu istisna fırlatır.
- **bool dönüşü**: Hiçbir yer okumaz; başarıyı işaret etmez.

### Tanım Dizisi

```php
$this->operations = array_merge($this->operations, [

    // Yaygın biçim: gönderilen ad => çalıştırmak için gereken yetkiler.
    'save_acme_settings' => ['privileges' => ['SETTINGS_OPERATION']],

    // 'method', gönderilen adı başka adlı bir metoda yöneltir; iki gönderilen adın
    // tek bir uygulamayı paylaşma yolu budur.
    'acme_retry'         => ['privileges' => ['SETTINGS_OPERATION'], 'method' => 'acme_run'],

    // 'allow_navigation' asenkron istek şartını kaldırır. Yalnız adres olarak
    // açılması gereken operation'lar için (dosya indirme) ve yalnız operation'ın
    // kendi tek kullanımlık jetonuyla birlikte.
    'download_acme_log'  => ['privileges' => ['SETTINGS_OPERATION'], 'allow_navigation' => true],

    // Müşteri tarafı operation'ı genelde yetki tanımlamaz: kendini metodun içinde
    // üye oturumuna göre kapılar.
    'acme_client_action' => [],
]);
```

- **privileges**: Anahtar dizisi; çağıranın herhangi biri yeter. Ret, yönlendirme değil mesaj.
- **method**: Gerçekten çağrılan metot; varsayılanı gönderilen ad.
- **allow_navigation**: Operation'ı taşıma kontrolünden muaf tutar. Yalnız indirmeler, jetonla birlikte.
- **Tanımsız ama mevcut**: Controller'daki bir metot, tanım kaydı olmadan da çağrılabilir ve **hiç yetki kontrolü olmadan** çalışır.

### Dağıtıcı Ne Yapar

| Adım | Davranış | Başarısız olursa |
| --- | --- | --- |
| Ad temizliği | Ad rota-güvenli bir dizeye filtrelenir; `main` bulunamadı işleyicisine gider | Bilinmeyen ad reddedilir |
| Lisans kontrolü | Aktif lisans ister; yardım ve yoklama operation'ı muaf | Yetki kontrolünden önce hata |
| Taşıma kontrolü | `X-Requested-With: XMLHttpRequest` taşıyan asenkron istek | Tanım izin vermiyorsa HTTP 403 |
| Yetki kontrolü | Tanım yetki listeliyorsa çalışır | Eksik yetkiyi adlandıran hata |
| Metodunuz | Yeni kurulmuş operation nesnesiyle çağrılır | Her istisna `{"status":"error","message":"..."}` olur |
| Yedek yol | Tanım ve metot yoksa ad bir kayıt kancasına sunulur | Dizi cevabı gönderilir |

- **register:admin.operations**: Kimsenin sahiplenmediği bir operation'a cevap vermek için son şans; dizi dönüşü gönderilir.
- **filter:admin.operation.before**: `before` kısaltmasının kancası. Dinleyici dizi döndürür: `error` engeller, `overwrite_vars` değişkenlerinizi değiştirir, `output` yerine cevap verir. Boş dizi sürdürür.
- **filter:admin.operation.after**: `after` kısaltması; yanıt değişkeni oluştuktan sonra çalışır. Aynı dönüş biçimi, ama `error` istisna fırlatır.
- **filter:api.response**: Çıktı yardımcısında, her dizi yanıtta operation adı bağlamıyla çalışır. İstisnalar buraya uğramaz; yanıt referansla değişir, dönüş kullanılmaz.

### Çağıranın Okuduğu Yanıt

| Anahtar | Değer | Panel ne yapar |
| --- | --- | --- |
| `status` | `successful` ya da `error` | Mesajı olan ve başarı olmayan her şey hata sayılır |
| `message` | Çevrilmiş metin | Bildirim, çağıran istediyse toast |
| `redirect` | Bir adres ya da `reload` ya da `script` | Gider, yeniler ya da `script` çalıştırır; mesajla bildirimi bekler |
| `redirect_delay` | Milisaniye | Beklemeyi ezer: mesajla beş saniye, mesajsız anında |
| `successToast` | Mantıksal değer | Toast biçimini çağıranın tercihine rağmen zorlar |
| `data` | Herhangi bir şey | Otomatik hiçbir şey; kendi geri çağırmanız okur |

## Örnek

Bir entegrasyon anahtarını döndüren operation ve onu çağıran kod.

```php
namespace WISECP\operations;

use Operation;
use Filter;
use Exception;

trait AcmeSettings
{
    public function rotate_acme_key(Operation $operation): bool
    {
        /** @var \WISECP\controllers\admin\settings $this */

        // 1. Tanıtım sisteminde reddedin; herhangi bir şey okunmadan önce.
        $operation->demo();

        // 2. Girdiyi okuyun. Asla $_POST: filtre tipi alan başına seçilir ve bir sır,
        //    noktalaması bozulmadan kalsın diye 'password' filtresinden geçmelidir.
        $id      = (int) Filter::init('POST/id', 'numbers');
        $label   = (string) Filter::init('POST/label', 'hclear');
        $confirm = (string) Filter::init('POST/confirm_password', 'password');

        // 3. BEFORE HOOKS: okumadan sonra, doğrulamadan önce; böylece bir dinleyici
        //    girdileri değiştirebilir ya da bizim yerimize cevap verebilir.
        # BEFORE HOOKS
        $hook = $operation->hook('before', get_defined_vars());
        if ($hook && $hook['overwrite'] ?? []) extract($hook['overwrite']);
        if ($hook && $hook['output'] ?? false) return $operation->output($hook['output']);

        // 4. Fırlatarak doğrulayın. Dağıtıcı bunların her birini, sizin tarafınızda hiçbir
        //    iş olmadan {"status":"error","message":"..."} biçimine çevirir.
        if (!$id) throw new Exception(\Language::gc('admin/settings/error-missing-id'));
        if ($label === '') throw new Exception(\Language::gc('admin/settings/error-label-empty'));

        $adata = \UserManager::LoginData('admin');
        if (!\User::_password_verify('admin', $confirm, $adata['password']))
            throw new Exception(\Language::g('needs/permission-delete-item-invalid-password'));

        // 5. İşin kendisi; trait'in controller'dan devraldığı model üzerinden.
        $key = \Utility::generate_hash(48);
        $this->model->set_acme_credentials($id, ['label' => $label, 'api_key' => $key]);

        // 6. Denetim izi. Üçüncü argüman, actions dil dosyasındaki bir anahtardır.
        \User::addAction((int) $adata['id'], 'alteration', 'changed-acme-key', ['id' => $id]);

        // 7. Yanıt bir değişkene konur ki after kancası onun yerine bir şey koyabilsin.
        $response = [
            'status'   => 'successful',
            'message'  => \Language::gc('admin/settings/success-acme-key-rotated'),
            'data'     => ['masked' => substr($key, 0, 6) . str_repeat('*', 10)],
        ];

        # AFTER HOOKS
        $hook = $operation->hook('after', get_defined_vars());
        if ($hook && $hook['overwrite'] ?? []) extract($hook['overwrite']);
        if ($hook && $hook['output'] ?? false) return $operation->output($hook['output']);

        // 8. Tek çıkış. output() kodlar, başlığı kurar ve true döner.
        return $operation->output($response);
    }
}
```

```php
namespace WISECP\controllers\admin;

use WISECP\operations\AcmeSettings;
use Controllers;
use Filter;

class settings extends Controllers
{
    use AcmeSettings;

    public function __construct()
    {
        parent::__construct();
        $this->checkLogin();

        // Kayıt ve erişim denetimi tek yerde. Bu kayıt olmadan da metoda ulaşılabilirdi
        // ve yetki kontrolünden geçmeden çalışırdı.
        $this->operations = array_merge($this->operations, [
            'rotate_acme_key' => ['privileges' => ['SETTINGS_OPERATION']],
        ]);
    }

    public function main()
    {
        // Ayrım noktası: gönderilen bir operation aşağıdaki sayfa metotlarına hiç ulaşmaz.
        if ($operation = Filter::init('REQUEST/operation')) return $this->operation($operation);

        // ... page_* dağıtımı buradan devam eder
        return '';
    }
}
```

Çağıran taraf.

```javascript
function rotateAcmeKey(btn) {
    WcpRequest(CONTROLLER_LINK, {
        method: 'POST',
        button: btn,                       // gidiş dönüş boyunca pasifleşir ve dönen simge alır
        buttonLoader: saving_loader,
        options: { headers: { 'X-Requested-With': 'XMLHttpRequest' } },
        data: {
            operation: 'rotate_acme_key',  // beyan edilen ad, birebir
            id: ACME_ID,
            label: document.getElementById('acmeLabel').value,
            confirm_password: document.getElementById('acmeConfirm').value,
        },
        // status/message/redirect bizim için ele alınır; bu yalnız ek kısım içindir.
        afterDone: (res) => {
            if (res.status !== 'successful') return;
            document.getElementById('acmeKeyMasked').textContent = res.data.masked;
        },
    });
}
```

Müşteri paneli operation'ında genel before ve after kancası yoktur.

```php
namespace WISECP\operations;

use Operation;
use Filter;
use Exception;

trait ClientAcme
{
    public function acme_disconnect(Operation $operation): bool
    {
        $operation->demo();

        // Bağlam operation'ın İÇİNDE çözülür: tanım dizisi bir kapı değil kayıttır ve
        // bir metot, kaydı olmadan da dağıtılabilir durumdadır.
        $member = \UserManager::LoginData('member');
        if (!$member) throw new Exception(\Language::gc('website/account/err-auth'));

        $uid = (int) ($member['id'] ?? 0);
        $sid = (int) Filter::init('POST/service_id', 'numbers');

        $service = \Services::get($sid);
        if (!$service || (int) ($service['owner_id'] ?? 0) !== $uid)
            throw new Exception(\Language::gc('website/account/err-not-found'));

        // Kendi alan kancalarınız, ait oldukları noktada. Bir kapı, boş olmayan bir
        // gerekçe döndürerek reddeder ve operation bunu bir hataya çevirir.
        // İki ad da modülünüzün ad alanında yaşar: asla bir çekirdek kancasını
        // ateşlemeyin ve kataloğun taşımadığı bir kanca uydurmayın.
        foreach (\Hook::run('gate:service.acme_disconnect', $sid, $uid) as $veto)
            if ($veto) throw new Exception((string) $veto);

        // set_options() diye bir helper yoktur. Seçenekler Services::set() üzerinden gidip
        // gelir, o da diziyi sizin için JSON'a çevirir: okuyun, birleştirin, haritanın
        // tamamını yazın.
        $options = $service['options'] ?? [];
        $options['acme_linked'] = 0;
        \Services::set($sid, ['options' => $options]);

        \Hook::run('action:service.acme_disconnected', $sid, $uid);

        return $operation->output([
            'status'  => 'successful',
            'message' => \Language::gc('website/account/acme-disconnected'),
        ]);
    }
}
```

## Tuzaklar

> **Public bir metot zaten bir uçtur**
> 
> Dağıtım, tanım kaydıyla ya da var olan bir metotla eşleşen her adı kabul eder; yardımcıları private tutun.

> **Aynı kural API'ye de gerekir**
> 
> API kaynakları doğrudan yardımcıya ya da modele gider; sadece operation'a eklenen kural jetonla aşılabilir kalır. Kuralı iki tarafın da çağırdığı bir metoda taşıyın.

> **Elle kodlamak çıktı filtresini atlar**
> 
> Kendi kodlamanızı yazmak yanıt filtresini ve kodlama işaretlerini atlar. Kanca dalları dahil, çıktı yardımcısıyla dönün.

> **Operation bir bağlantı değildir**
> 
> Yönetici operation'ı, istek kendini asenkron bir çağrı olarak tanıtmıyorsa reddedilir. İndirme için muafiyeti tanımlayın ve tek kullanımlık jeton ekleyin.

> **Demo koruması yalnız yazmalara aittir**
> 
> Liste ya da önizlemede hiçbir şeyi korumadan tanıtım sistemini bozar. Metot yazıyorsa koruma onun ilk satırı.

## İlgili Makaleler

- [Operation'lar](https://dev.wisecp.com/tr/operationlar)
- [Controller ve Yönlendirme](https://dev.wisecp.com/tr/controller-ve-yonlendirme)
- [Kullanıcı Girdisini Filtreleme](https://dev.wisecp.com/tr/kullanici-girdisini-filtreleme)
- [Admin JavaScript Kütüphanesi](https://dev.wisecp.com/tr/admin-javascript-kutuphanesi)
- [Kanca Dinleyicisi Yazma](https://dev.wisecp.com/tr/kanca-dinleyicisi-yazma)
- [Güvenlik Pratikleri](https://dev.wisecp.com/tr/guvenlik-pratikleri)
