CSV İçe Aktarma
Sütun eşleme, ayraç ve kodlama tespiti, dry-run önizleme, atomik veya kısmi commit ve indirilebilir hata raporu — PHP PDO, Ajax, DataTables ile.
Ekran Görüntüleri 4 görsel
Bu Örnek Ne Yapıyor?
- Üç adımlı sihirbaz: yükle → sütun eşle ve önizle → içe aktar
- Dosya bir kez yüklenir, oturuma bağlı bir token ile üç kez kullanılır
- Dry-run önizleme, gerçek aktarmayla AYNI doğrulama fonksiyonunu çalıştırır ama hiçbir şey yazmaz
- Ayraç tespiti (virgül, noktalı virgül, TAB, dikey çizgi) — tırnak içindeki ayraçlar sayılmaz
- Kodlama tespiti: UTF-8 BOM, UTF-8 ve Windows-1254 (ISO-8859-9)
- Sütun eşlemesi tahmin edilir ama dayatılmaz; Türkçe ve İngilizce başlık takma adları tanınır
- Hata ile uyarı ayrımı: kimlik alanı eksikse satır reddedilir, nitelik alanı kusurluysa girer
- Telefon normalizasyonu: +90, baştaki sıfır, boşluk ve tire temizlenip 10 haneye indirgenir
- Atomik mod: bir hata bile varsa hiçbir satır yazılmaz (HTTP 409)
- Kısmi mod: geçerli satırlar yazılır, atlananlar indirilebilir bir hata raporu CSV'sine düşer
- Tekrar politikası seçilebilir: veritabanındaki kaydı atla veya CSV ile güncelle
- İçe aktarma künyesi ve kayıtlar aynı transaction'da yazılır — izi olmayan kayıt oluşamaz
- Benzersiz e-posta indeksi; 23000 hatası indeks adına bakılarak ayırt edilir
- CSV formül enjeksiyonu koruması, CSRF, dört ayrı hız sınırı kovası
- Paylaşılabilir derin bağlantı (#kisi-4) ve mobilde sıfır yatay kaydırma
Nerede İşe Yarar?
- Müşteri, bayi veya abone listesini Excel'den panele aktarmak
- Ürün ve stok listelerini toplu güncellemek (tekrar politikası: güncelle)
- Web formu dökümlerini mevcut veritabanına tekrarsız eklemek
- Muhasebe ve finans gibi "yarım veri, yanlış veridir" durumlarında atomik aktarma
- Başka bir sistemden gelen, biçimi belirsiz CSV dosyalarını temizleyerek almak
- Kullanıcının kendi dosyasını yüklediği her panelde güvenli yükleme deseni
Gereksinimler
- PHP 8.0+ (pdo_mysql, mbstring, fileinfo) · MySQL 5.7+ veya MariaDB 10.3+ · Apache/mod_rewrite
Nasıl Kurulur?
- Depoyu indirin ya da ZIP olarak açın.
- Veritabanını kurun: mysql -u root -p < cy_import.sql (dosya veritabanını kendisi oluşturur).
- storage/imports/ klasörünün yazılabilir olduğundan emin olun.
- Projeyi bir web sunucusu altına koyun ya da php -S 127.0.0.1:8000 ile çalıştırın.
- Tarayıcıda açın; 33 kişi ve 6 içe aktarma kaydı dolu bir ekran gelir.
- "Örnek CSV ile dene" düğmesiyle akışı tek tıkla deneyin.
- Canlıya alırken system/config.local.php.example dosyasını config.local.php olarak kopyalayıp künyeyi oraya yazın.
Veritabanı şeması projedeki cy_csv_import.sql dosyasında.
Nasıl Çalışıyor?
CSV İçe Aktarma
PHP PDO · MySQL · AJAX · DataTables · Bootstrap 5 · Çılgın Yazılım Tasarım Kalıbı
İçe aktarmadan önce ne olacağını göster; sonra ya hepsini yaz ya da sadece temizleri.
🇹🇷 Türkçe · 🇬🇧 English
Canlı Demo
Kurulum yok, kayıt yok, indirme yok — tarayıcınızdan 3 saniyede deneyin.
▲ Görsele tıklayarak demoyu açabilirsinizDemoda 60 saniyede neleri deneyebilirsiniz?
| # | Şunu deneyin | Perde arkasında ne oluyor? | |||
|---|---|---|---|---|---|
| 1 | ▶ Örnek CSV ile dene düğmesine basın | Depodaki sample.csv yüklenir. Ayraç ; olarak içerikten tespit edilir, kodlama sınanır, başlıklar hedef alanlara tahminle eşlenir ("ad soyad" → Ad Soyad) | |||
| 2 | Önizle (dry-run) deyin | Veritabanına hiçbir şey yazılmaz. 14 satırın her biri tek tek doğrulanır ve özet çıkar: 6 temiz · 1 uyarı · 3 hata · 1 dosya içi tekrar · 3 veritabanı tekrarı | |||
| 3 | Kırmızı satırlara bakın | Doğrulama veri döndürmediği için ham hücreler gösterilir: Hatalı Kayıt \ | gecersiz-eposta \ | 000 \ | Konya. Hangi satırın neden reddedildiği okunabilir |
| 4 | "E-posta zaten varsa?" seçimini Güncelle yapın | Özet anında yeniden hesaplanır: "Eklenecek 7 / Güncellenecek 0" iken "Eklenecek 0 / Güncellenecek 10" olur. Karar, kararın sonucuna bakılarak verilir | |||
| 5 | Atomik modu seçip içe aktarın | HTTP 409 döner ve hiçbir kayıt yazılmaz — dosyada 3 hatalı satır var. Bu bir arıza değil, sözleşmenin çalışmasıdır | |||
| 6 | Kısmi moda geçip tekrar aktarın | 7 kayıt girer, 7 satır atlanır ve "Hata raporunu indir" düğmesi çıkar. İnen CSV, atlanan her satırı numarası ve sebebiyle geri verir | |||
| 7 | Kişiler tablosunda telefonlara bakın | CSV'de +90 533 210 4477, 0533 210 44 77 ve 533-987-6543 yazıyordu; hepsi 10 haneye indirgenip tek biçimde saklandı | |||
| 8 | 👁 Göz butonuna basın | "Bu kayıt nereden geldi?" bölümü açılır: hangi dosya, hangi ayraç, hangi kodlama, hangi mod. Adres çubuğu #kisi-4 olur — bağlantı paylaşılabilir | |||
| 9 | Geçmişte #3 numaralı satıra bakın | İptal edildi — atomik modda çalışmış, tek hatalı satır tüm aktarımı durdurmuş. Denenen ve başarısız olan aktarma da kayıttadır | |||
| 10 | Telefonunuzdan açın | Tablo yatay kaydırmaya zorlamaz; ikincil sütunlar gizlenir, bilgi detay modalında durur |
İpucu: Demoyu açıkken F12 → Network sekmesini açın.upload→preview→commitüçlüsünü, aralarında taşınantokenalanını ve HTTP durum kodlarını (200 / 403 / 409 / 422 / 429) canlı görebilirsiniz.
Demo alanı hakkında bilinmesi gerekenler
| Konu | Durum |
|---|---|
| Veriler | cy_import.sql içindeki 33 kişi + 6 içe aktarma kaydı. Tamamı uydurmadır; gerçek kişi verisi yoktur. |
| Sıfırlama | Demo veritabanı düzenli aralıklarla başlangıç haline döner; aktardığınız kayıtlar kalıcı değildir. |
| Yüklenen dosyalar | Geçici klasöre alınır, işlem biter bitmez silinir; kalanlar 30 dakika sonra otomatik temizlenir. Klasör web'e kapalıdır. |
| Kimlik doğrulama | Yoktur. Bilinçli bir tercihtir — örnek, içe aktarma akışına odaklanır. |
APP_DEBUG | Canlıda otomatik false — sunucu adından türetilir, yerelde true kalır. |
| Bağımlılık | Sıfır. Composer yok, npm yok, CDN yok. Demo internetsiz bir sunucuda da aynı çalışır. |
Demo geçici olarak kapalıysa endişelenmeyin: depoyu klonlayıp cy_import.sql'i içe aktarmanız aynı ekranı kendi bilgisayarınızda 2 dakikada ayağa kaldırır → Kurulum
Bu Proje Nedir?
Neredeyse her panelde bir "CSV yükle" düğmesi vardır ve arkasında genellikle tek bir döngü durur: dosyayı aç, satırları oku, hepsini INSERT et.
O döngü 800. satırda patladığında ilk 799 kayıt yarım girmiştir. Kullanıcı ne olduğunu göremez, hangi satırın bozuk olduğunu bilemez ve yapabileceği tek şey aynı dosyayı ikinci kez yüklemektir — bu sefer veriler ikiye katlanır. Böyle bir ekran bir içe aktarma aracı değil, kontrolsüz bir yazma noktasıdır.
Bu proje aynı işi üç adıma böler ve her adımın neden ayrı olduğunu anlatır:
- Yükle — dosya bir kez ağdan geçer, ayracı ve kodlaması içerikten tespit edilir
- Eşle ve önizle — her satır doğrulanır ve ne olacağı veritabanına dokunulmadan gösterilir
- Aktar — kullanıcı iki kararı verdikten sonra yazılır: hata olursa ne olsun? ve e-posta zaten varsa ne olsun?
Ve dördüncü bir şey daha yapar: her aktarmanın künyesini tutar. Üç hafta sonra "bu 400 kayıt nereden geldi?" diye sorulduğunda cevap ekranda durur — hangi dosya, hangi ayraç, hangi modda, kaç satır girdi, kaç tanesi atlandı.
Kimler için uygun?
- Kendi projesine dosyadan veri aktarma özelliği ekleyecekler
- "Aynı dosyayı iki kez yükledim, kayıtlar ikiye katlandı" sorununu üretimde yaşamış olanlar
- Türkçe Excel'den çıkan
;ayraçlı, ISO-8859-9 kodlamalı dosyalarla boğuşanlar - PHP + AJAX + DataTables üçlüsünü doğru öğrenmek isteyenler
- Bootstrap 5 üzerine kurulu, tekrar kullanılabilir bir tasarım kalıbı arayanlar
Klonla, cy_import.sql'i içe aktar, çalıştır. Başka hiçbir kurulum adımı yok. Composer yok, npm yok, internet bağlantısı bile gerekmiyor — tüm kütüphaneler proje içinde.
Bu proje, Çılgın Yazılım Kütüphanesi altında yayınlanan açıklamalı, üretime hazır örneklerden biridir.
İçindekiler
- Canlı Demo
- Ekran Görüntüleri
- Dört Kritik Karar
- Neler Var?
- Güvenlik: Neyi, Nasıl Kapattık?
- Kurulum
- Yapılandırma
- Kendi Projenize Eklemek
- Çılgın Yazılım Tasarım Kalıbı
- Dosya Yapısı
- Nasıl Çalışıyor?
- AJAX API Referansı
- Veritabanı Şeması
- Sık Sorulanlar
- Canlı Ortama Alırken
- Sorun Giderme
- Yol Haritası
- Katkı
- Lisans
Ekran Görüntüleri
Genel görünüm
Üstte sihirbaz, ortada içe aktarmanın hedef tablosu, altta her aktarmanın künyesi. Sayaç şeridi işin nabzını gösterir: kaç kişi var, kaçı CSV'den geldi, kaç aktarma yapıldı, kaç satır atlandı.

Dry-run önizleme — hiçbir şey yazılmadan
Her satırın ne olacağı önceden görülür. Dosya künyesi (ayraç, kodlama, sütun sayısı) eşleme alanlarının hemen üstündedir; beklenmedik bir sonuçta ilk bakılacak yer orasıdır. "Atomik mi kısmi mi?" kararı ancak bu tabloyu gördükten sonra anlamlıdır.

Kişi detayı — "bu kayıt nereden geldi?"
Bir kaydın hangi dosyadan, hangi ayraç ve kodlamayla, hangi modda geldiği tek ekranda. Adres çubuğu #kisi-4 olur; bağlantıyı gönderdiğinizde karşı taraf doğrudan o kaydı açar.

Mobil görünüm
Dar ekranda tablo yatay kaydırmaya zorlamaz: ikincil sütunlar gizlenir, bilgi detay modalında korunur. Dokunma hedefleri en az 32–44px'dir.
Üç modal:
| Modal | Açılış | İçerik |
|---|---|---|
| 👁 Kişi detayı | Kişi tablosundaki göz butonu | Alanların tam hâli + geldiği içe aktarmanın künyesi + paylaşılabilir #kisi-4 bağlantısı |
| 👁 İçe aktarma detayı | Geçmiş tablosundaki göz butonu | Satır sayaçları, mod, dedupe politikası, süre, "bu aktarmanın kişilerini göster" |
| 🗑 Silme onayı | Çöp kutusu butonu | Neyin silineceği + "bu e-posta yeniden aktarılabilir hâle gelir" uyarısı |
Dört Kritik Karar
1) Dry-run: yazmadan önce göstermek
İçe aktarma ekranlarının klasik kusuru, kullanıcıyı karanlıkta karar vermeye zorlamasıdır. "Yükle" düğmesine basana kadar dosyanın kaç satırının bozuk olduğu bilinmez; basıldıktan sonra da geri dönüş yoktur.
preview uç noktası dosyanın tamamını commit ile birebir aynı kodla (validate_row()) işler, ama tek bir INSERT çalıştırmaz:
// handle_preview() — hiçbir yazma yok, yalnızca sayım
$summary = ['total' => 0, 'ok' => 0, 'warning' => 0,
'error' => 0, 'dupe_file' => 0, 'dupe_db' => 0];Bu simetri şart: önizleme ayrı bir doğrulama kodu kullansaydı, zamanla ikisi ayrışır ve "önizlemede temizdi ama aktarırken patladı" durumu doğardı. Önizleme, aktarmanın kuru çalıştırılmasıdır — taklidi değil.
2) Hata ile uyarı ayrımı
Her kusuru hataya çevirmek kolaydır ama pahalıdır: %2'lik kirli veri yüzünden %98'lik temiz veriyi de reddetmek demektir.
// Ad yoksa satır YAZILAMAZ → hata
if (mb_strlen($name) < 2) {
return ['data' => [], 'error' => 'Ad en az 2 karakter olmalı', ...];
}
// Telefon tuhafsa satır yine de değerlidir → uyarı
if (strlen($digits) !== 10) {
return [$digits, 'Telefon 10 hane değil (yine de kaydedilecek)'];
}Kural şu: kaydın kimliğini kuran alanlar (ad, e-posta) eksikse satır reddedilir; kaydı zenginleştiren alanlar (telefon, şehir) kusurluysa satır girer ve kullanıcı uyarılır.
3) Tekrarın üç farklı anlamı
"Bu e-posta zaten var" cümlesi üç ayrı duruma karşılık gelir ve üçünün de çözümü farklıdır:
| Durum | Ne demek? | Ne yapılır? |
|---|---|---|
dupe_file | Aynı e-posta dosya içinde iki kez geçiyor | İlk görülen kazanır; ikincisi atlanır ve raporlanır |
dupe_db + dedupe=skip | Kayıt veritabanında zaten var | Mevcut kayda dokunulmaz; var olan veri kaynak sayılır |
dupe_db + dedupe=update | Kayıt var, ama CSV daha güncel | Mevcut kayıt CSV ile tazelenir; dosya kaynak sayılır |
Son satır bir tercih değil, verinin kime ait olduğuyla ilgili bir karardır: telefon güncelleme dosyasında CSV haklıdır, müşteri listesi eklemede veritabanı haklıdır. Bu yüzden seçim kullanıcıya bırakılır ve önizleme, seçim değiştiğinde anında yeniden hesaplanır.
Ama asıl koruma uygulama kodunda değil, veritabanı kısıtındadır:
UNIQUE KEY `uq_contacts_email` (`email`)"Önce
SELECT, yoksa INSERT" kontrolü iki eşzamanlı istekte yarışır ve kaybeder; UNIQUE indeks kaybetmez. Kod yine de önce SELECT yapar — ama bunu önizleme için yapar. Yazma anındaki gerçek koruma indeksin kendisidir ve yarış durumunda dönen hata indeks adına bakılarak ayırt edilir:
$isDupeEmail = $ex->getCode() === '23000'
&& str_contains($ex->getMessage(), 'uq_contacts_email');
if (!$isDupeEmail) {
throw $ex; // başka bir kısıt ihlali — yanlış mesajla gizleme
}
4) Künye ve kayıtlar aynı transaction'da
import_runs satırı, kişilerle aynı transaction içinde açılır ve aynı transaction içinde sayaçlarıyla güncellenir:
$db->beginTransaction();
$runStmt->execute([...]); // künye açılır (boş sayaçlarla)
$runId = (int) $db->lastInsertId();
foreach ($rows as $raw) { ... } // kişiler bu künyeye bağlanır
$db->prepare('UPDATE import_runs SET inserted = ...')->execute([...]);
$db->commit();Künye ayrı yazılsaydı, kişiler girip künye yazılamadığında izi olmayan 400 kayıt oluşurdu — tam olarak bu projenin çözmeye çalıştığı sorun. Atomik modda fren devreye girdiğinde
rollBack() künyeyi de siler; bu yüzden hemen ardından ayrı bir aborted künyesi yazılır: denenip iptal edilen aktarma da geçmişte görünmelidir.
Neler Var?
İçe aktarma çekirdeği
- Üç adımlı sihirbaz:
upload→preview→commit - Dosya bir kez yüklenir, token ile üç kez kullanılır
- Ayraç tespiti:
,;TAB|— tırnak içi ayraçlar sayılmaz - Kodlama tespiti: UTF-8 / UTF-8 BOM / ISO-8859-9 (Windows-1254)
- Sütun eşleme tahmini, kullanıcı tarafından değiştirilebilir
- Türkçe + İngilizce başlık takma adları (
eposta,mail,e-posta…) - Dry-run önizleme: hata / uyarı / dosya içi tekrar / DB tekrarı
- İki mod: atomik (hepsi ya da hiçbiri) ve kısmi
- İki dedupe politikası: mevcudu atla veya güncelle
- Telefon normalizasyonu:
+90,0, boşluk, tire → 10 hane - Hata raporu CSV: satır numarası + sebep + ham hücreler
Ekranlar ve altyapı
- Sayaç şeridi: kişi, CSV'den gelen, aktarma, atlanan satır, son aktarma
- Kişiler tablosu (DataTables server-side): arama, şehir ve kaynak filtresi
- İçe aktarma geçmişi: künye, sayaçlar, sonuç rozeti, süre
- "Bu aktarmanın kişilerini göster" — iki tabloyu bağlayan
import_id - CSV dışa aktarım, ekrandaki aynı filtrelerle
- Paylaşılabilir derin bağlantı:
#kisi-4,#aktarma-3 - Sürükle-bırak dosya yükleme
- Markayla uyumlu silme onayı modalı (
confirm()yok) - Çift gönderim koruması, toast bildirimleri,
aria-live - Dört ayrı hız sınırı kovası (upload / preview / commit / export)
- Mobil: 991 / 767 / 480px eşikleri, yatay kaydırma yok
Güvenlik: Neyi, Nasıl Kapattık?
Bu projede saldırı yüzeyi alışılmadık biçimde geniştir: içeriği tamamen kullanıcının belirlediği bir dosyayı okuyup ekrana basıyor ve veritabanına yazıyoruz.
| Açık | Tipik hatalı kod | Bu projede |
|---|---|---|
| SQL Injection | "... WHERE city = '".$_POST['city']."'" | Tüm sorgular prepared statement, EMULATE_PREPARES = false. Sıralama sütunu beyaz listeden geçer; source, mode, dedupe ve status değerleri sabit listelerle doğrulanır. |
| XSS (önizleme tablosunda) | $('#cell').html(row.name) | Önizlemedeki her hücre doğrudan yüklenen dosyadan gelir. Sunucuda e(), istemcide esc(). Bir CSV hücresinde ` bulunması kural dışı değil, beklentidir. |
| CSV formül enjeksiyonu | Hücreyi olduğu gibi yazmak | =, +, -, @ ile başlayan hücrelerin başına tek tırnak konur. Hata raporu saldırganın kendi metnini geri yazdığı için burada risk teorik değildir. |
| Path traversal | IMPORT_DIR.'/'.$_POST['token'].'.csv' | Token yalnızca onaltılık olabilir (ctype_xdigit), ayrıca dosya adı session_id() ile öneklenir — token'ı ele geçiren başka bir ziyaretçi bile dosyaya ulaşamaz. |
| Yüklenen dosyanın çalıştırılması | Dosyayı web köküne yazmak | Dosyalar storage/imports/ altına gider; klasörde Require all denied vardır ve içerik hiçbir zaman include edilmez. |
| Uzantıya güvenmek | if ($ext === 'csv') | Tür içerikten okunur (finfo); text/ dışındaki türler reddedilir. |
| CSRF | (genelde hiç yok)* | Oturuma bağlı 32 baytlık token, her yazma isteğinde, hash_equals() ile sabit zamanlı doğrulama. Token'sız istek → HTTP 403. |
| İzi olmayan kayıt | Önce INSERT, sonra ayrı bir INSERT | Kişiler ve içe aktarma künyesi tek transaction. Künye yazılamazsa kişiler de geri alınır. |
| Yarış durumunda çift kayıt | "Önce SELECT, yoksa INSERT" | uq_contacts_email benzersiz indeksi. 23000 hatası indeks adına bakılarak ayırt edilir; başka bir kısıt ihlali yutulmaz. |
| Bozuk kodlamanın yanıtı yutması | json_encode() → false → boş gövde | Dosya okunurken UTF-8'e çevrilir (csv_to_utf8()); ikinci hat olarak JSON_INVALID_UTF8_SUBSTITUTE. Tek bayt yüzünden 5000 satır kaybolmaz. |
| Kaynak tüketimi (DoS) | while (fgetcsv(...)) sınırsız | IMPORT_MAX_ROWS (5000), IMPORT_MAX_BYTES (5 MB), PAGE_SIZE_MAX (200), EXPORT_MAX_ROWS (5000) ve dört ayrı hız sınırı kovası. |
| Sahipsiz dosya birikmesi | Yüklenen dosyayı silmemek | İş biter bitmez unlink(); ayrıca her istekte fırsatçı gc, IMPORT_TTL_MIN (30 dk) dolmuş dosyaları temizler. |
| LIKE joker karakterleri | LIKE '%$search%' | %, _, \ kaçışlanır. |
| Bilgi sızıntısı | Ekrana basılan MySQL hataları | APP_DEBUG sunucu adından türetilir; canlıda otomatik false, detay error_log()'a gider. |
| Kurulum dosyasının indirilmesi | /cy_import.sql → HTTP 200 | .htaccess: .sql, .md, .json, .log` … kapalı (README dosyaları bilinçli istisnadır). |
Kurulum
Sadece görmek istiyorsanız kurulum gerekmez → Canlı Demoyu açın. Aşağıdaki adımlar projeyi kendi bilgisayarınızda çalıştırmak içindir (~2 dakika).
Gereksinimler
- PHP 8.0+ (
pdo_mysql,mbstringvefileinfoeklentileri) - MySQL 5.7+ veya MariaDB 10.3+
- Apache (XAMPP / WAMP / Laragon) — ya da PHP'nin yerleşik sunucusu
Adımlar
1 — Projeyi indirin
git clone https://github.com/CilginYazilim/PHP-MySQL-CSV-Import-Ice-Aktarma-PDO-Ajax-DataTables.git
cd PHP-MySQL-CSV-Import-Ice-Aktarma-PDO-Ajax-DataTables2 — Veritabanını oluşturun
cy_import.sql veritabanını da kendisi oluşturur; önceden cy_import adında bir veritabanı açmanıza gerek yok.
mysql -u root -p < cy_import.sqlphpMyAdmin ile: İçe Aktar → Dosya seç →
cy_import.sql → Başlat
3 — storage/imports/ klasörünün yazılabilir olduğundan emin olun
Windows'ta genelde sorun çıkmaz. Linux'ta:
chmod 750 storage/imports4 — Çalıştırın
php -S 127.0.0.1:8000XAMPP kullanıyorsanız projeyi
htdocs altına koyup şu adresi açın:http://localhost/PHP-MySQL-CSV-Import-Ice-Aktarma-PDO-Ajax-DataTables/
5 — Tarayıcıda açın → http://127.0.0.1:8000/
Karşınıza 33 kişi ve 6 örnek içe aktarma kaydı dolu, çalışır durumda bir ekran gelecek. ▶ Örnek CSV ile dene düğmesine basarak akışı hemen deneyebilirsiniz.
Yapılandırma
Tüm ayarlar system/config.php içindedir.
| Sabit | Varsayılan | Ne işe yarar | |
|---|---|---|---|
IMPORT_MAX_ROWS | 5000 | Bir dosyada işlenecek en fazla veri satırı | |
IMPORT_MAX_BYTES | 5 MB | Yüklenecek dosya boyutu tavanı | |
IMPORT_DIR | storage/imports | Geçici dosya klasörü (web'e kapalı) | |
IMPORT_TTL_MIN | 30 | Sahipsiz geçici dosyaların ömrü (dakika) | |
PREVIEW_MAX_ROWS | 200 | Önizleme tablosunda gösterilecek en fazla satır | |
DEDUPE_DEFAULT | skip | Açılıştaki tekrar politikası (skip \ | update) |
TARGET_FIELDS | name, email, phone, city | Eşlenebilecek hedef alanlar | |
REQUIRED_FIELDS | name, email | Eşlenmeden aktarma başlatılamayan alanlar | |
PAGE_SIZE_MAX | 200 | DataTables sayfa boyutu tavanı | |
EXPORT_MAX_ROWS | 5000 | CSV dışa aktarımda satır tavanı | |
RATE_LIMIT_* | [istek, saniye] | upload / preview / commit / export için ayrı kovalar |
Hedef alan eklemek
TARGET_FIELDS dizisine bir ad eklemek, eşleme ``'ini, sunucu doğrulamasını ve hata raporu başlıklarını birlikte günceller. Üç yer daha dokunulması gerekir:
field_label()— Türkçe etiketi (system/function.php)validate_row()— varsa doğrulama kuralıINSERT/UPDATEsorguları (system/ajax.php) vecontactstablosunda sütun
Takma ad tahmini için assets/js/import.js içindeki ALIASES nesnesine de bir satır ekleyin.
Şifreyi koda yazmayın
Canlı veritabanı künyesi config.php'ye yazılmaz: o dosya depoda durur ve her dağıtımda depodaki sürümle değiştirilir. Bunun yerine yanına config.local.php koyun (.gitignore içindedir):
cp system/config.local.php.example system/config.local.php
# sonra dört satırı doldurunOrtam değişkeni de kullanabilirsiniz:
# Linux / macOS
DB_HOST=127.0.0.1 DB_NAME=cy_import DB_USER=uygulama DB_PASS=gizli php -S 127.0.0.1:8000
# Windows (PowerShell)
$env:DB_PASS="gizli"; php -S 127.0.0.1:8000Öncelik sırası:
config.local.php → ortam değişkeni → yerel varsayılan.
Kendi Projenize Eklemek
İçe aktarma katmanını kendi tablonuza taşımak için sırayla:
1 — Hedef alanlarınızı tanımlayın
// system/config.php
define('TARGET_FIELDS', ['ad', 'stok_kodu', 'fiyat', 'adet']);
define('REQUIRED_FIELDS', ['ad', 'stok_kodu']);2 — Doğrulamayı yazın
validate_row() içindeki kuralları kendi alanlarınıza uyarlayın. Hata / uyarı ayrımını koruyun: kaydın kimliğini kuran alan eksikse hata, zenginleştiren alan kusurluysa uyarı.
3 — Benzersizlik anahtarınızı seçin
Bu projede email, sizde stok_kodu olabilir. Üç yeri birlikte değiştirin:
UNIQUE KEY `uq_urun_stok_kodu` (`stok_kodu`)// handle_preview: SELECT stok_kodu FROM urunler WHERE stok_kodu IN (…)
// handle_commit : str_contains($ex->getMessage(), 'uq_urun_stok_kodu')4 —
INSERT / UPDATE sorgularını değiştirin
handle_commit() içindeki iki prepared statement. Döngünün dışında hazırlanmalarını koruyun; içeride hazırlamak 5000 satırda 5000 gereksiz gidiş-dönüş demektir.
5 — Künye tablosunu olduğu gibi alın
import_runs tablosu alanlarınızdan bağımsızdır; hiç değiştirmeden kullanabilirsiniz.
Atlamayın:previewvecommitaynıvalidate_row()fonksiyonunu çağırmalıdır. İkisini ayırdığınız gün önizleme yalan söylemeye başlar.
Çılgın Yazılım Tasarım Kalıbı
assets/css/cilginyazilim.css dosyası bu projeye değil markaya aittir. Diğer örnek projelerde de aynı görsel dili kullanabilmek için ayrı bir dosya olarak tutulur; bu örneğe özgü her şey (sihirbaz adımları, bırakma alanı, önizleme tablosu, özet çipleri) assets/css/style.css içindedir.
Hazır bileşenler
| Sınıf | Ne işe yarar | ||
|---|---|---|---|
.cy-card / .cy-card__header / __body / __footer | Gradyan başlıklı ana kart | ||
.cy-brand / .cy-brand__mark / __title / __subtitle | Logo kutusu + başlık bloğu | ||
.cy-btn + --primary \ | --onbrand \ | --glass | Marka butonları |
.cy-btn-icon + --view \ | --edit \ | --delete | Tablo içi ikon butonları |
.cy-table / .cy-actions / .cy-id / .cy-name | Marka görünümlü tablo bileşenleri | ||
.cy-badge + --glass \ | --soft | Rozetler | |
.cy-modal / .cy-detail | Gradyan başlıklı modal ve etiket/değer listesi | ||
.cy-toast + --success \ | --danger \ | --info | Bildirim balonları |
Bu örneğe özgü (style.css): .cy-step / .cy-step__num, .cy-dropzone, .cy-filemeta / .cy-fm, .cy-chip, .cy-verdict, .cy-preview, .cy-choice, .cy-result, .cy-op--ok|warning|error|dupe_db|success|partial|aborted, .cy-src, .cy-count.
Renkleri değiştirmek
Tüm bileşenler CSS değişkenlerinden beslenir. Tek yeri değiştirmek yeter:
:root {
--cy-brand-900: #061321; /* Logodaki en koyu lacivert */
--cy-brand-600: #0b5cb5; /* Ana marka mavisi */
--cy-accent: #0ea5e9; /* Vurgu rengi */
--cy-gradient: linear-gradient(135deg, #061321, #0b5cb5 45%, #0284c7);
}
Koyu tema
Kullanıcının işletim sistemi koyu temadaysa otomatik devreye girer. Zorlamak isterseniz:
<html data-cy-theme="dark"> <!-- veya "light" -->
Dosya Yapısı
csv-import/
├── index.php → Arayüz. Veritabanına DOKUNMAZ; üç kart + üç modal.
├── cy_import.sql → Şema + 33 kişi + 6 içe aktarma künyesi (NOW() - INTERVAL)
├── sample.csv → Örnek dosya: her durumu temsil eden 14 satır
├── .htaccess → Dizin listeleme kapalı, .sql/.md kapalı (README istisna)
│
├── system/
│ ├── config.php → Ayarlar, PDO bağlantısı, APP_DEBUG türetimi
│ ├── config.local.php → (siz oluşturursunuz) canlı künye — .gitignore'da
│ ├── config.local.php.example
│ ├── function.php → CSRF, hız sınırı, CSV okuma/yazma, doğrulama
│ ├── ajax.php → 12 uç nokta (sihirbaz + ekranlar)
│ └── .htaccess → BEYAZ LİSTE: yalnızca ajax.php dışarı açık
│
├── storage/imports/ → Geçici CSV'ler. Web'e KAPALI, 30 dk sonra temizlenir.
│ ├── .htaccess → Require all denied
│ └── .gitignore → yüklenen dosyalar depoya girmez
│
├── assets/
│ ├── css/
│ │ ├── cilginyazilim.css → MARKA KALIBI (projeler arası ortak — dokunmayın)
│ │ ├── style.css → Yalnızca bu örneğe özgü stiller
│ │ ├── bootstrap.min.css
│ │ └── dataTables.bootstrap5.min.css
│ ├── js/
│ │ ├── import.js → Sihirbaz + tablolar + modallar (12 bölüm)
│ │ ├── jquery-3.7.0.js
│ │ ├── bootstrap.bundle.js
│ │ └── jquery.dataTables.min.js
│ └── images/logo.png
│
├── docs/screenshots/ → README'de kullanılan görseller
├── CHANGELOG.md
├── README.md · README.en.md
└── LICENSE → MIT
Nasıl Çalışıyor?
┌─ ADIM 1: upload ─────────────────────────────────────────────────────┐
│ · içerikten MIME kontrolü + boyut sınırı │
│ · csv_to_utf8() → BOM / UTF-8 geçerliliği / Windows-1254 │
│ · detect_delimiter() → , ; TAB | (tırnak içi ayraçlar sayılmaz) │
│ · fgetcsv ile ayrıştır, IMPORT_MAX_ROWS tavanını uygula │
│ · storage/imports/<session_id>_<token>.csv olarak sakla │
│ ↳ döner: token · başlık · ilk 5 satır · ayraç · kodlama · satır sayısı│
└──────────────────────────────────────────────────────────────────────┘
│ token
▼
┌─ ADIM 2: preview (DRY-RUN — VERİTABANINA HİÇBİR ŞEY YAZILMAZ) ──────┐
│ · load_and_map() → mapping doğrula (zorunlu alanlar, sütun var mı?) │
│ · dosyadaki e-postaları TEK sorguda sor: │
│ SELECT email FROM contacts WHERE email IN (…) ← 1 sorgu │
│ · her satır → validate_row() │
│ ok · warning · error · dupe_file · dupe_db │
│ ↳ döner: özet sayaçları · ilk 200 satırın önizlemesi · atomik uyarısı│
└──────────────────────────────────────────────────────────────────────┘
│ token + mapping + mode + dedupe
▼
┌─ ADIM 3: commit ─────────────────────────────────────────────────────┐
│ BEGIN │
│ INSERT import_runs (…) ← künye açılır, id alınır │
│ foreach satır: │
│ validate_row() │
│ ├─ hata + mode=atomic → ROLLBACK → 409 → aborted künyesi │
│ ├─ hata + mode=kısmi → hata raporuna ekle, atla │
│ ├─ DB'de var + skip → atla ve raporla │
│ ├─ DB'de var + update → UPDATE contacts │
│ └─ yeni → INSERT contacts (import_id = künye) │
│ UPDATE import_runs SET inserted, updated, skipped, status, … │
│ COMMIT │
│ ↳ hata varsa rapor CSV'si yazılır, kaynak dosya SİLİNİR │
└──────────────────────────────────────────────────────────────────────┘
Sorumluluk dağılımı
| Katman | Dosya | Sorumluluk |
|---|---|---|
| Sunum | index.php | Yalnızca HTML iskeleti. Veri yok, sorgu yok. |
| Arayüz mantığı | assets/js/import.js | Sihirbaz durumu, DataTables, modallar, kaçışlama |
| Uç noktalar | system/ajax.php | İstek doğrulama, işlem akışı, JSON yanıtı |
| Alan mantığı | system/function.php | CSV okuma, kodlama, doğrulama, CSV yazma |
| Ayar | system/config.php | Sabitler, PDO, APP_DEBUG türetimi |
AJAX API Referansı
Tüm istekler system/ajax.php adresine gider. İndirme uçları (error_csv, contacts_export) dışında POST ve CSRF token zorunludur.
upload · upload_sample — dosya yükle
multipart/form-data ile file alanı. upload_sample dosya beklemez; depodaki sample.csv ile aynı kod yolunu çalıştırır.
{
"success": true,
"token": "84351d3b8bb2c4ea6421ea499e87aaec",
"filename": "sample.csv",
"delimiter": ";",
"encoding": "UTF-8",
"header": ["ad soyad", "e-posta", "telefon", "şehir"],
"sample": [["Zeynep Kaya", "[email protected]", "0532 111 22 44", "Bursa"]],
"row_count": 14,
"targets": [{"key": "name", "label": "Ad Soyad", "required": true}]
}
preview — dry-run
İstek: token, mapping (JSON: {"name":0,"email":1,…}), dedupe
{
"success": true,
"summary": {"total": 14, "ok": 6, "warning": 1, "error": 3, "dupe_file": 1, "dupe_db": 3},
"will_insert": 7,
"will_update": 0,
"will_skip": 7,
"preview": [
{"line": 2, "status": "dupe_db", "status_label": "Veritabanında var",
"note": "Veritabanında var → ATLANACAK",
"data": {"name": "Zeynep Kaya", "email": "[email protected]", "phone": "5321112244", "city": "Bursa"},
"raw": []}
],
"preview_shown": 14,
"atomic_hint": "Atomik modda 3 hatalı satır TÜM aktarımı iptal eder."
}data boşsa satır doğrulamayı geçememiştir; o durumda raw dosyadaki ham hücreleri taşır.
commit — gerçek aktarma
İstek: token, mapping, mode (atomic \| skip_invalid), dedupe (skip \| update)
{
"success": true,
"run_id": 9,
"mode": "skip_invalid",
"mode_label": "Kısmi (hatalıları atla)",
"inserted": 7,
"updated": 0,
"skipped": 7,
"error_count": 7,
"duration_ms": 10,
"status": "partial",
"report_token": "3efdc2a4cce2e44e097f1ad25ac72d4e"
}Atomik modda hata bulunursa 409 döner ve hiçbir kayıt yazılmaz:
{"success": false, "description": "Atomik mod: 4. satır hatalı (Geçersiz e-posta). HİÇBİR kayıt yazılmadı.",
"line": 4, "reason": "Geçersiz e-posta", "mode": "atomic"}
error_csv — hata raporu (GET)
?action=error_csv&token= → UTF-8 BOM'lu, ; ayraçlı CSV indirir.
satir;hata;"Ad Soyad";E-posta;Telefon;Şehir
4;"Geçersiz e-posta";"Hatalı Kayıt";gecersiz-eposta;000;Konya
10;"Geçersiz e-posta";"'=HYPERLINK(""http://kotu.example.com"",""Fatura"")";sahte-eposta@;5551234567;Rizeİkinci satırdaki baştaki tek tırnağa dikkat: formül enjeksiyonu koruması.
stats — sayaç şeridi
{"success": true, "contacts": 33, "from_import": 30, "cities": 16,
"runs": 6, "skipped": 13, "errors": 8, "last_run": "29.08.2026 14:29",
"cities_list": ["Adana", "Ankara", "…"]}
contacts_list · contact_detail · contact_delete · contacts_export
contacts_list standart DataTables server-side yanıtı döner (draw, recordsTotal, recordsFiltered, data). Filtreler: search[value], f_city, f_source, f_run.
contacts_export GET'tir ve contact_filters() fonksiyonunu listeyle paylaşır — indirilen dosya ekranda görünenle birebir aynıdır.
runs_list · run_detail
runs_list filtresi: f_status (success \| partial \| aborted).
run_detail künyeye ek olarak still_here döner: o aktarmadan gelip hâlâ duran kayıt sayısı.
HTTP durum kodları
| Kod | Anlamı |
|---|---|
200 | İşlem başarılı |
400 | Geçersiz parametre (örn. hatalı ID) |
403 | CSRF token geçersiz veya oturum düşmüş |
404 | Kayıt / rapor bulunamadı |
405 | POST dışı istek |
409 | Atomik fren — dosyada hatalı satır var, hiçbir şey yazılmadı |
410 | Geçici dosyanın süresi dolmuş, yeniden yükleyin |
422 | Doğrulama hatası (dosya türü, boyut, eksik eşleme — errors alanı döner) |
429 | Hız sınırı aşıldı (retry_after saniye döner) |
500 | Sunucu / veritabanı hatası |
Veritabanı Şeması
CREATE TABLE `import_runs` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`filename` VARCHAR(190) NOT NULL,
`delimiter_label` VARCHAR(8) NOT NULL DEFAULT ',',
`encoding` VARCHAR(24) NOT NULL DEFAULT 'UTF-8',
`mode` ENUM('atomic','skip_invalid') NOT NULL DEFAULT 'skip_invalid',
`dedupe` ENUM('skip','update') NOT NULL DEFAULT 'skip',
`total_rows` INT UNSIGNED NOT NULL DEFAULT 0,
`inserted` INT UNSIGNED NOT NULL DEFAULT 0,
`updated` INT UNSIGNED NOT NULL DEFAULT 0,
`skipped` INT UNSIGNED NOT NULL DEFAULT 0,
`error_count` INT UNSIGNED NOT NULL DEFAULT 0,
`duration_ms` INT UNSIGNED NOT NULL DEFAULT 0,
`status` ENUM('success','partial','aborted') NOT NULL DEFAULT 'success',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_runs_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `contacts` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(150) NOT NULL,
`email` VARCHAR(190) NOT NULL,
`phone` VARCHAR(20) NOT NULL DEFAULT '',
`city` VARCHAR(80) NOT NULL DEFAULT '',
`source` ENUM('manual','import') NOT NULL DEFAULT 'import',
`import_id` INT UNSIGNED NULL DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_contacts_email` (`email`),
KEY `idx_contacts_city` (`city`),
KEY `idx_contacts_import` (`import_id`),
CONSTRAINT `fk_contacts_import` FOREIGN KEY (`import_id`)
REFERENCES `import_runs` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
| Karar | Neden |
|---|---|
uq_contacts_email | "Aynı dosyayı iki kez yükledim, kayıtlar ikiye katlandı" sorununun tek gerçek çözümü. Uygulama katmanındaki kontrol yarışır ve kaybeder; indeks kaybetmez |
delimiter_label, delimiter değil | DELIMITER, mysql istemcisinin komutudur. Satır başında geçtiğinde kurulum dosyası sessizce bozulur |
status ENUM (3 değer) | partial ile aborted farklı sorulara cevap verir: biri "kısmen girdi", diğeri "hiç girmedi". Tek bir success bayrağı bu ayrımı yutardı |
ON DELETE SET NULL | Künye silinse bile kişi kaydı durur, yalnızca izi kopar. CASCADE olsaydı bir künye silmek 400 kişiyi de silerdi |
source + import_id birlikte | import_id NULL olabilir (elle girilmiş ya da künyesi silinmiş). source bu iki durumu ayırt eder |
duration_ms | Performansı ölçülebilir kılar: 5000 satırlık bir aktarmanın 4 saniye mi 40 saniye mi sürdüğü, indeks kararlarının tek somut geri bildirimidir |
updated_at ON UPDATE | dedupe=update politikasının ne yaptığı veriden okunabilir: oluşturma ve güncelleme zamanı farklı olan kayıtlar tazelenmiş olanlardır |
| InnoDB | Transaction desteği — künye ile kişilerin birlikte yazılabilmesinin ön şartı |
Sık Sorulanlar
Neden dosyayı iki kez yüklemiyoruz — önizleme için bir, aktarma için bir?
İki sebeple. Birincisi hız: 5 MB'lık bir dosyayı iki kez göndermek kullanıcıyı bekletir. İkincisi ve asıl önemlisi tutarlılık: iki ayrı yükleme arasında dosya değişebilir. Önizlemede gördüğünüz veri ile aktarılan verinin aynı dosya olduğunu garanti etmenin tek yolu, dosyayı bir kez alıp bir token'la işaret etmektir.
Token oturuma bağlıdır ve IMPORT_TTL_MIN (30 dakika) sonra geçersizleşir. Süre dolarsa sunucu 410 döner ve kullanıcıdan yeniden yüklemesini ister.
Kodlama tespiti nasıl çalışıyor? Neden mb_detect_encoding() yetmiyor?
mb_detect_encoding() tek başına güvenilmezdir: ISO-8859-9 ile Windows-1254 arasında bayt düzeyinde ayrım yapılamaz ve fonksiyon hemen her baytı "latin1" olarak tanır — yani geçerli UTF-8 dosyaları da yanlışlıkla çevirir.
Bu yüzden sıra tersine çevrilmiştir:
- UTF-8 BOM varsa dosya kesin UTF-8'dir
mb_check_encoding($raw, 'UTF-8')geçiyorsa UTF-8'dir (ASCII de öyledir)- Ancak o zaman Windows-1254 varsayılıp çevrilir
Çeviri son çaredir, ilk tahmin değil. Türkçe Excel'in ürettiği dosyalar 3. adıma düşer ve doğru okunur.
Neden 5000 satır sınırı var? Daha büyük dosyaları nasıl aktarırım?
Sınırın sebebi kimlik doğrulaması olmayan bir uç noktaya sınırsız iş yaptırmamaktır. 100.000 satırlık bir dosya, tek bir istekte hem belleği hem işlemciyi tüketir; bu ucuz bir hizmet dışı bırakma kaldıracıdır.
Daha büyük dosyalar için üç yol var:
IMPORT_MAX_ROWSveIMPORT_MAX_BYTESdeğerlerini yükseltin (kimlik doğrulaması olan bir panelde makul)- Dosyayı parçalara bölüp sırayla aktarın —
import_runsher parçayı ayrı künyede tutar - Gerçekten büyük veri için
LOAD DATA INFILEkullanın; ama o zaman satır bazlı doğrulamadan ve önizlemeden vazgeçersiniz
Atomik mod gerçekten "hepsi ya da hiçbiri" mi?
Evet. Tüm satırlar tek bir transaction içinde işlenir; ilk hatada rollBack() çağrılır ve o ana kadar yazılan her şey geri alınır. Künye satırı da aynı transaction'da olduğu için o da silinir — bu yüzden hemen ardından ayrı bir aborted künyesi yazılır.
Bir uyarı: MySQL'de ALTER TABLE gibi DDL komutları örtük commit yapar. Bu kod yalnızca INSERT/UPDATE çalıştırdığı için sorun yoktur, ama içe aktarma sırasında şema değiştiren bir kod eklerseniz atomiklik garantisi kırılır.
Telefon numarasını neden değiştiriyorsunuz? Ham hâlini saklasam olmaz mı?
Aynı numara bir CSV'de altı farklı biçimde yazılabilir: 0532 111 22 33, +90 532 111 22 33, 905321112233, 532-111-22-33… Bunları olduğu gibi saklamak, "bu iki kayıt aynı kişi mi?" sorusunu sonsuza dek cevapsız bırakır ve telefonla arama yapmayı imkânsızlaştırır.
Normalizasyon kayıp veri değil, kanonik biçimdir: rakam dışı karakterler zaten bilgi taşımıyordu. Ekranda gösterirken format_phone() ile okunur hâle geri döndürülür.
10 haneye oturmayan numaralar hata sayılmaz: uyarıyla birlikte olduğu gibi saklanır, çünkü eksik bir telefon yüzünden geçerli bir kişi kaydını çöpe atmak veriyi düzeltmez — sadece kaybeder.
Hata raporu neden ayrı bir CSV? Ekranda göstersek yetmez mi?
Ekranda göstermek "kaç satır hatalı" sorusunu cevaplar; rapor ise düzeltmeyi mümkün kılar. Kullanıcı raporu indirir, Excel'de açar, satır numarasına ve sebebe bakarak kaynak dosyasını düzeltir ve yeniden yükler.
Rapor oturuma bağlı bir token'la korunur ve IMPORT_TTL_MIN sonra silinir. Formül enjeksiyonuna karşı hücreler csv_cell() ile korunur — bu dosya kullanıcının kendi (muhtemelen düşmanca) metnini geri yazdığı için burada koruma zorunludur.
dedupe=update mevcut kaydın hangi alanlarını değiştiriyor?
name, phone, city ve import_id. email değişmez — zaten eşleşme anahtarıdır. created_at de değişmez; kaydın ne zaman ilk girdiği bilgisi korunur, updated_at ise otomatik tazelenir.
Bu seçim bilinçlidir: bir güncelleme dosyası kaydın kimliğini değil, niteliklerini tazelemek içindir.
Canlı Ortama Alırken
- [ ]
APP_DEBUGzaten ortamdan türetiliyor — yine de canlıdafalseolduğunu doğrulayın - [ ]
system/config.local.phpoluşturun; künyeyiconfig.php'ye yazmayın - [ ] Veritabanı için
rootyerine sınırlı yetkili bir kullanıcı oluşturun - [ ]
storage/imports/klasörünün web'den erişilemediğini doğrulayın (tarayıcıda deneyin) - [ ] Klasör yazılabilir olsun ama çalıştırılabilir olmasın
- [ ] HTTPS kullanın;
session.cookie_secure = 1vesession.cookie_httponly = 1ayarlayın - [ ] Giriş sistemi ekleyin — kimlik doğrulaması olmayan bir içe aktarma ekranı, herkese açık bir yazma noktasıdır
- [ ]
IMPORT_MAX_ROWS/IMPORT_MAX_BYTESdeğerlerini sunucunuzunupload_max_filesizevepost_max_sizeayarlarıyla uyumlu tutun - [ ] Nginx kullanıyorsanız
.htaccessçalışmaz; şu iki yolu sunucu yapılandırmasından kapatın:
```nginx
location ~* \.(sql|log|ini|bak)$ { deny all; }
location ^~ /storage/ { deny all; }
```
- [ ] Geçici dosya temizliğinin çalıştığını doğrulayın (
storage/imports/birikmemeli)
Sorun Giderme
| Belirti | Çözüm |
|---|---|
| "Veritabanına bağlanılamadı" | MySQL çalışmıyor veya DB_* bilgileri hatalı. XAMPP panelinden MySQL'i başlatın. |
| "Dosya kaydedilemedi" | storage/imports/ yok ya da yazılabilir değil. Klasörü oluşturup izin verin. |
| "Yalnızca CSV/metin dosyası kabul edilir" | finfo dosyayı metin olarak tanımadı. Genelde gerçekten CSV olmayan (xlsx, zip) bir dosya seçilmiştir. fileinfo eklentisinin açık olduğunu da kontrol edin. |
| Türkçe karakterler önizlemede bozuk | Kodlama tespiti kaçırmış olabilir. Dosyayı Excel'de "CSV UTF-8" olarak yeniden kaydedip deneyin; künyedeki Kodlama alanı ne dediğine bakın. |
| Tüm satırlar tek sütuna düşmüş | Ayraç yanlış tespit edilmiş. Künyedeki Ayraç alanına bakın; dosyanın ilk satırında ayraç hiç geçmiyorsa varsayılan , kullanılır. |
| HTTP 410 "dosya bulunamadı" | Geçici dosyanın 30 dakikalık ömrü dolmuş. Dosyayı yeniden yükleyin. |
| HTTP 409 dönüyor | Bu bir arıza değil: atomik fren. Dosyada hatalı satır var ve hiçbir şey yazılmadı. Kısmi moda geçin ya da dosyayı düzeltin. |
| HTTP 403 dönüyor | Oturum düşmüş — sayfayı yenileyin. Sunucuda session.save_path yazılabilir olmalıdır. |
| CSV Excel'de bozuk görünüyor | İndirilen dosya UTF-8 BOM'lu ve ; ayraçlıdır. "Metni Sütunlara Dönüştür" ile açtıysanız ayracı ; seçin. |
$ is not defined | JavaScript yükleme sırası bozulmuş. jQuery her zaman en başta gelmelidir. |
| DataTables "Requested unknown parameter" | ` sayısı ile sunucudan dönen dizi uzunluğu farklı. İkisini eşitleyin. |
| Örnek dosya düğmesi 404 veriyor | sample.csv` proje kökünde olmalı; ZIP'ten çıkarırken atlanmış olabilir. |
Yol Haritası
- [ ] Sütun eşlemesini kaydetme: aynı biçimdeki dosya ikinci kez yüklendiğinde eşleme hazır gelsin
- [ ] Arka plan kuyruğu ile büyük dosya desteği (parça parça işleme + ilerleme çubuğu)
- [ ] Excel (XLSX) dosyalarını doğrudan okuma
- [ ] İçe aktarmayı geri alma: bir künyeye bağlı tüm kayıtları tek tıkla kaldırma
- [ ] Satır bazlı düzeltme: hatalı satırları ekranda düzeltip yeniden deneme
- [ ] Kullanıcı girişi ve rol tabanlı yetkilendirme
- [ ] PHPUnit ile birim testleri (
detect_delimiter,csv_to_utf8,normalize_phone,validate_row) - [ ] Koyu tema için elle açma/kapama düğmesi
Katkı
Bu proje herkese açıktır — dilediğiniz geliştirmeyle katkı sağlayabilirsiniz.
📦 Depo: github.com/CilginYazilim/PHP-MySQL-CSV-Import-Ice-Aktarma-PDO-Ajax-DataTables
| Nasıl katkı sağlarım? | Nereden |
|---|---|
| 🐛 Hata bildir | Issues |
| 💡 Özellik öner | Issues |
| 🔧 Kod gönder | Pull Requests |
| ❓ Soru sor | Discussions |
Katkı ölçütleri
- Kod açıklamalı olsun. Bu projenin temel amacı öğretmek; yorumsuz kod PR'ı geri döner.
previewvecommitaynı doğrulama fonksiyonunu kullanmalıdır. İkisini ayıran PR kabul edilmez — önizlemenin doğruluğu projenin tezidir.- Yeni bir yazma işlemi eklerken künye kaydını ve transaction'ı unutmayın.
- Tasarım değişikliklerini
style.cssüzerinden yapın;cilginyazilim.cssmarkaya aittir ve diğer projelerle ortaktır. - Yeni bir dış kütüphane eklemeden önce issue açıp tartışalım — proje bilinçli olarak bağımlılıksızdır.
Lisans
MIT — ticari kullanım dahil serbesttir.
Kaynak Kod soldaki ağaçtan bir dosya seçin
-
assets
-
css
- bootstrap.min.css 227.5 KB
- cilginyazilim.css 22 KB
- dataTables.bootstrap5.min.css 11.7 KB
- style.css 22.5 KB
-
images
- logo.png 70.4 KB
-
js
- bootstrap.bundle.js 203.2 KB
- dataTables.bootstrap5.min.js 2.3 KB
- import.js 28.2 KB
- jquery-3.7.0.js 278.3 KB
- jquery.dataTables.min.js 85.1 KB
-
-
docs
-
screenshots
- 01-genel-gorunum.png 445.4 KB
- 02-onizleme-dryrun.png 120 KB
- 03-kisi-detay.png 181.7 KB
- 04-mobil.png 237.7 KB
-
-
storage
-
imports
- .gitignore 25 B
- .htaccess 19 B
-
-
system
- .htaccess 1.7 KB
- ajax.php 30.6 KB
- config.local.php.example 1.5 KB
- config.php 11.5 KB
- function.php 18.1 KB
- .gitignore 1.2 KB
- .htaccess 3.6 KB
- CHANGELOG.md 13.2 KB
- cy_csv_import.sql 11.9 KB
- index.php 22.8 KB
- LICENSE 1.1 KB
- README.en.md 47.9 KB
- README.md 50.2 KB
- sample.csv 776 B
Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli
değerler gösterilmeden önce maskelenir (••••••••).
Sık Sorulan Sorular
Yüklenen dosyanın tamamını, gerçek içe aktarmayla birebir aynı doğrulama fonksiyonundan geçirir ama tek bir INSERT çalıştırmaz. Sonuçta her satırın ne olacağını gösteren bir özet çıkar: kaç satır temiz, kaç tanesi uyarılı, kaç tanesi hatalı, kaç tanesi dosya içinde veya veritabanında tekrar ediyor. Bu simetri bilinçlidir: önizleme ayrı bir kod kullansaydı zamanla ikisi ayrışır ve "önizlemede temizdi ama aktarırken patladı" durumu doğardı.
Atomik modda tüm satırlar tek bir transaction içinde işlenir ve ilk hatada her şey geri alınır; HTTP 409 döner ve veritabanına hiçbir satır girmez. Muhasebe ve finans gibi "yarım veri, yanlış veridir" durumları için uygundur. Kısmi modda geçerli satırlar yazılır, hatalılar atlanır ve satır numarasıyla birlikte indirilebilir bir CSV raporuna düşer. Büyük ve kirli dosyalarda tercih edilir, çünkü yüzde ikilik bozuk veri yüzünden yüzde doksan sekizlik temiz veriyi reddetmez.
Excel'in CSV kaydetme seçenekleri dosyayı çoğu zaman UTF-8 değil Windows-1254 olarak yazar; bu dosya doğrudan okunduğunda Türkçe harfler bozulur ve json_encode() geçersiz baytta sessizce false döner. Uygulama sırayla üç şeye bakar: önce UTF-8 BOM var mı, sonra içerik geçerli UTF-8 olarak çözümleniyor mu, ve ancak ikisi de olmazsa Windows-1254 varsayıp çevirir. Çeviri son çaredir; mb_detect_encoding() tek başına kullanılsaydı geçerli UTF-8 dosyaları da yanlışlıkla çevrilirdi.
Hayır. Asıl koruma uygulama kodunda değil, e-posta sütunundaki benzersiz indekstedir. "Önce SELECT, yoksa INSERT" kontrolü iki eşzamanlı istekte yarışır ve kaybeder; veritabanı kısıtı kaybetmez. Kod yine de önce SELECT yapar ama bunu önizleme için yapar, yani kullanıcıya "bu üç satır zaten var" diyebilmek için. Yazma anındaki gerçek koruma indeksin kendisidir ve dönen hata, indeks adına bakılarak diğer kısıt ihlallerinden ayırt edilir.
Dosyalar web'e kapalı bir klasöre (storage/imports) alınır ve adları oturum kimliğiyle öneklenir; token yalnızca onaltılık olabildiği için yol manipülasyonu mümkün değildir. Dosya türü uzantıdan değil içerikten okunur ve metin dışı türler reddedilir. İçe aktarma bittiğinde kaynak dosya hemen silinir; sahipsiz kalanlar otuz dakika sonra her istekte çalışan fırsatçı temizlikle kaldırılır. Dosya içeriği hiçbir zaman çalıştırılmaz, yalnızca okunur.
Dört yer: config.php içindeki TARGET_FIELDS ve REQUIRED_FIELDS sabitleri, function.php içindeki validate_row() doğrulama kuralları ve field_label() etiketleri, ajax.php içindeki INSERT ve UPDATE sorguları, ve benzersizlik anahtarınız (bu örnekte e-posta, sizde stok kodu olabilir) — indeks adını hem şemada hem hata ayıklama kontrolünde güncelleyin. İçe aktarma künyesi tablosu alanlarınızdan bağımsızdır, hiç değiştirmeden kullanabilirsiniz. Değiştirmemeniz gereken tek şey, önizleme ile aktarmanın aynı doğrulama fonksiyonunu çağırmasıdır.
Yorumlar 0 konuşma
Bu kod örneğine henüz yorum yapılmamış. Takıldığınız bir yer veya merak ettiğiniz bir ayrıntı varsa ilk soruyu siz sorun.
Soru Sor veya Yorum Yaz
Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.