Mengelola hubungan di Laravel, mengikuti pola repositori

120

Saat membuat aplikasi di Laravel 4 setelah membaca buku T. Otwell tentang pola desain yang baik di Laravel, saya mendapati diri saya membuat repositori untuk setiap tabel pada aplikasi.

Saya berakhir dengan struktur tabel berikut:

  • Siswa: id, name
  • Kursus: id, nama, teacher_id
  • Guru: id, nama
  • Tugas: id, name, course_id
  • Skor (bertindak sebagai poros antara siswa dan tugas): student_id, assignment_id, skor

Saya memiliki kelas repositori dengan metode temukan, buat, perbarui, dan hapus untuk semua tabel ini. Setiap repositori memiliki model Eloquent yang berinteraksi dengan database. Hubungan ditentukan dalam model sesuai dengan dokumentasi Laravel: http://laravel.com/docs/eloquent#relationships .

Saat membuat kursus baru, yang saya lakukan hanyalah memanggil metode create di Course Repository. Kursus itu memiliki tugas, jadi saat membuatnya, saya juga ingin membuat entri di tabel skor untuk setiap siswa dalam kursus. Saya melakukan ini melalui Assignment Repository. Ini menyiratkan repositori tugas berkomunikasi dengan dua model Eloquent, dengan model Tugas dan Siswa.

Pertanyaan saya adalah: karena aplikasi ini mungkin akan tumbuh dalam ukuran dan lebih banyak hubungan akan diperkenalkan, apakah itu praktik yang baik untuk berkomunikasi dengan model Eloquent yang berbeda di repositori atau haruskah ini dilakukan menggunakan repositori lain sebagai gantinya (maksud saya memanggil repositori lain dari repositori Tugas ) atau haruskah itu dilakukan bersama-sama dalam model Eloquent?

Juga, apakah praktik yang baik untuk menggunakan tabel skor sebagai poros antara tugas dan siswa atau haruskah itu dilakukan di tempat lain?

ehp
sumber

Jawaban:

71

Perlu diingat Anda sedang meminta pendapat: D

Ini milik saya:

TL; DR: Ya, tidak apa-apa.

Anda baik-baik saja!

Saya melakukan persis apa yang sering Anda lakukan dan merasa berhasil dengan baik.

Saya sering, bagaimanapun, mengatur repositori di sekitar logika bisnis daripada memiliki repo-per-tabel. Ini berguna karena ini adalah sudut pandang yang berpusat pada bagaimana aplikasi Anda seharusnya menyelesaikan "masalah bisnis" Anda.

Kursus adalah "entitas", dengan atribut (judul, id, dll.) Dan bahkan entitas lain (Tugas, yang memiliki atribut sendiri dan mungkin entitas).

Repositori "Kursus" Anda harus dapat mengembalikan atribut / Tugas Kursus dan Kursus (termasuk Tugas).

Untungnya, Anda dapat melakukannya dengan Eloquent.

(Saya sering berakhir dengan repositori per tabel, tetapi beberapa repositori digunakan lebih dari yang lain, dan memiliki lebih banyak metode. Repositori "kursus" Anda mungkin jauh lebih berfitur lengkap daripada repositori Tugas Anda, misalnya, jika Anda aplikasi berpusat lebih banyak di sekitar Kursus dan lebih sedikit tentang kumpulan Tugas Kursus).

Bagian yang sulit

Saya sering menggunakan repositori di dalam repositori saya untuk melakukan beberapa tindakan database.

Repositori apa pun yang mengimplementasikan Eloquent untuk menangani data kemungkinan akan mengembalikan model Eloquent. Dalam hal ini, tidak masalah jika model Kursus Anda menggunakan hubungan bawaan untuk mengambil atau menyimpan Tugas (atau kasus penggunaan lainnya). "Implementasi" kami dibangun di sekitar Eloquent.

Dari sudut pandang praktis, ini masuk akal. Kami tidak mungkin mengubah sumber data menjadi sesuatu yang tidak dapat ditangani oleh Eloquent (ke sumber data non-sql).

ORMS

Bagian tersulit dari pengaturan ini, setidaknya bagi saya, adalah menentukan apakah Eloquent benar-benar membantu atau merugikan kita. ORM adalah subjek yang rumit, karena meskipun sangat membantu kami dari sudut pandang praktis, ORM juga menggabungkan kode "entitas logika bisnis" Anda dengan kode yang melakukan pengambilan data.

Jenis ini membingungkan apakah tanggung jawab repositori Anda sebenarnya untuk menangani data atau menangani pengambilan / pembaruan entitas (entitas domain bisnis).

Lebih jauh, mereka bertindak sebagai objek yang Anda berikan ke pandangan Anda. Jika nanti Anda harus berhenti menggunakan model Eloquent dalam repositori, Anda harus memastikan variabel yang diteruskan ke tampilan Anda berperilaku dengan cara yang sama atau memiliki metode yang sama, jika tidak mengubah sumber data Anda akan mengubah dilihat, dan Anda (sebagian) kehilangan tujuan mengabstraksi logika Anda ke repositori di tempat pertama - pemeliharaan proyek Anda turun sebagai.

Bagaimanapun, ini adalah pemikiran yang agak tidak lengkap. Mereka, seperti yang dinyatakan, hanyalah pendapat saya, yang kebetulan adalah hasil dari membaca Domain Driven Design dan menonton video seperti keynote "paman bob" di Ruby Midwest dalam setahun terakhir.

fideloper
sumber
1
Menurut Anda, apakah itu alternatif yang baik jika repositori mengembalikan objek transfer data daripada objek yang fasih? Tentu saja ini akan menyiratkan konversi tambahan dari eloquent ke dto, tetapi dengan cara ini, setidaknya, Anda mengisolasi pengontrol / tampilan Anda dari implementasi orm saat ini.
federivo
1
Saya telah sedikit bereksperimen dengan itu sendiri dan menemukan sedikit di sisi yang tidak praktis. Meski begitu, saya suka ide itu secara abstrak. Namun, objek Collection database Illuminate bertindak seperti array dan objek Model bertindak seperti objek StdClass cukup sehingga kita dapat, secara praktis, tetap dengan Eloquent dan masih menggunakan array / objek di masa depan jika kita perlu.
fideloper
4
@fideloper Saya merasa bahwa jika saya menggunakan repositori, saya kehilangan seluruh keindahan ORM yang disediakan oleh Eloquent. Ketika mengambil objek akun melalui metode repositori $a = $this->account->getById(1)saya, saya tidak bisa begitu saja metode rantai seperti $a->getActiveUsers(). Oke, saya bisa menggunakan $a->users->..., tapi kemudian saya mengembalikan koleksi Eloquent dan tidak ada objek stdClass dan terikat ke Eloquent lagi. Apa solusinya? Mendeklarasikan metode lain di repositori pengguna seperti $user->getActiveUsersByAccount($a->id);? Akan sangat senang mendengar bagaimana Anda mengatasi ini ...
santacruz
1
ORM sangat buruk untuk arsitektur tingkat Enterprise (ish) karena mereka menyebabkan masalah seperti ini. Pada akhirnya, Anda harus memutuskan apa yang paling masuk akal untuk aplikasi Anda. Secara pribadi ketika menggunakan repositori dengan Eloquent (90% dari waktu!) Saya menggunakan Eloquent dan berusaha sekuat tenaga untuk memperlakukan model & koleksi seperti stdClasses & Array (karena Anda bisa!) Jadi jika perlu, beralih ke yang lain dimungkinkan.
fideloper
5
Lanjutkan dan gunakan model yang lambat dimuat. Anda dapat membuat model domain nyata berfungsi seperti itu jika Anda melewatkan penggunaan Eloquent. Tapi serius, apakah kamu akan pernah mengganti Eloquent? Untuk satu sen, untuk satu pon! (Jangan berlebihan mencoba untuk berpegang pada "aturan"! Saya melanggar semua milik saya sepanjang waktu).
fideloper
224

Saya menyelesaikan proyek besar menggunakan Laravel 4 dan harus menjawab semua pertanyaan yang Anda ajukan sekarang. Setelah membaca semua buku Laravel yang tersedia di Leanpub, dan banyak sekali Googling, saya menemukan struktur berikut.

  1. Satu kelas Model Eloquent per tabel data
  2. Satu kelas Repositori per Model Eloquent
  3. Kelas Layanan yang dapat berkomunikasi antara beberapa kelas Repositori.

Jadi katakanlah saya sedang membangun database film. Saya akan memiliki setidaknya kelas-kelas Model Eloquent berikut:

  • Film
  • Studio
  • Direktur
  • Aktor
  • Ulasan

Sebuah kelas repositori akan merangkum setiap kelas Model Eloquent dan bertanggung jawab untuk operasi CRUD pada database. Kelas repositori mungkin terlihat seperti ini:

  • MovieRepository
  • StudioRepository
  • DirectorRepository
  • ActorRepository
  • ReviewRepository

Setiap kelas repositori akan memperluas kelas BaseRepository yang mengimplementasikan antarmuka berikut:

interface BaseRepositoryInterface
{
    public function errors();

    public function all(array $related = null);

    public function get($id, array $related = null);

    public function getWhere($column, $value, array $related = null);

    public function getRecent($limit, array $related = null);

    public function create(array $data);

    public function update(array $data);

    public function delete($id);

    public function deleteWhere($column, $value);
}

Kelas Service digunakan untuk merekatkan beberapa repositori bersama-sama dan berisi "logika bisnis" aplikasi yang sebenarnya. Pengontrol hanya berkomunikasi dengan kelas Layanan untuk tindakan Buat, Perbarui, dan Hapus.

Jadi, ketika saya ingin membuat rekaman Film baru di database, kelas MovieController saya mungkin memiliki metode berikut:

public function __construct(MovieRepositoryInterface $movieRepository, MovieServiceInterface $movieService)
{
    $this->movieRepository = $movieRepository;
    $this->movieService = $movieService;
}

public function postCreate()
{
    if( ! $this->movieService->create(Input::all()))
    {
        return Redirect::back()->withErrors($this->movieService->errors())->withInput();
    }

    // New movie was saved successfully. Do whatever you need to do here.
}

Terserah Anda untuk menentukan bagaimana Anda POST data ke pengontrol Anda, tetapi katakanlah data yang dikembalikan oleh Input :: all () dalam metode postCreate () terlihat seperti ini:

$data = array(
    'movie' => array(
        'title'    => 'Iron Eagle',
        'year'     => '1986',
        'synopsis' => 'When Doug\'s father, an Air Force Pilot, is shot down by MiGs belonging to a radical Middle Eastern state, no one seems able to get him out. Doug finds Chappy, an Air Force Colonel who is intrigued by the idea of sending in two fighters piloted by himself and Doug to rescue Doug\'s father after bombing the MiG base.'
    ),
    'actors' => array(
        0 => 'Louis Gossett Jr.',
        1 => 'Jason Gedrick',
        2 => 'Larry B. Scott'
    ),
    'director' => 'Sidney J. Furie',
    'studio' => 'TriStar Pictures'
)

Karena MovieRepository seharusnya tidak mengetahui cara membuat rekaman Aktor, Sutradara, atau Studio di database, kami akan menggunakan kelas MovieService kami, yang mungkin terlihat seperti ini:

public function __construct(MovieRepositoryInterface $movieRepository, ActorRepositoryInterface $actorRepository, DirectorRepositoryInterface $directorRepository, StudioRepositoryInterface $studioRepository)
{
    $this->movieRepository = $movieRepository;
    $this->actorRepository = $actorRepository;
    $this->directorRepository = $directorRepository;
    $this->studioRepository = $studioRepository;
}

public function create(array $input)
{
    $movieData    = $input['movie'];
    $actorsData   = $input['actors'];
    $directorData = $input['director'];
    $studioData   = $input['studio'];

    // In a more complete example you would probably want to implement database transactions and perform input validation using the Laravel Validator class here.

    // Create the new movie record
    $movie = $this->movieRepository->create($movieData);

    // Create the new actor records and associate them with the movie record
    foreach($actors as $actor)
    {
        $actorModel = $this->actorRepository->create($actor);
        $movie->actors()->save($actorModel);
    }

    // Create the director record and associate it with the movie record
    $director = $this->directorRepository->create($directorData);
    $director->movies()->associate($movie);

    // Create the studio record and associate it with the movie record
    $studio = $this->studioRepository->create($studioData);
    $studio->movies()->associate($movie);

    // Assume everything worked. In the real world you'll need to implement checks.
    return true;
}

Jadi yang tersisa adalah pemisahan perhatian yang baik dan masuk akal. Repositori hanya mengetahui model Eloquent yang mereka sisipkan dan ambil dari database. Pengontrol tidak peduli dengan repositori, mereka hanya menyerahkan data yang mereka kumpulkan dari pengguna dan meneruskannya ke layanan yang sesuai. Layanan tidak peduli bagaimana data yang diterimanya disimpan ke database, itu hanya menyerahkan data relevan yang diberikan oleh pengontrol ke repositori yang sesuai.

Kyle Noland
sumber
8
Komentar ini sejauh ini merupakan pendekatan yang lebih bersih, lebih terukur, dan dapat dipelihara.
Andreas
4
+1! Itu akan sangat membantu saya, terima kasih telah berbagi dengan kami! Ingin tahu bagaimana Anda berhasil memvalidasi hal-hal di dalam layanan, jika memungkinkan, bisakah Anda menjelaskan secara singkat apa yang Anda lakukan? Terima kasih! :)
Paulo Freitas
6
Seperti yang dikatakan @PauloFreitas, akan menarik untuk melihat bagaimana Anda menangani bagian validasi, dan saya juga tertarik pada bagian pengecualian (apakah Anda menggunakan pengecualian, acara, atau Anda hanya menangani ini seperti yang Anda sarankan di kontroler melalui pengembalian boolean dalam layanan Anda?). Terima kasih!
Nicolas
11
Tulisan yang bagus, meskipun saya tidak yakin mengapa Anda menyuntikkan movieRepository ke MovieController karena pengontrol tidak boleh melakukan apa pun secara langsung dengan repositori, begitu pula metode postCreate Anda menggunakan movieRepository, jadi saya berasumsi Anda membiarkannya karena kesalahan ?
davidnknight
15
Pertanyaan tentang ini: mengapa Anda menggunakan repositori dalam contoh ini? Ini adalah pertanyaan yang jujur ​​- bagi saya, sepertinya Anda menggunakan repositori tetapi setidaknya dalam contoh ini repositori tidak benar-benar melakukan apa pun kecuali menyediakan antarmuka yang sama dengan Eloquent, dan pada akhirnya Anda masih terikat dengan Eloquent karena kelas layanan Anda menggunakan eloquent langsung di dalamnya ( $studio->movies()->associate($movie);).
Kevin Mitchell
5

Saya suka memikirkannya dalam kaitannya dengan apa yang dilakukan kode saya dan apa yang menjadi tanggung jawabnya, daripada "benar atau salah". Beginilah cara saya memisahkan tanggung jawab saya:

  • Pengontrol adalah lapisan HTTP dan mengarahkan permintaan melalui ke apis yang mendasarinya (alias, mengontrol aliran)
  • Model mewakili skema database, dan memberi tahu aplikasi seperti apa datanya, hubungan apa yang mungkin dimilikinya, serta atribut global apa pun yang mungkin diperlukan (seperti metode nama untuk mengembalikan nama depan dan belakang yang digabungkan)
  • Repositori mewakili kueri dan interaksi yang lebih kompleks dengan model (saya tidak melakukan kueri apa pun pada metode model).
  • Mesin telusur - kelas yang membantu saya membuat kueri penelusuran yang kompleks.

Dengan pemikiran ini, masuk akal setiap kali menggunakan repositori (apakah Anda membuat interfaces.etc. Adalah topik yang sama sekali lain). Saya suka pendekatan ini, karena itu berarti saya tahu persis ke mana harus pergi ketika saya perlu melakukan pekerjaan tertentu.

Saya juga cenderung membangun repositori dasar, biasanya kelas abstrak yang mendefinisikan default utama - pada dasarnya operasi CRUD, dan kemudian setiap anak dapat memperluas dan menambahkan metode seperlunya, atau membebani default. Menyuntikkan model Anda juga membantu pola ini menjadi cukup kuat.

Oddman
sumber
Dapatkah Anda menunjukkan implementasi BaseRepository Anda? Saya sebenarnya melakukan ini juga dan saya ingin tahu apa yang Anda lakukan.
Odyssee
Pikirkan getById, getByName, getByTitle, simpan metode type.etc. - umumnya metode yang berlaku untuk semua repositori dalam berbagai domain.
Oddman
5

Pikirkan Repositori sebagai lemari arsip yang konsisten untuk data Anda (bukan hanya ORM Anda). Idenya adalah Anda ingin mengambil data dengan mudah menggunakan API yang konsisten.

Jika Anda menemukan diri Anda hanya melakukan Model :: all (), Model :: find (), Model :: create (), Anda mungkin tidak akan mendapat banyak manfaat dari mengabstraksi repositori. Di sisi lain, jika Anda ingin melakukan lebih banyak logika bisnis ke kueri atau tindakan Anda, Anda mungkin ingin membuat repositori untuk mempermudah penggunaan API untuk menangani data.

Saya pikir Anda bertanya apakah repositori akan menjadi cara terbaik untuk menangani beberapa sintaks lebih verbose yang diperlukan untuk menghubungkan model terkait. Bergantung pada situasinya, ada beberapa hal yang dapat saya lakukan:

  1. Menggantung model anak baru dari model induk (satu-satu atau satu-banyak), saya akan menambahkan metode ke repositori anak seperti createWithParent($attributes, $parentModelInstance)dan ini hanya akan menambahkan $parentModelInstance->idke dalam parent_idbidang atribut dan memanggil buat.

  2. Melampirkan hubungan banyak-banyak, saya sebenarnya membuat fungsi pada model sehingga saya dapat menjalankan $ instance-> attachChild ($ childInstance). Perhatikan bahwa ini membutuhkan elemen yang ada di kedua sisi.

  3. Membuat model terkait dalam sekali jalan, saya membuat sesuatu yang saya sebut Gateway (mungkin agak lepas dari definisi Fowler). Cara saya dapat memanggil $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes) alih-alih sekumpulan logika yang dapat berubah atau yang akan memperumit logika yang saya miliki di controller atau perintah.

Ryan Tablada
sumber