İçe Aktarma Modülü Yazma

1.7k görüntülenme Markdown

Başka bir faturalama platformunu, göç sihirbazının adım adım sürdüğü bir sınıf yazarak parçalar hâlinde bu sisteme taşıyın.

Genel Bakış

İçe aktarma modülü tek yönlü bir göçtür: başkasının veritabanını okur, bu sistemin tablolarına yazar. Üç modül gelir: WHMCS, Blesta, WISECP.

Taban sınıf da arayüz de yoktur. Sihirbaz sınıfınızı adıyla örnekler, dört özellik atar ve yokladığı metotları çağırır.

En önemli biçim parça döngüsüdür. Göç tek istekte bitmez; sihirbaz tek bir veri tipini çağırır, modülünüz satır sayısı ve bitiş işareti döndürür.

Ön Koşullar

  • Kaynak veritabanına okuma erişimi.
  • Kaynak sırları şifreli saklıyorsa onun şifreleme anahtarı.
  • Alan Yardımcıları: içe aktarma ham insert yerine yardımcılardan geçer.
  • Bir geri dönüş noktası; sihirbaz kuyruğa almayı önerir.

Yapı

dosya düzeni
coremio/modules/Imports/AcmeBill/
├── AcmeBill.php      AcmeBill sınıfı, WISECP\Modules\Imports ad alanında
├── hooks.php         isteğe bağlı; yalnız içe aktarılan satırlar sonradan destek isterse
├── lang/
│   ├── en.php        name + area-* (platform kartı) + veri tipi başına bir -desc
│   └── tr.php
└── pages/
    └── index.php     bağlantı formu, sihirbazın platform kartı içinde render edilir
AdminTools::import() Her adımın gönderdiği operation; sınıfınızı çözer ve adım metodunu çağırır.
Modules::Load() Keşif: platform adıyla, ya da liste için All ile çağrılır.
Modules::getPage() pages/index.php dosyasını platform kartınıza koyar.
Config::setd() Kimlik eşlemesinin saklandığı yer; anahtar platform adı ve bağlantı jetonundan kurulur.
coremio/modules/Imports Klasörünüz buraya girer.

Adım Adım

1. İskeleti Kurun

  1. coremio/modules/Imports/AcmeBill/AcmeBill.php dosyasını WISECP\Modules\Imports namespace'inde oluşturun.
  2. Sihirbazın atadığı dört özelliği bildirin; aksi hâlde tipli sınıfta yazma hata verir.
  3. lang/en.php ve lang/tr.php yazın; area-* anahtarları platform kartını kurar.
  4. Veri tipi başına bir <type>-desc anahtarı ekleyin.

2. Bağlanın ve Veri Tiplerini Bildirin

  1. pages/index.php yazın: AcmeBill[db_host] gibi girdiler. Sihirbaz grubu tek dizi olarak verir.
  2. connect() uygulayın: doğrulayın, bağlanın, ayrıntılardan bir jeton üretin. O jeton eşlemenin saklama anahtarının yarısıdır.
  3. Kimlik bilgilerini türüne göre filtreleyin; parola geçirgen filtreden geçer.
  4. data_types() uygulayın: her kayıtta bir ad, bir açıklama ve bir required listesi bulunur.

3. Parça Döngüsünü Yazın

  1. pull_data() uygulayın: tip başına bir kez çağrılır ve yalnız satır sayar.
  2. transfer_data() uygulayın: eşlemeyi yükleyin, tipe özel metoda dağıtın, eşlemeyi her hâlükârda kaydedin, satır sayısı ile bitiş işaretini döndürün.
  3. Tip metodunda en yüksek eşlenmiş kimlikten büyük satırları seçin ve her yeni kimliği eşlemeye yazın.
  4. done değerini, son eşlenmiş kimlikten sonra bir şey kalıp kalmadığını kaynağa sorarak hesaplayın.

4. Zor Kısımlar

  1. Ön koşulları tip metodunun başında alın; tip tek başına seçilebilir.
  2. Moda uyun: temiz mod kimlikleri taşır ve dolu tabloyu reddeder; zenginleştirme yanına ekler.
  3. disconnect() uygulayın; her adımın sonunda çağrılır.
  4. Sonradan çalışacak bir şey varsa hooks.php ekleyin; Blesta çeviremediği parolayı ilk girişte yeniden yazar.

Referans

Sihirbaz Neyi Çağırır

Arayüz yoktur. Bu adlar her çağrıdan önce yoklanır.

sözleşme
// ── sihirbaz tarafından örneğe atanır, dördünü de bildirin ──────────────
public array  $lang;        // dil dosyanız, yöneticinin diline zaten çözülmüş
public string $area_link;   // sihirbazın kendi adresi, sayfanızın içindeki bağlantılar için
public string $name;        // modül adınız, örn. 'AcmeBill'
public object $controller;  // isteği koşturan admin controller'ı

// ── özellik varsa aktarım adımı tarafından okunur ───────────────────────
public array  $selected_data_types = [];   // bu koşuda seçilen her şey
public string $import_type         = 'enrich';   // 'enrich' | 'clean'
public int    $records_per_request = 10;         // operatörün seçtiği parça boyutu

// ── metotlar, her biri çağrılmadan önce method_exists() ile korunur ─────
public function area(): void;                  // bağlantı formunuzu basar
public function connect($info = []): void;         // $info = gönderilen kimlik bilgisi grubu, hazır üç modülde de tipsiz
public function disconnect(): void;                // her adımın sonunda çağrılır

// ['type' => ['name' => string, 'description' => string, 'required' => string[]]]
public function data_types(): array;

// ['status' => 'successful', 'count' => int, 'type' => string]
public function pull_data(string $type = ''): array;

// ['processed' => int, 'done' => bool]  - tek parça, tekrar tekrar çağrılır
public function transfer_data(string $type): array;

Adım Makinesi

Her eylem bir step alanıyla tek bir operation'a gönderir.

stepOperation ne yaparModülünüzde neyi çağırır
validationYedek tercihini saklar, tip listesini süzer.connect(), data_types()
pull_dataBir tipin satır sayısını ister.connect(), pull_data($type)
selectÖn koşulları doğrular, modu ve seçimi hatırlar.yalnız connect()
backup_statusGeri dönüş noktası; modül yüklenmeden önce ele alınır.hiçbir şey
transferKapı kancasını çalıştırır, tek parça aktarır.connect(), transfer_data($type)
connect() her adımda çalışır backup_status dışında. Ucuz tutun: parça başına bir kez.
kimlik bilgileri oturumdan gelir Bir kez gönderilir, oturumda şifreli tutulur, connect() metoduna yeniden verilir.
filter:module.import_data_types Doğrulama adımında tip listenizden geçer. Süzülmüş listeyi döndürün.
gate:module.import_run Her parçadan önce platform, tip ve modla çalışır. Boş olmayan bir dize döndürmek işlemi durdurur.
action:import.completed Her parçadan sonra sonuç dizinizle çalışır, yalnız sonda değil; dönüşü yoksayılır.

Kimlik Eşlemesi

Tek bir yapı üç iş yapar.

yükle, kullan, sakla
// Biçim: ['users' => [sourceId => newId, ...], 'orders' => [...], 'language' => 'english']
// "import_{$this->name}_{$this->token}" altında saklanır; jeton bağlantı ayrıntılarının
// bir özetidir, yani iki kaynak veritabanı asla tek bir koşunun ilerlemesini paylaşmaz.

public function initialize_data($predefined_data = []): void
{
    if ($this->data === null) {
        $data = Config::getd("import_" . $this->name . "_" . $this->token) ?: [];
        if (!$data || !is_array($data)) $data = [];
    }
    else $data = [];

    $this->data = array_replace_recursive($predefined_data, $data);
}

public function save_data(int|array $overwrite_data = -1): void
{
    $data = $this->data ?? [];
    if ($overwrite_data !== -1) $data = $overwrite_data;

    Config::setd("import_" . $this->name . "_" . $this->token, $data);
}

// Devam noktası, o tip için zaten kaydedilmiş en yüksek kaynak kimliğinden ibarettir.
public function last_id($array): int
{
    return $array ? max(array_keys($array)) : 0;
}
devam Sonraki parça en yüksek eşlenmiş kimlikten sonra başlar.
yineleme engeli Aynı süzgeç ikinci bir denemenin her şeyi iki kez almasını engeller; eşlemenin temizlenebilmesinin sebebi budur.
yabancı anahtar çözümü Bir hizmet satırı eski müşteri kimliğinin yenisini ister ve o cevap yalnız bu eşlemededir.
hata fırlatsa bile kaydedilir Dağıtıcı yakalar, eşlemeyi kaydeder ve yeniden fırlatır.

Zenginleştirme ve Temiz Mod

enrich (varsayılan) Satırları var olanların yanına ekler; yeni kimlikler yerelde üretilir.
clean Boş bir hedef içindir: kaynak kimlikleri taşınır, fatura numaraları tanıdık kalır.
temiz mod kapısı İlk parçada hedef tablo satır taşıyorsa hata fırlatın; yoksa taşınan kimlikler çakışır.
kapı yalnız ilk parçada çalışır Koşul temiz mod ve boş eşlemedir. İkinci parçadan sonra hedefteki satırlar sizin yazdıklarınızdır.

Örnek

İskelet artı bir veri tipi, sonra sihirbaz tarafı.

AcmeBill.php
<?php
namespace WISECP\Modules\Imports;

use Config;
use Database;
use Exception;
use Filter;
use Language;
use Modules;
use User;
use Utility;
use Validation;
use WDB;

class AcmeBill
{
    // Sihirbaz tarafından atanır. Burada bildirilmeyen tipli bir özellik yazmada hata verir.
    public array  $lang;
    public string $area_link;
    public string $name;
    public object $controller;

    // Varsa aktarım adımı tarafından okunur.
    public array  $selected_data_types = [];
    public string $import_type         = 'enrich';
    public int    $records_per_request = 10;

    public string $token = '';
    private ?Database $db  = null;
    private ?array $data   = null;

    public function __construct()
    {
        @set_time_limit(0);
    }

    public function area(): void
    {
        echo Modules::getPage("Imports", $this->name, Filter::route($_GET["page"] ?? "index"), [
            'module' => $this,
        ]);
    }

    public function connect($info = []): void
    {
        $host = Filter::html_clear($info["db_host"] ?? '') ?: 'localhost';
        $user = Filter::html_clear($info["db_username"] ?? '');
        $name = Filter::route($info["db_name"] ?? '');

        // Bilerek geçirgen: diğer her filtre bir parolayı güçlü kılan karakterleri kaldırır
        // ve bağlantı sonra kimsenin göremeyeceği bir sebeple başarısız olur.
        $pass = Filter::password($info["db_password"] ?? '');

        if (Validation::isEmpty($user)) throw new Exception(Language::gc("admin/tools/error6"));
        if (Validation::isEmpty($pass)) throw new Exception(Language::gc("admin/tools/error7"));
        if (Validation::isEmpty($name)) throw new Exception(Language::gc("admin/tools/error8"));

        try {
            $this->db = new Database("mysql", "pdo", $host, 3306, $user, $pass, null, $name, "utf8mb4", "utf8mb4_unicode_ci");
        }
        catch (Exception $e) {
            throw new Exception(Language::gc("admin/tools/error9", ['{message}' => $e->getMessage()]));
        }

        // İlerleme anahtarının yarısı: farklı bir kaynak veritabanı farklı bir koşu alır.
        $this->token = md5($host . "+++" . $user . "+++" . $pass . "+++" . $name);
    }

    public function disconnect(): void
    {
        $this->db->disconnect();
    }

    public function data_types(): array
    {
        return [
            'users' => [
                'name'        => Language::gc("admin/tools/import-result-users"),
                'description' => $this->lang["users-desc"] ?? '',
                'required'    => [],
            ],
            'invoices' => [
                'name'        => Language::gc("admin/tools/import-result-invoices"),
                'description' => $this->lang["invoices-desc"] ?? '',

                // Faturalar bir müşteri kimliği taşır ve onu yalnız users eşlemesi çevirebilir.
                // Burada listelenip aynı zamanda seçilmeyen bir tip aktarımdan önce reddedilir.
                'required'    => ['users'],
            ],
        ];
    }

    public function pull_data(string $type = ''): array
    {
        $table = $type === 'users' ? 'clients' : 'invoices';
        $count = $this->db->select("COUNT(id) as total")->from($table)->build();

        return [
            'status' => "successful",
            'count'  => $count ? (int) $this->db->getObject()->total : 0,
            'type'   => $type,
        ];
    }

    public function transfer_data(string $type): array
    {
        $this->initialize_data();

        try {
            $result = match ($type) {
                'users'    => $this->users(),
                'invoices' => $this->invoices(),
                default    => [],
            };
        }
        catch (Exception $e) {
            // Yeniden fırlatmadan önce sakla: zaten yazılmış satırlar eşlemesini korur,
            // böylece yeniden deneme onları ikinci kez almak yerine devam eder.
            $this->save_data();
            throw $e;
        }

        $this->save_data();

        return $result;
    }

    public function users(): array
    {
        $processed = 0;
        $last_id   = $this->last_id($this->data["users"] ?? []);

        $rows = $this->db->select()->from("clients");
        $rows->where("id", ">", $last_id);
        $rows->order_by("id ASC");
        $rows->limit($this->records_per_request);
        $rows = $rows->build() ? $rows->fetch_assoc() : [];

        foreach ($rows as $row) {
            $userId = (int) User::create([
                'type'          => "member",
                'status'        => ($row["status"] ?? '') === 'Active' ? "active" : "passive",
                'name'          => Filter::html_clear($row["firstname"] ?? ''),
                'surname'       => Filter::html_clear($row["lastname"] ?? ''),
                'full_name'     => Filter::html_clear(trim(($row["firstname"] ?? '') . ' ' . ($row["lastname"] ?? ''))),
                'email'         => $row["email"] ?? '',
                'creation_time' => $row["created_at"] ?? null,
            ]);
            if (!$userId) continue;

            // Eşleme kaydı hem devam noktası, hem yineleme kalkanı, hem de fatura tipinin
            // bu müşteriyi yeniden bulmak için kullanacağı anahtardır. Hemen yazın.
            $this->data["users"][(int) $row["id"]] = $userId;
            $processed++;
        }

        // Sayıları karşılaştırmak yerine kaynağa ne kaldığını sorun: koşu ortasında
        // kaynakta silinen bir satır aksi hâlde döngüyü sonsuza dek sürdürür.
        $last_id = $this->last_id($this->data["users"] ?? []);
        $done    = !$this->db->select("id")->from("clients")->where("id", ">", $last_id)->limit(1)->build();

        return ['processed' => $processed, 'done' => $done];
    }

    public function invoices(): array
    {
        $processed = 0;
        $last_id   = $this->last_id($this->data["invoices"] ?? []);

        // Temiz mod kaynak kimliklerini taşır, bu yüzden boş bir tablo ister. Kontrol yalnız
        // ilk parçada koşar: ikinciden itibaren bulunan satırlar bizim yazdıklarımızdır.
        if ($this->import_type === "clean" && $last_id === 0) {
            if (WDB::select("id")->from("invoices")->limit(1)->build())
                throw new Exception(Language::gc("admin/tools/import-type-clean-error"));
        }

        $rows = $this->db->select()->from("invoices");
        $rows->where("id", ">", $last_id);
        $rows->order_by("id ASC");
        $rows->limit($this->records_per_request);
        $rows = $rows->build() ? $rows->fetch_assoc() : [];

        foreach ($rows as $row) {
            // Yabancı anahtarlar eşleme üzerinden çözülür, asla kaynak kimliği üzerinden değil.
            $ownerId = (int) ($this->data["users"][(int) ($row["client_id"] ?? 0)] ?? 0);
            if (!$ownerId) continue;   // sahip bu koşuda değil; ön koşul denetimi bunu engeller

            $data = [
                'owner_id' => $ownerId,
                'total'    => (float) ($row["total"] ?? 0),
                'status'   => ($row["status"] ?? '') === 'Paid' ? "paid" : "unpaid",
                'cdate'    => $row["created_at"] ?? null,
            ];

            // Kaynak numaralandırmasını yalnız temiz mod korur.
            if ($this->import_type === "clean") $data["id"] = (int) $row["id"];

            WDB::insert("invoices", $data);
            $this->data["invoices"][(int) $row["id"]] = (int) WDB::lastID();
            $processed++;
        }

        $last_id = $this->last_id($this->data["invoices"] ?? []);
        $done    = !$this->db->select("id")->from("invoices")->where("id", ">", $last_id)->limit(1)->build();

        return ['processed' => $processed, 'done' => $done];
    }

    public function initialize_data($predefined_data = []): void
    {
        $data = $this->data === null
            ? (Config::getd("import_" . $this->name . "_" . $this->token) ?: [])
            : [];
        if (!is_array($data)) $data = [];

        $this->data = array_replace_recursive($predefined_data, $data);
    }

    public function save_data(int|array $overwrite_data = -1): void
    {
        $data = $this->data ?? [];
        if ($overwrite_data !== -1) $data = $overwrite_data;

        Config::setd("import_" . $this->name . "_" . $this->token, $data);
    }

    public function last_id($array): int
    {
        return $array ? max(array_keys($array)) : 0;
    }
}
pages/index.php
<?php if (!defined("CORE_FOLDER")) die("403 Forbidden"); ?>
<div class="cred-stack">
    <div>
        <label for="AcmeBill_Host" class="form-label"><?php echo Language::gc("admin/tools/import-db-host"); ?></label>
        <!-- Her girdi platform adıyla ad alanlanır: grubun tamamı tek bir dizi
             olarak varır ve connect() metoduna $info olarak verilir. -->
        <input id="AcmeBill_Host" type="text" class="form-control" name="AcmeBill[db_host]" value="">
    </div>
    <div>
        <label for="AcmeBill_Name" class="form-label"><?php echo Language::gc("admin/tools/import-db-name"); ?></label>
        <input id="AcmeBill_Name" type="text" class="form-control" name="AcmeBill[db_name]" value="" data-role="db-name">
    </div>
    <div>
        <label for="AcmeBill_User" class="form-label"><?php echo Language::gc("admin/tools/import-db-username"); ?></label>
        <input id="AcmeBill_User" type="text" class="form-control" name="AcmeBill[db_username]" value="">
    </div>
    <div>
        <label for="AcmeBill_Pass" class="form-label secret-input"><?php echo Language::gc("admin/tools/import-db-password"); ?></label>
        <input id="AcmeBill_Pass" type="password" class="form-control" name="AcmeBill[db_password]" value="">
    </div>
</div>
sihirbaz tarafı, kopyalamayın
// operations/AdminTools.php - örneğinizin nasıl kurulup sürüldüğü, indirgenmiş.
$load = Modules::Load("Imports", $platform);
if (!$load) throw new Exception("The platform is not supported. #1");

$instanceKey = "\\WISECP\\Modules\\Imports\\" . $platform;
if (!class_exists($instanceKey)) throw new Exception("The platform is not supported. #2");

$instance             = new $instanceKey();
$instance->lang       = $load["lang"] ?? [];
$instance->area_link  = LinkGenerator::admin("tools-1", ["imports"]);
$instance->name       = $platform;
$instance->controller = $this;

if (method_exists($instance, "connect")) $instance->connect($platform_info);

// Ön koşullar seçim adımında, tek bir satır bile taşınmadan önce uygulanır.
foreach ($data_Types as $selected_type)
    foreach (($data_types[$selected_type]["required"] ?? []) as $needed)
        if (!in_array($needed, $data_Types, true))
            throw new Exception(Language::gc("admin/tools/import-missing-prerequisite", [
                '{type}'   => $data_types[$selected_type]["name"] ?? $selected_type,
                '{needed}' => $data_types[$needed]["name"] ?? $needed,
            ]));

// Aktarım adımı, parça başına bir kez.
if (property_exists($instance, "selected_data_types")) $instance->selected_data_types = $data_types_s;
if (property_exists($instance, "import_type"))         $instance->import_type         = $import_type;
if (property_exists($instance, "records_per_request")) $instance->records_per_request = $records_per_request;

$result = $instance->transfer_data($type);       // ['processed' => int, 'done' => bool]

if (method_exists($instance, "disconnect")) $instance->disconnect();

Tuzaklar

Bayat eşleme hiçbir şey aktarmaz ama başarı bildirir

Parça sorgusu eşlenmiş her şeyi atladığı için ikinci deneme hiç satır bulmaz ve çalışmış gibi görünür. "Baştan başla" için eşlemeyi temizleyin.

Her ön koşulu bildirin

Yabancı anahtarı başka bir tipin eşlemesinden çözen tip onu required içinde adlandırmalıdır. Yazmazsanız arama anında eşleme boştur: satırlar sahipsiz yazılır ve hiçbir şey hata vermez.

Her kimlik bilgisini türüne göre filtreleyin

Grubun tamamını tek bir metin filtresinden geçirmek parolayı ve şifreleme anahtarını sessizce bozar; arıza yanlış kimlik bilgisi gibi görünür. Sırlar için geçirgen filtre kullanın.

done değerini sayarak hesaplamayın

İşlenen satırları baştaki toplamla karşılaştırmak, kaynak değişir değişmez bozulur. Son eşlenmiş kimlikten sonra ne kaldığını kaynağa sorun.

Yardımcılar üzerinden yazın

Müşteri, sipariş, hizmet ve faturaların ham insert'ün atladığı yan etkileri vardır. Global sınıfları açıkça içe alın.

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.