Управление отношениями в Laravel с соблюдением шаблона репозитория


120

При создании приложения в Laravel 4 после прочтения книги Т. Отвелла о хороших шаблонах проектирования в Laravel я обнаружил, что создаю репозитории для каждой таблицы в приложении.

В итоге я получил следующую структуру таблицы:

  • Студенты: id, имя
  • Курсы: id, name, teacher_id
  • Учителя: id, имя
  • Назначения: id, name, course_id
  • Баллы (действует как стержень между студентами и заданиями): student_id, assignment_id, scores

У меня есть классы репозитория с методами поиска, создания, обновления и удаления для всех этих таблиц. У каждого репозитория есть модель Eloquent, которая взаимодействует с базой данных. Отношения определены в модели согласно документации Laravel: http://laravel.com/docs/eloquent#relationships .

При создании нового курса все, что я делаю, это вызываю метод create в репозитории курсов. В этом курсе есть задания, поэтому при их создании я также хочу создать запись в таблице оценок для каждого студента курса. Я делаю это через Репозиторий назначений. Это означает, что репозиторий заданий взаимодействует с двумя моделями Eloquent, с моделью Assignment и Student.

Мой вопрос: поскольку это приложение, вероятно, вырастет в размере и будет введено больше взаимосвязей, рекомендуется ли общаться с различными моделями Eloquent в репозиториях, или это должно быть сделано с использованием других репозиториев (я имею в виду вызов других репозиториев из репозитория назначений ) или это нужно делать во всех моделях Eloquent?

Кроме того, является ли хорошей практикой использование таблицы оценок в качестве ориентира между заданиями и учениками или это нужно делать где-то еще?

Ответы:


71

Имейте в виду, что вы спрашиваете мнение: D

Вот мой:

TL; DR: Да, это нормально.

У тебя хорошо получается!

Я делаю именно то, что вы делаете часто, и считаю, что это отлично работает.

Однако я часто организую репозитории на основе бизнес-логики вместо того, чтобы иметь репозиторий для каждой таблицы. Это полезно, поскольку это точка зрения, сосредоточенная на том, как ваше приложение должно решать вашу «бизнес-проблему».

Курс - это «сущность» с атрибутами (название, идентификатор и т. Д.) И даже другими сущностями (назначения, которые имеют свои собственные атрибуты и, возможно, сущности).

Репозиторий «Курс» должен иметь возможность возвращать Курс и атрибуты / Задания курсов (включая Задания).

К счастью, этого можно добиться с помощью Eloquent.

(Я часто получаю репозиторий для каждой таблицы, но некоторые репозитории используются гораздо больше, чем другие, и поэтому имеют гораздо больше методов. Ваш репозиторий "курсов" может быть намного более полнофункциональным, чем ваш репозиторий заданий, например, если ваш приложения сосредоточены больше на курсах и меньше на коллекции заданий курсов).

Сложная часть

Я часто использую репозитории внутри своих репозиториев для выполнения некоторых действий с базой данных.

Любой репозиторий, который реализует Eloquent для обработки данных, скорее всего, вернет модели Eloquent. В этом свете нормально, если ваша модель курса использует встроенные отношения для извлечения или сохранения заданий (или любого другого варианта использования). Наша «реализация» построена на Eloquent.

С практической точки зрения в этом есть смысл. Мы вряд ли изменим источники данных на то, что Eloquent не сможет обработать (на источник данных, отличный от sql).

ORMS

Самая сложная часть этой настройки, по крайней мере для меня, - это определить, действительно ли Eloquent нам помогает или вредит. ORM - сложная тема, потому что, хотя они очень помогают нам с практической точки зрения, они также объединяют ваш код «сущностей бизнес-логики» с кодом, выполняющим извлечение данных.

Такого рода путаница заключается в том, действительно ли ваш репозиторий отвечает за обработку данных или за получение / обновление сущностей (сущностей бизнес-домена).

Более того, они действуют как те самые объекты, которые вы передаете своим представлениям. Если позже вам придется отказаться от использования моделей Eloquent в репозитории, вам необходимо убедиться, что переменные, передаваемые вашим представлениям, ведут себя одинаково или имеют те же доступные методы, в противном случае изменение источников данных приведет к изменению ваших представлений, и вы (частично) потеряли цель абстрагировать свою логику до репозиториев - ремонтопригодность вашего проекта снижается как.

Во всяком случае, это несколько неполные мысли. Это, как уже говорилось, всего лишь мое мнение, которое, как оказалось, является результатом чтения Domain Driven Design и просмотра видеороликов, таких как лейтмотив «дяди Боба» на Ruby Midwest в течение последнего года.


1
На ваш взгляд, было бы хорошей альтернативой, если бы репозитории возвращали объекты передачи данных вместо красноречивых объектов? Конечно, это повлечет за собой дополнительное преобразование из красноречивого в dto, но таким образом, по крайней мере, вы изолируете свои контроллеры / представления от текущей реализации orm.
federivo

1
Я немного поэкспериментировал с этим и нашел это непрактичным. При этом мне нравится абстрактная идея. Однако объекты Collection базы данных Illuminate действуют так же, как массивы, а объекты Model действуют так же, как объекты StdClass, так что мы можем, практически говоря, придерживаться Eloquent и по-прежнему использовать массивы / объекты в будущем, если нам это понадобится.
fideloper

4
@fideloper Я чувствую, что, используя репозитории, я теряю всю красоту ORM, которую предоставляет Eloquent. При получении объекта учетной записи с помощью метода моего репозитория $a = $this->account->getById(1)я не могу просто связать такие методы, как $a->getActiveUsers(). Хорошо, я мог бы использовать $a->users->..., но затем я возвращаю коллекцию Eloquent без объекта stdClass и снова привязан к Eloquent. Что с этим делать? Объявление другого метода в пользовательском репозитории вроде $user->getActiveUsersByAccount($a->id);? Хотел бы услышать, как вы решите эту проблему ...
santacruz

1
ORM ужасны для архитектуры уровня Enterprise (ish), потому что они вызывают подобные проблемы. В конце концов, вы должны решить, что лучше всего подходит для вашего приложения. Лично при использовании репозиториев с Eloquent (90% времени!) Я использую Eloquent и изо всех сил стараюсь обращаться с моделями и коллекциями, такими как stdClasses и Arrays (потому что вы можете!), Поэтому, если мне нужно, можно переключиться на что-то еще.
fideloper

5
Продолжайте и используйте модели с отложенной загрузкой. Вы можете заставить реальные доменные модели работать таким образом, если когда-нибудь откажетесь от использования Eloquent. А если серьезно, ты собираешься выключатель из красноречивых когда - либо? За пенни, за фунт! (Не переусердствуйте, пытаясь придерживаться «правил»! Я все время нарушаю все свои).
fideloper

224

Я заканчиваю большой проект с использованием Laravel 4, и мне нужно было ответить на все вопросы, которые вы задаете прямо сейчас. После прочтения всех доступных книг по Laravel на Leanpub и множества поисковиков в Google, я придумал следующую структуру.

  1. Один класс Eloquent Model для каждой таблицы данных
  2. Один класс репозитория для каждой модели Eloquent
  3. Класс службы, который может взаимодействовать между несколькими классами репозитория.

Допустим, я создаю базу данных фильмов. У меня были бы по крайней мере следующие классы Eloquent Model:

  • Фильм
  • студия
  • директор
  • Актер
  • Обзор

Класс репозитория будет инкапсулировать каждый класс Eloquent Model и будет отвечать за операции CRUD в базе данных. Классы репозитория могут выглядеть так:

  • MovieRepository
  • StudioRepository
  • DirectorRepository
  • ActorRepository
  • ReviewRepository

Каждый класс репозитория будет расширять класс BaseRepository, который реализует следующий интерфейс:

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);
}

Класс Service используется для объединения нескольких репозиториев и содержит реальную «бизнес-логику» приложения. Контроллеры только связываются с классами обслуживания для создания, обновления и удаления действий.

Поэтому, когда я хочу создать новую запись фильма в базе данных, мой класс MovieController может иметь следующие методы:

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.
}

Вам решать, как вы отправляете данные POST в свои контроллеры, но предположим, что данные, возвращаемые Input :: all () в методе postCreate (), выглядят примерно так:

$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'
)

Поскольку MovieRepository не должен знать, как создавать записи Actor, Director или Studio в базе данных, мы будем использовать наш класс MovieService, который может выглядеть примерно так:

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;
}

Итак, у нас осталось хорошее, разумное разделение проблем. Репозитории осведомлены только о модели Eloquent, которую они вставляют и извлекают из базы данных. Контроллеры не заботятся о репозиториях, они просто передают данные, которые они собирают от пользователя, и передают их соответствующей службе. Сервис не заботится о том, как данные, которые он получает, сохраняется в базе данных, он просто передает соответствующие данные, которые он предоставил контроллером, в соответствующие репозитории.


8
Этот комментарий, безусловно, более чистый, масштабируемый и удобный в обслуживании подход.
Андреас

4
+1! Это мне очень поможет, спасибо, что поделились с нами! Не могли бы вы вкратце объяснить, что вы делали, если хотите знать, как вам удалось проверить вещи внутри служб? В любом случае, спасибо! :)
Пауло Фрейтас

6
Как сказал @PauloFreitas, было бы интересно посмотреть, как вы обрабатываете часть проверки, и меня также заинтересует часть исключений (вы используете исключения, события или просто обрабатываете это, как вы, кажется, предлагаете в своем контроллер через логический возврат в ваших сервисах?). Спасибо!
Николас

11
Хорошая запись, хотя я не уверен, почему вы вводите movieRepository в MovieController, поскольку контроллер не должен ничего делать напрямую с репозиторием, а ваш метод postCreate не использует movieRepository, поэтому я предполагаю, что вы оставили его по ошибке ?
davidnknight

15
Вопрос по этому поводу: почему в этом примере вы используете репозитории? Это честный вопрос - для меня похоже, что вы используете репозитории, но, по крайней мере, в этом примере репозиторий на самом деле ничего не делает, кроме предоставления того же интерфейса, что и Eloquent, и, в конце концов, вы все еще привязаны к Eloquent, потому что ваш класс обслуживания использует красноречие прямо в нем ( $studio->movies()->associate($movie);).
Кевин Митчелл

5

Мне нравится думать об этом с точки зрения того, что делает мой код и за что он отвечает, а не «правильно или неправильно». Вот как я разделяю свои обязанности:

  • Контроллеры являются уровнем HTTP и направляют запросы через базовый API (он же контролирует поток).
  • Модели представляют схему базы данных и сообщают приложению, как выглядят данные, какие отношения они могут иметь, а также любые глобальные атрибуты, которые могут быть необходимы (например, метод имени для возврата сцепленных имени и фамилии)
  • Репозитории представляют собой более сложные запросы и взаимодействия с моделями (я не выполняю никаких запросов по методам модели).
  • Поисковые системы - классы, которые помогают мне строить сложные поисковые запросы.

Имея это в виду, имеет смысл каждый раз использовать репозиторий (создаете ли вы интерфейсы и т. Д. - это совершенно другая тема). Мне нравится этот подход, потому что он означает, что я точно знаю, куда идти, когда мне нужно выполнить определенную работу.

Я также стараюсь создать базовый репозиторий, обычно абстрактный класс, который определяет основные значения по умолчанию - в основном операции CRUD, а затем каждый дочерний элемент может просто расширять и добавлять методы по мере необходимости или перегружать значения по умолчанию. Внедрение вашей модели также помогает этому паттерну быть достаточно надежным.


Можете ли вы показать свою реализацию вашего BaseRepository? Я тоже так делаю, и мне любопытно, что ты сделал.
Odyssee

Подумайте о getById, getByName, getByTitle, сохраните методы типа и т. Д. - обычно методы, которые применяются ко всем репозиториям в различных доменах.
Oddman

5

Думайте о репозиториях как о едином картотеке ваших данных (а не только ваших ORM). Идея состоит в том, что вы хотите собирать данные в единый простой в использовании API.

Если вы обнаружите, что просто выполняете Model :: all (), Model :: find (), Model :: create (), вы, вероятно, не получите особой выгоды от абстрагирования репозитория. С другой стороны, если вы хотите добавить немного больше бизнес-логики к своим запросам или действиям, вы можете создать репозиторий, чтобы упростить использование API для работы с данными.

Я думаю, вы спрашивали, будет ли репозиторий лучшим способом справиться с более подробным синтаксисом, необходимым для соединения связанных моделей. В зависимости от ситуации я могу сделать следующее:

  1. Подвешивая новую дочернюю модель к родительской модели (один-один или один-много), я бы добавил метод в дочерний репозиторий что-то вроде, createWithParent($attributes, $parentModelInstance)и это просто добавило бы $parentModelInstance->idв parent_idполе атрибутов и вызовет create.

  2. Присоединяя отношение «многие-многие», я фактически создаю функции на моделях, чтобы я мог запускать $ instance-> attachChild ($ childInstance). Обратите внимание, что для этого требуются существующие элементы с обеих сторон.

  3. Создавая связанные модели за один проход, я создаю нечто, что называю шлюзом (это может немного отличаться от определений Фаулера). Я могу вызвать $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes) вместо связки логики, которая может измениться или усложнить логику, которая есть у меня в контроллере или команде.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.