Laravel 13 & Paper ile Sıfırdan Veritabanısız İçerik Yönetimi İnşası
Geleneksel web uygulamalarında içerik yönetimi (CMS), teknik dokümantasyon veya blog modülleri genellikle MySQL, PostgreSQL veya SQLite gibi ilişkisel veritabanları üzerinde kurgulanır. Ancak teknik notlar, eğitim rehberleri ve portföy içerikleri söz konusu olduğunda veritabanı bağımlılığı birtakım dezavantajlar doğurur:
- İçerik değişikliklerinin Git commit geçmişinde (
git diff) satır satır izlenememesi, - Ortamlar arası (Local, Staging, Production) veritabanı taşıma (migration/dump) maliyetleri,
- Yapay zeka ajanlarının dosya sistemindeki dosyalara doğrudan erişemeyip karmaşık DB araçlarına ihtiyaç duyması.
Laravel Paper (jacobjoergensen/laravel-paper), bu problemi ortadan kaldırarak Eloquent benzeri bir API ile doğrudan Markdown ve YAML dosyalarını veritabanı tabloları gibi sorgulamamızı sağlar.
Bu rehberde, sıfırdan Laravel 13 üzerinde Paper paketini entegre edecek, modelimizi PHP 8.3+ öznitelikleriyle (Attributes) yapılandıracak ve yüksek performanslı in-memory caching ile çalışan bir Flat-File içerik sistemi kuracağız.
1. Rehber Özeti & Neler Öğreneceğiz?
Bu rehberi tamamladığınızda şu yetkinlikleri kazanmış olacaksınız:
- Flat-File Mimari Prensipleri: Veritabanı olmadan GitOps tabanlı içerik saklama avantajları.
- Paper Paketi Entegrasyonu: Composer kurulumu ve temel konfigürasyon.
- PHP 8.3+ Attributes ile Model Tanımı:
#[Driver('markdown')]ve#[ContentPath('...')]kullanımı. - Frontmatter & Markdown Ayrıştırma: Başlık, zorluk, etiketler ve adımların dinamik accessor metodları ile modellenmesi.
- In-Memory Caching & Mtime Doğrulaması: Dosya okuma maliyetlerini sıfıra indiren önbellek optimizasyonu.
2. Ön Koşullar & Gerekli Araçlar
| Bileşen | Minimum Sürüm | Amaç |
|---|---|---|
| PHP | 8.3+ | PHP 8 Attributes ve modern accessor sözdizimi |
| Laravel | 13.x | Web çatısı |
| Composer | 2.7+ | Bağımlılık yönetimi |
| Git | Güncel | İçeriklerin versiyon kontrolü |
3. Aşama Aşama Adımlar
Adım 1: Laravel Paper Paketinin Kurulumu ve Yapılandırılması
Terminali açın ve Laravel projenizin kök dizininde jacobjoergensen/laravel-paper paketini Composer ile projenize dahil edin:
composer require jacobjoergensen/laravel-paper
Paket, Laravel'in paket keşif (package discovery) mekanizması sayesinde servis sağlayıcısını otomatik olarak kaydeder. Gerekirse ayarları özelleştirmek için yapılandırma dosyasını yayınlayabilirsiniz:
php artisan vendor:publish --tag=paper-config
Bu komut config/paper.php dosyasını oluşturur. Varsayılan ayarlar çoğu Markdown projesi için hazırdır.
Adım 2: İçerik Depolama Dizin Yapısının ve İzinlerinin Oluşturulması
İçeriklerimizi saklamak için projenin kök dizininde content/learn/ adında bir dizin oluşturuyoruz:
mkdir -p content/learn
Dizinin web sunucusu (veya yerel PHP işlemi) tarafından okunabilir olduğundan emin olun:
chmod -R 755 content/learn
Her içerik bu klasör altında birer .md dosyası olarak saklanacaktır (örneğin content/learn/modern-mcp-servers.md). Dosya adı varsayılan olarak modelin slug veya id değeri kabul edilir.
Adım 3: Paper Modelinin PHP 8.3+ Attribute'ları ile Tanımlanması
Laravel 13'te Paper modeli tanımlarken PHP özniteliklerinden (Attributes) faydalanırız. Eloquent Model sınıfından miras alıp Paper trait'ini dahil ediyoruz.
app/Models/LearnEntry.php modelimizi oluşturalım:
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use JacobJoergensen\LaravelPaper\Attributes\ContentPath;
use JacobJoergensen\LaravelPaper\Attributes\Driver;
use JacobJoergensen\LaravelPaper\Paper;
#[Driver('markdown')]
#[ContentPath('content/learn')]
class LearnEntry extends Model
{
use Paper;
protected $guarded = [];
/**
* Markdown metnini HTML çıktısına dönüştürür.
*/
public function getHtmlBodyAttribute(): string
{
return Str::markdown((string) ($this->content ?? ''));
}
/**
* Ortalama okuma süresini dakika bazında hesaplar.
*/
public function getReadingTimeAttribute(): int
{
$content = (string) ($this->content ?? '');
$words = preg_match_all('/\p{L}+/u', strip_tags($content), $matches) ? count($matches[0]) : str_word_count(strip_tags($content));
return max(1, (int) ceil($words / 180));
}
/**
* Frontmatter'daki difficulty değerini döner (varsayılan: 'Orta Seviye').
*/
public function getDifficultyAttribute(): string
{
return (string) ($this->attributes['difficulty'] ?? $this->difficulty ?? 'Orta Seviye');
}
/**
* Bootstrap 5 rozet sınıfını döner.
*/
public function getDifficultyBadgeClassAttribute(): string
{
return match ($this->difficulty) {
'Başlangıç' => 'text-bg-success',
'Orta Seviye' => 'text-bg-primary',
'İleri Seviye' => 'text-bg-warning',
default => 'text-bg-secondary',
};
}
}
Özniteliklerin Anlamı:
#[Driver('markdown')]: Dosyalarınleague/commonmarkvespatie/yaml-front-matterile ayrıştırılacağını belirtir.#[ContentPath('content/learn')]: Dosyaların aranacağı göreceli disk yolunu tanımlar.use Paper;: Modeleall(),where(),sortBy(),firstWhere()gibi sorgu yeteneklerini ekler.
Adım 4: YAML Frontmatter ve Markdown İçerik Standartlarının Belirlenmesi
Her dosya --- ayraçları arasında YAML üstverileriyle başlamalıdır.
Örnek bir içerik şablonu:
---
title: "Laravel 13 & Paper ile Sıfırdan Veritabanısız İçerik Yönetimi İnşası"
subtitle: "Veritabanı Bağımsızlığı ve Git Tabanlı CMS"
category: "Backend & Mimari"
status: "completed"
type: "guide"
difficulty: "Başlangıç"
duration: "15 dk uygulama"
prerequisites:
- "PHP 8.3+"
- "Laravel 13"
tags:
- "Laravel 13"
- "Paper"
icon: "fab fa-laravel"
gradient: "linear-gradient(135deg, #ef4444 0%, #f97316 100%)"
date: "2026-03-02"
---
# Rehber Başlığı
İçerik buraya yazılır...
Model, YAML bloktaki her anahtarı bir model niteliği ($entry->title, $entry->difficulty) olarak erişilebilir kılar. Markdown gövdesi ise $entry->content alanına yerleştirilir.
Adım 5: Controller ile Sorgulama, Sıralama ve Filtreleme Mantığının Kurulması
Controller katmanında Paper modellerini tıpkı Eloquent gibi sorgulayabiliriz.
app/Http/Controllers/LearnController.php dosyasını oluşturalım:
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\LearnEntry;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class LearnController extends Controller
{
public function index(Request $request): View
{
// 1. Tüm içerikleri tarihe göre azalan sırada al
$entries = LearnEntry::all()->sortByDesc('date');
// 2. İstatistikleri hesapla
$stats = [
'total' => $entries->count(),
'completed' => $entries->where('status', 'completed')->count(),
'learning' => $entries->where('status', 'learning')->count(),
'experimenting' => $entries->where('status', 'experimenting')->count(),
];
// 3. Benzersiz kategorileri listele
$categories = $entries->pluck('category')->filter()->unique()->values()->all();
return view('learn.index', compact('entries', 'stats', 'categories'));
}
public function show(string $slug): View
{
// Dosya adı veya slug eşleşmesi
$entry = LearnEntry::all()->firstWhere('slug', $slug);
if (!$entry) {
abort(404, 'Eğitim içeriği bulunamadı.');
}
// Önceki ve sonraki kayıt navigasyonu
$allEntries = LearnEntry::all()->sortBy('date')->values();
$currentIndex = $allEntries->search(fn ($item) => ($item->slug ?? $item->id) === $slug);
$previous = $currentIndex > 0 ? $allEntries->get($currentIndex - 1) : null;
$next = ($currentIndex !== false && $currentIndex < $allEntries->count() - 1) ? $allEntries->get($currentIndex + 1) : null;
return view('learn.show', compact('entry', 'previous', 'next'));
}
}
Adım 6: Production Ortamı İçin In-Memory Dosya Önbellekleme (Caching) Optimizasyonu
Her HTTP isteğinde diskteki onlarca Markdown dosyasını açıp YAML parse etmek disk I/O yükü getirebilir. Laravel Cache katmanını kullanarak içerikleri belleğe alabilir ve dosya değişiklik zamanını (mtime) izleyerek otomatik önbellek yenilemesi yapabiliriz:
// app/Models/LearnEntry.php içine eklenebilir veya Servis katmanında kullanılabilir:
public static function getCachedEntries()
{
return cache()->remember('paper_learn_entries', now()->addDay(), function () {
return static::all()->sortByDesc('date');
});
}
İçerik deploy edildiğinde veya Git push yapıldığında önbelleği tek komutla temizleyebilirsiniz:
php artisan cache:clear
4. Yaygın Hatalar ve Çözümleri (Troubleshooting)
1. YAML Parse Error: "A colon cannot be used in an unquoted mapping value"
- Neden: Başlık veya alt başlıklarda tırnak işareti olmadan
:(iki nokta üst üste) karakteri kullanılması. - Çözüm: YAML frontmatter içindeki metin alanlarını her zaman çift tırnak
"..."içine alın.
2. Dosya Bulunamadı (ContentPath Error)
- Neden:
#[ContentPath('content/learn')]yolunun yanlış yazılması veya klasörün olmaması. - Çözüm:
base_path('content/learn')klasörünün mevcut olduğundan vemkdir -p content/learnçalıştırıldığından emin olun.
3. Tarih Formatı Ayrıştırma Hatası
- Neden: Frontmatter'daki
datealanının Carbon tarafından tanınmayan bir formatta olması. - Çözüm: Tarihleri daima ISO-8601 standardında (
YYYY-MM-DD, örn:2026-03-02) yazın.
5. Test, Doğrulama & Canlı Çalıştırma
Modelin doğru çalıştığını php artisan tinker veya tek satırlık test komutuyla doğrulayalım:
php artisan tinker --execute="echo 'Kayıt Sayısı: ' . \App\Models\LearnEntry::all()->count() . PHP_EOL;"
Beklenen Çıktı:
Kayıt Sayısı: 4
Bir kaydın detaylarını incelemek için:
php artisan tinker --execute="
$entry = \App\Models\LearnEntry::all()->first();
dump($entry->title, $entry->difficulty, $entry->steps_count, $entry->reading_time);
"
Tüm testler başarıyla geçtiğinde; veritabanı kurulumuna gerek duymayan, Git ile tam uyumlu ve ışık hızında çalışan flat-file içerik yönetiminiz kullanıma hazırdır!