Desenho do padrão de repositório adequado em PHP?

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

Comecei recentemente a aprender TDD no PHP, e estou a perceber que a minha base de dados está muito ligada ao resto da minha aplicação. Li sobre repositórios e usei um recipiente de Coi para "injectar" isso nos meus controladores. Coisas muito fixes. Mas agora tenho algumas perguntas práticas sobre o design do repositório. Considere o siga o 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)
    {
    }
}

Número 1: demasiados campos

todos estes métodos de procura usam uma abordagem select all fields (SELECT *). No entanto, em meus aplicativos, eu estou sempre tentando limitar o número de campos que eu recebo, como isso muitas vezes adiciona despesas gerais e atrasa as coisas. Para aqueles que usam este padrão, como você lida com isso?

Número 2: demasiados métodos

Enquanto esta aula está bonita agora, sei que numa aplicação do mundo real preciso de muito mais métodos. Para exemplo:

  • findAllByNameAndStatus
  • findAllInCountry
  • encontra todos os endereços electrónicos
  • encontra-se a byageandgender
  • encontra-se por toda a parte por toda a parte e por toda a parte
  • Etc.
Como podem ver, pode haver uma longa lista de métodos possíveis. E então se você adicionar na questão de seleção de campo acima, o problema piora. No passado, eu normalmente punha toda esta 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 o meu repositório aproxima-te, não quero acabar com isto.

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

}

questão # 3: impossível de corresponder a uma interface

vejo a vantagem em usar interfaces para repositórios, para que possa trocar a minha implementação (para fins de teste ou outros). O meu entendimento das interfaces é que elas definem um contrato que uma implementação deve seguir. Isto é óptimo até começar a adicionar métodos adicionais aos seus repositórios, como findAllInCountry(). Agora eu preciso atualizar minha interface para também ter este método, caso contrário, outras implementações podem não tê-lo, e isso poderia quebrar a minha aplicação. Isto parece uma loucura...uma caixa de cauda a abanar o cão.

Padrão De Especificação? Isto leva-me a acreditar que o repositório só deve ter um número fixo de métodos (como save(), remove(), find(), findAll(), etc). Mas como faço para fazer pesquisas específicas? Já ouvi falar do padrão de especificação , mas parece-me que isto só reduz um conjunto inteiro de registos (via IsSatisfiedBy()), que claramente tem grandes problemas de desempenho se você está puxando de uma base de dados.

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

Author: Karl Hill, 2013-04-23

11 answers

Pensei em responder à minha pergunta. O que se segue é apenas uma maneira de resolver as questões 1-3 na minha pergunta original.

Disclaimer: eu posso nem sempre usar os Termos certos ao descrever padrões ou técnicas. Desculpa por isso.

Os Objectivos:

  • crie um exemplo completo de um controlador básico para visualização e edição Users.
  • todos os códigos devem ser testáveis e ridicularizáveis.
  • O controlador deve não tenha idéia de onde os dados são armazenados (o que significa que pode ser alterado).
  • exemplo para mostrar uma implementação SQL (mais comum).
  • para o desempenho máximo, os controladores só devem receber os dados de que necessitam-sem campos extra.
  • A implementação deve alavancar algum tipo de Mapeador de dados para facilitar o desenvolvimento.
  • a implementação deve ter a capacidade de realizar pesquisas de dados complexas.

A Solução

Estou a dividir o meu armazenamento persistente. (base de dados) interacção em duas categorias: R (ler) e CUD (criar, actualizar, apagar). A minha experiência tem sido que ler é realmente o que faz com que uma aplicação para abrandar. E enquanto a manipulação de dados (CUD) é realmente mais lenta, ela acontece muito menos frequentemente, e é, portanto, muito menos preocupante.

CUD (criar, actualizar, apagar) é fácil. Isto envolverá trabalhar com modelos reais , que são então passados para o meu Repositories para persistência. Note, meus repositórios ainda fornecerão um método de leitura, mas simplesmente para a criação de objetos, não para exibição. Mais sobre isso mais tarde.

R (Read) não é assim tão fácil. Não há modelos aqui, apenas objetos de valor . Utilize arrays se preferir . Estes objetos podem representar um único modelo ou uma mistura de muitos modelos, qualquer coisa realmente. Estes não são muito interessantes por si só, mas como eles são gerados é. Estou a usar o que estou a chamar.

O Código:

Utilizador Modelo Vamos começar com o nosso modelo básico de utilizador. Note que não há nenhuma extensão ORM ou material de banco de dados em tudo. Pura glória modelo. Junta as tuas 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 o meu repositório de utilizadores, quero criar a minha interface de repositório. Isto irá definir o "contrato" que os repositórios devem seguir para serem usados pelo meu controlador. Lembre-se, meu controlador não vai saber onde os dados estão realmente Opcao.

Repare que os meus repositórios só conterão estes três métodos. O método save() é responsável pela criação e atualização de usuários, simplesmente dependendo se o objeto do usuário tem ou não um conjunto de id.
interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

Implementação do repositório SQL

Agora vou criar a minha implementação da interface. Como mencionado, meu exemplo seria com uma base de dados SQL. Note o uso de um mapeador de dados para evitar ter que escrever SQL repetitivo consulta.
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');
    }
}

Query Object Interface

Agora com CUD (criar, actualizar, apagar) tratado pelo nosso repositório, podemos concentrar-nos no R (ler). Objetos de consulta são simplesmente uma encapsulação de algum tipo de lógica de pesquisa de dados. Eles não são construtores de pesquisas. Abstraindo-o como o nosso repositório, podemos alterar a sua implementação e testá-lo mais facilmente. Um exemplo de um objeto de consulta pode ser um AllUsersQuery ou AllActiveUsersQuery, ou mesmo MostCommonUserFirstNames.

Pode ser pensando " não posso simplesmente criar métodos nos meus repositórios para essas consultas?"Sim, mas é por isto que não estou a fazer isto.

    Os meus repositórios são feitos para trabalhar com objectos modelo. Numa aplicação do mundo real, porque precisaria eu de obter o campo password Se estou à procura de listar todos os meus utilizadores?
  • Os repositórios são muitas vezes específicos do modelo, mas as consultas envolvem muitas vezes mais do que um modelo. Então, em que repositório você coloca seu método?
  • Isto mantém os meus repositórios muito simples. uma classe inchada de métodos. Todas as perguntas estão agora organizadas nas suas próprias classes.
  • realmente, neste momento, os repositórios existem simplesmente para abstrair a minha camada de base de dados.
Por exemplo, criarei um objeto de consulta para procurar "AllUsers". Aqui está a interface:
interface AllUsersQueryInterface
{
    public function fetch($fields);
}

A Implementação Do Objecto De Pesquisa

É 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 retornados-o campo. Isto é o mais longe que eu quero ir com a manipulação da consulta realizada. Lembre-se, meus objetos de consulta não são construtores de consultas. Eles simplesmente executam uma consulta específica. No entanto, como eu sei que provavelmente vou estar usando este muito, em uma série de situações diferentes, estou me dando a capacidade de especificar os campos. Nunca quero devolver campos de 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 ao controlador, quero mostrar outro exemplo para ilustrar o quão poderoso isto é. Talvez eu tenha um motor de relatório e preciso criar um relatório para AllOverdueAccounts. Isso pode ser complicado com meu mapeador de dados, e eu posso querer escrever algum Real SQL nesta situação. Não há problema, aqui está o que este objeto de consulta poderia parecer:
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...";
    }
}
Isto mantém toda a minha lógica para este relatório numa aula, e é fácil de testar. Posso gozar com o conteúdo do meu coração, ou até usar uma implementação completamente diferente.

O Controlador

Agora a parte divertida que traz tudo as peças juntas. Repare que estou a usar uma injecção de dependência. Tipicamente dependências são injetadas no construtor, mas eu prefiro injetá-las diretamente em meus métodos de controle (rotas). Isto minimiza o gráfico de objetos do controlador, e eu realmente o acho mais legível. Note, Se você não gosta desta abordagem, basta usar o método de construção tradicional.
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:

As coisas importantes a notar aqui são que quando estou modificando (criando, atualizando ou apagando) entidades, estou trabalhando com objetos de modelo real, e realizando a persistência através de meus repositórios.

No entanto, quando estou a mostrar (a seleccionar dados e a enviá-los para as vistas) não estou a trabalhar com objectos de modelo, mas sim com objectos de valor simples e antigos. Eu só seleciono os campos que eu preciso, e ele foi projetado para que eu possa maximizar o meu desempenho de pesquisa de dados.

Os meus repositórios ficam muito limpos, e em vez disso esta" confusão " está organizada no meu modelo. consulta. Eu uso um mapeador de dados para ajudar no desenvolvimento, pois é ridículo escrever SQL repetitivo para tarefas comuns. No entanto, você absolutamente pode escrever SQL quando necessário (consultas complicadas, relatórios, etc.). E quando o fazes, fica bem escondido numa aula devidamente nomeada. Adorava ouvir a tua opinião sobre a minha abordagem!

Atualização De Julho De 2015:

Perguntaram-me nos comentários onde acabei com tudo isto. Bem, não tão longe, na verdade. Na verdade, ainda não gosto de repositórios. Eu acho que eles exageram para pesquisas básicas (especialmente se você já está usando uma ORM), e confuso ao trabalhar com consultas mais complicadas. Geralmente trabalho com um ORM de estilo ActiveRecord, por isso, na maioria das vezes, vou apenas referenciar esses modelos directamente ao longo da minha aplicação. No entanto, em situações em que tenho consultas mais complexas, vou usar objetos de consulta para torná-los mais reutilizáveis. Devo também notar que injecte sempre os meus modelos nos meus métodos, tornando-os mais fáceis de gozar nos meus testes.
 220
Author: Jonathan, 2017-05-23 11:47:29
Com base na minha experiência, aqui estão algumas respostas às suas perguntas:

P: Como lidamos com trazer de volta campos de que não precisamos?

R: {[14] } pela minha experiência isto resume-se a lidar com entidades completas versus consultas ad-hoc.

Uma entidade completa é algo como um objecto User. Tem propriedades e métodos, etc. É um cidadão de primeira classe na sua base de dados.

Uma consulta ad-hoc devolve alguns dados, mas nós não sei qualquer coisa para além disso. À medida que os dados são transmitidos ao redor da aplicação, é feito sem contexto. É um...? A {[[0]} com algumas informações anexadas? Não sabemos.

Prefiro trabalhar com entidades completas.

Você está certo que muitas vezes irá trazer dados de volta que não irá usar, mas pode abordar isto de várias formas:

  1. aggressively cache as Entidades de modo que você só paga o preço de leitura uma vez a partir da base de dados.
  2. Passe mais tempo modelando suas entidades para que tenham boas distinções entre elas. (Considerar dividir uma grande entidade em duas entidades menores, etc.)
  3. considere ter várias versões de entidades. Podes ter um para a parte de trás e talvez um para as chamadas do AJAX. Um pode ter 10 propriedades e um tem 3 propriedades.
As desvantagens de trabalhar com consultas ad hoc:
    Acaba-se com os mesmos dados em muitas consultas. Por exemplo, com um User, você vai acabar escrevendo essencialmente o mesmo select * para muitas chamadas. Uma chamada terá 8 de 10 campos, uma terá 5 de 10, uma terá 7 de 10. Porque não substituir todos por uma chamada que recebe 10 de 10? A razão disso é ruim é que é assassinato para re-fator / teste / zombaria. Torna-se muito difícil argumentar a alto nível sobre o seu código ao longo do tempo. Em vez de declarações como "por que o {[[0]} é tão lento?"você acaba rastreando consultas pontuais e então as correções de bugs tendem a ser pequenas e localizar. É muito difícil substituir a tecnologia subjacente. Se você armazenar tudo em MySQL agora e quer se mudar para MongoDB, é muito mais difícil substituir 100 chamadas ad-hoc do que é um punhado de entidades.

Q: terei muitos métodos no meu repositório.

R: {[14] eu realmente não vi nenhuma maneira de contornar isso, a não ser consolidar chamadas. O método chama em seu repositório realmente mapear para recursos em sua aplicação. Melhor características, As chamadas mais específicas de dados. Você pode empurrar para trás em recursos e tentar fundir chamadas semelhantes em um.

A complexidade no final do dia tem de existir algures. Com um padrão de repositório nós o empurramos para a interface do repositório em vez de talvez fazer um monte de procedimentos armazenados. Às vezes tenho que dizer a mim mesmo, "bem, tinha que dar em algum lugar! Não há balas de prata."
 51
Author: ryan1234, 2013-04-23 22:33:47

Uso as seguintes interfaces:

  • Repository - carrega, insere, actualiza e apaga entidades
  • Selector - encontra entidades baseadas em filtros, num repositório
  • Filter - encapsula a lógica de filtragem

O meu {[3] } é agnóstico na base de dados; de facto, não especifica qualquer persistência; pode ser qualquer coisa: base de dados SQL, ficheiro xml, serviço remoto, um estrangeiro do espaço exterior, etc. Para procurar capacidades, o Repository constrói um Selector que pode ser filtrado, LIMIT - ed, ordenado e contado. No final, o selector obtém um ou mais Entities da persistência.

Aqui está um código de amostra:

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

O ideea é que o genérico Selector USA Filter mas a implementação SqlSelector usa SqlFilter; o SqlSelectorFilterAdapter adapta um genérico Filter a um concreto SqlFilter.

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

Outras implementações do selector, como InMemorySelector, transformem de Filter para InMemoryFilter usando o seu InMemorySelectorFilterAdapter específico; assim, cada implementação do selector vem com o seu próprio adaptador de filtros.

Usando esta estratégia, o código do meu cliente (na camada bussines) não se importa com um repositório ou implementação de selectores específicos.

/** @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();

P. S. Esta é uma simplificação do meu verdadeiro código

 22
Author: Constantin Galbenu, 2016-08-18 09:45:31
Vou acrescentar um pouco disto, porque estou a tentar perceber tudo isto sozinho.

#1 e 2

Este é o lugar perfeito para a sua ORM fazer o trabalho pesado. Se você está usando um modelo que implementa algum tipo de ORM, você pode apenas usar seus métodos para cuidar dessas coisas. Faça suas próprias funções orderBy que implementam os métodos eloquentes, se necessário. Usando eloquente, por exemplo:
class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }
O que parece estar à procura é uma ORM. Por nada. O repositório não pode ser baseado em torno de um. Isso exigiria que o Usuário estendesse eloquente, mas eu pessoalmente não vejo isso como um problema.

Se você quiser evitar uma ORM, então você teria que "rolar o seu próprio" para obter o que você está procurando.

#3

As Interfaces não devem ser difíceis e rápidas. Algo pode implementar uma interface e adicioná-la. O que ele não pode fazer é falhar em implementar uma função necessária dessa interface. Você também pode estender interfaces como aulas para manter as coisas secas. Dito isto, estou a começar a perceber, mas estas realizações ajudaram-me.
 5
Author: Will, 2013-04-25 21:06:51
Só posso comentar a forma como lidamos com isto. Em primeiro lugar, o desempenho não é muito um problema para nós, mas ter um código limpo/adequado é.

Em primeiro lugar, definimos modelos como um UserModel que usa um ORM para criar objectos UserEntity. Quando um UserEntity é carregado a partir de um modelo, todos os campos são carregados. Para campos referentes a entidades estrangeiras usamos o modelo estrangeiro apropriado para criar as respectivas entidades. Para essas entidades, os dados serão carregados à procura. A tua reacção inicial pode ser ...???...!!! deixe-me dar um exemplo um pouco de um 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. A ORM contém um mapeamento e utiliza-o para Injectar todos os campos dessa entidade na entidade. Para campos estrangeiros, no entanto, apenas os IDs desses objetos são carregados. Neste caso, os OrderModel criam OrderEntity s apenas com os ids dos referenciados pedido. Quando PersistentEntity::getField é chamado pela OrderEntity a entidade instrui o seu modelo a carregar todos os campos para os OrderEntitys. Todos os OrderEntityassociados a uma Userentidade são tratados como um conjunto de resultados e serão carregados de uma vez.

A magia aqui é que o nosso modelo e ORM injectam todos os dados nas Entidades e que as entidades apenas fornecem funções de invólucro para o método genérico getField fornecido por PersistentEntity. Para resumir, carregamos sempre todos os campos, mas campos referentes a uma entidade estrangeira são carregados quando necessário. Apenas carregar um monte de campos não é realmente um problema de desempenho. Carregar todas as entidades estrangeiras possíveis, no entanto, seria uma enorme redução de desempenho.

Passo agora ao carregamento de um conjunto específico de utilizadores, com base numa cláusula "where". Nós fornecemos um pacote orientado a objetos de classes que lhe permitem especificar uma expressão simples que pode ser colada em conjunto. No código de exemplo chamei-lhe GetOptions. É uma embalagem para todas as opções possíveis para uma consulta selecionada. Contém coleção de cláusulas de onde, um grupo por cláusula e tudo mais. Onde as cláusulas são bastante complicadas, mas você poderia, obviamente, fazer uma versão mais simples facilmente.
$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

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

Peço desculpa por esta resposta complicada. Tentei resumir o nosso quadro o mais rápido e claro possível. Se tiver mais perguntas, sinta-se à vontade para as fazer. actualiza a minha resposta.

Editar: para além disso, se realmente não quiser carregar alguns campos imediatamente, poderá indicar uma opção de carregamento preguiçosa no seu mapeamento ORM. Como todos os campos são eventualmente carregados através do método getField, Você poderia 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
Author: TFennis, 2013-04-25 14:26:17
Estas são algumas soluções diferentes que já vi. Há prós e contras para cada um deles, mas cabe a você decidir.

Número 1: demasiados campos

Este é um aspecto importante especialmente quando se tem em conta apenas os exames de índice . Vejo duas soluções para resolver este problema. Você pode atualizar suas funções para ter em um parâmetro de array opcional que conteria uma lista de colunas para retornar. Se este parâmetro estiver vazio, você devolverá todos os colunas na consulta. Isto pode ser um pouco estranho; baseado no parâmetro você pode recuperar um objeto ou um array. Você também pode duplicar todas as suas funções para que você tenha 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);
}

Número 2: demasiados métodos

Trabalhei brevemente com o propulsor ORM há um ano e isto baseia-se no que me lembro dessa experiência. Propel tem a opção de gerar a sua estrutura de classes com base no esquema de base de dados existente. Cria dois objetos para cada tabela. O primeiro objeto é uma longa lista de função de acesso semelhante ao que você tem listado atualmente; findByAttribute($attribute_value). O próximo objeto herda deste primeiro objeto. Você pode atualizar este objeto-criança para construir em suas funções getter mais complexas.

Outra solução estaria a usar __call() para mapear funções não definidas para algo accionável. O seu método seria capaz de analisar o findById e findByName em diferentes consultas.

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 isto ajude, pelo menos, um pouco.
 3
Author: Logan Bailey, 2013-04-25 14:58:58

Sugiro https://packagist.org/packages/prettus/l5-repository como fornecedor para implementar Repositórios/Critérios etc ... em Laravel5: d

 0
Author: abenevaut, 2018-11-12 17:47:47

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

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

Para uso externo/endpoint, gosto muito do método GraphQL.

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}
 0
Author: AVProgrammer, 2018-12-22 00:57:39

Questão # 3: impossível corresponder a uma interface

Eu vejo a vantagem em usar interfaces para repositórios, para que eu possa trocar a minha implementação (para fins de teste ou outros). As a compreensão das interfaces é que elas definem um contrato que um a implementação deve seguir-se. Isto é óptimo até começares a adicionar métodos adicionais para os seus repositórios, como o findAllInCountry(). Agora Eu necessidade de atualizar a minha interface para também ter este método, caso contrário, outros implementações podem não tê-lo, e isso pode quebrar a minha aplicação. Isto parece uma loucura...uma caixa de cauda a abanar o cão.

O meu instinto diz-me que isto talvez precise de uma interface que implemente métodos optimizados de consulta, juntamente com métodos genéricos. As consultas sensíveis ao desempenho devem ter métodos específicos, 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.

O genérico métodos permitiriam que qualquer consulta fosse implementada, e assim impediriam mudanças durante um período de transição. Os métodos específicos permitem otimizar uma chamada quando faz sentido, e ela pode ser aplicada a vários prestadores de serviços.

Esta abordagem seria semelhante às implementações de hardware que executam tarefas específicas optimizadas, enquanto as implementações de software fazem o trabalho leve ou implementação flexível.
 0
Author: Brian, 2020-02-03 23:05:01

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

No entanto, há outra solução se você não quiser ir para o graphQL por agora. Usando um DTO onde um objecto é usado para marcar os dados entre processos, neste caso entre o serviço/controlador e o repositório.

Uma resposta elegante já está fornecida acima, no entanto eu vou tente dar outro exemplo que eu acho que é mais simples e pode servir como ponto de partida para um novo projeto.

Como mostrado no código, precisaríamos apenas de 4 métodos para operações CRUD. o método find seria usado para listar e ler através do argumento do objeto. Os Serviços de infra-estrutura poderiam construir o objeto de consulta definido com base em uma string de pesquisa URL ou com base em parâmetros específicos.

O objecto da consulta (SomeQueryDto) também pode implementar uma interface específica, se necessário. e é fácil de ser estendido mais tarde 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;
    }
}

Uso de exemplo:

$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);
 0
Author: kordy, 2020-03-03 17:06:57
   class Criteria {}
   class Select {}
   class Count {}
   class Delete {}
   class Update {}
   class FieldFilter {}
   class InArrayFilter {}
   // ...

   $crit = new Criteria();  
   $filter = new FieldFilter();
   $filter->set($criteria, $entity, $property, $value);
   $select = new Select($criteria);
   $count = new Count($criteria);
   $count->getRowCount();
   $select->fetchOne(); // fetchAll();
Então acho que ...
 0
Author: Sudo, 2020-06-29 21:18:13