Dari separation of concerns sampai service class, kita menata lapisan aplikasi di atas satu asumsi: query database ditulis langsung lewat Eloquent, dan tidak ada yang protes. Pattern terakhir yang kita bahas justru memprotes hal itu. Repository pattern mengusulkan satu lapisan khusus antara aplikasi dan penyimpanan data: interface yang mendefinisikan apa yang dibutuhkan, plus implementasi yang tahu bagaimana mengambilnya. Pattern ini populer di banyak framework — tetapi di Laravel ia memicu perdebatan panjang, dan chapter ini akan memperlakukannya secara jujur: kita bangun repository sungguhan, uji janji-janjinya, lalu ukur apakah imbalannya sepadan dengan biayanya di aplikasi Laravel.

Pola Repository dalam Satu Pandangan #

Perjanjian repository dituangkan dalam interface — di sini, satu method untuk mengambil post terbaru milik seorang penulis:

php
<?php

namespace App\Contracts;

use App\Models\User;
use Illuminate\Support\Collection;

interface PostRepositoryInterface
{
    /**
     * Post terbaru milik satu penulis, sudah memuat relasi user.
     *
     * @return Collection<int, \App\Models\Post>
     */
    public function latestByAuthor(User $author, int $limit = 5): Collection;
}

Implementasi Eloquent mengisi perjanjian tersebut — query yang tadinya akan menyebar di controller kini hidup di satu tempat:

php
<?php

namespace App\Repositories;

use App\Contracts\PostRepositoryInterface;
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Collection;

class EloquentPostRepository implements PostRepositoryInterface
{
    public function latestByAuthor(User $author, int $limit = 5): Collection
    {
        return Post::query()
            ->where('user_id', $author->id)
            ->with('user')
            ->latest()
            ->limit($limit)
            ->get();
    }
}

Interface diikat ke implementasi di AppServiceProvider, sama seperti binding PusherNotifikasi dan PaymentGateway sebelumnya:

php
$this->app->bind(PostRepositoryInterface::class, EloquentPostRepository::class);

Sejak baris tersebut, semua bagian aplikasi cukup bergantung pada PostRepositoryInterface dan biarkan container yang menyediakan implementasinya — mekanisme yang sudah kita pahami tuntas di chapter Service Container .

Repository di Aksi #

Controller memakai repository lewat method injection, tanpa tahu Eloquent ikut terlibat:

php
<?php

// app/Http/Controllers/PostController.php — method tambahan
public function karya(Request $request, User $user, PostRepositoryInterface $posts): string
{
    $limit = (int) $request->query('limit', 5);

    $terbaru = $posts->latestByAuthor($user, $limit);

    return 'Post terbaru '.$user->name.': '.$terbaru->pluck('title')->implode(', ');
}

Feature test menutup lingkaran: memastikan binding terpasang, query mengembalikan urutan yang benar, dan response controller memakai hasilnya:

php
<?php

// tests/Feature/PostRepositoryPatternTest.php — dua dari tiga test di berkas ini
public function test_repository_interface_terikat_ke_implementasi_eloquent(): void
{
    $repo = $this->app->make(PostRepositoryInterface::class);

    $this->assertInstanceOf(EloquentPostRepository::class, $repo);
}

public function test_controller_menampilkan_hasil_query_repository(): void
{
    $budi = User::factory()->create(['name' => 'Budi']);
    $this->isiContohData($budi); // tiga post Budi dengan created_at berurutan
    Post::factory()->create([
        'user_id' => User::factory()->create(['name' => 'Siti'])->id,
        'title' => 'Post Milik Siti',
        'slug' => 'post-milik-siti',
    ]);

    $response = $this->get("/penulis/{$budi->id}/posts?limit=2");

    $response->assertOk();
    $response->assertSee('Post terbaru Budi: Post Ketiga Budi, Post Kedua Budi');
}

Output yang Dihasilkan:

text
  PASS  Tests\Feature\PostRepositoryPatternTest
  ✓  repository interface terikat ke implementasi eloquent                  0.13s
  ✓  controller menampilkan hasil query repository                        0.04s
  ✓  query scope memberi hasil yang sama                                   0.01s

  Tests:    3 passed (5 assertions)
  Duration: 0.25s

Janji yang Ditepati? #

Pola repository lazim dijajakan dengan tiga janji. Mari periksa satu per satu pada konteks Laravel:

JanjiKenyataan di Laravel
Bisa mengganti persistenceEloquent sendiri sudah lapisan abstraksi atas SQL; dan selama repository mengembalikan model Eloquent, pemanggil tetap menikahi Eloquent
Lebih mudah diujiTest Eloquent sudah cepat dengan SQLite + RefreshDatabase; mock repository justru sering menutup bug query yang sebenarnya
Query dipakai ulangIni janji yang paling konsisten terpenuhi — query kompleks yang dipanggil dari banyak tempat berhenti terduplikasi

Janji pertama layak dijelaskan. Kita memang bisa menulis ApiPostRepository yang mengambil data dari HTTP, mengikat ulang interface, dan controller tidak berubah. Tetapi method repository mengembalikan Collection berisi model Eloquent — begitu pemanggil mulai memanggil $post->user->name, penggantian persistence yang tidak menghasilkan model Eloquent tetap memaksa kita menulis ulang pemanggilnya. Abstraksi yang bocor seperti ini adalah inti kritik terhadap pattern ini.

Kritik Jujur dan Komunitas #

Polemik di komunitas Laravel berakar pada satu fakta: Eloquent sudah adalah abstraksi akses data. Menambah repository di atasnya berarti membuat abstraksi di atas abstraksi, dan pada aplikasi kecil-menengah hasilnya mudah ditebak — kumpulan method pass-through satu baris (Post::all(), Post::find($id)) yang hanya menambah satu berkas, satu interface, dan satu binding per model tanpa menambah satu pun perilaku. Gaya resmi Laravel sendiri tidak memakai pattern ini: dokumentasi dan scaffolding bawaan memanggil Eloquent langsung dari controller, dan tutorial resmi laravel.com konsisten dengan pendekatan tersebut.

Biaya yang jarang dihitung: setiap operasi data baru harus melewati tiga perubahan berkas (interface, implementasi, pemanggil), dan tim harus menjaga disiplin agar tidak ada yang “curang” memanggil Post::where() dari tempat lain — disiplin yang tidak dipaksakan oleh apa pun.

Kapan Repository Benar-benar Membantu #

Dua situasi membuat biayanya masuk akal. Pertama, query kompleks yang dipakai ulang: pencarian berparameter, agregasi laporan, query lintas tabel yang muncul di beberapa service — repository menjadikannya satu kelas dengan nama method yang bisa dibaca. Kedua, beberapa sumber data untuk satu entitas: artikel yang datang dari database di lingkungan lokal tetapi dari API pihak ketiga di production — dua implementasi, satu interface, pemilihan lewat binding sesuai environment.

Alternatif yang Lebih Ringan #

Untuk kebutuhan query terpusat tanpa lapisan baru, Eloquent menyediakan dua alat bawaan. Query scope menyimpan batasan query sebagai bagian dari model:

php
<?php

// app/Models/Post.php — local scope
public function scopeLatestByAuthor(Builder $query, User $author, int $limit = 5): Builder
{
    return $query->where('user_id', $author->id)->latest()->limit($limit);
}

Pemakaiannya langsung dari mana saja, dan test di atas membuktikan hasilnya identik dengan versi repository:

php
public function test_query_scope_memberi_hasil_yang_sama(): void
{
    $budi = User::factory()->create(['name' => 'Budi']);
    $this->isiContohData($budi);

    $posts = Post::latestByAuthor($budi, 2)->get();

    $this->assertSame(
        ['Post Ketiga Budi', 'Post Kedua Budi'],
        $posts->pluck('title')->all(),
    );
}

Alternatif kedua adalah method statis atau method biasa pada model (Post::karyaTerbaru($budi, 2)) untuk query yang lebih operasional — pendalaman keduanya sudah kita lewati di chapter Query Scopes dan pembahasan Eloquent pada dokumentasi eloquent . Keduanya memberi sentralisasi query tanpa interface, tanpa binding, tanpa berkas tambahan.

Keputusan akhirnya adalah soal ukuran dan nyeri: mulai tanpa repository, dekatkan query ke model dengan scope, dan biarkan duplikasi yang muncul — bukan antisipasi — yang memutuskan kapan lapisan ekstra ini layak dibangun. Dengan semua pola struktural sudah di meja, Section ini berlanjut ke pola yang lebih kecil dan lincah: action class, satu kelas untuk satu aksi tunggal.