API Ucu Açma

1.7k görüntülenme Markdown

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.

kimlik bilgisi ekranı
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.
  • 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'ı

imza
// 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ümesiHedef kitle değeriKaydın yanıtladığı adres
kimlik isteyen adminadmin/api/v1/admin/{desen}
müştericlient/api/v1/client/{desen}
serbestmodule/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

imza
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

tam imzalar
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

imza
public static function set($key, $values, $merge = false): array|false;
çağrı
// Üçü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 verilenNeyi açarÖnerilebilir mi
Module:Addons/MyModule/item_rebuildyalnız o uçevet
Module:Addons/*sistemdeki tüm Addons modüllerihayı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ü.

src/ApiSurface.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;
    }
}
hooks.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();
MyModule.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
}
src/ApiBridge.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:

AdminArea.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.

Faydalı oldu mu?

Geri bildiriminiz için teşekkürler!

Hâlâ Yardıma mı İhtiyacınız Var?

Yukarıda bulamadığınız her şey için destek ekibimiz her zaman yanınızda.