Gerenciando relacionamentos no Laravel, aderindo ao padrão de repositório

120

Ao criar um aplicativo no Laravel 4, depois de ler o livro de T. Otwell sobre bons padrões de design no Laravel, me vi criando repositórios para todas as tabelas do aplicativo.

Acabei com a seguinte estrutura de tabela:

  • Alunos: id, nome
  • Cursos: id, nome, teacher_id
  • Professores: id, nome
  • Tarefas: id, nome, course_id
  • Pontuações (atua como um pivô entre os alunos e as tarefas): student_id, assignment_id, scores

Eu tenho classes de repositório com métodos find, create, update e delete para todas essas tabelas. Cada repositório possui um modelo Eloquent que interage com o banco de dados. Os relacionamentos são definidos no modelo de acordo com a documentação do Laravel: http://laravel.com/docs/eloquent#relationships .

Ao criar um novo curso, tudo o que faço é chamar o método create no Repositório do Curso. Esse curso tem tarefas, portanto, ao criar um, também quero criar uma entrada na tabela de pontuação para cada aluno do curso. Eu faço isso através do Repositório de Tarefas. Isso implica que o repositório de tarefas se comunica com dois modelos Eloquent, com o modelo de Tarefas e Alunos.

Minha pergunta é: como esse aplicativo provavelmente aumentará de tamanho e mais relacionamentos serão introduzidos, é uma boa prática se comunicar com diferentes modelos Eloquent em repositórios ou isso deve ser feito usando outros repositórios (quero dizer, chamar outros repositórios do repositório de atribuição ) ou deve ser feito nos modelos Eloquent todos juntos?

Além disso, é uma boa prática usar a tabela de pontuações como um pivô entre tarefas e alunos ou deve ser feita em outro lugar?

ehp
fonte

Respostas:

71

Lembre-se de que você está pedindo opiniões: D

Aqui está o meu:

TL; DR: Sim, tudo bem.

Você está indo bem!

Faço exatamente o que você está fazendo frequentemente e acho que funciona muito bem.

No entanto, costumo organizar repositórios em torno da lógica de negócios, em vez de ter um repo-per-table. Isso é útil, pois é um ponto de vista centrado na maneira como seu aplicativo deve resolver seu "problema de negócios".

Um Curso é uma "entidade", com atributos (título, ID, etc) e até outras entidades (Atribuições, que têm seus próprios atributos e possivelmente entidades).

Seu repositório "Curso" deve poder retornar um curso e os atributos / atribuições dos cursos (incluindo a atribuição).

Você pode conseguir isso com a Eloquent, felizmente.

(Geralmente, acabo com um repositório por tabela, mas alguns são usados ​​muito mais do que outros, e por isso têm muitos outros métodos. Seu repositório de "cursos" pode ter muito mais recursos do que o repositório de atribuições, por exemplo, se o seu o aplicativo centra-se mais nos cursos e menos na coleção de tarefas dos cursos).

A parte complicada

Costumo usar repositórios dentro dos meus repositórios para executar algumas ações do banco de dados.

Qualquer repositório que implemente o Eloquent para manipular dados provavelmente retornará modelos Eloquent. Nessa perspectiva, não há problema se o modelo do curso usar relacionamentos internos para recuperar ou salvar as atribuições (ou qualquer outro caso de uso). Nossa "implementação" é criada em torno do Eloquent.

Do ponto de vista prático, isso faz sentido. É improvável que alteremos as fontes de dados para algo que a Eloquent não possa manipular (para uma fonte de dados não sql).

ORMS

A parte mais complicada dessa configuração, pelo menos para mim, é determi- nar se o Eloquent está realmente nos ajudando ou prejudicando. ORMs são um assunto complicado, porque, embora nos ajudem bastante do ponto de vista prático, eles também associam o código das "entidades lógicas de negócios" ao código que faz a recuperação de dados.

Isso meio que confunde se a responsabilidade do seu repositório é realmente manipular dados ou manipular a recuperação / atualização de entidades (entidades do domínio comercial).

Além disso, eles agem como os próprios objetos que você transmite às suas visualizações. Se, posteriormente, você precisar se livrar do uso de modelos Eloquent em um repositório, precisará garantir que as variáveis ​​passadas para suas visualizações se comportem da mesma maneira ou tenham os mesmos métodos disponíveis; caso contrário, a alteração de suas fontes de dados passará a alterar seu visualizações e você (parcialmente) perdeu o objetivo de abstrair sua lógica para repositórios em primeiro lugar - a manutenção do seu projeto diminui como.

Enfim, esses são pensamentos um tanto incompletos. Eles são, como afirmado, apenas minha opinião, que é o resultado da leitura do Domain Driven Design e da exibição de vídeos como a palestra de "tio bob's" no Ruby Midwest no ano passado.

fideloper
fonte
1
Na sua opinião, seria uma boa alternativa se os repositórios retornassem objetos de transferência de dados em vez de objetos eloquentes? Obviamente, isso implicaria uma conversão extra de eloquente para dto's, mas desta forma, pelo menos, você isola seus controladores / visualizações da implementação atual do orm.
federivo 16/09
1
Eu mesmo experimentei um pouco e achei um pouco impraticável. Dito isto, eu gosto dessa ideia em abstrato. No entanto, os objetos Collection do banco de dados do Illuminate agem exatamente como matrizes e os objetos Model agem como objetos StdClass o suficiente para que possamos, na prática, ficar com o Eloquent e ainda usar matrizes / objetos no futuro, se necessário.
Fideloper 16/09/2013
4
@ fideloper Sinto que, se eu usar repositórios, perco toda a beleza do ORM que o Eloquent fornece. Ao recuperar um objeto de conta através do meu método de repositório, $a = $this->account->getById(1)não posso simplesmente encadear métodos como $a->getActiveUsers(). Ok, eu poderia usar $a->users->..., mas retornarei uma coleção Eloquent e nenhum objeto stdClass e estou vinculado a Eloquent novamente. Qual a solução para isso? Declarando outro método no repositório do usuário como $user->getActiveUsersByAccount($a->id);? Gostaria de saber como você resolver isso ...
santacruz
1
Os ORMs são terríveis para a arquitetura de nível corporativo (ish) porque causam problemas como este. No final, você precisa decidir o que faz mais sentido para a sua aplicação. Pessoalmente, ao usar repositórios com Eloquent (90% do tempo!), Eu uso o Eloquent e tento o máximo possível para tratar modelos e coleções como stdClasses & Arrays (porque você pode!).
Fideloper 17/01/14
5
Vá em frente e use modelos preguiçosos. Você pode fazer com que modelos de domínio real funcionem dessa maneira, se você pular o uso do Eloquent. Mas falando sério, você vai mudar o Eloquent sempre? Por um centavo, por um quilo! (Não exagere tentando seguir as "regras"! Eu quebro todas as minhas o tempo todo).
Fideloper 17/01
224

Estou finalizando um grande projeto usando o Laravel 4 e tive que responder a todas as perguntas que você está fazendo no momento. Depois de ler todos os livros disponíveis do Laravel no Leanpub e toneladas de Google, criei a seguinte estrutura.

  1. Uma classe Eloquent Model por tabela de dados
  2. Uma classe de Repositório por Modelo Eloquent
  3. Uma classe de serviço que pode se comunicar entre várias classes de repositório.

Então, digamos que estou construindo um banco de dados de filmes. Eu teria pelo menos as seguintes classes do Modelo Eloquent:

  • Filme
  • Estúdio
  • Diretor
  • Ator
  • Reveja

Uma classe de repositório encapsularia cada classe do Modelo Eloquent e seria responsável pelas operações de CRUD no banco de dados. As classes de repositório podem ter a seguinte aparência:

  • MovieRepository
  • StudioRepository
  • DirectorRepository
  • ActorRepository
  • ReviewRepository

Cada classe de repositório estenderia uma classe BaseRepository que implementa a seguinte interface:

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

Uma classe de serviço é usada para colar vários repositórios e contém a "lógica de negócios" real do aplicativo. Os controladores se comunicam apenas com as classes de serviço para as ações Criar, Atualizar e Excluir.

Portanto, quando eu quero criar um novo registro de filme no banco de dados, minha classe MovieController pode ter os seguintes métodos:

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

Depende de você determinar como você envia os dados para seus controladores, mas digamos que os dados retornados por Input :: all () no método postCreate () sejam algo como:

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

Como o MovieRepository não deve saber como criar registros de ator, diretor ou estúdio no banco de dados, usaremos nossa classe MovieService, que pode ser algo assim:

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

Então, o que nos resta é uma separação agradável e sensata de preocupações. Os repositórios conhecem apenas o modelo Eloquent que eles inserem e recuperam do banco de dados. Os controladores não se importam com os repositórios, apenas entregam os dados que coletam do usuário e os transmitem ao serviço apropriado. O serviço não se importa como os dados recebidos são salvos no banco de dados, apenas entrega os dados relevantes que foram fornecidos pelo controlador aos repositórios apropriados.

Kyle Noland
fonte
8
Este comentário é de longe a abordagem mais limpa, escalável e sustentável.
Andreas
4
+1! Isso vai me ajudar muito, obrigado por compartilhar conosco! Pensando em como você conseguiu validar as coisas dentro dos serviços, se possível, você poderia explicar brevemente o que fez? Obrigado mesmo assim! :)
Paulo Freitas
6
Como o @PauloFreitas disse, seria interessante ver como você lida com a parte de validação, e eu também estaria interessado na parte de exceções (você usa exceção, eventos ou apenas lida com isso, como parece sugerir em seu controlador através de um retorno booleano em seus serviços?). Obrigado!
Nicolas
11
Boa gravação, embora eu não tenha certeza do motivo pelo qual você está injetando movieRepository no MovieController, pois o controlador não deve fazer nada diretamente com o repositório, nem seu método postCreate usando o movieRepository, por isso suponho que você o tenha deixado por engano ?
Davidnknight
15
Pergunta sobre isso: por que você está usando repositórios neste exemplo? Esta é uma pergunta honesta - para mim, parece que você está usando repositórios, mas pelo menos neste exemplo o repositório não está realmente fazendo nada, mas fornecendo a mesma interface que o Eloquent, e no final você ainda está vinculado ao Eloquent porque sua classe de serviço está usando eloquente diretamente nela ( $studio->movies()->associate($movie);).
Kevin Mitchell
5

Eu gosto de pensar nisso em termos do que meu código está fazendo e do que é responsável, em vez de "certo ou errado". É assim que eu separo minhas responsabilidades:

  • Controladores são a camada HTTP e encaminham solicitações até as APIs subjacentes (ou seja, ele controla o fluxo)
  • Os modelos representam o esquema do banco de dados e informam ao aplicativo como são os dados, quais relacionamentos eles podem ter, bem como quaisquer atributos globais que possam ser necessários (como um método de nome para retornar um nome e sobrenome concatenados)
  • Os repositórios representam as consultas e interações mais complexas com os modelos (não faço nenhuma consulta sobre os métodos do modelo).
  • Mecanismos de pesquisa - classes que me ajudam a criar consultas de pesquisa complexas.

Com isso em mente, faz sentido sempre usar um repositório (se você cria interfaces.etc. É um tópico totalmente diferente). Eu gosto dessa abordagem, porque significa que sei exatamente para onde ir quando precisar fazer um determinado trabalho.

Eu também tendem a criar um repositório base, geralmente uma classe abstrata que define os principais padrões - basicamente operações CRUD, e cada filho pode apenas estender e adicionar métodos conforme necessário, ou sobrecarregar os padrões. Injetar seu modelo também ajuda esse padrão a ser bastante robusto.

Homem estranho
fonte
Você pode mostrar sua implementação do seu BaseRepository? Na verdade, eu também faço isso e estou curioso para saber o que você fez.
Odyssee
Pense em getById, getByName, getByTitle, salve o tipo methods.etc. - geralmente métodos que se aplicam a todos os repositórios em vários domínios.
Oddman 4/01/19
5

Pense nos Repositórios como um arquivo consistente dos seus dados (não apenas dos ORMs). A idéia é que você deseja coletar dados em uma API consistente e simples de usar.

Se você estiver apenas fazendo Model :: all (), Model :: find (), Model :: create (), provavelmente não se beneficiará muito de abstrair um repositório. Por outro lado, se você quiser fazer um pouco mais de lógica comercial em suas consultas ou ações, poderá criar um repositório para facilitar o uso da API para lidar com dados.

Eu acho que você estava perguntando se um repositório seria a melhor maneira de lidar com algumas das sintaxes mais detalhadas necessárias para conectar modelos relacionados. Dependendo da situação, há algumas coisas que posso fazer:

  1. Pendurando um novo modelo filho de um modelo pai (um-um ou um-muitos), eu adicionaria um método ao repositório filho, algo como createWithParent($attributes, $parentModelInstance)isso e isso apenas adicionaria $parentModelInstance->ido parent_idcampo de atributos e chamaria create.

  2. Anexando um relacionamento muitos-muitos, eu realmente crio funções nos modelos para poder executar $ instance-> attachChild ($ childInstance). Observe que isso requer elementos existentes nos dois lados.

  3. Criando modelos relacionados em uma execução, crio algo que chamo de Gateway (pode ser um pouco diferente das definições de Fowler). Como posso chamar $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes) em vez de um monte de lógica que pode mudar ou complicar a lógica que tenho em um controlador ou comando.

Ryan Tablada
fonte