# RbnStudio — İçerik yönetimi (makale, haber, kategori, taslak)
> **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/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-86`).
**Ö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-70`).
### 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 `
` 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[]['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: " 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
ile birleşir :495-505
```
## 5. Tuzaklar (kodda ölçülmüş)
1. **`deleteDraft()` çağrısı var, metot yok — PHP fatal.** ⚠️
`DraftsController::delete()` (`:118`)
`$this->repository('app.contentDraft')->deleteDraft($id)` çağırıyor; ancak
`ContentDraftRepository` bu 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 `__call` yoktur (`BaseRepository` yalnız
`ActionProviderTrait`, `ContentQueryTrait`, `ContentCacheTrait` kullanır,
`Core/Base/Data/BaseRepository.php:20`; hiçbirinde `__call` yok), bu yüzden PHP
`Error: Call to undefined method` fırlatır → HTTP 500.
Bu paketin **en ciddi** bulgusudur: `GET/POST /admin/studio/drafts/delete/{id}`
ucu her çağrıldığında 500 verir.
2. **`blogAutopilot` servisi ve `Ssblogs.post` görünümü framework'te yok.**
`DraftsController::generate()` (`:128-130`) ikisini de kullanıyor.
Ölçüldü: tüm workspace taramasında `blogAutopilot` ve `Ssblogs` iç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](../../../acik-sorular.md).
3. **`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` — hepsi `admin.studio..delete` adı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üzeyi `destroyDraft`/`deletePost` çağrılarına bağlı (tuzak 1 nedeniyle zaten
500 veriyor), ama `drafts` dışındaki gruplarda `deleteCategory()` gibi gerçek
silme yollarına ulaşabilir. RbnAdmin'daki karşılıklarının tamamı POST'tur.
4. **`registerMap()` iki anahtarı da boş döndürüyor** (`:25-31`):
`services` ve `providers` boş dizi. Bu, RbnStudio'nın **kendi servis veya
provider'ı olmadığı** anlamına gelir — doğru ve kasıtlı (içerik servisleri
projeye aittir). Ama `Route::load()` bu metodu çağırmaya devam eder, yani
her istekte boş dizi üretilir (`:26-31`).
5. **`resolveCategoryModel()` / `resolveDraftModel()` sabit `project_key`'e
bakmaz.** `getContentConfig()` içindeki `project_key` **aktif** projedir
(`:26`); taslak ve kategori modelleri evrenseldir (`app.contentCategory`,
`app.contentDraft`) ve her projede aynıdır. Bu doğru bir sadeleştirmedir.
6. **`getActiveSocialPlatforms()` her çağrıda `getPlatform()` ile provider'ın
durumunu değiştirir** (`SocialMediaProvider` durumlu bir nesnedir,
bkz. [RbnAdmin Providers §5.9](../RbnAdmin/Providers.md)). Bu metot
`getPlatform('instagram')` **ve** `getPlatform('facebook')` çağırır (`:286,303`);
ikinci çağrı birincinin `activeData`'sını siler ama birincinin değerleri
**zaten kopyalanmıştır** (`:287-293`), yani bu kullanım güvenlidir.
7. **`executeSocialModal()` `$type` değerini doğrulamaz.** `'news'` dışındaki her
değer `blog` gibi davranır (`:327`, `:348`) — yani `/studio/news/social-modal/1`
ucu bir hata vermez, **yanlış** içerik gösterir.
8. **`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-70`) panel tarafındaki açık karşılığıdır:
ham istisna metni (SQLSTATE, dosya yolu) JSON gövdesine gider.
9. **Sosyal medya alanları kapatılınca temizlenmiyor.** §4.2. `hasSocial()` false
ise `social_summary`/`social_hashtags` gönderilmez; servis alanı yok saydığı için
eski değer kalır.
10. **`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.
11. **`DraftsController::save()` toplu eklemede `id` dalını atlar.**
`if ($id)` dalı önce çalışır (`:65-74`), sonra `bulk_mode` (`:76-98`).
Yani `id` **ve** `bulk_mode` birlikte gönderilirse toplu ekleme **sessizce
yok sayılır** ve tek kayıt güncellenir.
12. **`CategoriesController::save()` varsayılan tipi `getRouteConfig` ile
çözüyor, `getContentConfig` ile değil.** `:89-90`:
`$this->getRouteConfig($projectKey, 'has_blog')` — ama `hasBlog()` (§4.1)
`content.blog` anahtarı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`) ve `index()` (`:31-32`) içinde de
vardır — üç kez.
13. **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`).
14. **`StudioMap::MAP` ile `ModuleData` arasındaki bağ `#[Bundle(map: …)]`
özniteliğidir.** `ModuleData.php:15-19`. `ModuleDataDriver` bu haritadan
`sub_modules`'ı alır (`Core/System/Discovery/Engine/Drivers/ModuleDataDriver.php:169`).
15. **`DraftsController::index()` kategori tipini `blog`'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ı:
```php
// 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:
```php
// 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](../RbnAdmin/README.md) — panelin bu paketle ortak rotaları, `SocialMediaProvider`
* [RbnAuth](../RbnAuth/README.md) — rotaların `admin` middleware'i
* [Core/Database/Repositories](../../../Core/Database/Repositories.md) — `ContentCategoryRepository`, `ContentDraftRepository`
* [Packages/RbnPipeline](../../../Packages/RbnPipeline.md) — `task.content_rewrite` builder'ı (⏳ ayrı görev)
* [Packages/RbnApi](../../../Packages/RbnApi.md) — `service('api')->gemini()` (⏳ ayrı görev)
* [acik-sorular](../../../acik-sorular.md)