Doğrulanan kod tabanı: d49b4413 (dal feat/fw-license-master) · Tarih: 2026-10-05 · Yayın: 0.9.7 = bu commit + sonrası; belge yalnız doğrulama anındaki kodu anlatır
Kaynak klasör: Bundles/Internal/Backstage/ — 24 *.php + 12 *.rbn.php görünüm.
Envanter: 24 php dosyasının 24'ü bu belgede anlatıldı.
Belge tek dosyadır çünkü hiçbir alt dal 20 *.php eşiğini geçmez
(SPEC §1 "alan büyükse (≥20 php)" kuralı; en büyük alt dal Controllers/ = 4).
1. Ne işe yarar, kimler kullanır
Backstage, panelin kendisini ayarlayan modüldür — sitenin içeriğini değil,
yönetim arayüzünü yönetir:
- Sidebar — panel menülerinin kategori + hiyerarşik menü ağacı (rol süzgeçli).
- E-posta — SMTP/gönderen/genel e-posta ayarları ve test e-postası gönderimi.
- Ayar Mimarisi —
z_settings ve z_setting_groups tablolarının yapısal
yönetimi: alan ekleme, tip/etiket/rol/yardım metni düzenleme.
Modül diğer modüllerin ayar verisini de okur (BackstageService::getEmailSettings()
repository('project.settings') üzerinden email grubunu çeker), ama yazma
yetkisi yalnız kendi alanlarındadır.
Kimler çağırır: geliştirici paneli (#[Module(panel:'developer', context:'panel')],
BackstageController.php:15).
2. Klasör/dosya envanteri (24 php)
2.1 Controllers/ (4)
| Dosya |
Görev |
Önemli public yöntemler |
BackstageController.php |
Modül kökü. #[Module(name:'backstage', data:ModuleData::class, panel:'developer', context:'panel')] — service: bildirimi yok, bu yüzden $this->service alt modüllerde SubModule attribute'inden gelir. |
index() (:23) |
SidebarController.php |
#[SubModule(entity:'sidebar', service:'sidebar', provider:'sidebar')]. Kategori ve menü yönetimi; rol listesini AuthRole::ROLES'ten alır. |
index(): void (:20), getModalData($id): array (:35 protected), categorySave(): void (:65), menuSave(): void (:78) |
EmailController.php |
#[SubModule(entity:'email', service:'backstage', provider:'backstage', model:'Settings')]. E-posta ayarları, test gönderimi, iki senaryolu güncelleme. |
index() (:18), manage() (:28), getModalData($id) (:39 protected), testMail() (:63), updateMailSettings() (:91) |
SettingsArchitectController.php |
#[SubModule(entity:'settings', service:'backstage', provider:'backstage')]. Ayar grubu ve ayar alanı yönetimi; tek kişilik modülde en çok uç. |
index(): void (:17), group($id): void (:27), getModalData($id) (:48 protected), createSetting(): void (:87), updateSetting(): void (:112), createGroup(): void (:134), updateGroup(): void (:148) |
2.2 Services/ (2)
| Dosya |
Görev |
Önemli public yöntemler |
BackstageService.php |
Ayar servisi ($targetModel='Settings'). Okuma tarafı repository üzerinden; yazma tarafı CrudSettingsProvider'a devredilir. getEmailSettings() ayarları beş kümeye böler. |
getEmailSettings(): array (:21), getGroups(): array (:59), getSettingsRaw(string $groupKey): array (:69), getGroupWithSettings(int $id): array (:80), createSetting(array $data) (:106), updateSetting(int $id,array $data) (:110), createGroup(array $data) (:114), updateGroup(int $id,array $data) (:118), bulkUpdateSettings(array $settings) (:122), remove(string $entity,int $id): bool (:143) |
SidebarService.php | Sidebar servisi ($targetModel='SidebarMenus'). Yazma sonrası önbellek düşürür;clearSidebarCache()**tüm rollerin** anahtarlarını toplu siler. |categories(): array(:24),getStructure(string $userRole): array(:33),tree(): array(:41),saveCategory(array $data): bool|int(:49),saveMenu(array $data): bool|int(:61),clearSidebarCache(): self` (:73) |
2.3 Providers/ (3)
| Dosya |
Görev |
Önemli public yöntemler |
BackstageProvider.php |
Ayar/grup okuma uzmanı. target(string $modelName): self hedef modeli çalışma anında değiştirir (akıcı). getSettingsGroup() z_settings ⨝ z_setting_groups birleştirir. |
target(string $modelName): self (:27), getGroups(): array (:37), findGroup(int $id): ?array (:45), getSettingsGroup(string $type,?string $role=null,bool $onlyActive=true): array (:54) |
CrudSettingsProvider.php |
Ayar tek yazma kapısı. Her metotta redirect_url, view, id alanlarını siler ("parazit koruması"). |
createSetting(array $data): int\|bool (:18), updateSetting(int $id,array $data): bool (:40), createGroup(array $data): int\|bool (:55), updateGroup(int $id,array $data): bool (:64), delete(string $entity,int $id): bool (:74) |
SidebarProvider.php |
Sidebar sorgu uzmanı. afterBoot() URI'den hedef modeli seçer (/category ⇒ SidebarCategories). getSidebarStructure() rol bazlı önbellek kullanır. |
afterBoot(): void (:23 protected), getSidebarStructure(string $userRole): array (:37), getMenusForCategory(int,string,string): array (:81 protected), getTree(int $parentId=0,int $depth=0,?array $elements=null): array (:117), getCategories(): array (:141), getParentMenus(): array (:150), saveCategory(array $data): bool\|int (:162), saveMenu(array $data): bool\|int (:177), checkRoleAccess(?string $required,string $userRole): bool (:192 protected) |
2.4 Handlers/ (1)
| Dosya |
Görev |
Önemli public yöntemler |
SidebarHandler.php |
Sidebar yazma uzmanı: slug türetme, atomik sıralama (transaction), alt öğe korumalı silme. |
saveCategory(array $data,?int $id=null): bool (:20), saveMenu(array $data,?int $id=null): bool (:32), reorder(string $type,array $ids): bool (:44), deleteCategory(int $id): bool (:66), deleteMenu(int $id): bool (:74) |
2.5 Models/ (2)
| Dosya |
Görev |
Önemli public yöntemler |
ModuleData.php |
#[Bundle(name:'backstage', context:'developer', map:BackstageMap::MAP)]. 2 servis + 1 handler + 3 provider alias'ı; 4 rota grubu (dashboard, sidebar, email, settings). |
registerMap(): array (:26), registerRoutes(): void (:48) |
BackstageMap.php |
Menü haritası: 1 kimlik + 3 alt modül; sidebar ve settings alt modüllerinin alt dalları yalnız controller bilgisi taşır (başlık/iikon yok). |
const MAP (:19) |
2.6 Views/ (12)
| Görünüm |
Sağlayan uç |
Görev |
backstage_dash.rbn.php |
BackstageController::index |
Modül ana ekranı |
Sidebar/index.rbn.php |
SidebarController::index |
Kategori + menü ağacı |
Sidebar/Partials/modal_category.rbn.php |
SidebarController::modal (view=modal_category) |
Kategori ekle/düzenle |
Sidebar/Partials/modal_menu.rbn.php |
aynı (view=modal_menu) |
Menü ekle/düzenle (ikon seti menu, üst menü listesi) |
Emails/index.rbn.php |
EmailController::index |
SMTP / gönderen / genel kartları |
Emails/manage.rbn.php |
EmailController::manage |
E-posta alan şeması + sıralama |
Emails/Partials/modal.rbn.php |
EmailController::modal |
E-posta alanı ekle/düzenle |
Emails/Partials/modal_role.rbn.php |
aynı |
Alan erişim rolü seçimi |
Settings/index.rbn.php |
SettingsArchitectController::index |
Ayar grubu listesi |
Settings/group_settings.rbn.php |
SettingsArchitectController::group |
Bir grubun alanları |
Settings/Partials/modal.rbn.php |
SettingsArchitectController::modal |
Ayar alanı ekle/düzenle |
Settings/Partials/modal_group.rbn.php |
aynı (view=modal_group) |
Grup ekle/düzenle (ikon seçici popular) |
Kapsama: 24/24.
3. Akış
bir panel isteği
→ SidebarService::getStructure($userRole) SidebarService.php:33
→ SidebarProvider::getSidebarStructure($userRole) SidebarProvider.php:37
→ cache()->remember("sidebar_{$activeProject}_{$userRole}", …) :53
├─ SidebarCategoriesModel: is_active=1, order_num ASC :54-58
├─ her kategori için getMenusForCategory() :63
│ ├─ SidebarMenusModel: category_id + parent_id=0 + is_active=1
│ │ + order_num ASC :86-92
│ ├─ checkRoleAccess($menu['required_role'], $userRole) :96
│ └─ children: parent_id = $menu['id'], is_active=1 :101-106
└─ sonuç: [{name, icon, menus:[…]}] :66-70
Rol kapısı (SidebarProvider.php:192):
required_role boşsa → true
userRole === 'developer' → true (geliştirici her şeyi görür)
aksi halde: AuthRole::ROLES[$userRole]['level'] >= AuthRole::ROLES[$required]['level']
POST backstage/sidebar/category/save
→ SidebarController::categorySave() SidebarController.php:65
→ request->form(['category_name' => 'required'])
→ $this->service->saveCategory($data) SidebarService.php:49
→ SidebarProvider::saveCategory() SidebarProvider.php:162
├─ category_slug yoksa turkishSlug(category_name) :165-167
├─ BaseModel::fillProjectKeyIfMissing($data) :169
└─ SidebarCategoriesModel->save($data) :171
→ başarılıysa clearCache() SidebarService.php:53
POST backstage/sidebar/menu/reorder → 'bulkOrder' (BulkControllerTrait)
→ SidebarHandler::reorder('menu', $ids) SidebarHandler.php:44
├─ field = 'category' ? 'category_order' : 'menu_order' :46-47
├─ beginTransaction() :49
├─ her id için update(id, [field => index+1]); başarısızsa rollBack + false :52-57
└─ commit() / catch → rollBack + false :58-63
3.3 E-posta ayarları ve test gönderimi
GET backstage/email
→ EmailController::index() EmailController.php:18
→ BackstageService::getEmailSettings() BackstageService.php:21
→ repository('project.settings')->fetch([group_key=>'email',
project_key=>active_project_key()]) :24-27
→ beş kümeye ayrıştırır: smtp / sender / general / enabled / mapped :29-52
→ render('Emails/index', array_merge($emailData)) EmailController.php:22
POST backstage/email/test-mail
→ EmailController::testMail() EmailController.php:63
→ settings = service('settings')->read('email') :65
→ targetEmail = settings['email_admin_address'] ?? null :66
→ service('rbnEmail')->sendTemplate('test_connection', $targetEmail, $data) :78
$data = ['time' => now('d.m.Y H:i:s'),
'site_name' => service('seo')?->get('site_title') ?? 'RBN Framework'] :73-76
→ Route->handleResult($result, [success_message, error_message]) :81-84
3.4 Ayar mimarisi
GET backstage/settings/group/{id}
→ SettingsArchitectController::group($id) SettingsArchitectController.php:27
→ BackstageService::getGroupWithSettings($id) BackstageService.php:80
→ fetch(['target'=>'groups','id'=>$id]) → ilk kayıt
→ fetch(['group_id'=>$id]) → alanlar :93-95
→ render('Settings/group_settings', ['group'=>…, 'settings'=>…]) :36
POST backstage/settings/createSetting
→ createSetting() :87
→ request->form([11 kural]) :91-103
→ BackstageService::createSetting() BackstageService.php:106
→ CrudSettingsProvider::createSetting() CrudSettingsProvider.php:18
├─ unset(redirect_url, view, id) :21
├─ setting_key yoksa: turkishSlug(label_tr) → '-' yerine '_' :23-26
├─ is_active yoksa 1 :27
├─ field_options boşsa null :30-32
└─ SettingsModel->create($data) :34
4. Yapılandırma / ayar anahtarları ve sabitler
| Öğe |
Yer |
Değer |
Not |
| Sidebar önbellek anahtarı |
SidebarProvider.php:53 |
"sidebar_{$activeProject}_{$userRole}" |
[FW-ALTYAPI-3 / H · G4] düzeltmesi: anahtar önceki hâlinde kiracıyı içermiyordu, bir proje başka projenin kenar çubuğunu gösteriyordu (yorum :44-48) |
| Servis önbellek anahtarı |
SidebarService.php:16 |
navigation_sidebar |
clearSidebarCache() bunu da siler (:77) |
clearSidebarCache() kapsamı |
SidebarService.php:75 |
AuthRole::ROLES anahtarları → sidebar_{rol} + navigation_sidebar |
Rol sayısı artarsa yeni rolün önbelleği otomatik kapsama girer |
| E-posta grupları |
BackstageService.php:44,46 |
smtp_host/port/username/password → smtp; email_from_name/address → sender |
Diğer aktif alanlar general'a düşer |
| E-posta anahtarı |
EmailController.php:66 |
email_admin_address |
testMail() hedefi; yoksa hata verir (:69) |
| Yeni e-posta alanı varsayılanları |
EmailController.php:50 |
group_id=3, required_role='developer', field_type='text' |
Sabit grup id |
| Yeni ayar varsayılanları |
SettingsArchitectController.php:72 |
group_id istekten, required_role='developer', field_type='text' |
Burada grup id isteğe bağlıdır |
| Toplu e-posta güncelleme önbelleği |
BackstageService.php:130 |
settings_group_seo, settings_group_system |
İki anahtar sabit; başka grup kazanılırsa önbelleği düşmez |
| Sidebar sıralama alanları |
SidebarHandler.php:47 |
category_order / menu_order |
order_num değil — okuma sırası order_num, yazma sırası bu alanlara gider |
| Panel sırası okuma alanı |
SidebarProvider.php:57,:90 |
order_num |
|
AuthRole::ROLES |
Bundles/RbnSuite/RbnAuth/Models/AuthRole |
— |
Backstage bu sabiti doğrudan kullanır (SidebarController.php:41, SidebarProvider.php:199, SidebarService.php:75) |
5. Tuzaklar ve kurallar (kodda görülen, ölçülmüş)
- Kök
Module attribute'inde service: yok. BackstageController'da
#[Module(name:'backstage', data:…, panel:'developer', context:'panel')]
(BackstageController.php:15). SyshubController ve WebhubController service: bildiriyor.
Kök controller $this->service kullanmadığı için sorun çıkmıyor; alt modüller
SubModule attribute'inden çözüyor (ComponentContext.php:52).
SidebarProvider::afterBoot() URI'ye bakar. str_contains($uri, '/category') ⇒
SidebarCategories, aksi halde /menu ⇒ SidebarMenus (SidebarProvider.php:25).
URI ikisini de içermiyorsa SidebarMenus kalır. Bu, aynı provider'ın iki modeli
karıştırmasına dayanır — getCategories()/getParentMenus() açık model adı kullandığı
için etkilenmez.
- Sıralama alanı ikiye ayrılmış. Okuma
order_num (SidebarProvider.php:57,:90),
yazma menu_order/category_order (SidebarHandler.php:47). Sıralama arayüzü ikinci alanı
doldurduğunda panelde hiçbir görünüm değişmez; tersi de doğrudur.
- Alt öğe koruması sessiz
false döner. deleteCategory() menüsü olan kategoriyi
silmez (SidebarHandler.php:68), deleteMenu() alt menüsü olanı silmez (:76-77).
Webhub'taki FrontendMenuProvider::destroy() ile aynı desen. Kullanıcıya neden
söylenmediği için controller mesajı geneldir.
reorder() transaction'ı model üzerinden yürütüyor. $model->beginTransaction()
(SidebarHandler.php:49) — model nesnesi üzerinden, yani bağlantı bu modelin
bağlandığı PDO'da. update() çağrıları aynı modelde olduğu için tutarlıdır; farklı bir
bağlantıya kaçarsa commit/rollback etkisiz kalır (kodda böyle bir çağrı yok).
- Yazma iki farklı servise dağılmış durumda.
SidebarService::saveCategory() →
SidebarProvider::saveCategory(); SidebarHandler::saveCategory() ise ayrı bir
yol (SidebarHandler.php:20). Handler, ModuleData.php:34'te handlers.sidebar olarak
kayıtlı olsa da hiçbir controller'dan çağrılmıyor — kayıtlı ama kullanılmıyor.
BackstageService okumada repository, yazmada provider kullanıyor. getGroups() ve
getSettingsRaw() → repository('project.settings')->fetch() (:61,:71);
createSetting() vb. → provider('CrudSettingsProvider') (:108,:111,:116,:120,:145).
Bu, Anayasa §8'in repository kuralıyla uyumlu tek yazma kapısıdır.
fetch() sözleşmesi: target anahtarı hedef tabloyu seçer. fetch(['target'=>'groups'])
grup tablosunu, fetch(['group_id'=>$id]) ayar tablosunu okur (BackstageService.php:58,:94).
getSettingsGroup() JOIN'de tablo ön ekini zorlar. select('z_settings.*') ve
join('z_setting_groups', …) (BackstageProvider.php:57) — tablo ön eki kodda
gömülü; ön ek değişirse bu sorgu sessizce bozulur.
CrudSettingsProvider "parazit koruması" her metotta tekrarlanır.
unset($data['redirect_url'], $data['view'], $data['id']) (:21,:42,:57,:66) —
id de silindiği için updateSetting() çağırdığı $id argümandan gelir; veri
dizisindeki id yok sayılır.
bulkUpdateSettings() önbellek anahtarları eksik. Yalnız settings_group_seo ve
settings_group_system düşürülür (:130-131). email grubu toplu güncellendiğinde
önbellek düşmez — EmailController::updateMailSettings() tam olarak bu yolu kullanır
(EmailController.php:97).
testMail() email_admin_address yoksa hiç göndermez, ayar önce kaydedilmelidir
(EmailController.php:68). site_name kaynak olarak service('seo')?->get('site_title')
kullanılır (:75) — bu anahtar Webhub'ın yazdığı meta-title/site-title ile aynı değildir.
EmailController::updateMailSettings() iki senaryo, iki doğrulama. settings dizisi
varsa doğrulamasız toplu yazma (:96-99); yoksa 8 kurallu yapısal güncelleme
(:102-113). Ham request->input('settings') kullanıldığı için toplu yolda beyaz liste yok.
6. Örnek (gerçek koddan)
// Bundles/Internal/Backstage/Handlers/SidebarHandler.php:44-64
public function reorder(string $type, array $ids): bool
{
$model = ($type === 'category') ? $this->SidebarCategoriesModel : $this->SidebarMenusModel;
$field = ($type === 'category') ? 'category_order' : 'menu_order';
$model->beginTransaction();
try {
foreach ($ids as $index => $id) {
if (!$model->update((int) $id, [$field => $index + 1])) {
$model->rollBack();
return false;
}
}
$model->commit();
return true;
} catch (Exception $e) {
$model->rollBack();
return false;
}
}
// Bundles/Internal/Backstage/Providers/CrudSettingsProvider.php:18-35
public function createSetting(array $data): int|bool
{
// 🛡️ [PARASITE PROTECTION] - Veri tabanında olmayan UI alanlarını temizle
unset($data['redirect_url'], $data['view'], $data['id']);
if (empty($data['setting_key']) && !empty($data['label_tr'])) {
$slug = $this->helper('text')->turkishSlug($data['label_tr']);
$data['setting_key'] = str_replace('-', '_', $slug);
}
$data['is_active'] = $data['is_active'] ?? 1;
// 🛡️ [JSON PROTECTION] - Boş gelen JSON alanlarını null yap
if (isset($data['field_options']) && empty($data['field_options'])) {
$data['field_options'] = null;
}
return $this->SettingsModel->create($data);
}
7. İlgili belgeler