Design de padrão de repositório adequado em PHP?


291

Prefácio: Estou tentando usar o padrão de repositório em uma arquitetura MVC com bancos de dados relacionais.

Recentemente, comecei a aprender TDD em PHP e estou percebendo que meu banco de dados está acoplado muito próximo ao restante do meu aplicativo. Eu li sobre repositórios e usei um contêiner de IoC para "injetá-lo" nos meus controladores. Coisas muito legais. Mas agora temos algumas perguntas práticas sobre o design do repositório. Considere o seguinte exemplo.

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

Problema nº 1: muitos campos

Todos esses métodos de localização usam uma SELECT *abordagem de selecionar todos os campos ( ). No entanto, nos meus aplicativos, estou sempre tentando limitar o número de campos que recebo, pois isso geralmente aumenta a sobrecarga e torna as coisas mais lentas. Para aqueles que usam esse padrão, como você lida com isso?

Problema nº 2: muitos métodos

Embora essa classe pareça boa no momento, eu sei que em um aplicativo do mundo real eu preciso de muito mais métodos. Por exemplo:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Etc.

Como você pode ver, pode haver uma lista muito, muito longa de métodos possíveis. E se você adicionar o problema de seleção de campo acima, o problema piorará. No passado, eu normalmente colocava toda essa lógica no meu controlador:

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')
            ->byCountry('Canada')->orderBy('name')->rows();

        return View::make('users', array('users' => $users));
    }
}

Com minha abordagem de repositório, não quero terminar com isso:

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

Problema nº 3: impossível corresponder a uma interface

Vejo o benefício no uso de interfaces para repositórios, para que eu possa trocar minha implementação (para fins de teste ou outros). Meu entendimento das interfaces é que elas definem um contrato que uma implementação deve seguir. Isso é ótimo até você começar a adicionar métodos adicionais aos seus repositórios, como findAllInCountry(). Agora, preciso atualizar minha interface para também ter esse método, caso contrário, outras implementações podem não tê-lo e isso pode interromper meu aplicativo. Por isso parece insano ... um caso de rabo balançando o cachorro.

Padrão de especificação?

Isto leva-me a acreditar que repositório deve ter apenas um número fixo de métodos (como save(), remove(), find(), findAll(), etc). Mas então, como executo pesquisas específicas? Já ouvi falar do Padrão de especificação , mas parece-me que isso reduz apenas um conjunto inteiro de registros (via IsSatisfiedBy()), que claramente apresenta grandes problemas de desempenho se você estiver usando um banco de dados.

Socorro?

Claramente, preciso repensar um pouco as coisas ao trabalhar com repositórios. Alguém pode esclarecer como isso é melhor tratado?

Respostas:


208

Eu pensei em dar uma olhada em responder minha própria pergunta. O que segue é apenas uma maneira de resolver os problemas de 1 a 3 na minha pergunta original.

Isenção de responsabilidade: nem sempre posso usar os termos corretos ao descrever padrões ou técnicas. Desculpe por isso.

Os objetivos:

  • Crie um exemplo completo de um controlador básico para visualização e edição Users.
  • Todo o código deve ser totalmente testável e zombável.
  • O controlador não deve ter idéia de onde os dados estão armazenados (o que significa que eles podem ser alterados).
  • Exemplo para mostrar uma implementação SQL (mais comum).
  • Para desempenho máximo, os controladores devem receber apenas os dados de que precisam - sem campos extras.
  • A implementação deve alavancar algum tipo de mapeador de dados para facilitar o desenvolvimento.
  • A implementação deve ter a capacidade de executar pesquisas de dados complexas.

A solução

Estou dividindo minha interação de armazenamento persistente (banco de dados) em duas categorias: R (Ler) e CUD (Criar, Atualizar, Excluir). Minha experiência foi que as leituras são realmente o que faz com que um aplicativo desacelere. E embora a manipulação de dados (CUD) seja realmente mais lenta, isso acontece com muito menos frequência e, portanto, é muito menos preocupante.

CUD (Criar, Atualizar, Excluir) é fácil. Isso envolverá o trabalho com modelos reais , que são passados ​​ao meu Repositoriespara persistência. Observe que meus repositórios ainda fornecerão um método Read, mas simplesmente para criação de objeto, não para exibição. Mais sobre isso mais tarde.

R (Ler) não é tão fácil. Nenhum modelo aqui, apenas objetos de valor . Use matrizes, se preferir . Esses objetos podem representar um único modelo ou uma mistura de muitos modelos, qualquer coisa realmente. Eles não são muito interessantes por si só, mas como são gerados. Estou usando o que estou chamando Query Objects.

O código:

Modelo de Usuário

Vamos começar de maneira simples com nosso modelo básico de usuário. Observe que não há nenhum material de extensão ou banco de dados do ORM. Apenas pura glória do modelo. Adicione seus getters, setters, validação, o que for.

class User
{
    public $id;
    public $first_name;
    public $last_name;
    public $gender;
    public $email;
    public $password;
}

Interface do Repositório

Antes de criar meu repositório de usuários, desejo criar minha interface de repositório. Isso definirá o "contrato" que os repositórios devem seguir para serem usados ​​pelo meu controlador. Lembre-se, meu controlador não saberá onde os dados estão realmente armazenados.

Observe que meus repositórios contêm apenas esses três métodos. O save()método é responsável por criar e atualizar usuários, simplesmente dependendo de o objeto de usuário ter ou não um ID definido.

interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

Implementação de Repositório SQL

Agora, para criar minha implementação da interface. Como mencionado, meu exemplo seria com um banco de dados SQL. Observe o uso de um mapeador de dados para evitar a necessidade de gravar consultas SQL repetitivas.

class SQLUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function find($id)
    {
        // Find a record with the id = $id
        // from the 'users' table
        // and return it as a User object
        return $this->db->find($id, 'users', 'User');
    }

    public function save(User $user)
    {
        // Insert or update the $user
        // in the 'users' table
        $this->db->save($user, 'users');
    }

    public function remove(User $user)
    {
        // Remove the $user
        // from the 'users' table
        $this->db->remove($user, 'users');
    }
}

Interface de objeto de consulta

Agora, com o CUD (Criar, Atualizar, Excluir) atendido pelo nosso repositório, podemos nos concentrar no R (Ler). Objetos de consulta são simplesmente um encapsulamento de algum tipo de lógica de pesquisa de dados. Eles não são construtores de consultas. Abstraindo-o como nosso repositório, podemos alterar sua implementação e testá-lo mais facilmente. Um exemplo de um Objeto de Consulta pode ser um AllUsersQueryou AllActiveUsersQuery, ou mesmo MostCommonUserFirstNames.

Você pode estar pensando "não posso simplesmente criar métodos em meus repositórios para essas consultas?" Sim, mas eis por que não estou fazendo isso:

  • Meus repositórios foram criados para trabalhar com objetos de modelo. Em um aplicativo do mundo real, por que eu precisaria entrar em passwordcampo se estou procurando listar todos os meus usuários?
  • Os repositórios geralmente são específicos do modelo, mas as consultas geralmente envolvem mais de um modelo. Então, em qual repositório você coloca seu método?
  • Isso mantém meus repositórios muito simples - não uma classe de métodos inchada.
  • Todas as consultas agora estão organizadas em suas próprias classes.
  • Realmente, neste momento, os repositórios existem simplesmente para abstrair minha camada de banco de dados.

Para o meu exemplo, criarei um objeto de consulta para pesquisar "AllUsers". Aqui está a interface:

interface AllUsersQueryInterface
{
    public function fetch($fields);
}

Implementação de objeto de consulta

É aqui que podemos usar um mapeador de dados novamente para ajudar a acelerar o desenvolvimento. Observe que estou permitindo um ajuste no conjunto de dados retornado - os campos. Isso é tanto quanto eu quero ir com a manipulação da consulta realizada. Lembre-se de que meus objetos de consulta não são construtores de consultas. Eles simplesmente realizam uma consulta específica. No entanto, como sei que provavelmente usarei muito este, em várias situações diferentes, estou me dando a capacidade de especificar os campos. Eu nunca quero retornar campos que não preciso!

class AllUsersQuery implements AllUsersQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch($fields)
    {
        return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
    }
}

Antes de passar para o controlador, quero mostrar outro exemplo para ilustrar o quão poderoso isso é. Talvez eu tenha um mecanismo de relatório e precise criar um relatório para AllOverdueAccounts. Isso pode ser complicado com meu mapeador de dados e talvez eu queira escrever algo real SQLnessa situação. Não tem problema, aqui está a aparência desse objeto de consulta:

class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch()
    {
        return $this->db->query($this->sql())->rows();
    }

    public function sql()
    {
        return "SELECT...";
    }
}

Isso mantém toda a minha lógica deste relatório em uma classe e é fácil de testar. Eu posso zombar do conteúdo do meu coração ou até mesmo usar uma implementação completamente diferente.

O controlador

Agora a parte divertida - reunir todas as peças. Observe que estou usando injeção de dependência. Normalmente, as dependências são injetadas no construtor, mas eu realmente prefiro injetá-las diretamente nos meus métodos de controlador (rotas). Isso minimiza o gráfico de objetos do controlador e, na verdade, acho mais legível. Observe que, se você não gosta dessa abordagem, basta usar o método tradicional do construtor.

class UsersController
{
    public function index(AllUsersQueryInterface $query)
    {
        // Fetch user data
        $users = $query->fetch(['first_name', 'last_name', 'email']);

        // Return view
        return Response::view('all_users.php', ['users' => $users]);
    }

    public function add()
    {
        return Response::view('add_user.php');
    }

    public function insert(UserRepositoryInterface $repository)
    {
        // Create new user model
        $user = new User;
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the new user
        $repository->save($user);

        // Return the id
        return Response::json(['id' => $user->id]);
    }

    public function view(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('view_user.php', ['user' => $user]);
    }

    public function edit(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('edit_user.php', ['user' => $user]);
    }

    public function update(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Update the user
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the user
        $repository->save($user);

        // Return success
        return true;
    }

    public function delete(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Delete the user
        $repository->delete($user);

        // Return success
        return true;
    }
}

Pensamentos finais:

O importante a ser observado aqui é que, quando estou modificando (criando, atualizando ou excluindo) entidades, estou trabalhando com objetos de modelo reais e realizando a persistência nos meus repositórios.

No entanto, quando estou exibindo (selecionando dados e enviando-os para as visualizações), não estou trabalhando com objetos de modelo, mas com objetos de valor antigos simples. Eu seleciono apenas os campos necessários e ele foi projetado para maximizar o desempenho da minha pesquisa de dados.

Meus repositórios permanecem muito limpos e, em vez disso, essa "bagunça" é organizada em minhas consultas de modelo.

Eu uso um mapeador de dados para ajudar no desenvolvimento, pois é ridículo escrever SQL repetitivo para tarefas comuns. No entanto, você pode escrever SQL sempre que necessário (consultas complicadas, relatórios, etc.). E quando você faz isso, está bem escondido em uma classe devidamente nomeada.

Eu adoraria ouvir sua opinião sobre a minha abordagem!


Atualização de julho de 2015:

Me perguntaram nos comentários onde acabei com tudo isso. Bem, na verdade não tão longe assim. Na verdade, ainda não gosto de repositórios. Acho-os um exagero para pesquisas básicas (especialmente se você já estiver usando um ORM) e confuso ao trabalhar com consultas mais complicadas.

Geralmente trabalho com um ORM estilo ActiveRecord, portanto, na maioria das vezes, apenas referencio esses modelos diretamente em todo o meu aplicativo. No entanto, nas situações em que tenho consultas mais complexas, usarei objetos de consulta para torná-las mais reutilizáveis. Também devo observar que sempre injeto meus modelos nos meus métodos, facilitando a zombaria nos meus testes.


4
@PeeHaa Novamente, era para manter os exemplos simples. É muito comum deixar trechos de código fora de um exemplo se eles não pertencerem especificamente ao tópico em questão. Na realidade, eu passaria minhas dependências.
24714 Jonathan

4
Interessante que você divida sua criação, atualização e exclusão da sua leitura. Achei que valeria a pena mencionar a Segregação de responsabilidade de consulta por comando (CQRS), que formalmente faz exatamente isso. martinfowler.com/bliki/CQRS.html
Adam

2
@ Jonathan Faz um ano e meio desde que você respondeu sua própria pergunta. Eu queria saber se você ainda está satisfeito com sua resposta e se esta é a sua principal solução agora para a maioria dos seus projetos? Nas últimas semanas, estive lendo allot em repositórios e vi várias pessoas terem sua própria interpretação de como deve ser implementado. Você está chamando de objetos de consulta, mas esse é um padrão existente, certo? Eu acho que já vi isso sendo usado em outros idiomas.
Boedy

1
@ Jonathan: Como você lida com as consultas que deveriam limitar o usuário a não ser "ID", mas por "nome de usuário" ou consultas ainda mais complicadas com mais de uma condição?
Gizzmo 28/10

1
@Gizzmo Usando objetos de consulta, você pode passar parâmetros adicionais para ajudar com suas consultas mais complicadas. Por exemplo, você pode fazer isso no construtor: new Query\ComplexUserLookup($username, $anotherCondition). Ou, faça isso através dos métodos setter $query->setUsername($username);. Você pode realmente projetar isso, no entanto, faz sentido para seu aplicativo em particular e acho que os objetos de consulta deixam muita flexibilidade aqui.
6136 Jonathan

48

Com base na minha experiência, aqui estão algumas respostas para suas perguntas:

P: Como lidamos com a recuperação de campos de que não precisamos?

R: Da minha experiência, isso realmente se resume a lidar com entidades completas versus consultas ad-hoc.

Uma entidade completa é algo como um Userobjeto. Possui propriedades e métodos, etc. É um cidadão de primeira classe na sua base de código.

Uma consulta ad-hoc retorna alguns dados, mas não sabemos nada além disso. À medida que os dados são passados ​​pelo aplicativo, isso é feito sem contexto. É um User? A Usercom algumas Orderinformações anexadas? Nós realmente não sabemos.

Eu prefiro trabalhar com entidades completas.

Você está certo ao retornar com frequência dados que não usará, mas pode resolver isso de várias maneiras:

  1. Faça o cache agressivo das entidades para que você pague o preço de leitura apenas uma vez no banco de dados.
  2. Gaste mais tempo modelando suas entidades para que elas tenham boas distinções entre elas. (Considere dividir uma entidade grande em duas entidades menores etc.)
  3. Considere ter várias versões de entidades. Você pode ter um Userpara o back-end e talvez um UserSmallpara chamadas AJAX. Um pode ter 10 propriedades e um tem 3 propriedades.

As desvantagens de trabalhar com consultas ad-hoc:

  1. Você acaba essencialmente com os mesmos dados em várias consultas. Por exemplo, com a User, você terminará escrevendo essencialmente o mesmo select *para muitas chamadas. Uma chamada recebe 8 de 10 campos, uma recebe 5 de 10, uma recebe 7 de 10. Por que não substituir todas por uma chamada que recebe 10 de 10? A razão pela qual isso é ruim é que é crime re-fatorar / testar / zombar.
  2. Torna-se muito difícil argumentar em alto nível sobre seu código ao longo do tempo. Em vez de declarações como "Por que as coisas são Usertão lentas?" você acaba rastreando consultas pontuais e, portanto, as correções de bugs tendem a ser pequenas e localizadas.
  3. É realmente difícil substituir a tecnologia subjacente. Se você armazena tudo no MySQL agora e deseja migrar para o MongoDB, é muito mais difícil substituir 100 chamadas ad-hoc do que um punhado de entidades.

P: Terei muitos métodos no meu repositório.

R: Realmente não vi outra maneira de consolidar as chamadas. O método que chama no seu repositório realmente mapeia os recursos em seu aplicativo. Quanto mais recursos, mais chamadas específicas de dados. Você pode refazer os recursos e tentar mesclar chamadas semelhantes em uma.

A complexidade no final do dia tem que existir em algum lugar. Com um padrão de repositório, nós o inserimos na interface do repositório, em vez de talvez fazer vários procedimentos armazenados.

Às vezes eu tenho que dizer a mim mesmo: "Bem, tinha que dar em algum lugar! Não há balas de prata".


Obrigado pela resposta muito completa. Você me fez pensar agora. Minha grande preocupação aqui é que tudo o que leio diz não SELECT *seleciona apenas os campos necessários. Por exemplo, veja esta pergunta . Quanto a todas as consultas ad-hock de que você fala, certamente entendo de onde você é. No momento, tenho um aplicativo muito grande que possui muitos deles. Esse foi o meu "Bem, tinha que dar em algum lugar!" momento, optei pelo desempenho máximo. No entanto, agora estou lidando com MUITAS consultas diferentes.
23413 Jonathan

1
Um pensamento de acompanhamento. Vi uma recomendação para usar uma abordagem R-CUD. Como readsgeralmente ocorrem problemas de desempenho, você pode usar uma abordagem de consulta mais personalizada para eles, que não se traduz em objetos de negócios reais. Então, para create, updatee delete, usar um ORM, que trabalha com objetos inteiros. Alguma opinião sobre essa abordagem?
23713 Jonathan

1
Como uma nota para usar "selecione *". Eu fiz isso no passado e funcionou bem - até atingirmos os campos varchar (max). Aqueles mataram nossas perguntas. Portanto, se você possui tabelas com ints, pequenos campos de texto, etc., não é tão ruim assim. Parece pouco natural, mas o software segue esse caminho. O que era ruim é subitamente bom e vice-versa.
precisa saber é o seguinte

1
A abordagem R-CUD é realmente CQRS
MikeSW

2
@ ryan1234 "A complexidade no final do dia deve existir em algum lugar." Obrigado por isso. Me faz sentir melhor.
31414

20

Eu uso as seguintes interfaces:

  • Repository - carrega, insere, atualiza e exclui entidades
  • Selector - localiza entidades baseadas em filtros, em um repositório
  • Filter - encapsula a lógica de filtragem

My Repositoryé independente de banco de dados; de fato, não especifica nenhuma persistência; poderia ser qualquer coisa: banco de dados SQL, arquivos xml, serviço remoto, um alienígena do espaço exterior, etc. Para pesquisar capacidades, as Repositoryconstrói um Selectorque pode ser filtrada, LIMIT-ed, classificadas e contadas. No final, o seletor busca um ou mais Entitiesda persistência.

Aqui está um exemplo de código:

<?php
interface Repository
{
    public function addEntity(Entity $entity);

    public function updateEntity(Entity $entity);

    public function removeEntity(Entity $entity);

    /**
     * @return Entity
     */
    public function loadEntity($entityId);

    public function factoryEntitySelector():Selector
}


interface Selector extends \Countable
{
    public function count();

    /**
     * @return Entity[]
     */
    public function fetchEntities();

    /**
     * @return Entity
     */
    public function fetchEntity();
    public function limit(...$limit);
    public function filter(Filter $filter);
    public function orderBy($column, $ascending = true);
    public function removeFilter($filterName);
}

interface Filter
{
    public function getFilterName();
}

Então, uma implementação:

class SqlEntityRepository
{
    ...
    public function factoryEntitySelector()
    {
        return new SqlSelector($this);
    }
    ...
}

class SqlSelector implements Selector
{
    ...
    private function adaptFilter(Filter $filter):SqlQueryFilter
    {
         return (new SqlSelectorFilterAdapter())->adaptFilter($filter);
    }
    ...
}
class SqlSelectorFilterAdapter
{
    public function adaptFilter(Filter $filter):SqlQueryFilter
    {
        $concreteClass = (new StringRebaser(
            'Filter\\', 'SqlQueryFilter\\'))
            ->rebase(get_class($filter));

        return new $concreteClass($filter);
    }
}

A idéia é que o genérico Selectoruse, Filtermas a implementação SqlSelectoruse SqlFilter; o SqlSelectorFilterAdapteradapta um genérico Filtera um concreto SqlFilter.

O código do cliente cria Filterobjetos (que são filtros genéricos), mas na implementação concreta do seletor, esses filtros são transformados em filtros SQL.

Outras implementações de seletor, como InMemorySelector, transformam de Filterpara InMemoryFilterusar seu específico InMemorySelectorFilterAdapter; portanto, toda implementação de seletor vem com seu próprio adaptador de filtro.

Usando essa estratégia, meu código de cliente (na camada de negócios) não se importa com uma implementação específica de repositório ou seletor.

/** @var Repository $repository*/
$selector = $repository->factoryEntitySelector();
$selector->filter(new AttributeEquals('activated', 1))->limit(2)->orderBy('username');
$activatedUserCount = $selector->count(); // evaluates to 100, ignores the limit()
$activatedUsers = $selector->fetchEntities();

PS Esta é uma simplificação do meu código real


"Repositório - carrega, insere, atualiza e exclui entidades", é isso que uma "camada de serviço", "DAO" e "BLL" podem fazer
Yousha Aleayoub

5

Vou acrescentar um pouco sobre isso, pois atualmente estou tentando entender tudo isso sozinho.

# 1 e 2

Este é o local perfeito para o seu ORM fazer o trabalho pesado. Se você estiver usando um modelo que implementa algum tipo de ORM, basta usar seus métodos para cuidar dessas coisas. Faça suas próprias funções orderBy que implementam os métodos Eloquent, se necessário. Usando o Eloquent, por exemplo:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

O que você parece estar procurando é um ORM. Não há razão para o seu Repositório não poder se basear em um. Isso exigiria que o Usuário fosse eloquente, mas pessoalmente não vejo isso como um problema.

Se, no entanto, você deseja evitar um ORM, precisará "rodar o seu próprio" para obter o que procura.

# 3

As interfaces não devem ser requisitos rígidos e rápidos. Algo pode implementar uma interface e adicionar a ela. O que ele não pode fazer é deixar de implementar uma função necessária dessa interface. Você também pode estender interfaces como classes para manter as coisas secas.

Dito isto, estou apenas começando a entender, mas essas realizações me ajudaram.


1
O que eu não gosto nesse método é que, se você tivesse um MongoUserRepository, isso e seu DbUserRepository retornariam objetos diferentes. Db retornando um Eloquent \ Model e Mongo algo próprio. Certamente uma implementação melhor é fazer com que ambos os repositórios retornem instâncias / coleções de uma classe Entity \ User separada. Desta forma, você não são mistakingly confiar em métodos DB Eloquent \ Modelo de quando você passar a usar o MongoRepository
danharper

1
Definitivamente, eu concordo com você. O que eu provavelmente faria para evitar isso nunca seria usar esses métodos fora da classe exigente Eloquent. Portanto, a função get provavelmente deve ser privada e usada apenas dentro da classe, pois, como você apontou, retornaria algo que outros repositórios não poderiam.
Will

3

Só posso comentar sobre como lidamos com isso (na minha empresa). Antes de tudo, o desempenho não é um problema muito grande para nós, mas ter um código limpo / adequado é.

Antes de tudo, definimos modelos como um UserModelque usa um ORM para criar UserEntityobjetos. Quando a UserEntityé carregado de um modelo, todos os campos são carregados. Para campos que referenciam entidades estrangeiras, usamos o modelo estrangeiro apropriado para criar as respectivas entidades. Para essas entidades, os dados serão carregados ondemand. Agora sua reação inicial pode ser ... ??? ... !!! deixe-me dar um exemplo um pouco de exemplo:

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

No nosso caso, $dbé um ORM capaz de carregar entidades. O modelo instrui o ORM a carregar um conjunto de entidades de um tipo específico. O ORM contém um mapeamento e o usa para injetar todos os campos dessa entidade na entidade. Para campos estrangeiros, no entanto, apenas os IDs desses objetos são carregados. Nesse caso, OrderModelcria OrderEntitys com apenas os IDs dos pedidos referenciados. Quando PersistentEntity::getFieldé chamado pela OrderEntityentidade, a entidade instrui seu modelo para carregar preguiçosamente todos os campos nos OrderEntitys. Todos os OrderEntitys associados a uma UserEntity são tratados como um conjunto de resultados e serão carregados de uma só vez.

A mágica aqui é que nosso modelo e ORM injetam todos os dados nas entidades e essas entidades apenas fornecem funções de invólucro para o getFieldmétodo genérico fornecido por PersistentEntity. Para resumir, sempre carregamos todos os campos, mas os campos que fazem referência a uma entidade estrangeira são carregados quando necessário. Carregar vários campos não é realmente um problema de desempenho. Carregar todas as entidades estrangeiras possíveis, no entanto, seria uma enorme redução de desempenho.

Agora, vamos carregar um conjunto específico de usuários, com base na cláusula where. Fornecemos um pacote de classes orientado a objetos que permite especificar expressões simples que podem ser coladas. No código de exemplo, eu o nomeei GetOptions. É um invólucro para todas as opções possíveis para uma consulta selecionada. Ele contém uma coleção de cláusulas where, um grupo por cláusula e tudo mais. Nossas cláusulas where são bastante complicadas, mas você pode obviamente criar uma versão mais simples com facilidade.

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

Uma versão mais simples desse sistema seria passar a parte WHERE da consulta como uma string diretamente para o modelo.

Sinto muito por esta resposta bastante complicada. Tentei resumir nossa estrutura o mais rápido e claro possível. Se você tiver outras perguntas, não hesite em perguntar e atualizarei minha resposta.

EDIT: Além disso, se você realmente não deseja carregar alguns campos imediatamente, você pode especificar uma opção de carregamento lento no seu mapeamento ORM. Como todos os campos são eventualmente carregados pelo getFieldmétodo, você pode carregar alguns campos no último minuto quando esse método é chamado. Este não é um problema muito grande em PHP, mas eu não recomendaria para outros sistemas.


3

Estas são algumas soluções diferentes que eu já vi. Existem prós e contras em cada um deles, mas cabe a você decidir.

Problema nº 1: muitos campos

Esse é um aspecto importante, especialmente quando você considera as verificações somente de índice . Eu vejo duas soluções para lidar com esse problema. Você pode atualizar suas funções para obter um parâmetro de matriz opcional que conteria uma lista de colunas para retornar. Se esse parâmetro estiver vazio, você retornará todas as colunas na consulta. Isso pode ser um pouco estranho; com base no parâmetro, você pode recuperar um objeto ou uma matriz. Você também pode duplicar todas as suas funções para ter duas funções distintas que executam a mesma consulta, mas uma retorna uma matriz de colunas e a outra retorna um objeto.

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

Problema nº 2: muitos métodos

Trabalhei brevemente com a Propel ORM há um ano e isso se baseia no que me lembro dessa experiência. O Propel tem a opção de gerar sua estrutura de classes com base no esquema de banco de dados existente. Ele cria dois objetos para cada tabela. O primeiro objeto é uma longa lista de funções de acesso semelhante à que você listou atualmente; findByAttribute($attribute_value). O próximo objeto herda deste primeiro objeto. Você pode atualizar esse objeto filho para criar suas funções getter mais complexas.

Outra solução seria usar __call()para mapear funções não definidas para algo acionável. Seu __callmétodo seria capaz de analisar o findById e findByName em consultas diferentes.

public function __call($function, $arguments) {
    if (strpos($function, 'findBy') === 0) {
        $parameter = substr($function, 6, strlen($function));
        // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0]
    }
}

Espero que isso ajude pelo menos alguns o quê.



0

Concordo com @ ryan1234 que você deve passar objetos completos dentro do código e usar métodos de consulta genéricos para obter esses objetos.

Model::where(['attr1' => 'val1'])->get();

Para uso externo / de ponto final, gosto muito do método GraphQL.

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}

0

Problema nº 3: impossível corresponder a uma interface

Vejo o benefício em usar interfaces para repositórios, para que eu possa trocar minha implementação (para fins de teste ou outros). Meu entendimento das interfaces é que elas definem um contrato que uma implementação deve seguir. Isso é ótimo até você começar a adicionar métodos adicionais aos seus repositórios, como findAllInCountry (). Agora, preciso atualizar minha interface para também ter esse método, caso contrário, outras implementações podem não tê-lo e isso pode interromper meu aplicativo. Por isso parece insano ... um caso de rabo balançando o cachorro.

Meu instinto me diz que isso talvez exija uma interface que implemente métodos otimizados para consultas juntamente com métodos genéricos. As consultas sensíveis ao desempenho devem ter métodos direcionados, enquanto as consultas pouco frequentes ou leves são tratadas por um manipulador genérico, talvez a despesa do controlador fazendo um pouco mais de malabarismo.

Os métodos genéricos permitiriam que qualquer consulta fosse implementada e, assim, evitariam interromper as alterações durante um período de transição. Os métodos direcionados permitem otimizar uma chamada quando faz sentido e podem ser aplicados a vários provedores de serviços.

Essa abordagem seria semelhante às implementações de hardware que executam tarefas otimizadas específicas, enquanto as implementações de software fazem o trabalho leve ou a implementação flexível.


0

Eu acho que o graphQL é um bom candidato para fornecer uma linguagem de consulta em larga escala sem aumentar a complexidade dos repositórios de dados.

No entanto, existe outra solução, se você não quiser usar o graphQL por enquanto. Usando um DTO em que um objeto é usado para transportar os dados entre processos, neste caso entre o serviço / controlador e o repositório.

Uma resposta elegante já foi fornecida acima, no entanto, tentarei dar outro exemplo que acho mais simples e poderia servir como ponto de partida para um novo projeto.

Como mostrado no código, precisaríamos de apenas 4 métodos para operações CRUD. o findmétodo seria usado para listar e ler passando o argumento do objeto. Os serviços de back-end podem criar o objeto de consulta definido com base em uma string de consulta de URL ou com base em parâmetros específicos.

O objeto de consulta ( SomeQueryDto) também pode implementar uma interface específica, se necessário. e é fácil de ser estendido posteriormente sem adicionar complexidade.

<?php

interface SomeRepositoryInterface
{
    public function create(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function update(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function delete(int $id): void;

    public function find(SomeEnitityQueryInterface $query): array;
}

class SomeRepository implements SomeRepositoryInterface
{
    public function find(SomeQueryDto $query): array
    {
        $qb = $this->getQueryBuilder();

        foreach ($query->getSearchParameters() as $attribute) {
            $qb->where($attribute['field'], $attribute['operator'], $attribute['value']);
        }

        return $qb->get();
    }
}

/**
 * Provide query data to search for tickets.
 *
 * @method SomeQueryDto userId(int $id, string $operator = null)
 * @method SomeQueryDto categoryId(int $id, string $operator = null)
 * @method SomeQueryDto completedAt(string $date, string $operator = null)
 */
class SomeQueryDto
{
    /** @var array  */
    const QUERYABLE_FIELDS = [
        'id',
        'subject',
        'user_id',
        'category_id',
        'created_at',
    ];

    /** @var array  */
    const STRING_DB_OPERATORS = [
        'eq' => '=', // Equal to
        'gt' => '>', // Greater than
        'lt' => '<', // Less than
        'gte' => '>=', // Greater than or equal to
        'lte' => '<=', // Less than or equal to
        'ne' => '<>', // Not equal to
        'like' => 'like', // Search similar text
        'in' => 'in', // one of range of values
    ];

    /**
     * @var array
     */
    private $searchParameters = [];

    const DEFAULT_OPERATOR = 'eq';

    /**
     * Build this query object out of query string.
     * ex: id=gt:10&id=lte:20&category_id=in:1,2,3
     */
    public static function buildFromString(string $queryString): SomeQueryDto
    {
        $query = new self();
        parse_str($queryString, $queryFields);

        foreach ($queryFields as $field => $operatorAndValue) {
            [$operator, $value] = explode(':', $operatorAndValue);
            $query->addParameter($field, $operator, $value);
        }

        return $query;
    }

    public function addParameter(string $field, string $operator, $value): SomeQueryDto
    {
        if (!in_array($field, self::QUERYABLE_FIELDS)) {
            throw new \Exception("$field is invalid query field.");
        }
        if (!array_key_exists($operator, self::STRING_DB_OPERATORS)) {
            throw new \Exception("$operator is invalid query operator.");
        }
        if (!is_scalar($value)) {
            throw new \Exception("$value is invalid query value.");
        }

        array_push(
            $this->searchParameters,
            [
                'field' => $field,
                'operator' => self::STRING_DB_OPERATORS[$operator],
                'value' => $value
            ]
        );

        return $this;
    }

    public function __call($name, $arguments)
    {
        // camelCase to snake_case
        $field = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name));

        if (in_array($field, self::QUERYABLE_FIELDS)) {
            return $this->addParameter($field, $arguments[1] ?? self::DEFAULT_OPERATOR, $arguments[0]);
        }
    }

    public function getSearchParameters()
    {
        return $this->searchParameters;
    }
}

Exemplo de uso:

$query = new SomeEnitityQuery();
$query->userId(1)->categoryId(2, 'ne')->createdAt('2020-03-03', 'lte');
$entities = $someRepository->find($query);

// Or by passing the HTTP query string
$query = SomeEnitityQuery::buildFromString('created_at=gte:2020-01-01&category_id=in:1,2,3');
$entities = $someRepository->find($query);
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.