Eklenti Modülü Yazma

1.7k görüntülenme Markdown

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 ve Modül Yapılandırması; 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ı

yerleşim
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
config.php, sözleşmenin tamamı
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şimNasılBunu yapan bir modül
Admin panelde bir sayfaadminArea() artı bir görünümEkranı olan her eklenti
Müşteri panelinde bir sayfaclientArea() artı yapılandırma bayrağıKum havuzu örneği
Çekirdek ekranına işaretleme enjekte etmeBir 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 vermeBir action: kancasıMuhasebe köprüsü, fatura resmileştiğinde
Kendi API uçlarıfilter:api.routes artı işleyici metotlarCanlı sohbet aracı
Ek admin menü girdisiregister:admin.menuSohbet ve virüs tarayıcı
Çekirdek controller'a ek operationregister:admin.operationsVirü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

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

tam imzalar
// 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;
MetotNe zaman çalışırfalse döndürmek ne demek
fieldsAyar ekranı açılırken ve kayıtta bir kez dahageçerli değil
save_fieldsAyarlar kaydedilirken, hiçbir şey yazılmadan önceHata özelliğindeki mesajla iptal
enableOperatör eklentiyi açtığındaKapalı kalır
disableOperatör eklentiyi kapattığındaAçık kalır
uninstallEklenti kaldırılırkenKaldırma reddedilir
adminAreaYönetici eklenti sayfasını açtığındageçerli değil
clientAreaOturum açmış bir müşteri eklenti sayfasını açtığındageçerli değil
mainBir ziyaretçi genel sayfayı açtığındageçerli değil

Sayfa Tanımı

adminArea() ve clientArea() ne döndürür
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.

adın iki tarafta da çözülüşü
$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.

sınıf
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];
    }
}
hooks.php, erişimin gerçekleştiği yer
<?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) : '';
    });
}
ayarların geri okunması
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.

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.