Laravel 30.05.2026 12 dk okuma 2 görüntüleme

Repository Design Pattern Nedir? Laravel’de Nasıl Kullanılır?

Repository Design Pattern kavramını, Laravel projelerinde interface, Eloquent repository, service layer, dependency injection ve test edilebilir mimari örnekleriyle anlatıyorum.

Repository Design Pattern Nedir? Laravel’de Nasıl Kullanılır? kapak görseli

Repository Design Pattern, veri erişim kodunu controller veya service içinden ayırıp ayrı bir sınıfta toplama yaklaşımıdır. Laravel’de çoğu zaman Eloquent zaten güçlü bir repository gibi davranır; bu yüzden bu pattern her projede otomatik kullanılmalı demek doğru değildir. Ama büyük projelerde, iş kurallarının arttığı yerlerde, veri kaynağının değişebileceği durumlarda ve test yazmak istediğinde repository yapısı ciddi düzen sağlar.

Bu yazıda Repository Design Pattern nedir, Laravel’de ne zaman kullanılır, ne zaman abartı olur, interface neden gerekir, Eloquent repository nasıl yazılır, service container binding nasıl yapılır ve controller/service içinde nasıl kullanılır adım adım anlatacağım.

Repository Design Pattern Nedir?

Repository Pattern’in ana fikri şudur: Uygulamanın iş kuralları verinin nereden ve nasıl geldiğini bilmek zorunda kalmasın. Controller veya service “aktif yazıları getir” der; bu verinin Eloquent’ten mi, cache’ten mi, API’den mi, farklı bir tablodan mı geldiğini repository sınıfı yönetir.

Basit akış şöyledir:

Controller
    ↓
Service
    ↓
Repository Interface
    ↓
Eloquent Repository
    ↓
Database

Bu sayede controller HTTP isteğiyle ilgilenir, service iş kuralını yürütür, repository veri erişimini yönetir. Her sınıf kendi sorumluluğunda kalır.

Laravel’de Repository Pattern Şart mı?

Hayır, şart değildir. Laravel’in Eloquent ORM’i zaten çok güçlüdür. Küçük bir blog, basit bir CRUD panel veya birkaç tabloluk uygulamada her model için repository yazmak gereksiz soyutlama olabilir. Örneğin şu kod küçük projede gayet anlaşılırdır:

$posts = Post::query()
    ->where('is_published', true)
    ->latest()
    ->paginate(10);

Ama proje büyüdükçe aynı sorgu farklı controller’larda tekrar ediyorsa, sorgular karmaşıklaşıyorsa, cache eklemek istiyorsan, testlerde gerçek veritabanına daha az bağımlı kalmak istiyorsan veya ileride veri kaynağı değişebilir diyorsan repository mantıklı hale gelir.

Ne Zaman Kullanılır?

  • Aynı veri erişim sorguları birçok yerde tekrar ediyorsa.
  • Controller içinde Eloquent sorguları çok uzadıysa.
  • İş kuralları service katmanında toplanıyorsa.
  • Unit testlerde repository mocklamak istiyorsan.
  • Cache, API, dosya veya farklı veri kaynaklarını tek arayüz arkasında saklamak istiyorsan.
  • Takım içinde “veri erişimi burada, iş kuralı burada” sınırını netleştirmek istiyorsan.

Ne Zaman Kullanılmaz?

  • Proje çok küçükse ve sadece basit CRUD yapıyorsa.
  • Her model için otomatik repository üretip içine sadece `all`, `find`, `create` yazacaksan.
  • Eloquent’in sağladığı ilişki, scope ve query builder özelliklerini gereksiz yere saklayacaksan.
  • Soyutlama kodu, çözdüğü problemden daha büyük hale geliyorsa.

Repository Pattern iyi kullanılırsa mimariyi temizler; ezbere kullanılırsa projeyi kalabalıklaştırır.

Örnek Senaryo: Blog Yazıları

Bir Laravel blog projesi düşünelim. `Post` modelimiz var. Yayındaki yazıları listeleyeceğiz, slug ile yazı bulacağız, admin panelde taslak/yayın durumunu yöneteceğiz. Bu iş için repository oluşturalım.

1. Model ve Migration

Önce model oluşturalım:

php artisan make:model Post -m

`database/migrations/...create_posts_table.php`:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('excerpt')->nullable();
            $table->longText('body');
            $table->boolean('is_published')->default(false);
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Migration çalıştır:

php artisan migrate

`app/Models/Post.php`:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = [
        'title',
        'slug',
        'excerpt',
        'body',
        'is_published',
        'published_at',
    ];

    protected $casts = [
        'is_published' => 'boolean',
        'published_at' => 'datetime',
    ];

    public function scopePublished(Builder $query): Builder
    {
        return $query
            ->where('is_published', true)
            ->whereNotNull('published_at')
            ->where('published_at', '<=', now());
    }
}

2. Repository Interface Oluştur

Interface, uygulamanın repository’den ne beklediğini tanımlar. Uygulama “EloquentPostRepository” sınıfına değil, “PostRepositoryInterface” sözleşmesine bağımlı olur.

Klasör ve dosya oluştur:

mkdir app\Contracts
nano app\Contracts\PostRepositoryInterface.php

`app/Contracts/PostRepositoryInterface.php`:

<?php

namespace App\Contracts;

use App\Models\Post;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;

interface PostRepositoryInterface
{
    public function paginatePublished(int $perPage = 10): LengthAwarePaginator;

    public function latestPublished(int $limit = 5): Collection;

    public function findPublishedBySlug(string $slug): ?Post;

    public function create(array $data): Post;

    public function update(Post $post, array $data): Post;
}

Burada dikkat et: interface içinde “nasıl” değil, “ne” var. Veriyi Eloquent mi getiriyor, cache mi kullanıyor, başka API’ye mi gidiyor; interface bunu bilmez.

3. Eloquent Repository Yaz

Şimdi interface’i uygulayan Eloquent repository sınıfını oluşturalım:

mkdir app\Repositories
nano app\Repositories\EloquentPostRepository.php

`app/Repositories/EloquentPostRepository.php`:

<?php

namespace App\Repositories;

use App\Contracts\PostRepositoryInterface;
use App\Models\Post;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;

class EloquentPostRepository implements PostRepositoryInterface
{
    public function paginatePublished(int $perPage = 10): LengthAwarePaginator
    {
        return Post::query()
            ->published()
            ->latest('published_at')
            ->paginate($perPage);
    }

    public function latestPublished(int $limit = 5): Collection
    {
        return Post::query()
            ->published()
            ->latest('published_at')
            ->limit($limit)
            ->get();
    }

    public function findPublishedBySlug(string $slug): ?Post
    {
        return Post::query()
            ->published()
            ->where('slug', $slug)
            ->first();
    }

    public function create(array $data): Post
    {
        return Post::create($data);
    }

    public function update(Post $post, array $data): Post
    {
        $post->update($data);

        return $post->refresh();
    }
}

Artık yazı listeleme ve bulma sorguları controller içinde dağınık durmuyor. Hepsi repository sınıfında toplandı.

4. Service Container Binding

Laravel’in service container sistemi, interface istendiğinde hangi sınıfın verileceğini bilir. Bunu `AppServiceProvider` içinde tanımlarız.

`app/Providers/AppServiceProvider.php`:

<?php

namespace App\Providers;

use App\Contracts\PostRepositoryInterface;
use App\Repositories\EloquentPostRepository;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(
            PostRepositoryInterface::class,
            EloquentPostRepository::class
        );
    }

    public function boot(): void
    {
        //
    }
}

Bu binding sayesinde Laravel, constructor içinde `PostRepositoryInterface` gördüğünde otomatik olarak `EloquentPostRepository` nesnesi verir.

5. Controller İçinde Kullanım

Controller oluşturalım:

php artisan make:controller BlogController

`app/Http/Controllers/BlogController.php`:

<?php

namespace App\Http\Controllers;

use App\Contracts\PostRepositoryInterface;
use Illuminate\Http\Request;

class BlogController extends Controller
{
    public function __construct(
        private readonly PostRepositoryInterface $posts
    ) {
    }

    public function index()
    {
        $posts = $this->posts->paginatePublished(10);

        return view('blog.index', compact('posts'));
    }

    public function show(string $slug)
    {
        $post = $this->posts->findPublishedBySlug($slug);

        abort_unless($post, 404);

        return view('blog.show', compact('post'));
    }
}

Controller artık Eloquent sorgusunu bilmiyor. Sadece repository’den veri istiyor. Bu ayrım özellikle controller büyüdükçe kodu okunabilir tutar.

6. Route Tanımları

`routes/web.php`:

use App\Http\Controllers\BlogController;
use Illuminate\Support\Facades\Route;

Route::get('/blog', [BlogController::class, 'index'])
    ->name('blog.index');

Route::get('/blog/{slug}', [BlogController::class, 'show'])
    ->name('blog.show');

7. Service Layer ile Daha Temiz Kullanım

Küçük projede controller doğrudan repository kullanabilir. Ama iş kuralları artarsa service layer eklemek daha temiz olur. Örneğin admin yazı oluştururken slug üretmek, yayın tarihi ayarlamak, cache temizlemek gibi işlemler service içinde kalabilir.

mkdir app\Services
nano app\Services\PostService.php

`app/Services/PostService.php`:

<?php

namespace App\Services;

use App\Contracts\PostRepositoryInterface;
use App\Models\Post;
use Illuminate\Support\Str;

class PostService
{
    public function __construct(
        private readonly PostRepositoryInterface $posts
    ) {
    }

    public function createFromAdmin(array $data): Post
    {
        $data['slug'] = $data['slug'] ?? Str::slug($data['title']);

        if (($data['is_published'] ?? false) && empty($data['published_at'])) {
            $data['published_at'] = now();
        }

        return $this->posts->create($data);
    }
}

Bu yaklaşımda repository veri erişimini, service ise iş kuralını yönetir. İkisini karıştırmamak önemlidir.

Repository İçine Ne Yazılır, Ne Yazılmaz?

Repository içine veri erişim operasyonları yazılır:

  • Yayınlanmış yazıları getir.
  • Slug ile yazı bul.
  • Kategoriye göre yazıları listele.
  • Arama sorgusu çalıştır.
  • Veriyi oluştur, güncelle, sil.

Repository içine iş kuralı doldurmak doğru değildir. Örneğin “yazı yayına alınırken admin’e mail gönder”, “SEO skoru hesapla”, “bildirim üret” gibi işler service veya action sınıflarına daha uygundur.

8. Cache Eklemek İstersek

Repository yapısının avantajlarından biri cache gibi detayları tek noktaya alabilmektir:

use Illuminate\Support\Facades\Cache;

public function latestPublished(int $limit = 5): Collection
{
    return Cache::remember("posts.latest.{$limit}", now()->addMinutes(10), function () use ($limit) {
        return Post::query()
            ->published()
            ->latest('published_at')
            ->limit($limit)
            ->get();
    });
}

Controller bu cache detayını bilmez. Sadece `latestPublished` metodunu çağırır.

9. Test Yazmak

Repository interface kullandığında service testlerinde repository’yi mocklamak kolaylaşır. Örneğin `PostService` için basit test:

php artisan make:test PostServiceTest --unit

`tests/Unit/PostServiceTest.php`:

<?php

namespace Tests\Unit;

use App\Contracts\PostRepositoryInterface;
use App\Models\Post;
use App\Services\PostService;
use Mockery;
use Tests\TestCase;

class PostServiceTest extends TestCase
{
    public function test_it_generates_slug_when_creating_post(): void
    {
        $repository = Mockery::mock(PostRepositoryInterface::class);

        $repository
            ->shouldReceive('create')
            ->once()
            ->with(Mockery::on(fn ($data) => $data['slug'] === 'merhaba-laravel'))
            ->andReturn(new Post([
                'title' => 'Merhaba Laravel',
                'slug' => 'merhaba-laravel',
            ]));

        $service = new PostService($repository);

        $post = $service->createFromAdmin([
            'title' => 'Merhaba Laravel',
            'body' => 'İçerik',
            'is_published' => false,
        ]);

        $this->assertSame('merhaba-laravel', $post->slug);
    }
}

Bu testte gerçek veritabanına dokunmadan service davranışını kontrol ettik. Repository interface burada test edilebilirliği artırdı.

10. Repository Testi de Yazılabilir

Repository’nin Eloquent sorgularını doğru çalıştırdığını görmek için feature/integration test yazabilirsin:

php artisan make:test EloquentPostRepositoryTest

Test içinde gerçek veritabanı kullanılır:

<?php

namespace Tests\Feature;

use App\Models\Post;
use App\Repositories\EloquentPostRepository;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class EloquentPostRepositoryTest extends TestCase
{
    use RefreshDatabase;

    public function test_it_finds_only_published_post_by_slug(): void
    {
        Post::create([
            'title' => 'Yayında',
            'slug' => 'yayinda',
            'body' => 'İçerik',
            'is_published' => true,
            'published_at' => now(),
        ]);

        Post::create([
            'title' => 'Taslak',
            'slug' => 'taslak',
            'body' => 'İçerik',
            'is_published' => false,
        ]);

        $repository = new EloquentPostRepository();

        $this->assertNotNull($repository->findPublishedBySlug('yayinda'));
        $this->assertNull($repository->findPublishedBySlug('taslak'));
    }
}

11. Klasör Yapısı Nasıl Olmalı?

Basit ve anlaşılır bir yapı şöyle olabilir:

app/
  Contracts/
    PostRepositoryInterface.php
  Repositories/
    EloquentPostRepository.php
  Services/
    PostService.php
  Http/
    Controllers/
      BlogController.php
  Models/
    Post.php

İsimlendirmede tutarlı olmak önemlidir. Interface ve implementation isimleri açık olmalı: `PostRepositoryInterface`, `EloquentPostRepository` gibi.

12. Sık Yapılan Hatalar

Her modele repository yazmak: Problem yoksa soyutlama eklemek çözüm değildir. Önce ihtiyaç oluşsun.

Repository içine iş kuralı koymak: Repository veri erişim katmanıdır. İş akışı service/action tarafında kalmalıdır.

Interface yazıp container binding yapmamak: Laravel hangi implementation’ı vereceğini bilemez. `AppServiceProvider` binding şarttır.

Eloquent’i tamamen saklamaya çalışmak: Laravel’de Eloquent güçlüdür. Repository kullanırken Eloquent’in pratik avantajlarını yok etmemek gerekir.

Metod adlarını çok genel bırakmak: `getData`, `process`, `handle` gibi belirsiz adlar yerine `paginatePublished`, `findPublishedBySlug` gibi niyeti net adlar kullanılmalıdır.

13. Kısa Özet

Controller: HTTP isteğini alır, response döner.
Service: İş kurallarını yürütür.
Repository Interface: Uygulamanın veri erişim sözleşmesini tanımlar.
Eloquent Repository: Eloquent ile gerçek veri erişimini yapar.
Service Container: Interface ile implementation eşleşmesini yönetir.

Sonuç

Repository Design Pattern, Laravel projelerinde controller’ları hafifletmek, veri erişim kodunu tek yerde toplamak ve service katmanını test edilebilir hale getirmek için güçlü bir yaklaşımdır. Fakat her projeye otomatik eklenmesi gereken bir kural değildir. Küçük CRUD projelerinde Eloquent’i doğrudan kullanmak daha sade olabilir. Proje büyüdüğünde, sorgular tekrar ettiğinde, cache veya farklı veri kaynakları devreye girdiğinde repository pattern gerçekten değer üretir.

Benim önerim şu: Önce Eloquent scope ve ilişkilerini iyi kullan. Controller şişmeye, sorgular tekrar etmeye ve iş kuralları dağılmaya başladığında repository + service layer yapısına geç. Böylece pattern’i ezbere değil, ihtiyaca göre kullanmış olursun.

Kaynak notu: Bu yazı Laravel’in service container, dependency injection, Eloquent ve testing yaklaşımı temel alınarak hazırlanmıştır. Repository Pattern Laravel’in zorunlu bir parçası değil, doğru yerde kullanıldığında mimariyi temizleyen bir tasarım tercihidir.

#Repository Pattern #Laravel mimari #Service Container #Eloquent #Dependency Injection #SOLID