Orta
veri-isleme/excel-ice-disa-aktarma
Excel İçe / Dışa Aktarma
- PHP 8
- PDO
- MySQL
- Ajax
- +4
171 inceleme 88 canlı açılış 3 beğeni
Veri İşleme Orta
PHP PDO ve MySQL ile yumuşak silme (soft delete) ve çöp kutusu: kayıt silinmez, deleted_at sütunu işaretlenir. Tek kapsam kaynağı, aktif kayıtlar arasında benzersizlik, geri alma çakışmasında 409, onaydaki sayının sözleşme olarak doğrulanması ve cron ile otomatik temizlik — kalıbın dört gerçek tuzağı tek örnekte.
Veritabanı şeması projedeki cy_trash.sql dosyasında.
Bir kullanıcı yanlışlıkla "Sil" düğmesine bastı. Kayıt gitti. Yedekten dönmek saatler sürer ve o yedek dünkü hâli getirir.
Çözüm bilinir: kaydı silme, silinmiş olarak işaretle. Adı yumuşak silme (soft delete), kalıbı basit: bir deleted_at sütunu. Ama basit görünen bu kalıp, uygulamada dört yerde ısırır:
// Sanılan: tek satır yeter
UPDATE notes SET deleted_at = NOW() WHERE id = ?
// Gerçek: artık HER sorguya bir koşul eklemek zorundasınız
SELECT * FROM notes WHERE deleted_at IS NULL -- listede
SELECT COUNT(*) FROM notes WHERE deleted_at IS NULL -- sayaçta
UPDATE notes SET ... WHERE id = ? AND deleted_at IS NULL -- düzenlemedeactive_scope() / trash_scope()title üzerinde UNIQUE indeks varsa? → çöpteki kayıt yeni kaydı engeller. Çözüm: aktif kayıtlar arasında benzersizlikKimler için uygun?
SoftDeletes trait'inin ne yaptığını merak edenlerKlonla, cy_trash.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.
// TİPİK HATALI KOD — koşul her dosyaya kopyalanmış
$rows = $db->query("SELECT * FROM notes WHERE deleted_at IS NULL");
$count = $db->query("SELECT COUNT(*) FROM notes"); // ← unutuldu!
// BU PROJEDE — tek kaynak
function active_scope(): string { return 'deleted_at IS NULL'; }
function trash_scope(): string { return 'deleted_at IS NOT NULL'; }Koşulu bir fonksiyonun arkasına almak, onu değiştirilebilir de kılar. Yarın "silinmiş ama arşivlenmemiş" gibi üçüncü bir durum gerekirse, düzeltilecek tek bir yer vardır.
-- TİPİK HATALI ŞEMA
UNIQUE KEY (title)
-- "Alışveriş" notunu çöpe attınız. Aynı başlıkla yeni bir not
-- açamıyorsunuz — çöptekiyle çakışıyor. Kullanıcı sebebini anlamaz.
-- BU PROJEDE
is_active TINYINT GENERATED ALWAYS AS
(CASE WHEN deleted_at IS NULL THEN 1 ELSE NULL END) VIRTUAL,
UNIQUE KEY uq_notes_active_title (title, is_active)NULL değerler UNIQUE indekste çakışmaz. Çöpteki her kaydın is_active değeri NULL olduğu için birbirleriyle de, aktif kayıtla da çakışmazlar. Aktif kayıtların hepsinde değer 1 olduğundan, aralarında yalnızca bir tanesi aynı başlığı taşıyabilir.
Bu, yumuşak silmenin en çok atlanan ayrıntısıdır — ve genellikle üründe, kullanıcı "aynı adı neden kullanamıyorum?" diye sorduğunda fark edilir.
// TİPİK HATALI KOD — ham veritabanı hatası kullanıcıya çıkar
$db->query("UPDATE notes SET deleted_at = NULL WHERE id = $id");
// → SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry…
// BU PROJEDE — önce sorulur, sonra cevaplanır
$chk = $db->prepare('SELECT id FROM notes WHERE title = :t AND ' . active_scope());
// çakışma varsa → 409 + "Geri almadan önce aktif olanı yeniden adlandırın."Kontrol ve güncelleme tek transaction içinde, FOR UPDATE kilidiyle yapılır. Yine de UNIQUE indeksin kendisi son savunma hattı olarak yerinde durur: kontrol ile güncelleme arasına giren bir istek, veritabanı seviyesinde durdurulur.
// SANILAN: "30 gün sonra silinir"
// GERÇEK: 30 gün sonra SİLİNMEYE UYGUN hale gelir.
// Silme işi cron'undur:
// 15 3 * * * php /yol/bin/purge.phpArayüzdeki "Süresi dolanları temizle" düğmesi ile bin/purge.php aynı purge_expired_trash() fonksiyonunu çağırır. İki ayrı temizleme kodu yazmak, birini düzeltip diğerini unutmanın kesin yoludur — ve unutulan taraf cron olursa fark hiç görünmez.
// İSTEMCİ: kullanıcıya GÖSTERDİĞİ sayıyı geri gönderir
post('empty_trash', { expected_count: lastCount })
// SUNUCU: kendi sayar, tutmuyorsa HİÇBİR ŞEYE DOKUNMAZ
if ($current !== $expected) {
json_error('Liste değişti. Çöp kutusunda şu an ' . $current . ' not var.', 409);
}Sayım ve silme aynı transaction içindedir; ayrı olsalardı ikisi arasında eklenen bir kayıt sayıma girmeden silinirdi.
Yumuşak silme çekirdeği
deleted_at kalıbı, tek kapsam kaynağıUPDATE/DELETE'in WHERE'indeÇöp kutusu
expected_count sözleşmesiyleOtomatik temizlik
bin/purge.php — cron / Görev Zamanlayıcı betiğiPHP_SAPI kontrolü ve .htaccessArayüz ve tasarım
confirm() kutusu yerine markayla uyumlu onay penceresi#not-13)| Açık | Tipik hatalı kod | Bu projede |
|---|---|---|
| GET ile silme | <a href="sil.php?id=5"> | Yazma işlemleri yalnızca POST; GET isteği 405 döner |
| CSRF | Jeton yok | Oturuma bağlı 32 baytlık jeton, her istekte hash_equals() ile doğrulanır |
| SQL Injection | Dize birleştirme ile sorgu | Tüm sorgular prepared statement, EMULATE_PREPARES = false |
LIKE joker istismarı | LIKE '%$q%' | \, % ve _ kaçışlanır |
| Kapsam atlaması | Sahiplik/kapsam PHP'de kontrol edilir | Koşul her sorgunun WHERE'inde — atlanabilir bir adım değil |
| Yarış durumu (geri alma) | Kontrol ve güncelleme ayrı | Tek transaction + FOR UPDATE; UNIQUE indeks son savunma hattı |
| Yarış durumu (boşaltma) | Sayım ve silme ayrı | Tek transaction + expected_count denetimi |
| Kazayla toplu silme | Tek onayla her şey gider | Kalıcı silme ayrı kovada (15 istek / 60 sn), ayrı onay, ayrı renk |
| Kaba kuvvet / kötüye kullanım | Sınır yok | Üç ayrı kova: read 180/60sn, write 60/60sn, destroy 15/60sn |
| XSS | .html() ile basılan kullanıcı verisi | Sunucuda e(), istemcide esc(); not gövdesi asla ham basılmaz |
| Bozuk UTF-8 yanıtı yutar | json_encode() sessizce false döner | JSON_INVALID_UTF8_SUBSTITUTE |
| Cron betiğinin web'den çalıştırılması | Korumasız bin/ klasörü | PHP_SAPI !== 'cli' kontrolü ve bin/.htaccess — iki katman |
| Bilgi sızdıran hatalar | Canlıda SQL metni ekrana basılır | APP_DEBUG sunucu adından türetilir; canlıda false |
| Yapılandırma sızıntısı | config.php doğrudan indirilebilir | system/ beyaz listedir: yalnızca ajax.php açık |
| Şema/veri sızıntısı | /cy_trash.sql → HTTP 200 | .sql, .md, .json, .log, .ini, .bak, .example kapalı (README*.md bilinçli istisna) |
| Clickjacking | Başlık yok | X-Frame-Options: SAMEORIGIN — görünmez bir çerçevede "Çöpü boşalt" tıklatılamaz |
| MIME sniffing | Başlık yok | X-Content-Type-Options: nosniff |
Çöp kutusu kendiliğinden boşalmaz. Saklama süresi dolan kayıtları silmek için bin/purge.php düzenli olarak çalıştırılır.
Linux / cron — her gece 03:15:
15 3 * * * php /var/www/soft-delete-trash/bin/purge.php >> /var/log/cy-trash-purge.log 2>&1C:\xampp\php\php.exe C:\xampp\htdocs\soft-delete-trash\bin\purge.php[2026-08-31 03:15:00] cy-trash purge: 3 kayıt silindi (>30 gün), 4 ms
0 döner.PHP_SAPI !== 'cli' kontrolü ve bin/.htaccess içinde Require all denied vardır. İki katman bilinçlidir: .htaccess'in okunmadığı bir sunucuda (nginx) PHP kontrolü çalışır.Tüm ayarlar system/config.php içindedir. Veritabanı künyesi oraya yazılmaz — system/config.local.php dosyasına yazılır; o dosya .gitignore içindedir, depoya gitmez ve deploy sırasında silinmez.
Bu projede fazladan bir sebep var: bin/purge.php ayrı bir süreçtir ve aynı yapılandırmayı okur. Künyeyi iki yerde tutmak, birini güncelleyip diğerini unutmanın kesin yoludur — ve unutulan taraf cron olursa temizlik sessizce durur.
| Sabit | Varsayılan | Ne işe yarar |
|---|---|---|
TRASH_RETENTION_DAYS | 30 | Çöpteki kayıt kaç gün sonra temizlenmeye uygun olur. |
TRASH_WARN_DAYS | 7 | Kalan gün bunun altına düşünce satır uyarı rengine geçer. |
NOTE_TITLE_MAX / NOTE_BODY_MAX | 150 / 5000 | Doğrulama sınırları. |
PAGE_SIZE_DEFAULT / PAGE_SIZE_MAX | 50 / 200 | Liste boyutu ve tavanı. |
RATE_LIMIT_READ | [180, 60] | Liste ve arama. |
RATE_LIMIT_WRITE | [60, 60] | Ekle / güncelle / çöpe at / geri al. |
RATE_LIMIT_DESTROY | [15, 60] | Kalıcı silme ve boşaltma — geri alınamaz, en dar kova. |
APP_DEBUG | (otomatik) | Sunucu adından türetilir; canlı alan adında kendiliğinden kapanır. |
ALTER TABLE urunler
ADD COLUMN deleted_at DATETIME NULL DEFAULT NULL,
ADD COLUMN is_active TINYINT GENERATED ALWAYS AS
(CASE WHEN deleted_at IS NULL THEN 1 ELSE NULL END) VIRTUAL,
ADD KEY idx_urunler_deleted (deleted_at, id);
-- Benzersiz bir alanınız varsa DÜZ UNIQUE'i kaldırıp bunu koyun:
ALTER TABLE urunler
DROP INDEX uq_urunler_kod,
ADD UNIQUE KEY uq_urunler_aktif_kod (kod, is_active);
require 'system/function.php';
$rows = $db->query('SELECT * FROM urunler WHERE ' . active_scope())->fetchAll();
$db->prepare('UPDATE urunler SET deleted_at = NOW() WHERE id = :id AND ' . active_scope())
->execute([':id' => $id]);
// Kendi tablonuz için purge_expired_trash()'in bir kopyasını yazın
// ya da tablo adını parametre alacak şekilde genelleştirin.
$silinen = purge_expired_trash($db, TRASH_RETENTION_DAYS);
Uyarı: Yumuşak silme, veritabanı seviyesindeki ON DELETE CASCADE davranışını devre dışı bırakır. Bir ürünü yumuşak sildiğinizde, ona bağlı satırlar öylece kalır. Bunu ya elle (aynı transaction içinde) ya da sorgularınızda ilişkili tablonun kapsamını da kontrol ederek çözmeniz gerekir. Bu, yumuşak silmenin gerçek bedelidir ve her tablo için ödemeye değmeyebilir.
Arayüz, tüm Çılgın Yazılım örneklerinde ortak olan tasarım kalıbını kullanır:
| Dosya | Kapsam | Değiştirilir mi? |
|---|---|---|
assets/css/cilginyazilim.css | Marka kalıbı — kartlar, butonlar, tablolar, rozetler, modal | Hayır. Projeler arası ortaktır. |
assets/css/style.css | Yalnızca bu sayfaya özgü parçalar (sayaç şeridi, sekmeler, durum rozetleri) | Evet |
Yükleme sırası: bootstrap → cilginyazilim → style. Renkler doğrudan yazılmaz, CSS değişkenlerinden okunur (--cy-brand-600, --cy-danger …).
Aynı kalıpla hazırlanmış diğer örnekler: cilginyazilim.com/kutuphane
.
├── bin/
│ ├── .htaccess → klasör web'e TÜMÜYLE kapalı
│ └── purge.php → cron betiği: süresi dolanları kalıcı siler
├── system/
│ ├── .htaccess → BEYAZ LİSTE: yalnızca ajax.php açık
│ ├── ajax.php → 10 uç nokta
│ ├── config.php → yapılandırma + PDO bağlantısı
│ ├── config.local.php → (siz oluşturursunuz; .gitignore içinde)
│ ├── config.local.php.example
│ └── function.php → kapsam kaynağı, CSRF, hız sınırı, temizlik
├── assets/
│ ├── css/ → bootstrap.min · cilginyazilim (marka) · style
│ ├── js/ → jquery · bootstrap.bundle · trash.js
│ └── images/logo.png
├── docs/screenshots/
├── .htaccess → dizin listeleme kapalı, dosya türü kuralları, güvenlik başlıkları
├── .env.example → Veritabanı bilgileri (isteğe bağlı) — .gitignore içinde
├── cy_trash.sql → şema + 11 aktif + 9 çöpteki not (NOW() - INTERVAL ile)
├── index.php → arayüz (veritabanına dokunmaz)
├── CHANGELOG.md
├── LICENSE
├── README.md
└── README.en.md
┌──────────────────────────────┐
"Sil" düğmesi ────> │ UPDATE SET deleted_at=NOW() │ kayıt DURUYOR
└──────────────┬───────────────┘
│
┌─────────────────┴──────────────────┐
│ │
↩ "Geri al" ⏳ süre doluyor
│ │
┌────────────┴────────────┐ ┌────────────┴─────────────┐
│ Aynı başlıkta AKTİF │ │ deleted_at < NOW()-30gün │
│ kayıt var mı? │ │ → "temizlenmeye hazır" │
└────────────┬────────────┘ └────────────┬─────────────┘
┌──────┴──────┐ │
yok var │
│ │ │
deleted_at=NULL 409 döner bin/purge.php (cron)
(geri alındı) "önce yeniden │
adlandırın" DELETE — geri alınamaz
HER SORGUDA: active_scope() → deleted_at IS NULL
trash_scope() → deleted_at IS NOT NULL
↑ tek kaynak; listede, sayaçta, güncellemede aynısıTüm uçlar system/ajax.php altındadır, POST ister ve CSRF jetonu taşır.
Yanıt biçimi:
// Başarılı
{ "success": true, "type": "success", "description": "Not geri alındı." }
// Hatalı
{ "success": false, "type": "danger", "description": "…", "errors": { "title": "…" } }
list — aktif veya çöp listesi
| Parametre | Varsayılan | Not | |
|---|---|---|---|
view | active | active \ | trash |
search | — | Başlık ve içerikte arar; % ve _ kaçışlanır | |
limit | 50 | En çok 200 |
{
"success": true, "view": "trash", "count": 9, "retention_days": 30, "warn_days": 7,
"rows": [
{ "id": 16, "title": "Q2 bütçe taslağı", "excerpt": "Yerini Q3 taslağı aldı…",
"updated": "06.08.2026 06:39", "deleted": "06.08.2026 06:39",
"days_left": 5, "is_expired": false }
]
}days_left ve is_expired SQL'de hesaplanır: PHP'nin ve MySQL'in saat dilimi eşit olmak zorunda değildir, ve deleted_at MySQL'in NOW() değeriyle yazılmıştır.
stats — sayaç şeridi
{ "success": true,
"stats": { "aktif": 11, "copte": 9, "son_gunler": 2, "suresi_dolan": 3, "toplam": 20 } }COUNT yazmak beş ayrı anlık görüntü demektir; sorgular arasında bir kayıt çöpe taşınırsa toplamlar birbirini tutmaz ve ekranda imkânsız bir satır belirir.
fetch — tek kayıt (detay penceresi)
Çöpteki kayıt da getirilebilir — kullanıcı geri almadan önce içine bakabilmeli. Ama editable: false gelir ve save zaten aktif kapsam dışına yazmaz. Kural iki yerde birden durur.
{ "success": true, "note": {
"id": 13, "title": "Alışveriş listesi", "body": "…",
"created": "22.07.2026 06:39", "updated": "31.07.2026 06:39", "deleted": "30.08.2026 06:39",
"in_trash": true, "editable": false, "days_left": 29, "is_expired": false,
"can_restore": false } }can_restore: false → aynı başlıkta aktif bir kayıt var. Arayüz düğmeyi baştan pasifleştirir.
save — ekle / güncelle
id = 0 ise ekler, değilse günceller. Güncelleme yalnızca aktif kayda uygulanır.
| Durum | Ne zaman |
|---|---|
422 | Başlık 2-150 karakter değil ya da içerik 5000'i aşıyor (errors alan adıyla gelir) |
409 | Aynı başlıkta aktif bir not var |
404 | Kayıt yok ya da çöpte |
Çakışma indeks adına bakarak ayırt edilir (uq_notes_active_title); yalnızca 23000 koduna bakmak yetmez, aynı kod başka kısıt ihlallerinde de gelir.
soft_delete / restore / restore_all
soft_delete → deleted_at = NOW(). Kapsam koşulu UPDATE'in WHERE'indedir; zaten çöpteyse 404.restore → deleted_at = NULL. Çakışma varsa 409 ve conflict_id. Kontrol + güncelleme tek transaction, FOR UPDATE kilidiyle.restore_all → çakışanları atlar, kaçının atlandığını söyler:{ "success": true, "restored": 8, "skipped": 1,
"description": "8 not geri alındı. 1 not, aynı başlıkta aktif bir kayıt olduğu için atlandı." }delete_forever / empty_trash / purge_expired
Üçü de destroy kovasındadır (15 istek / 60 sn) — geri alınamaz işlemler ayrı bir sınırla korunur.
delete_forever → yalnızca çöpteki kayda uygulanır. Aktif bir kayda denenirse 404 ve "önce çöpe taşıyın".empty_trash → expected_count zorunludur. Sunucunun saydığıyla tutmuyorsa 409 ve hiçbir şey silinmez.purge_expired → yalnızca TRASH_RETENTION_DAYS'ten eski kayıtları siler. bin/purge.php ile aynı fonksiyonu çağırır.notes
├── id INT UNSIGNED AUTO_INCREMENT (214'ten başlar)
├── title VARCHAR(150)
├── body TEXT
├── created_at TIMESTAMP
├── updated_at TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
├── deleted_at DATETIME NULL ← NULL = aktif, dolu = çöpte
├── is_active TINYINT GENERATED ALWAYS AS
│ (CASE WHEN deleted_at IS NULL THEN 1 ELSE NULL END) VIRTUAL
├── UNIQUE KEY uq_notes_active_title (title, is_active)
├── KEY idx_notes_deleted (deleted_at, id)
└── KEY idx_notes_active_updated (is_active, updated_at)
| Karar | Neden |
|---|---|
deleted_at, is_deleted değil | Bir bayrak yalnızca "silindi mi?" der. Tarih ayrıca ne zaman silindiğini söyler — saklama süresi hesabı buna dayanır ve bayrakla mümkün olmaz. |
DATETIME, TIMESTAMP değil | TIMESTAMP'in üst sınırı 2038'dir ve oturum saat dilimine göre dönüştürülür. deleted_at bir olay anıdır; DATETIME sürprizsizdir. |
is_active VIRTUAL | Değer diskte tutulmaz, indeks için okunurken hesaplanır. Sütuna hiç bakmıyoruz; yalnızca UNIQUE indeksin varlık sebebi. |
UNIQUE (title, is_active) | Benzersizliği aktif kayıtlarla sınırlar. NULL'lar UNIQUE indekste çakışmadığı için çöpteki kayıt yolu tıkamaz. |
idx_notes_deleted (deleted_at, id) | Liste sorgusu deleted_at ile filtreler, id/deleted_at ile sıralar; tek indeks ikisini birden karşılar. |
AUTO_INCREMENT = 214 | Bu uygulamada kayıt gerçekten silinir (kalıcı silme ve purge); numaralar boşalır. Yeni bir kayıt silinmiş bir numarayı devralırsa #not-42 bağlantısı yanlış kayda gider. |
Yumuşak silme her tabloda kullanılmalı mı?
Hayır. Bedeli gerçektir: her sorguya bir koşul eklersiniz, ON DELETE CASCADE devre dışı kalır, benzersizlik kısıtlarını yeniden düşünmeniz gerekir ve tablo sürekli büyür.
Kârlı olduğu yerler: kullanıcının kendi ürettiği ve yanlışlıkla silebileceği veriler (notlar, dosyalar, müşteri kayıtları). Kârsız olduğu yerler: günlük tabloları, oturum kayıtları, önbellek satırları, çoktan-çoğa ara tabloları.
Kural: geri alınması bir insanı rahatlatacaksa yumuşak silin.
Neden is_deleted bayrağı değil de tarih?
Bir bayrak yalnızca "silindi mi?" sorusunu cevaplar. Tarih ayrıca ne zaman silindiğini söyler ve bu olmadan saklama süresi hesaplanamaz: "30 gün sonra temizle" kuralını is_deleted = 1 ile yazamazsınız.
Ayrıca tarih, bayrağın yaptığı her şeyi zaten yapar: deleted_at IS NULL bir bayrak kadar hızlı ve indekslenebilir bir koşuldur.
Çöp kutusu sonsuza dek büyür mü?
Cron kurulmazsa evet — ve bu, yumuşak silmenin en sık atlanan sonucudur. bin/purge.php düzenli çalıştığında tablo kendiliğinden dengelenir.
Arayüzdeki "Temizlenmeye hazır" sayacı bu yüzden var: sıfırdan büyük kalıyorsa cron çalışmıyordur.
Yabancı anahtarlar ve ON DELETE CASCADE ne olacak?
Yumuşak silme onları devre dışı bırakır: satır gerçekten silinmediği için cascade hiç tetiklenmez. Bir kategoriyi yumuşak sildiğinizde ürünleri öylece kalır ve sorgularınız onları göstermeye devam eder.
İki seçenek var: ilişkili kayıtları aynı transaction içinde birlikte yumuşak silmek, ya da sorgularda üst kaydın kapsamını da kontrol etmek (JOIN … AND k.deleted_at IS NULL). Bu örnek tek tablo üzerinden anlattığı için o karmaşıklığa girmiyor; kendi projenizde ilk düşüneceğiniz şey bu olmalı.
Laravel'in SoftDeletes trait'i ile aynı şey mi?
Aynı kalıp, evet. Laravel deleted_at sütununu kullanır ve global bir kapsam (global scope) ekleyerek koşulu her sorguya kendisi koyar.
Fark şurada: bu örnekte kapsamı siz koyarsınız (active_scope()), yani ne olduğunu görürsünüz. Laravel'de withTrashed() demeyi unutmak, burada koşulu yazmayı unutmakla aynı sonucu verir — sihir, sorunu ortadan kaldırmaz, yalnızca görünmez kılar.
Aynı başlıkta iki not neden olamıyor?
Bu örnekte bilinçli bir kısıt: benzersizlik kuralı olan bir alanın yumuşak silmeyle nasıl çakıştığını göstermek için. Gerçek bir not uygulamasında başlıklar genellikle benzersiz olmak zorunda değildir.
Kendi projenizde bu kısıt gerekmiyorsa uq_notes_active_title indeksini kaldırın; kalan her şey aynen çalışır.
Kayıt "silindi" ama kullanıcı verisi hâlâ duruyor — KVKK/GDPR?
Önemli bir nokta. Yumuşak silme, veriyi saklamaya devam etmek demektir. Kişisel veri söz konusuysa saklama süresi bir hukuki gerekliliktir, bir tercih değil.
Bu örnekte TRASH_RETENTION_DAYS ve bin/purge.php tam olarak bunun içindir: silme talebi belirli bir süre sonra gerçekten yerine getirilir. Cron'un çalıştığından emin olun; çalışmıyorsa "sildik" demeniz doğru olmaz.
system/config.local.php oluşturuldu; canlı veritabanı künyesi oradabin/purge.php günlüğüne bakın)TRASH_RETENTION_DAYS sizin için doğru mu? (kişisel veri varsa hukuki süreye bakın)APP_DEBUG kapalı (canlı alan adında kendiliğinden kapanır — yine de doğrulayın)RATE_LIMIT_* değerleri trafiğinize göre ayarlandı/cy_trash.sql, /system/config.php, /bin/purge.php adresleri 403 dönüyor/README.md 200 dönüyor (vitrin için) ama /CHANGELOG.md 403secure bayrağını kendiliğinden alacak| Belirti | Sebep | Çözüm |
|---|---|---|
Arama kutusu hiçbir şey bulmuyor, 500 dönüyor | Aynı adlı yer tutucu iki kez kullanılmış | EMULATE_PREPARES = false iken ad tekrar edemez; :q1, :q2 diye ayırın |
SQLSTATE[HY000]: ... generated column | MySQL 5.6 ya da MariaDB 10.1 | Üretilen sütun 5.7+ / 10.2+ gerektirir |
| Çöpteki notu geri alamıyorum | Aynı başlıkta aktif bir not var | Aktif olanı yeniden adlandırın; detay penceresi bunu zaten söylüyor |
| Çöp kutusu hiç boşalmıyor | Cron kurulu değil | bin/purge.php'yi zamanlayıcıya ekleyin |
| Türkçe karakterler bozuk | .sql yanlış karakter setiyle içe aktarıldı | mysql --default-character-set=utf8mb4 < cy_trash.sql |
403 "Oturum doğrulaması başarısız" | Oturum düştü ya da CSRF jetonu eski | Sayfayı yenileyin |
Sürekli 429 | Hız sınırı sayaç dosyaları | sys_get_temp_dir()/cy_trash_rate klasörünü silin |
| Liste ile sayaç farklı sayı gösteriyor | Kapsam koşulu bir yerde unutulmuş | active_scope() / trash_scope() dışında elle yazılmış koşul aramayın — ekleyin |
deleted_by) — çok kullanıcılı kurulumlar içinlimit)Katkılar memnuniyetle karşılanır.
git checkout -b ozellik/harika-seygit commit -m 'Harika şey eklendi'git push origin ozellik/harika-seyHata bildirimi ve öneriler için Issues bölümünü kullanabilirsiniz.
MIT — bkz. LICENSE. Ticari projelerde de özgürce kullanabilirsiniz.
Çılgın Yazılım · Kütüphane · GitHub
Bu örneği faydalı bulduysanız ⭐ vermeyi unutmayın.
İncelemek istediğiniz dosyayı ağaçtan seçin.
Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli değerler gösterilmeden önce maskelenir (••••••••).
Hayır. Bedeli gerçektir: her sorguya bir koşul eklersiniz, ON DELETE CASCADE devre dışı kalır, benzersizlik kısıtlarını yeniden düşünmeniz gerekir ve tablo sürekli büyür. Kârlı olduğu yerler kullanıcının kendi ürettiği ve yanlışlıkla silebileceği verilerdir: notlar, dosyalar, müşteri kayıtları. Kârsız olduğu yerler günlük tabloları, oturum kayıtları, önbellek satırları ve çoktan-çoğa ara tablolarıdır. Kural şu: geri alınması bir insanı rahatlatacaksa yumuşak silin.
Bir bayrak yalnızca "silindi mi?" sorusunu cevaplar. Tarih ayrıca ne zaman silindiğini söyler ve bu olmadan saklama süresi hesaplanamaz — "30 gün sonra temizle" kuralını is_deleted = 1 ile yazamazsınız. Ayrıca tarih, bayrağın yaptığı her şeyi zaten yapar: deleted_at IS NULL bir bayrak kadar hızlı ve indekslenebilir bir koşuldur.
title üzerinde düz bir UNIQUE indeks olsaydı, bir notu çöpe atıp aynı başlıkla yenisini oluşturamazdınız; sildiğiniz bir şey hâlâ yolunuzu tıkardı. Çözüm, benzersizliği yalnızca aktif kayıtlar arasında kurmaktır: deleted_at NULL iken 1, aksi hâlde NULL üreten bir generated column ve UNIQUE(title, is_active) indeksi. NULL değerler UNIQUE indekste çakışmaz, bu yüzden çöpte kaç kayıt olursa olsun sorun çıkmaz.
Hayır — ve bu ayrım önemlidir. Saklama süresi dolan bir kayıt yalnızca "temizlenmeye hazır" hale gelir; silinmesi bin/purge.php çalıştığında olur. Arayüz bu yüzden "silinecek" değil "temizlenmeye hazır" der: cron kurulmamışsa o kayıtlar sonsuza dek çöpte durur ve kullanıcıya tutulamayacak bir söz verilmemiş olur. Arayüzdeki "Süresi dolanları temizle" düğmesi cron ile aynı fonksiyonu çağırır.
Yumuşak silme onları devre dışı bırakır: satır gerçekten silinmediği için cascade hiç tetiklenmez. Bir kategoriyi yumuşak sildiğinizde ürünleri öylece kalır ve sorgularınız onları göstermeye devam eder. İki seçenek var: ilişkili kayıtları aynı transaction içinde birlikte yumuşak silmek, ya da sorgularda üst kaydın kapsamını da kontrol etmek. Kendi projenizde ilk düşüneceğiniz şey bu olmalı.
Aynı kalıp, evet. Laravel deleted_at sütununu kullanır ve global bir kapsam ekleyerek koşulu her sorguya kendisi koyar. Fark şurada: bu örnekte kapsamı siz koyarsınız, yani ne olduğunu görürsünüz. Laravel'de withTrashed() demeyi unutmak, burada koşulu yazmayı unutmakla aynı sonucu verir — sihir sorunu ortadan kaldırmaz, yalnızca görünmez kılar.
Önemli bir nokta. Yumuşak silme, veriyi saklamaya devam etmek demektir. Kişisel veri söz konusuysa saklama süresi bir hukuki gerekliliktir, bir tercih değil. Bu örnekteki TRASH_RETENTION_DAYS ve bin/purge.php tam olarak bunun içindir: silme talebi belirli bir süre sonra gerçekten yerine getirilir. Cron'un çalıştığından emin olun; çalışmıyorsa "sildik" demeniz doğru olmaz.
Bu örneğin daha gelişmişi projenizde mi lazım?
İhtiyacınızı anlatın; bu yapıyı sizin sisteminize uyarlayalım ya da size özel geliştirelim.
Henüz yorum yok. Takıldığınız bir yer varsa ilk soruyu siz sorun.
Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.
Orta
veri-isleme/excel-ice-disa-aktarma
171 inceleme 88 canlı açılış 3 beğeni
Orta
veri-isleme/php-csv-import
140 inceleme 50 canlı açılış 1 beğeni
İleri
uygulama-iskeleti/php-baslangic-sablonu
309 inceleme 150 canlı açılış 6 beğeni 2 yorum