# İçe Aktarma Modülü Yazma

https://dev.wisecp.com/tr/ice-aktarma-modulu-yazma

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ı](https://dev.wisecp.com/tr/alan-yardimcilari): içe aktarma ham insert yerine yardımcılardan geçer.
- Bir geri dönüş noktası; sihirbaz kuyruğa almayı önerir.

## Yapı

```bash
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.

```php
// ── 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.

| step | Operation ne yapar | Modülünüzde neyi çağırır |
| --- | --- | --- |
| `validation` | Yedek tercihini saklar, tip listesini süzer. | `connect()`, `data_types()` |
| `pull_data` | Bir 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_status` | Geri dönüş noktası; modül yüklenmeden **önce** ele alınır. | hiçbir şey |
| `transfer` | Kapı 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.

```php
// 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ı.

```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;
    }
}
```

```html
<?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>
```

```php
// 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.

## İlgili Makaleler

- [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi)
- [Alan Yardımcıları](https://dev.wisecp.com/tr/alan-yardimcilari)
- [WDB ile Sorgulama](https://dev.wisecp.com/tr/wdb-ile-sorgulama)
- [Modülden Kanca Kaydetme](https://dev.wisecp.com/tr/modulden-kanca-kaydetme)
- [Kullanıcı Girdisini Filtreleme](https://dev.wisecp.com/tr/kullanici-girdisini-filtreleme)
- [Operation'lar](https://dev.wisecp.com/tr/operationlar)
