# API Ucu Açma

https://dev.wisecp.com/tr/api-ucu-acma

Modülünüzün yeteneklerini tek bir kancaya rota kaydı ekleyerek REST ucu olarak yayınlarsınız; çekirdeğe dokunmadan.

## Genel Bakış

Kimlik doğrulama, uç başına izinler, hız sınırı, CORS, idempotency ve istek kaydı zaten var. Siz hangi adresin ne yaptığını söylersiniz, gerisini Kernel metodunuzu çağırmadan önce halleder.

Sistem sahibinin gördüğü bir onay kutusudur: her uç, API kimlik bilgileri ekranında ayrı bir satır olur ve anahtar yalnız işaretlenen uçlara ulaşır.

```text
Ayarlar -> API kimlik bilgileri -> Oluştur
  [ ] GET    /admin/mymodule/items
  [x] POST   /admin/mymodule/items/{id}/rebuild      <- yalnız bu verildi
  [ ] DELETE /admin/mymodule/items/{id}
```

## Ön Koşullar

- `hooks.php` dosyası olan bir modül; bkz. [Modülden Kanca Kaydetme](https://dev.wisecp.com/tr/modulden-kanca-kaydetme).
- Yazma uçları için, ikinci bir uygulama yerine köprüleyeceğiniz mevcut bir panel operasyonu.
- Modülün `src/` sınıfları otomatik yüklenmez; `hooks.php` neye başvuruyorsa onu `include_once` edin.

## Yapı

- **filter:api.routes**: Tek bağlantı noktası; her hedef kitle için bir kez, rota listesi referansla çalışır. Listeye ekleyin; dönüş yoksayılır.
- **Routes / ClientRoutes / ModuleRoutes**: Üç kayıt defteri; her biri kancayı kendi `$audience` değeriyle çalıştırır (aşağıdaki tabloya bakın).
- **Kernel::dispatch()**: `Module:` ile başlayan bir grup görünce isteği modül örneğinize verir.
- **Config::set()**: Onay kutularınızı bellekteki izin kataloğuna yayınlar.

## Adım Adım

### Uç Kümesini Tek Yerde Bildirin

1. `src/ApiSurface.php` oluşturun: bir `GROUP` sabiti ve her ucu listeleyen bir `map()` (fiil, yol, aksiyon, hedef).
2. Rotaları ve izin kataloğunu aynı listeden türetin; böylece rota, kapsam ve onay kutusu ayrışamaz.
3. Sabit yolları parametrik ikizlerinden önce yazın. Yönlendirici aynı parça sayısında ilk eşleşeni alır.

### Rotaları Kaydedin

1. `hooks.php` içinde `filter:api.routes` kancasına bir dinleyici ekleyin ve hedef kitle sizinki değilse dönün.
2. Tuple'larınızı `$routes` dizisine ekleyin. İki parametre de referansla gelir, yani ekleyin, döndürmeyin.
3. İzin kataloğunu **dosya gövdesinde**, her dinleyicinin dışında yayınlayın.

### İşleyici Metotları Yazın

1. Uç başına bir `api_{aksiyon}` metodu ekleyin. Sert bir `method_exists` kontrolü vardır, `__call` yanıt veremez.
2. Her metoda köprüye ileten aynı tek satırlık gövdeyi verin ve onları bildirimden üretin.
3. Bir `Response` döndürün ya da Kernel'in normalize ettiği eski zarf dizisini verin.
4. Liste için zarfı çekirdek kaynaklarıyla aynı tutun: girdide `page`, `limit` ve `search`; çıktıda `meta.total`, `meta.page`, `meta.limit` ve `meta.next_page`.

### Yazma İşlerini Panel Operasyonuna Köprüleyin

1. Superglobal'leri yansıtın, `op_{ad}` metodunu çağırın, çıktısını yakalayın ve bir `finally` bloğunda geri koyun.
2. Yakaladığınız zarfı çevirin: `data.html` düşürülür, `message` `meta` içine taşınır, fırlatılan exception doğrulama hatasına dönüşür.
3. Operasyonun yetki kapısı API çağrısında kenara çekilsin; Kernel daha dar bir kontrol uygulamıştır.

## Referans

### Kanca ve Rota Tuple'ı

```php
// Dinleyici. İki argüman da referanstır; $routes dizisine ekleyin, hiçbir şey döndürmeyin.
Hook::add('filter:api.routes', 20, function (array &$routes, string &$audience): void {
    if ($audience !== 'admin') return;               // 'admin' | 'client' | 'module'
    // ...
});

// Tek bir kayıt. 6 numaralı indeks yalnız serbest yüzeyde vardır.
// [0] string  $method    GET | POST | PUT | PATCH | DELETE
// [1] string  $pattern   yüzey önekinden sonraki tam yol; {x} bir yol parametresi yakalar
// [2] string  $group     'Module:{Type}/{Name}' isteği modül örneğinize yönlendirir
// [3] string  $action    izin adı VE api_{action} metodunun son eki
// [4] bool    $public    false = kimlik bilgisi gerekir (varsayılan false)
// [5] bool    $authOnly  false = "Group/Action" kapsamını da AYRICA dayat
//                        admin/client tarafında varsayılan false, serbest yüzeyde TRUE
// [6] string  $audience  yalnız serbest yüzey: 'admin' (varsayılan) | 'client' | 'any'
$routes[] = ['POST', 'mymodule/items/{id}/rebuild', 'Module:Addons/MyModule', 'item_rebuild', false, false];
```

| Uç kümesi | Hedef kitle değeri | Kaydın yanıtladığı adres |
| --- | --- | --- |
| kimlik isteyen admin | `admin` | `/api/v1/admin/{desen}` |
| müşteri | `client` | `/api/v1/client/{desen}` |
| serbest | `module` | `/api/v1/{desen}` |

Bir rotanın dayattığı kapsam her zaman `{grup}/{aksiyon}`'dur, yani yukarıdaki kayıt `Module:Addons/MyModule/item_rebuild` iznine karşılık harcanır. Sona eklemek bir çekirdek adresini gölgelemez: çekirdek kayıtları önce girer ve yönlendirici ilk eşleşmeyi döndürür. Birini devralmak için tuple'ı yerinde düzenleyin.

### İşleyici Sözleşmesi

```php
public function api_item_rebuild(Request $request, array $match): Response;

// $request, her özelliği public ve ayrıştırılmış halde:
//   string  $method          'GET', 'POST', ...
//   string  $audience        'admin' | 'client' | ''
//   array   $segments        '/' ile bölünmüş yol
//   string  $resource        ilk parça
//   array   $query           sorgu dizesi
//   array   $body            çözülmüş JSON gövdesi (ya da form gövdesi)
//   array   $headers         küçük harfe çevrilmiş başlık adları
//   string  $ip              çözümlenen istemci adresi
//   ?string $token           gönderilmişse ham kimlik bilgisi
//   ?string $idempotencyKey
//   string  $rawBody

// $match, yönlendiricinin ürettiği:
//   'group'    => 'Module:Addons/MyModule'
//   'action'   => 'item_rebuild'
//   'params'   => ['id' => '42']       ada göre anahtarlanmış {x} yakalamaları
//   'scope'    => 'Module:Addons/MyModule/item_rebuild'
//   'public'   => false
//   'authOnly' => false
//   'audience' => 'admin'
```

### Yanıtı Kurma

```php
class Response
{
    public function __construct(int $status = 200, array $payload = []);

    public static function success($data = null, int $status = 200, array $meta = []): self;
    public static function error(string $code, string $message, int $status = 400, array $details = []): self;
    public static function fromLegacy(array $ret): self;          // {status, message, data} zarfı

    public function withHeader(string $name, string $value): self;
    public function withHeaders(array $headers): self;
    public function getStatus(): int;
    public function getPayload(): array;
    public function send(): void;                                 // bunu Kernel çağırır, siz değil
}

// Hatalar döndürülmez, fırlatılır. Her fabrika kendi HTTP durumunu taşır.
class ApiException extends Exception
{
    public static function badRequest(string $message, string $code = 'bad_request', array $details = []);
    public static function unauthorized(string $message = 'Authentication required.', string $code = 'unauthorized');
    public static function forbidden(string $message = 'Insufficient scope.', string $code = 'forbidden');
    public static function notFound(string $message = 'Resource not found.', string $code = 'not_found');
    public static function methodNotAllowed(string $message = 'Method not allowed.', string $code = 'method_not_allowed');
    public static function validation(string $message, array $details = [], string $code = 'validation_failed');
    public static function rateLimited(string $message = 'Too many requests.', string $code = 'rate_limited');
    public static function server(string $message = 'Internal server error.', string $code = 'server_error');
}
```

### Onay Kutularını Yayınlama

```php
public static function set($key, $values, $merge = false): array|false;
```

```php
// Üçüncü argüman birleştirmeyi array_replace_recursive yerine array_merge yapar; böylece
// aksiyon listeniz aynı adlı bir listeye indeks indeks karışmak yerine bütün olarak yazılır.
// Kataloğun diğer grupları iki durumda da yerinde kalır.
Config::set('api-actions', ['Module:Addons/MyModule' => ['items_list', 'item_rebuild']], true);
```

Yazılan yalnız bellekteki kopyadır; `Config::save()` burada çağrılmaz, yani `coremio/configuration/api-actions.php` çekirdeğin gönderdiği hâliyle kalır.

### Verilen Bir Kapsam Nereye Kadar Uzanır

| Anahtara verilen | Neyi açar | Önerilebilir mi |
| --- | --- | --- |
| `Module:Addons/MyModule/item_rebuild` | yalnız o uç | evet |
| `Module:Addons/*` | **sistemdeki tüm Addons modülleri** | hayır |
| `*` | admin API'sinin tamamı | hayır |

> **Tek bir modül için joker yoktur**
> 
> Kapı, gereken kapsamı *ilk* slash'ta keser ve grubunuz zaten bir slash içerir. Yani `Module:Addons/MyModule/x` kapsamının grubu `Module:Addons`'tur ve onunla eşleşen tek joker diğer bütün Addons modülleriyle de eşleşir.

## Örnek

Asgari bir admin uç kümesi: bildirim, kayıt, bir işleyici ve köprü.

```php
<?php
namespace WISECP\Modules\Addons\MyModule\Src;

use Config;

final class ApiSurface
{
    public const GROUP = 'Module:Addons/MyModule';

    /** [method, path, action, target]; sabit yollar {id} ikizlerinden ÖNCE. */
    public static function map(): array
    {
        return [
            ['GET',    'mymodule/items',              'items_list',   'read:items_list'],
            ['GET',    'mymodule/items/export',       'items_export', 'read:items_export'],
            ['GET',    'mymodule/items/{id}',         'item_detail',  'read:item_detail'],
            ['POST',   'mymodule/items',              'item_save',    'op:save_item'],
            ['POST',   'mymodule/items/{id}/rebuild', 'item_rebuild', 'op:rebuild_item'],
            ['DELETE', 'mymodule/items/{id}',         'item_delete',  'op:delete_item'],
        ];
    }

    public static function routes(): array
    {
        $routes = [];
        foreach (self::map() as [$method, $path, $action])
            $routes[] = [$method, $path, self::GROUP, $action, false, false];

        return $routes;
    }

    public static function publish_permission_catalog(): void
    {
        $actions = array_map(static fn (array $e): string => $e[2], self::map());
        if (!$actions) return;

        Config::set('api-actions', [self::GROUP => $actions], true);
    }

    /** @return array{0:string,1:string}|null bir aksiyon için [kind, name], örn. ['op', 'rebuild_item'] */
    public static function target(string $action): ?array
    {
        foreach (self::map() as $entry)
            if ($entry[2] === $action) return array_pad(explode(':', $entry[3], 2), 2, '');

        return null;
    }
}
```

```php
<?php
use WISECP\Modules\Addons\MyModule\Src\ApiSurface;

include_once __DIR__ . DS . 'src' . DS . 'ApiSurface.php';
include_once __DIR__ . DS . 'src' . DS . 'ApiBridge.php';

// Adresler: admin yüzeyi, kimlik bilgisi zorunlu, kapsam uç başına dayatılır.
Hook::add('filter:api.routes', 20, function (&$routes, &$audience) {
    if ($audience !== 'admin') return;

    foreach (ApiSurface::routes() as $route) $routes[] = $route;
});

// Onay kutuları: DOSYA GÖVDESİNDE, asla yukarıdaki dinleyicinin içinde değil.
ApiSurface::publish_permission_catalog();
```

```php
use WISECP\Api\Core\Request;
use WISECP\Api\Core\Response;
use WISECP\Modules\Addons\MyModule\Src\ApiBridge;

class MyModule extends AddonModule
{
    public function api_items_list(Request $request, array $match): Response
    {
        return ApiBridge::dispatch('items_list', $request, $match);
    }

    public function api_item_rebuild(Request $request, array $match): Response
    {
        return ApiBridge::dispatch('item_rebuild', $request, $match);
    }

    // ... bildirilen her aksiyon için bir tane; ApiSurface::map() üzerinden üretin
}
```

```php
public static function op(string $name, Request $request, array $match): Response
{
    $area  = self::area();                         // lisans kapısı + AdminArea örneği
    $input = array_merge($request->query, $request->body, $match['params'] ?? []);

    $snapshot = [
        'post'    => $_POST,
        'get'     => $_GET,
        'request' => $_REQUEST,
        'method'  => $_SERVER['REQUEST_METHOD'] ?? '',
    ];

    $_POST = $_REQUEST = $input;
    $_GET  = [];
    $_SERVER['REQUEST_METHOD'] = 'POST';           // operasyonlar form POST varsayar

    self::$in_api = true;
    $failure = null;

    ob_start();
    try     { $area->{'op_' . $name}(new Operation($name)); }
    catch   (Exception $e) { $failure = $e; }
    finally {
        $printed = (string) ob_get_clean();
        self::$in_api = false;

        $_POST    = $snapshot['post'];             // ödünç alınan isteği temiz geri ver
        $_GET     = $snapshot['get'];
        $_REQUEST = $snapshot['request'];
        $_SERVER['REQUEST_METHOD'] = $snapshot['method'];
    }

    if ($failure) throw ApiException::validation($failure->getMessage(), [], 'operation_failed');

    $envelope = Utility::jdecode($printed, true) ?: [];
    $data     = $envelope['data'] ?? null;

    if (is_array($data)) unset($data['html']);     // panelin modal gövdesi API yükü değildir

    return Response::success($data, 200, ['message' => $envelope['message'] ?? '']);
}
```

Operasyonun kendi yetki kapısının köprü için, yalnız köprü için kenara çekilmesi gerekir:

```php
private function require_operation(): void
{
    // API çağrısı sırasında oturum açmış operatör yoktur ve Kernel zaten daha dar bir
    // izni denetledi: tam olarak bu ucun kapsamını.
    if (ApiBridge::in_api()) return;

    if (!\Admin::isPrivilege(['MY_MODULE_OPERATION']))
        throw new Exception($this->lang['err-no-privilege'] ?? 'You do not have permission for this action.');
}
```

## Tuzaklar

> **Önce bildirilen parametrik yol sabit ikizini yutar**
> 
> `items/{id}` satırı `items/export` satırının üstündeyse, export isteği detay işleyicisinden **200** döner ve `id = "export"` olur. Ne log düşer ne bir şey başarısız olur. Adresi somut bir değerle yönlendiriciden geçirip dönen aksiyona bakın.

> **Kataloğu dinleyici içinde yayınlamak tüm kutuları gizler**
> 
> Ayarlar ekranı kataloğu kapsam haritasından önce okur, dolayısıyla henüz tetiklenmemiş bir dinleyici hiçbir şey yayınlamaz. Rotalar yine çalışır ve elle yazılmış bir anahtar yine kapsam kontrolünden düşer; bu da sorunu sıralama değil izin hatası gibi gösterir. Çağrıyı dosya gövdesinden yapın.

> **Sihirli __call yanıt vermez**
> 
> Dağıtıcı çağırmadan önce `method_exists` ile bakar ve bulamazsa `404 action_not_implemented` döner. Bu kasıtlıdır: uç başına bir gerçek metot, her ucun ayrı ayrı verilebilir olmasını sağlar.

> **Kernel::internal bu uçlara ulaşamaz**
> 
> Argümanını ilk slash'ta böler, grubunuzda zaten bir slash olduğu için çözüm `endpoint_not_found` ile düşer. Sistemin içindeki kod doğrudan kendi sınıflarınızı çağırır.

> **Kategori etiketi ham grup adını gösterir**
> 
> Sekme etiketi, modülün ekleyemediği bir çekirdek çeviri anahtarından gelir; ekran `Module:Addons/MyModule` ifadesini olduğu gibi gösterir. Görünen metni değiştirirseniz onay kutusu değerlerine dokunmayın: onlar kapının karşılaştırdığı kapsam dizeleridir.

> **Lisanslı modül lisans kapısını kendi tekrarlar**
> 
> Panel dağıtıcısı her operasyondan önce lisansı denetler ve API isteği o dağıtıcıdan geçmez. Aynı kontrolü köprünüzün başına koyun ve `403` yanıtlayın. Lisans alan adına bağlı olduğu için bu kapı yerel makinede sürekli kapalıdır.

## İlgili Makaleler

- [Modülden Kanca Kaydetme](https://dev.wisecp.com/tr/modulden-kanca-kaydetme)
- [Admin Sayfası Ekleme](https://dev.wisecp.com/tr/admin-sayfasi-ekleme)
- [API Kimlik Doğrulama ve İzinler](https://dev.wisecp.com/tr/api-kimlik-dogrulama-ve-izinler)
- [İstek ve Yanıt Biçimi](https://dev.wisecp.com/tr/istek-ve-yanit-bicimi)
- [Operation'lar](https://dev.wisecp.com/tr/operationlar)
- [WISECP API'si](https://dev.wisecp.com/tr/wisecp-apisi)
