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 Repositories
para 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 AllUsersQuery
ou 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
password
campo 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 SQL
nessa 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.