İçe Aktarma Modülü 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ı: içe aktarma ham insert yerine yardımcılardan geçer.
- Bir geri dönüş noktası; sihirbaz kuyruğa almayı önerir.
Yapı
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
All ile çağrılır.
pages/index.php dosyasını platform kartınıza koyar.
Adım Adım
1. İskeleti Kurun
coremio/modules/Imports/AcmeBill/AcmeBill.phpdosyasınıWISECP\Modules\Importsnamespace'inde oluşturun.- Sihirbazın atadığı dört özelliği bildirin; aksi hâlde tipli sınıfta yazma hata verir.
lang/en.phpvelang/tr.phpyazın;area-*anahtarları platform kartını kurar.- Veri tipi başına bir
<type>-descanahtarı ekleyin.
2. Bağlanın ve Veri Tiplerini Bildirin
pages/index.phpyazın:AcmeBill[db_host]gibi girdiler. Sihirbaz grubu tek dizi olarak verir.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.- Kimlik bilgilerini türüne göre filtreleyin; parola geçirgen filtreden geçer.
data_types()uygulayın: her kayıtta bir ad, bir açıklama ve birrequiredlistesi bulunur.
3. Parça Döngüsünü Yazın
pull_data()uygulayın: tip başına bir kez çağrılır ve yalnız satır sayar.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.- Tip metodunda en yüksek eşlenmiş kimlikten büyük satırları seçin ve her yeni kimliği eşlemeye yazın.
donedeğerini, son eşlenmiş kimlikten sonra bir şey kalıp kalmadığını kaynağa sorarak hesaplayın.
4. Zor Kısımlar
- Ön koşulları tip metodunun başında alın; tip tek başına seçilebilir.
- Moda uyun: temiz mod kimlikleri taşır ve dolu tabloyu reddeder; zenginleştirme yanına ekler.
disconnect()uygulayın; her adımın sonunda çağrılır.- Sonradan çalışacak bir şey varsa
hooks.phpekleyin; 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.
// ── 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) |
backup_status dışında. Ucuz tutun: parça başına bir kez.
connect() metoduna yeniden verilir.
Kimlik Eşlemesi
Tek bir yapı üç iş yapar.
// 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;
}
Zenginleştirme ve Temiz Mod
Örnek
İskelet artı bir veri tipi, sonra sihirbaz tarafı.
<?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;
}
}
<?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>
// 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
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.
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.
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.
İş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.
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
Geri bildiriminiz için teşekkürler!
Yukarıda bulamadığınız her şey için destek ekibimiz her zaman yanınızda.