RbnStudio — İçerik yönetimi (makale, haber, kategori, taslak)
Doğrulanan kod tabanı:
d49b4413(dalfeat/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/RbnSuite/RbnStudio/— 16*.php
(Controllers 5 · Models 2 · Views 9)
Envanter: 16 php dosyasının 16'sı aşağıdaki tabloda anlatıldı.
Bu paket 20 eşiğinin altında olduğu için tek belge yeterlidir.
1. Ne işe yarar, kimler kullanır
RbnStudio, panelin içerik üretim yarısıdır: yayınlanmış makaleler, yayınlanmış
haberler, içerik kategorileri ve yapay zekâ destekli taslak/fikir havuzu. Dört alt
modül (posts, news, categories, drafts) vardır ve her biri kendi rotası,
kontrolörü ve görünümleriyle ayrıdır.
Bu paket RbnAdmin'ın yanında, aynı admin middleware grubu altında yüklenir
(Core/Routes/Mappings/core.php:80).
Önemli mimari ayrım: RbnStudio'nın kendi veri modeli/tablosu yoktur.
Tüm içerik, projelerin tanımladığı servisler (content.blog.service,
content.news.service) ve evrensel app.contentCategory / app.contentDraft
depoları üzerinden çalışır. Bu yüzden Models/ klasöründe yalnız iki yapılandırma
sınıfı vardır (ModuleData, StudioMap); veri modelleri
Core/Database/Repositories/Project/ altındadır.
2. Dosya envanteri (16 dosya)
2.1 Controllers/ — 5
| Dosya | Görev | Önemli public yöntemler (imza) |
|---|---|---|
RbnStudioController.php |
Paket kökü. #[Module(name:'studio', data: ModuleData::class, context:'backend')] (:14-18). Alt modüllerin ortak motoru |
korumalı: getContentConfig(?string $key=null, ?string $projectKey=null): mixed (:24), hasBlog/hasNews/hasCategory/hasDraft/hasSocial(?string $projectKey=null): bool (:38-73), resolvePostService/resolvePostModel/resolveCategoryModel/resolveCategoryService/resolveDraftModel/resolveDraftService/resolveNewsService/resolveNewsModel (:78-157), executeGenerateImage(string $type='blog') (:163), executeRewrite(string $type='blog') (:225), getActiveSocialPlatforms(?string $projectKey=null): array (:267), executeSocialModal(string $type, int\|string\|null $id=null): mixed (:318), executeSocialShare(string $type='blog') (:382) |
PostsController.php |
Yayınlanmış makaleler. #[SubModule(module:'studio', entity:'posts')] (:13) |
index(), save(), delete($id), edit(?int $id=null), rewrite(), generateImage(), socialModal(?int $id=null), socialShare() |
NewsController.php |
Yayınlanmış haberler. #[SubModule(module:'studio', entity:'news')] (:13) |
index(), save(), delete($id), edit(?int $id=null), generateImage(), rewrite(), socialModal(?int $id=null), socialShare() |
CategoriesController.php |
İçerik kategorileri. #[SubModule(module:'studio', entity:'categories', model:'app.contentCategory', repository:'app.contentCategory')] (:13) |
index(), save(), delete($id), status(), modal($id=null, ?string $view=null) |
DraftsController.php |
Taslak/fikir havuzu. #[SubModule(module:'studio', entity:'drafts', model:'app.contentDraft', repository:'app.contentDraft')] (:13) |
index(), save(), delete($id), generate(int $id), modal($id=null, ?string $view=null) |
Gözlem: PostsController ve NewsController özniteliklerinde service/repository
yoktur; içerik servisi çalışma anında getRouteConfig()['content'] üzerinden
çözülür (:26 → RbnStudioController::resolvePostService()). CategoriesController ve
DraftsController ise repository verdiği için activeService kurauluşta
çözülür (ComponentHydratorTrait.php:69).
2.2 Models/ — 2
| Dosya | Görev | Önemli üyeler |
|---|---|---|
ModuleData.php |
Rota tanımı (tüm 4 alt modül burada). #[Bundle(name:'studio', context:'panel', map: StudioMap::MAP)] (:15-19) |
registerMap(): array → boş diziler (:25-31), registerRoutes(): void (:36-86) |
StudioMap.php |
Menü ağacı | MAP (:15-41): module_name, title, icon, sub_modules (4 dal) |
2.3 Views/ — 9
| Dosya | Render eden | Beklediği değişkenler |
|---|---|---|
Posts/index.rbn.php |
PostsController::index() (Controllers/PostsController.php:89) |
posts, pager, categories, categoryMap, categorySlugMap, activeTab, counts, rewrittenCount, hasCategory, hasSocial, socialPlatforms |
Posts/form.rbn.php |
PostsController::edit() (:166) |
post, id, categories, projectKey, hasCategory, hasSocial |
Posts/Partials/social_modal.rbn.php |
RbnStudioController::executeSocialModal() (:365) |
post, id, type, platforms, publishedUrl, imageUrl, previewImageUrl, projectKey, ajax (:1-8) |
News/index.rbn.php |
NewsController::index() (Controllers/NewsController.php:95) |
Posts/index ile aynı küme, rewrittenCount yok |
News/form.rbn.php |
NewsController::edit() (:170) |
Posts/form ile aynı küme |
Categories/index.rbn.php |
CategoriesController::index() (Controllers/CategoriesController.php:56) |
categories, pager, totalCount, currentType, hasBlog, hasNews, blogCount, newsCount |
Categories/Partials/modal.rbn.php |
CategoriesController::modal() (:141) |
@var array\|null $category, @var int\|null $id, @var bool $isEdit, @var bool $hasBlog, @var bool $hasNews, @var string $currentType (:1-6) |
Drafts/index.rbn.php |
DraftsController::index() (Controllers/DraftsController.php:46) |
drafts, pager, categories, categoryMap, totalCount |
Drafts/Partials/modal.rbn.php |
DraftsController::modal() (:157) |
@var array\|null $draft, @var int\|null $id, @var bool $isEdit, @var array $categories (:1-4) |
3. Rotalar (ModuleData::registerRoutes(), 4 grup / 33 uç)
| Grup | Prefix | Kontrolör | Uç sayısı | Grup satırı |
|---|---|---|---|---|
| Categories | studio/categories |
CategoriesController |
7 | :39-47 |
| Drafts | studio/drafts |
DraftsController |
7 | :50-58 |
| Posts | studio/posts |
PostsController |
10 | :61-72 |
| News | studio/news |
NewsController |
9 | :75-85 |
Toplam: 33 uç (ölçüldü: Select-String "Route::(get|post)\(" → 33 eşleşme;
bunların 33'ü de ->name('admin.studio.…') ile adlandırılmış, adsız uç yok).
RbnAdmin'ın aksine bu rotaların tamamı ->name('admin.studio.…') ile
adlandırılmıştır — handleResult()'ın returnPath()'i adlandırılmış rotaları
doğru çözer.
Üç grup Route::get('delete/{id:[0-9]+}') ve Route::post('delete/{id:[0-9]+}')
olarak iki kez kayıt etmiştir (:43-44, :52-53, :63-64, :78-79) ve ikisine
aynı isim vermiştir (admin.studio.categories.delete iki kez). Bu, düzeltilmiş
bir hatanın kalıntısı gibi görünüyor ve iki riski var: (a) aynı isimli iki rota
adlandırma defterinde çakışır, (b) GET delete bir CSRF yüzeyidir — kurbanın
tarayıcısında <img src="…/studio/posts/delete/12"> kaydı siler.
RbnAdmin'daki karşılıklarının tamamı POST'tur.
4. Akışlar
4.1 Konfigürasyon çözümleme — paketin kalbi
RbnStudio hiçbir şeyi "açık/kapalı" kendi bilmez; her yetenek
project-routemap.php içindeki view_mapping[<proje>]['content'] altında
tanımlanan bir anahtara bağlıdır:
RbnStudioController::getContentConfig($key) :24-33
→ getRouteConfig($projectKey, 'content') :31
→ ResolvesProjectConfigTrait::getRouteConfig() Core/Base/Concerns/Data/ResolvesProjectConfigTrait.php:71
→ Paths::project()->configs('project-routemap.php') (statik olarak bir kez require edilir, :74-76)
→ $map['view_mapping'][$projectKey] :109
hasBlog() → !empty(content.blog) :38-41
hasNews() → !empty(content.news) :46-49
hasCategory()→ !empty(content.category) :54-57
hasDraft() → !empty(content.draft) :62-65
hasSocial() → !empty(content.social) :70-73
Bu yüzden üç alt modül index() başında kendi varlıklarını doğrular ve yoksa
/admin/studio/posts'e yönlendirir: NewsController::index() (:23-26),
CategoriesController::index() (:23-26), DraftsController::index() (:23-26).
PostsController::index() bunu yapmaz — blog kapalıysa boş liste basar
(:26 servisi null döner, :52 boş dizi verir).
4.2 Makale kaydetme
POST /admin/studio/posts/save → ModuleData.php:62 → PostsController::save() :107
→ $this->request->input(...) ile 11 alan toplanır :112-124
(doğrulama YOK — service katmanına bırakılmış)
→ hasSocial() ise social_summary + social_hashtags eklenir :126-129
→ resolvePostService()->savePost($data) :131-132
→ handleResult($result, 'Makale işlemi', 'posts', $context) :134-135
hasSocial() false ise social_summary/social_hashtags alanları hiç
gönderilmez; servis tarafında "silme" değil, "yazma" olur — yani sosyal medya
alanı bir kez yazıldıktan sonra sosyal medya kapalısa kalıcı olarak kalır.
savePost() bu alanları doğrulamaz.
4.3 Listeleme ve sekme rozetleri
PostsController::index() (:19-102) ve NewsController::index() (:19-107)
aynı mantığı iki kez uygular (gövde neredeyse satır satır aynıdır;
tek fark News'in rewrittenCount'u render verisine koymaması, :95-106).
Her ikisi de rozet sayıları için tüm kayıtları ikinci kez çeker
(:56, NewsController.php:63) ve sayıyı PHP içinde döngüyle hesaplar — yani
getPosts() üç kez çağrılır (sorgu + rozet + sayfalama). Bu, veri büyüdükçe
üç katına çıkan bir sorgudur.
4.4 AI görsel üretimi
POST /admin/studio/posts/generate-image → executeGenerateImage('blog') RbnStudioController.php:163
├─ id / title / custom_prompt / project_key okunur :164-167
├─ prompt = custom_prompt ?: title :169
├─ service('api')->gemini('image', $prompt, ['project_key'=>…, 'id'=>…]) :175-178
├─ service('image')->base64Image($base64, "images/{$type}/".date('Y/m'),
│ 'public', ['extension'=>'webp']) :189-194
├─ $result['url'|'image_url'|'image'] = '/' . path :203-206
└─ id > 0 ise savePost(['id'=>…, 'image'=>…, 'image_prompt'=>…]) :208-217
type değeri yalnız klasör yolu (images/blog/ ↔ images/news/) ve hangi
servisin güncelleneceğini belirler (:209); news dışındaki her değer blog
gibi davranır.
4.5 İçerik yeniden yazma
POST /admin/studio/posts/rewrite → executeRewrite('blog') :225
├─ id boşsa 400-benzeri JSON hata :229-231
├─ local_model = type==='news' ? resolveNewsModel() : resolvePostModel() :235
├─ category_model = resolveCategoryModel() (varsayılan 'app.contentCategory') :236,102
└─ builder('task.content_rewrite')->autopilot([… 'force'=>true]) :238-247
Builder'ın kaydı framework'tedir:
task.content_rewrite → RbnPipeline\Builders\Tasks\ContentRewriteTaskBuilder
(Packages/PackageData.php:95, sınıf Packages/RbnPipeline/Builders/Tasks/ContentRewriteTaskBuilder.php:18).
4.6 Taslaktan makale üretimi
GET /admin/studio/drafts/generate/{id} → DraftsController::generate(int $id) :125
→ service('blogAutopilot')->generateArticle(
$id, 'Ssblogs.post', 'app.contentDraft', 'app.contentCategory',
$projectKey, ['has_author_comment'=>true]) :128-135
⚠️ blogAutopilot servisi framework içinde yoktur. Ölçüldü:
grep blogAutopilot E:\localhost → tek eşleşme, bu satırın kendisi.
'Ssblogs.post' görünüm adı da tek eşleşme. Bu uç, ilgili servis ve görünüm
yalnız belirli bir projede tanımlıysa çalışır; aksi halde 500 verir.
Ayrıntı: §5.2.
4.7 Sosyal medya paylaşımı
GET social-modal/{id} → executeSocialModal($type,$id) :318
├─ hasSocial() değilse 403 :323-325
├─ post yoksa 404 :330-332
├─ platformlar = getActiveSocialPlatforms() :334
│ → apiManager->resolveApiKey('instagram'|'facebook', $pk)
│ + apiKeys[INSTAGRAM_ACCESS_TOKEN|…] yedeği :281-283, 297-299
├─ kategori slug → yayın URL'si: {haber|blog}/{slug}/{post.slug} :348-350
├─ görsel: mutlak URL → targetProjectUrl(), önizleme → '/' . path :357-363
└─ render('Posts/Partials/social_modal', [… 'ajax'=>true]) :365-375
POST social-share → executeSocialShare($type) :382
├─ caption + hashtag + "Detaylar: <url>" birleşimi :431-432
├─ facebook: görsel varsa postPhoto(), yoksa postFeed() :446-449
├─ instagram: görsel YOKSA paylaşım yapılamaz :474-486
└─ sonuçlar platform başına toplanır, mesaj <br> ile birleşir :495-505
5. Tuzaklar (kodda ölçülmüş)
-
deleteDraft()çağrısı var, metot yok — PHP fatal. ⚠️
DraftsController::delete()(:118)
$this->repository('app.contentDraft')->deleteDraft($id)çağırıyor; ancak
ContentDraftRepositorybu metodu taşımıyor, gerçek adıdestroyDraft()
(Core/Database/Repositories/Project/ContentDraftRepository.php:91).
Ölçüldü:method_exists(ContentDraftRepository::class, 'deleteDraft')→
false. Depo katmanında__callyoktur (BaseRepositoryyalnız
ActionProviderTrait,ContentQueryTrait,ContentCacheTraitkullanır,
Core/Base/Data/BaseRepository.php:20; hiçbirinde__callyok), bu yüzden PHP
Error: Call to undefined methodfırlatır → HTTP 500.
Bu paketin en ciddi bulgusudur:GET/POST /admin/studio/drafts/delete/{id}
ucu her çağrıldığında 500 verir. -
blogAutopilotservisi veSsblogs.postgörünümü framework'te yok.
DraftsController::generate()(:128-130) ikisini de kullanıyor.
Ölçüldü: tüm workspace taramasındablogAutopilotveSsblogsiçin her biri
tek eşleşme (yani çağıranın kendisi). Bu uç, servis ve görünüm yalnız
belirli projelerde tanımlıysa çalışır; tanımlı değilse 500 verir.
Framework paketi olduğu için en azından yapılandırılabilir bir servis adı
olması beklenirdi — şu an projeye gömülü bir bağımlılık. Açık soru maddesi:
acik-sorular §2.10. -
Route::get('delete/{id}')üç grupta da kayıtlı — CSRF yüzeyi.
Dört grupta da ardışık GET+POST çifti vardır ve ikisi de aynı ada bağlıdır:
categories:43(GET)/:44(POST), drafts:52/:53,
posts:63/:64, news:78/:79— hepsiadmin.studio.<x>.deleteadıyla.
Yani adlandırma defterinde dört çift çakışma vardır.
GET sürümüCrudControllerTrait/kontrolör mantığına gider; kalıcı tehlike
düzeyidestroyDraft/deletePostçağrılarına bağlı (tuzak 1 nedeniyle zaten
500 veriyor), amadraftsdışındaki gruplardadeleteCategory()gibi gerçek
silme yollarına ulaşabilir. RbnAdmin'daki karşılıklarının tamamı POST'tur. -
registerMap()iki anahtarı da boş döndürüyor (:25-31):
servicesveprovidersboş dizi. Bu, RbnStudio'nın kendi servis veya
provider'ı olmadığı anlamına gelir — doğru ve kasıtlı (içerik servisleri
projeye aittir). AmaRoute::load()bu metodu çağırmaya devam eder, yani
her istekte boş dizi üretilir (:26-31). -
resolveCategoryModel()/resolveDraftModel()sabitproject_key'e
bakmaz.getContentConfig()içindekiproject_keyaktif projedir
(:26); taslak ve kategori modelleri evrenseldir (app.contentCategory,
app.contentDraft) ve her projede aynıdır. Bu doğru bir sadeleştirmedir. -
getActiveSocialPlatforms()her çağrıdagetPlatform()ile provider'ın
durumunu değiştirir (SocialMediaProviderdurumlu bir nesnedir,
bkz. RbnAdmin Providers §5.9). Bu metot
getPlatform('instagram')vegetPlatform('facebook')çağırır (:286,303);
ikinci çağrı birincininactiveData'sını siler ama birincinin değerleri
zaten kopyalanmıştır (:287-293), yani bu kullanım güvenlidir. -
executeSocialModal()$typedeğerini doğrulamaz.'news'dışındaki her
değerbloggibi davranır (:327,:348) — yani/studio/news/social-modal/1
ucu bir hata vermez, yanlış içerik gösterir. -
executeSocialShare()istisna mesajını kullanıcıya sızdırır.
catch (\Throwable $e) { $results['facebook'] = ['success'=>false, 'message'=>$e->getMessage()]; }
(:463,:489). Bu, RbnAuth'ın giriş akışında kapatılmış sızıntı
deseninin (AuthController.php:54) panel tarafındaki açık karşılığıdır:
ham istisna metni (SQLSTATE, dosya yolu) JSON gövdesine gider. -
Sosyal medya alanları kapatılınca temizlenmiyor. §4.2.
hasSocial()false
isesocial_summary/social_hashtagsgönderilmez; servis alanı yok saydığı için
eski değer kalır. -
DraftsController::save()toplu eklemede sonuç sayısını kendisi uyduruyor.
$savedCount++(:93) her döngüde artar, döngü sonunda
"{$savedCount} adet yeni taslak başarıyla eklendi."basılır (:97) —
saveDraft()dönüş değeri denetlenmez. Depo yazma başarısız olsa bile
"başarıyla eklendi" denir. Tekli eklemede (:102-109) sonuç denetlenir. -
DraftsController::save()toplu eklemedeiddalını atlar.
if ($id)dalı önce çalışır (:65-74), sonrabulk_mode(:76-98).
Yaniidvebulk_modebirlikte gönderilirse toplu ekleme sessizce
yok sayılır ve tek kayıt güncellenir. -
CategoriesController::save()varsayılan tipigetRouteConfigile
çözüyor,getContentConfigile değil.:89-90:
$this->getRouteConfig($projectKey, 'has_blog')— amahasBlog()(§4.1)
content.bloganahtarını okur. İki farklı anahtar ailesidir; biri
tanımlıysa diğeri değilse tip yanlış çözülür. Aynı desen
CategoriesController::modal()(:137-138) veindex()(:31-32) içinde de
vardır — üç kez. -
Liste ekranları veriyi üç kez çekiyor. §4.3.
getPosts()sorgu + rozet +
sayfalama için üç kez çağrılır (PostsController.php:52,56).NewsController
da aynı (:59,63). -
StudioMap::MAPileModuleDataarasındaki bağ#[Bundle(map: …)]
özniteliğidir.ModuleData.php:15.ModuleDataDriverbu haritadan
sub_modules'ı alır (Core/System/Discovery/Engine/Drivers/ModuleDataDriver.php:169). -
DraftsController::index()kategori tipiniblog'a sabitler
(:43,:155):getCategories(['type'=>'blog', …]). Taslaklar haber
kategorileriyle ilişkilendirilemez — bilinçli bir kısıt.
6. Örnek (gerçek koddan)
Yeteneğin varlığına göre yönlendirme — paketin tipik kalıbı:
// Bundles/RbnSuite/RbnStudio/Controllers/NewsController.php:21-26
$projectKey = $this->activeProjectKey();
if (!$this->hasNews($projectKey)) {
$this->response->redirect('/admin/studio/posts');
return;
}
ve konfigürasyondan servis çözümleme:
// Bundles/RbnSuite/RbnStudio/Controllers/RbnStudioController.php:78-82
protected function resolvePostService(?string $projectKey = null): ?object
{
$conf = $this->getContentConfig('blog', $projectKey);
return !empty($conf['service']) ? $this->service($conf['service']) : null;
}
7. İlgili belgeler
- RbnAdmin — panelin bu paketle ortak rotaları,
SocialMediaProvider - RbnAuth — rotaların
adminmiddleware'i - Core/Database/Repositories —
ContentCategoryRepository,ContentDraftRepository - Packages/RbnPipeline —
task.content_rewritebuilder'ı (⏳ ayrı görev) - Packages/RbnApi —
service('api')->gemini()(⏳ ayrı görev) - acik-sorular