📝 Basit Anlatım (Herkes İçin)
WidgetManagement modülü, web sitelerinde "bloklardan" oluşan sayfalar tasarlamayı sağlar. Tıpkı Lego gibi, önceden tasarlanmış bileşenleri (hero başlık, card listesi, form vb.) sürükle-bırak yöntemiyle sayfaya ekleyebilir, özelleştirebilirsiniz.
Widget nedir? Hazır bir bileşen şablonudur. Örneğin "Hero" widget'ı büyük başlıklı bir banner, "Card Grid" widget'ı ise çok sayıda kartı yan yana gösteren bir bloktur.
Nasıl çalışır?
- Admin panelinde widget galerisi açılır (hazır şablonlar görülür)
- "Hero" widget'ı seçilir ve sayfaya eklenir (TenantWidget oluşur)
- Widget kopyalanabilir, ayarları değiştirilebilir (başlık, renkler vb.)
- Widget'ın "item'leri" vardır (örneğin "3 item'li card widget" → 3 kart)
- Sayfaya açılır, ziyaretçiler göz atabilir, form doldurabilir vb.
💡 Neden önemli? Bu sistem sayesinde kodlamacı olmayan insanlar da sayfa tasarlayabilir, ürün kataloğu oluşturabilir, haber sayfaları düzenleyebilir.
🔧 Teknik Detaylar (Geliştiriciler İçin)
Modül Dosya Yapısı
Modules/WidgetManagement/ ├── App/ │ ├── Http/ │ │ ├── Controllers/ # Preview, FormBuilder controller'ları │ │ └── Livewire/ # Widget yönetim UI bileşenleri (9 component) │ ├── Models/ # Widget, TenantWidget, WidgetItem, WidgetCategory │ ├── Services/ # WidgetService, WidgetItemService, Render services │ ├── Support/ # ShortcodeParser, HandlebarsRenderer │ ├── Jobs/ # BulkDeleteWidgets, BulkUpdateWidgets │ └── Helpers/ # WidgetHelper.php (15+ helper function) ├── database/ │ ├── migrations/ # Central DB migrations (Widget, Category, Module) │ ├── migrations/tenant/ # Tenant DB migrations (TenantWidget, WidgetItem) │ └── seeders/ # Demo widget seeder'ları ├── config/ │ └── config.php ├── routes/ │ ├── admin.php # Admin panel yönlendirmeler │ └── web.php # (boş - frontend route yok) ├── resources/views/ │ ├── blocks/ # Widget şablon dosyaları (40+ folder) │ │ ├── hero/ # Hero banner widget'ları │ │ ├── layout/ # Layout widget'ları (grid, columns) │ │ ├── cards/ # Card widget'ları │ │ ├── form/ # Form widget'ları │ │ ├── modules/ # Module veri kaynağı (Portfolio, Blog vb.) │ │ └── ... │ ├── livewire/ # Admin UI view'ları │ └── widget/ # Widget render helper view'ları └── lang/ # Türkçe ve İngilizce dil dosyaları
📊 Veritabanı Mimarisi
Central Database (tuufi_4ekim)
| Tablo | Amaç | Kritik Alanlar |
|---|---|---|
| widget_categories | Widget kategorilerine ayırma | title, slug, is_active, parent_id (hierarchical) |
| widgets | Widget şablonları (master) | name, slug, type, content_html, settings_schema, has_items |
| widget_modules | Hangi modülde hangi widget var | widget_id (FK), module_id (FK) |
| tenant_widgets ⚠️ | Kiraları tarafından oluşturulan widget instance'ları | widget_id (FK to central), order, settings (JSON), position |
| widget_items ⚠️ | Widget item'leri (dinamik widget'lar) | tenant_widget_id (FK), content (JSON), order |
Tenant Database (her kiracı)
| Tablo | Amaç | Yapı |
|---|---|---|
| tenant_widgets | Kiracı tarafından oluşturulan widget instance'ları | widget_id (cross-DB FK), settings (JSON), custom_html, custom_js, position |
| widget_items | Widget'ın dinamik item'leri | tenant_widget_id (FK), content (JSON), order |
1. Widget (Central Model)
class Widget extends Model {
use CentralConnection, HasMediaManagement;
// Temel özellikler
- id: PK
- name, slug, type (file|module|standard)
- content_html, content_css, content_js
- item_schema (JSON - dinamik item'ler için)
- settings_schema (JSON - widget ayarları)
- has_items: boolean
- is_active, is_core
// İlişkiler
- hasMany('TenantWidget') ← Bunu kaç kere kullandı
- belongsTo('WidgetCategory')
- belongsToMany('Module') ← Hangi modüllerde aktif
}
✅ İndeksler: type, is_active, has_items, composite (is_active + type)
2. TenantWidget (Tenant Model)
class TenantWidget extends Model {
// Kiracı tarafından oluşturulan widget örneği
- id: PK
- widget_id: FK → Widget (merkezi DB'de)
- order: sıralama
- position: sayfa bölümü (header, footer, sidebar vb.)
- settings: JSON (başlık, renkler, kontrol ayarları)
- display_title: widget başlığı
// Custom HTML/CSS/JS için
- is_custom: bool
- custom_html, custom_css, custom_js
- is_active: bool
// İlişkiler
- belongsTo('Widget') ← Merkezi şablona bağlantı
- hasMany('WidgetItem') ← Dinamik item'ler
}
✅ İndeksler: widget_id, is_active, position, composite (widget_id + is_active)
3. WidgetItem (Tenant Model)
class WidgetItem extends Model {
// Widget'ın dinamik item'leri (örn: Card Grid widget'ında her kart)
- id: PK
- tenant_widget_id: FK → TenantWidget
- content: JSON (title, description, image, URL vb.)
- order: sıralama
// İlişkiler
- belongsTo('TenantWidget')
}
✅ İndeksler: tenant_widget_id, order
4. WidgetCategory (Central Model)
class WidgetCategory extends Model {
use Sluggable, CentralConnection;
// Widget'ları kategorilendirme
- widget_category_id: PK
- title, slug, icon
- parent_id: Alt kategori için
- is_active: bool
// İlişkiler
- hasMany('Widget')
- hasMany('WidgetCategory', 'parent_id') ← Hierarchical
- belongsTo('WidgetCategory', 'parent_id') ← Parent kategori
}
✅ İlişki Türü: Hierarchical (alt kategoriler)
Frontend Render Akışı: @widget(tenantWidgetId)
1. Blade'de: @widget(5)
2. Helper: widget_by_id(5) → WidgetService.renderSingleWidget()
3. Cache kontrol: Redis'te var mı? EVET → Return
4. Cache miss: TenantWidget::with('widget', 'items').find(5)
5. Render:
- İf custom (is_custom=true): custom_html render et
- Else: Widget.content_html render et
- Items varsa: {{#each items}} template işle
- Settings varsa: {{variable}} değişkenlerini değiştir
- Module widget: ilişkili tablo verilerini çek (Handlebars)
7. HTML output → Sayfa'ya embed
8. Cache'e kaydet (1440 dakika)
Admin Panel Yönetim Akışı
Admin: /admin/widgetmanagement
↓
WidgetComponent (Livewire) → Widget listesi + Filtreler
↓
createInstance(widgetId):
- Widget::find(widgetId) → Şablonu getir
- TenantWidget::create([widget_id, order, settings])
- Cache temizle: clearWidgetCache()
Widget ayarları: /admin/widgetmanagement/settings/{tenantWidgetId}
↓
WidgetSettingsComponent (Livewire) → Form başlığı, renkler vb. güncelle
↓
Widget item'leri: /admin/widgetmanagement/items/{tenantWidgetId}
↓
WidgetItemComponent (Livewire) → Her item düzenle/ekle/sil
↓
Sil/Güncelle → Cache temizle → Frontend yenile
Frontend (View'larda Kullanım)
@widget(tenantWidgetId)
ID'ye göre widget render et
@widgetblock('slug')
Slug'a göre widget render et
@widgets('position')
Belirli pozisyondaki tüm widget'ları render et
@parsewidgets($content)
İçerikteki widget shortcode'ları işle (örn: [[widget:hero param1=val1]])
@modulewidget(moduleWidgetId)
Module widget'ını render et
PHP Helper Functions
widget_by_id(id) // ID'ye göre render widget_by_slug(slug, params) // Slug'a göre, params ile widgets_by_position(position) // Pozisyon'a göre render parse_widget_shortcodes(html) // Shortcode'ları işle widget_cached(id, ttl, params) // Cache ile render module_widget_by_id(id) // Module widget render module_widgets_by_module(modId)// Module'daki tüm module widget'ları widget_file_by_id(id) // File widget render
WidgetService (Ana Service)
Singletonized: app('widget.service')
Temel metodlar:
- getAllWidgets() → Tüm widget'lar
- getActiveWidgets() → Aktif widget'lar
- getWidgetsByType(type) → Türe göre
- getWidgetsForModule(modId) → Modüle göre
- getTenantWidgets(position) → Kiracı widget'ları
- renderWidgetsInPosition() → Pozisyon'daki widget'ları render
- renderSingleWidget() → Tekil widget render (cache ile)
- processWidget() → Widget HTML işleme
- renderWidgetHtml() → Handlebars şablon işleme
- clearWidgetCache() → Cache temizle
Cache stratejisi:
- Key format: widget_{tenantId}_widget_{widgetId}
- TTL: 1440 dakika (24 saat)
- Kullanım: settings, CSS/JS, item'ler değişirse otomatik sıfırlanır
WidgetRenderService (Şablon İşleme)
Sorumlulukları:
- processVariables() → {{title}} gibi değişkenleri değiştir
- processItems() → {{#each items}} döngüleri işle
- processModuleData() → Module verilerini Handlebars'la işle
- processConditionalBlocks() → {{#if condition}} blokları işle
Template Syntax:
- Değişkenler: {{variable}}, {{widget.title}}, {{items.0.name}}
- Koşullar: {{#if condition}}...{{/if}}
- Döngüler: {{#each items}}...{{/each}}
- Handlebars desteği: LitHTML/Handlebars.js
WidgetItemService (Item Yönetimi)
- getItemsForWidget() → Widget'ın item'lerini al - addItem() → Item ekle (auto UUID, is_active=true) - updateItem() → Item güncelle - deleteItem() → Item sil - reorderItems() → Item'leri sırala
ShortcodeParser (İçerik Parse)
Desteklenen syntaxlar: [[widget:slug param1=val1 param2=val2]] [[widget:slug]]İçerik[[/widget:slug]] [[module:moduleId]] [[file:fileId]] Örnek: [[widget:hero title="Başlık" subtitle="Alt başlık"]] [[widget:cards]] [[widget:card title="Card 1"]] [[/widget:cards]]
Database Dağılımı
- Widget (CentralConnection trait) - Widget şablonları
- WidgetCategory (CentralConnection trait) - Kategoriler
- widget_modules - Widget-Module ilişkisi
- tenant_widgets ⚠️ - Tüm kiracılar için (cross-DB FK destekleme)
- widget_items ⚠️ - Tüm kiracılar için (cross-DB FK destekleme)
- tenant_widgets - Kiracının oluşturduğu widget instance'ları
- widget_items - Kiracının widget item'leri
- ⚠️ widget, widget_categories, widget_modules YOK! (merkezi DB'den okunur)
1. Kiracı, admin panelinde /admin/widgetmanagement'e gider
2. TenantAware middleware kiracıyı set eder
3. Widget::all() → Merkezi DB'den (şablonlar)
4. TenantWidget::all() → Tenant DB'den (kiracının widget instance'ları)
5. Widget şablonu render: Merkezi DB'deki content_html + Tenant DB'deki settings
6. Cross-DB relationship: TenantWidget.widget_id → Widget.id (merkezi DB)
| Component | URL/Rota | Fonksiyonu |
|---|---|---|
| WidgetComponent | /admin/widgetmanagement | Widget listesi, ara, filtrele, instance oluştur |
| WidgetManageComponent | /admin/widgetmanagement/manage/{id} | Widget şablonunu edit (HTML/CSS/JS), schema oluştur |
| WidgetGalleryComponent | /admin/widgetmanagement/gallery | Widget galerisi, görsel önizleme |
| WidgetSettingsComponent | /admin/widgetmanagement/settings/{tenantWidgetId} | Widget ayarları düzenle (title, colors, custom values) |
| WidgetItemComponent | /admin/widgetmanagement/items/{tenantWidgetId} | Widget item'lerini listele, drag-drop sırala |
| WidgetItemManageComponent | /admin/widgetmanagement/manage/item/{tenantWidgetId}/{itemId} | Tekil item düzenle (title, description, image vb.) |
| WidgetCodeEditorComponent | /admin/widgetmanagement/code-editor/{id} | Widget'ın HTML/CSS/JS kodunu editör'de düzenle |
| WidgetCategoryComponent | /admin/widgetmanagement/category | Widget kategorilerini listele ve yönet |
| FileWidgetListComponent | /admin/widgetmanagement/file-widgets | File widget'ları listele (view dosyası yükleme) |
| ModuleWidgetListComponent | /admin/widgetmanagement/modules | Module widget'ları listele (Blog, Portfolio vb.) |
| WidgetFormBuilderComponent | /admin/widgetmanagement/form-builder/{widgetId}/items | Item ve Settings schema oluştur (drag-drop form designer) |
Admin Rota Yapısı
Prefix: /admin/widgetmanagement
Middleware: ['admin', 'tenant']
Public (view):
- GET / → Widget listesi
- GET /gallery → Galeri
- GET /items/{tenantWidgetId} → Item listesi
- GET /preview/template/{widgetId} → Widget önizlemesi
- GET /preview/instance/{tenantWidgetId} → Instance önizlemesi
Protected (root role):
- GET /manage/{id?} → Widget editör
- GET /code-editor/{id} → Kod editör
- GET /file-widgets → File widget'ları
- GET /modules → Module widget'ları
Protected (update permission):
- GET /settings/{tenantWidgetId} → Widget ayarları
- GET /manage/item/{tenantWidgetId}/{itemId} → Item editör
- POST /form-builder/{widgetId}/save/{schemaType} → Schema kaydet
Preview endpoints:
- GET /preview/embed/{tenantWidgetId}
- GET /preview/embed/json/{tenantWidgetId}
- GET /api/module/{moduleId}
Widget Type'ları
- Blade view dosyasından render edilir
- file_path: 'blocks/hero/simple/view'
- View: widgetmanagement::blocks.hero.simple.view
- Statik widget'lar (değişebilir ama item yok)
- Diğer modüllerden veri çeker (Blog, Portfolio vb.)
- data_source: 'ModuleName/ClassName'
- Handlebars şablon ile render
- Dinamik veri işleme (loop, condition)
- HTML/CSS/JS içeriği içeren widget'lar
- content_html, content_css, content_js
- Item'ler olabilir (dinamik item'ler)
- Settings ile özelleştirilebilir
- TenantWidget'ın kendi HTML/CSS/JS'i
- custom_html, custom_css, custom_js
- Widget şablonunu override eder
- Her kiracı kendi custom widget'ını oluşturabilir
Mevcut Widget Şablonları (40+)
Hero widget'ları: - blocks/hero/simple → Basit başlık - blocks/hero/hero-1 → İmaj ile - blocks/hero/ixtif-hero → İxtif özel Layout widget'ları: - blocks/layout/one-column → 1 kolon - blocks/layout/two-columns → 2 eşit - blocks/layout/three-columns → 3 eşit - blocks/layout/four-columns → 4 eşit - blocks/layout/masonry-grid → Masonry - blocks/layout/asymmetric-grid → Asimetrik - blocks/layout/sidebar-main-sidebar → Kenar çubuğu Card widget'ları: - blocks/cards/basic → Basit kartlar - blocks/cards/grid → Grid düzeni Form widget'ları: - blocks/form/contact-form → İletişim formu - blocks/form/contact-assistant → AI asistan formu - blocks/form/muzibu-contact → Muzibu özel Module widget'ları: - blocks/modules/portfolio/list → Portfolio listesi - blocks/modules/portfolio/detail → Portfolio detay - blocks/modules/page/home → Ana sayfa - blocks/modules/page/recent → Yeni sayfalar - blocks/modules/announcement/list → Duyuru listesi Diğer: - blocks/testimonials/basic → Testimoniallar - blocks/features/basic → Özellikler - blocks/media/image → Resim - blocks/content/hero → İçerik - blocks/content/text → Metin
Cache Sistemi
WidgetService::$useCache = true
TTL: 1440 dakika (24 saat)
Key: widget_{tenantId}_widget_{tenantWidgetId}
Cache İnvalidasyon (Otomatik)
TenantWidget değişirse:
- update() → clearWidgetCache() çağrılır
- delete() → clearWidgetCache() çağrılır
- Livewire save → clearWidgetCache()
WidgetItem değişirse:
- create/update/delete → clearWidgetCache()
Position değişirse:
- TenantWidget::where('position', $pos) → Cache temizle
Settings değişirse:
- TenantWidget.update(['settings' => ...]) → Cache temizle
Static Assets Cache (CSS/JS)
Widget render sırasında:
- WidgetService::$loadedCssFiles[] → Benzersiz dosyaları topla
- WidgetService::$loadedJsFiles[] → Benzersiz dosyaları topla
- Sayfanın footer'ında:
{{ WidgetService::getStylesOutput() }}
{{ WidgetService::getScriptsOutput() }}
Avantaj:
- Tekrarlanan CSS/JS yüklenmez
- Performance optimize edilir
- Sayfa yükleme süresi azalır
Media Yönetimi (Spatie MediaLibrary)
Widget, Spatie MediaLibrary trait'ini kullanır
Collections: Dinamik (field name bazında)
Disk: Tenant-aware (merkezi vs tenant storage)
Example: Widget.registerMediaCollections() → boş (runtime'da field name'e göre belirlenir)
Storage Disk Yapısı
Widget Model getMediaDisk() metodu:
- Tenant context varsa → tenant disk kullanılır
- Central context varsa → public disk kullanılır
- Disk path: storage/app/public/
- URL: /storage/tenant{tenantId}/ (tenant context)
Örnek:
- Merkezi: /storage/widgets/hero-bg.jpg
- Tenant 2 (ixtif): /storage/tenant2/widgets/hero-bg.jpg
- Tenant 1001 (muzibu): /storage/tenant1001/widgets/hero-bg.jpg
TenantWidget.widget_id → Widget (merkezi DB)
MySQL'de kısıtlama yoktur, Laravel açıkça FK belirtmemişti
İndeksler eklenmiş (lookup hızlandırma) ✅
Widget.content_html düzenlenmesi → Tüm TenantWidget'ları etkiler!
Yeni TenantWidget'lar yeni HTML alır
Eski TenantWidget'lar eski HTML'i kullanmaya devam eder (is_custom=false ise)
Çözüm: Widget versiyonlama sistemi (ileride eklenebilir)
Regular: {{variable}} → Server-side PHP tarafından değiştir
Handlebars: {{#if}}, {{#each}} → Browser'da Handlebars.js çalıştır
WidgetService::setHandlebarsUsage(false) → Regular mode
Module widget'ları Handlebars tercih eder (dinamik veri işleme)
WidgetRenderService::escapeHtml() → String değerleri escape et
HTML content'e DOMPurify.js eklenebilir (ileride)
Admin editör'de malicious code girilebilir ⚠️ → Güvenilir editor tarafından kontrol
- Widget query: with('widget', 'items') eager loading
- Cache: 24 saat TTL, Redis tabanlı
- İndeksler: widget_id, is_active, position, composite indices
- Static CSS/JS: Benzerlik tespiti, duplikasyon önleme
- Module data: Lazy loading, query optimization
Widget Instance Oluşturma Akışı
Admin Paneli
↓
WidgetComponent.createInstance(widgetId)
↓
Widget::find(widgetId) ← Merkezi DB'den şablonu getir
↓
TenantWidget::create([
'widget_id' => $widgetId,
'order' => $maxOrder + 1,
'settings' => ['title' => $widget->name, 'unique_id' => UUID]
])
↓
Database: tenant_{kiracı}.tenant_widgets INSERT
↓
clearWidgetCache(tenant()->id, null) ← Kiracının tüm position cache'ini temizle
↓
Frontend'de @widgets('position') çağrılırsa:
↓
Yeni TenantWidget render edilir ✅
Widget Item Ekleme Akışı
Admin: /admin/widgetmanagement/manage/item/{tenantWidgetId}
↓
WidgetItemManageComponent.save()
↓
WidgetItemService.addItem(tenantWidgetId, {
'title' => 'Yeni Card',
'description' => '...',
'image' => 'file.jpg'
})
↓
1. unique_id OTOMATIK oluştur (UUID)
2. is_active = true (VARSAYILAN)
3. order = max(order) + 1
4. Database: tenant_{kiracı}.widget_items INSERT
↓
5. clearWidgetCache(tenant()->id, tenantWidgetId)
↓
Frontend'de @widget(tenantWidgetId) çağrılırsa:
↓
1. Cache miss (temizlendi)
2. TenantWidget::with('widget', 'items').find(tenantWidgetId)
3. Widget.content_html (şablon)
4. WidgetItems processItems() ile {{#each items}}...{{/each}} işle
5. New item görünür ✅
Widget Render (Frontend) Akışı
Blade: @widget(5)
↓
Helper: widget_by_id(5)
↓
Cache key: widget_{tenantId}_widget_5
↓
Cache varsa? EVET → Return (24 saat eski veri)
Cache miss? → Devam et
↓
TenantWidget::with('widget', 'items').find(5)
↓
TenantWidget.is_custom == true?
↓
EVET: custom_html render et
HAYIR:
1. Widget.content_html al
2. WidgetItems varsa: renderService.processItems()
3. Settings varsa: renderService.processVariables()
4. Module widget ise: getData() → renderService.processModuleData()
5. CSS/JS dosyaları ekle
↓
Cache'e kaydet (TTL: 1440 dakika)
↓
HTML çıktı + CSS/JS global array'e ekle
↓
Page footer'da:
WidgetService::getStylesOutput() →