Usamos cookies para medir audiência e melhorar sua experiência. Você pode aceitar ou recusar a qualquer momento. Veja sobre o iMasters.
Boa tarde Galera do Fórum Imasters,
Tenho algumas dúvidas em relação aos Values Objects, Data Access Object, Models e Data Transfer Object.
Pelo que entendi, os VOs e os DTOs, são apenas para armazenar e transferir os valores entre as camadas e classes. Então os são a mesma coisa? Por que recebe estes 2 nomes?
Outra dúvida que tenho, é que a model tbm pode ter propriedades para armazenar os valores do objeto, assim como o DTO e VO, além de ter métodos que tratam da regra de negócio, por exemplo, verificar se os valores das propriedades são válidos, etc.
Gostaria de saber qual é a melhor forma de integrar tudo isso com o banco de dados, ou seja, colocar as operações de CRUD tudo dentro da Model, ou usar VO e passá-lo como parâmetro, etc.
Valeu galera...
>
vo e dto são a mesma coisa, so muda o autor. se n me engano quem usa o termo dto é o martin fwoler.
seu entendimento sobre vo/dto esta certo.
E os models? Seria um DTO ou VO, porém com as regras de negócio embutidas na mesma classe?
Qual melhor pattern a usar? DTO + DAO ou uma Model com as operações de CRUD dentro da mesma?
Abração
eu gosto mais de criar classes utilitarias do q colocar a regra de negocio no model. pq o codigo pode ser melhor reaproveitado...
uso mais DTO + DAO.
>
eu gosto mais de criar classes utilitarias do q colocar a regra de negocio no model. pq o codigo pode ser melhor reaproveitado...
uso mais DTO + DAO.
Teria algum exemplo de uso com essas classes utilitárias?
manipulção de datas, por exemplo pegar dia util, ultimo dia do mes e outras coisa chatas. ao inves de colocar esse codigo no model você cria uma classe DataUtils, hoje ela começa so com o metodo getDiaUtil(); em outro projeto você tem a necessidade de pegar o ultimo dia do mes... você cria o algoritmo e adiciona na classe DataUtils. Ao logo do tempo você vai ter um biblioteca de classes organizada.
DTO e VO são a mesma coisa:
Definição (DTO, VO, pra que isso?):
- Value Object (VO): nome usado na primeira edição do livro Core J2EE Patterns
Como eu estava pesquisando sistemas distribuídos em java, encontrei sobre isso.
/applications/core/interface/imageproxy/imageproxy.php?img=http://martinfowler.com/eaaCatalog/dtoSketch.gif&key=1a22898ee1723fe632d7b4502fee415d91f1ce2fb32b7f00a6fb4237a7aab179" alt="dtoSketch.gif" />
Se visualizar a imagem, que foi retirada do site do Martin Fowler. Verá que a camada DTO converte o xml para um objeto (toXmlElement() e vice-versa (readXml). Isso facilita muito em sistemas distribuídos.
Simplificando, é uma maneira de enviar um objeto, ao invés de atributos de um objeto, por parâmetro para um método. Se fosse ter de enviar os atributos, teria de ser em N parâmetros. Como é apenas um objeto, é apenas um parâmetro. Em um sistema distribuído faz muito sentido. Já em um sistema não distribuído, a camada de negócios trata do objeto em si, sem necessidade de possuir uma camada somente para transferência. Porém, é, também, muito útil em um sistema não distribuído, pois abstrai a criação do objeto entre a camada view e a camada de negócios.
Só, ainda não vi a necessidade de seu uso em um sistema não distribuído. Arquitetura DAO sempre me atendeu perfeitamente, MVC também. Profissionalmente, eu utilizo o CodeIgniter, framework de arquitetura MVC. Para sistemas web (não sites), eu utilizo uma arquitetura que aprendi quando estudei C# (WebForms), que nada mais seria que a camada DAO dividida em duas (Data Access e Business Layer). Infelizmente não sei o nome, e provavelmente é mais usado do que imagino (com outro nome obviamente). No bom e velho java, Data Access Object \m/.
>
nem dto, nem vo, e nem dao sao pattern...o dao eh uma camada da arquitetura 5 tiers, a arquitetura mvc tem 3 camadas: view controller e model, no 5 tiers o model se desmembra em: action, acoes do model, ou as regras de negocio, o dao a camada real de persistencia dos dados no banco, e bean, a entidade.
qual o melhor, depende do projeto, nos aki gostamos do mvc, os japoneses do 5 tiers...
o vo, e o dto sao a mesma coisa, realmente, e o dao a persistencia...
sobre crud, em oo, eu prefiro usar o padrao active record, pra mim eh o padrao q mais permite flexibilizacao e mapeamento objeto-relacional..ha um problema, o acitve record eh muito parecido com o row data gateway:o row data gateway recupera os dados somente pra um momento,, ou seja, recuperou, tera q armazena-lo em uma variavel, ou seja, stateless, ja o active record nao, você o instanciou e recuperou os dados, enquanto o objeto estiver vivo, os dados estao la, ou seja, statefull, ha outro detalhe tb, o row data gateway nao usa o padrao query object, o q o torna inflexivel, o active record usa o query object, o q o torna fexivel....
Sobre o Active Record, pelo o que eu li no livro "PHP - Programando com Orientação à Objetos", ele obriga toda tabela ter um campo id, este sendo númerico, o que não acontece no dia-a-dia, já que podemos usar como chave primária qualquer campo que não seja nulo e não se repita. Outra coisa que não gostei, é que obrigatoriamente ele seleciona todos os campos de uma tabela.
Gostaria de saber se há algum pattern diferente do active record que permita eu selecionar apenas alguns campos do bd ou selecionar alguns campos com join entre 2 ou mais tabelas, etc...
Sinceramente, os exemplos que eu vi eu não gostei devido os objetos ficarei realizando diversas consultas ao carregar dados de tabelas separadas ao invés de usar joins, union, etc...
Abração
>
ledo engano, no livro foi sim um numero, mas chave primaria pode , como você disse, qq tipo nao repetido, logo um varchar tb pode, desde q nao venha a se repetir...a forma mais tradicional de uma chave primaria eh usa-la com numero e auto incremento, eu estou desenvolvendo um sistema onde a chave primaria eh concatenada, ou seja formada, pelo numero do cliente, numero do trabalho do cliente no ano, ano, e revisao do trabalho (por ter sido refeito varias vezes encara-se como revisao)...
agora pra selecionar so alguns campos, creio (nao tenho certeza) q nao exista, pois tanto o table row, row data gateway, table data gateway e o active record selecionam todos os campos....a nao ser q você use um collection ou um lazy initiliazation....
Estava pensando no pattern Repository, na qual eu passaria no construtor o nome da classe e um objeto criteria do Query Object no método de buscar no banco e o mesmo me retornaria os dados. Acho que isso seria uma alternativa, sendo passado junto no parâmetro os campos que desejo retornar.
sobre crud, em oo, eu prefiro usar o padrao active record, pra mim eh o padrao q mais permite flexibilizacao e mapeamento objeto-relacional..ha um problema, o acitve record eh muito parecido com o row data gateway:o row data gateway recupera os dados somente pra um momento,, ou seja, recuperou, tera q armazena-lo em uma variavel, ou seja, stateless, ja o active record nao, você o instanciou e recuperou os dados, enquanto o objeto estiver vivo, os dados estao la, ou seja, statefull, ha outro detalhe tb, o row data gateway nao usa o padrao query object, o q o torna inflexivel, o active record usa o query object, o q o torna fexivel....
Está enganado quanto a isso.
Tanto Row [Data] Gateway quanto Active Record são stateful, pois ambos armazenam as propriedades (campos da tabela) no objeto.
A diferença é que o Active Record mantém dentro de si toda a função de persistência, violando o Princípio da Responsabilidade Única (SRP), enquanto que um objeto Row Gateway SEMPRE utiliza um objeto Table Data Gateway para a realizar a persistência, apesar de possuir métodos de persistência, como save, delete e update, internamente eles delegam essa função para o TDG.
A grosso modo, TDG e RDG são o desmembramento do Active Record em duais estruturas mais coesas, com uma única responsabilidade.
nem dto, nem vo, e nem dao sao pattern...
Errado, Fowler descreve ambos como padrões, entretanto, VO é uma definição antiga e de certa forma, errônea. DTO ou TO é a forma mais correta, embora VO ainda seja muito utilizado.
DAO é um padrão de acesso à camada de persistência.
Model é um nível maior ainda de abstração, presente em padrões de arquitetura, como o MVC e o Presentation-Model.
Ele representa o modelo (ah, vá?) da sua aplicação.
- M-ma-mas que isso quer dizer?
O Model, meu caro padawan, representa a lógica de negócio da sua aplicação.
Não está bom ainda né?
Vamos exemplificar. Pense numa calculadora, tipo aquela do windows ou a gcalc dos ambientes gnome.
P: Qual é a lógica de negócio de uma calculadora?
R: Fazer operações matemáticas.
Ou seja, no Model da calculadora, devemos ter funções tais como soma, subtração, raiz quadrada, seno, cosseno, tangente hiperbólica (sim, uma calculadora científica).
A nossa aplicação, além realizar operações, na maioria dos casos, precisa armazenar os dados sobre os quais aplicar essas operações. No caso da nossa calculadora, não é necessário, pois tudo o que ela processa são números. Quando você a desliga, todos os dados são perdidos. Se você quiser manter salvo algum histórico de operação, precisará então de uma camada de persistência de dados.
Vamos pensar agora em uma aplicativo de perguntas e respostas. Ele mostra algumas perguntas na tela, as quais o usuário deve responder.
P: Quais as operações da aplicação?
R: Procurar por perguntas, Processar respostas, Verificar se a resposta está correta, Autenticar usuários etc, etc, etc...
P: O que essa aplicação deve armazenar?
R: Perguntas, suas respostas, usuários do sistema, etc...
Tudo o que se lista acima em conjunto compõe o famigerado Model.
Perceba que o conceito é meio geral. Ficam perguntas: como representar essas informações no meu sistema? como fazer o acesso aos dados armazenados num meio persistente, como o disco?
Dessas perguntas, após muita experimentação, chegou-se aos padrões de representação de objetos (DTO, RowDataGateway, Active Record, Plain Old Object, etc) e de acesso à persistência (TableDataGateway, DAO, etc).
Só para sintetizar tudo: DTO, DAO, VO, TDG, RDG, etc. se relacionam com o Model numa relação parte para todo, ou seja, eles em conjunto (não todos ao mesmo tempo) compõem o que chamamos Model.
desculpe henrique, eu esqueci que nao sei nada...
Igor.php, se você não tem nada pertinente ao tópico, por favor, não poste besteira.
Se tiver alguma dúvida, leia as regras.
Igor.php leia as regras e, em seguida, leia a MP que te enviei.
Amigos, desconsiderem os posts #13, #14 e #15 e continuem com a discussão normal e que vai gerar conhecimento para todos.
>
Igor.php, se você não tem nada pertinente ao tópico, por favor, não poste besteira.
Se tiver alguma dúvida, leia as regras.
Concordo. Igor deu sua opnião, e não aceita ser corrigo por programadores mais experiêntes. Na boa cara, este seu comentário é totalmente infantil. Se você acha que não sabe nada, então aprimore-se, afinal, ninguém nasce sabendo.
O Henrique apenas afirmou que suas respostas estão erradas, e ele fez mais que certo, pois não é legal passar informações erradas por pessoas que procuram entender algum conceito. Mas se você não tem humildade em aeitar as críticas, me desculpe mas você não será / é um péssimo profissional, pois as críticas nos faz evoluir.
Eu sei que não estou participando deste tópico, mas pode ter certeza que leio os posts pra aprender.
Desculpe a minha sinceridade ignorante, mas este fórum é uma enorme fonte de conhecimento, além de que, estamos lidando com profissionais, e sempre tem pessoas dispostas a nos ajudar quando estamos com dúvidas em determinado assunto.
>
A diferença é que o Active Record mantém dentro de si toda a função de persistência, violando o Princípio da Responsabilidade Única (SRP), enquanto que um objeto Row Gateway SEMPRE utiliza um objeto Table Data Gateway para a realizar a persistência, apesar de possuir métodos de persistência, como save, delete e update, internamente eles delegam essa função para o TDG.
Me interessei pelo TDG e RDG, mais do que pelo Active Record. Teria algum exemplo dele em PHP ou algum diagrama? Com ele seria possível selecionar apenas alguns campos do bd, ao invés de todos? E para se trabalhar com JOINS nas consultas, é bem flexível utilizar este pattern?
É muito flexível. Estou na aula agora e tenho prova amanhã. Talvez na sexta à noite consiga montar algo.
Enquanto isso, sugiro você dar uma olhada no Zend Framework:
:seta: http://framework.zend.com/manual/en/zend.db.table.html
:seta: http://framework.zend.com/manual/en/zend.db.table.row.html
#18
por definição TDG ou Table Data Gateway age como uma ponte, um gateway para uma tabela.
Apenas uma instância carrega todos os rows.
No Active Record você tem acesso as colunas (rows) de uma tabela e adiciona a lógica do negócio (business logic ou business model)
Acho uma boa opção optar pelo Table Data Gateway ao Active Record.
De forma resumida e grosseira, o "TDG" não prende a lógica do negócio a modelagem de dados. Já no AR isso é o que acontece.
Uso prático
Se optar em usar o ZendFramework, tem um recurso onde poderá desativar a verificação de integridade para "driblar" a definição do padrão TDG, permitindo o uso de relacionamentos com JOIN
$sel = $this->select();
$sel->setIntegrityCheck(false) // true (default): não permite join, false: permite join
->from($this)
->join('tabela1', 'tabela1.id' = 'tabela2.tabela1_id');
return $this->fetchAll($sel);
:seta:Zend_Db_Table_Selects
veja tb exemplo prático com DataMapper
Bom, o que era pra ser um exemplozinho bem básico, que eu levaria 3 horas no máximo pra bolar, se transformou num dia inteiro de programação.
A minha ideia inicial era simples demais, não iria funcionar para a exemplificação, aí tive que tirar algumas coisas do conjunto de classes que eu já possuo aqui, porque se não seria tenso demais explicar amiúde cada detalhe.
Tentei ir direto ao ponto aqui, sem classes abstratas ou interfaces, apenas as classes concretas. Por esse motivo, essa implementação é presa a um único SGBD (MySQL), utilizando uma única API de acesso (MySQLi). Se não fizesse assim, teria que construir alguns Adapters também, que tornaria o exemplo ainda mais extenso.
A ideia é criar Tables e Rows genéricos, que são definidos em tempo de execução. Não é difícil encontrar exemplos por aí que te dizem se tratar de um RDG, mas na verdade, é um DTO (VO) ou DataMapper, dos quais eu pessoalmente não sou muito fã pois eles amarram sua implementação. Para cada entidade na minha aplicação eu preciso criar uma classe Mapper diferente. Se somar isso ao fato de ao utilizar MVC eu já ter que criar no mínimo 1 Model e alguns Controllers, além de ter alguns arquivos Views, cada inserção de entidade no sistema é um pé no saco.
Na implementação que eu criei para este exemplo, o objeto table é definido a partir de um "mapa de colunas", passado para o construtor, que contém informações sobre o nome e o tipo da coluna e se a mesma é chave primária ou não. Para não complicar demais, não há suporte a chaves compostas e elas devem ser preferencialmente inteiros com auto incremento.
Para as consultas ao banco, estou utilizando prepared statements, que facilitam bastante nossa vida na prevenção de SQL Injection. Ao invés de fazer a camada de segurança manual, deixamos a cargo do SGBD fazê-lo.
Para o melhor entendimento, dêem uma olhada no diagrama de classe do conjunto:
/applications/core/interface/imageproxy/imageproxy.php?img=http://www.freeimagehosting.net/newuploads/yz52z.png&key=64ab67984a9054a785647c2fff09aaf662220833a2a122fa30c0764e5dbb8084" alt="yz52z.png" />
Caso não conheçam UML, vou tentar traduzir:
-
Table possui uma instância de Driver, para executar funções sobre o banco de dados.
-
Row possui uma instância de Table. Não existe nada relacionado à persistência de dados em Row, tudo é feito a partir de Table.
-
Row normalmente não precisa de acesso direto ao Driver, então não há uma instância deste dentro dele. A única exceção é que após uma inserção, Row precisa da informação do último id inserido no banco, para poder setar campos com auto incremento. Para isso, ele faz uso do objeto Driver da tabela Table que faz referência.
-
Table não precisa conhecer Row, pois ela recebe apenas arrays de dados para as operações. A única exceção à regra é quando retornamos dados do banco. Quando isso ocorre, Table instancia objetos Row. Também é possível criar um novo objeto Row a partir de Table.
Observe que apesar de fazer sentido o uso de composição/agregação (afinal, uma tabela possui linhas), isso não é feito.
Por quê?
Porque o objeto TDG (Table Data Gateway) (no caso, Table) armazena apenas dados sobre a ESTRUTURA da tabela, enquanto que o objeto RDG (Row Data Gateway) armazena os dados propriamente ditos.
Já vi implementações que adicionam os objetos RDG em um array dentro do objeto TDG. Eu acho meio ilógico isso pelo simples fato de que dificilmente teremos TODAS as linhas da tabela buscadas de uma vez só, então não faz sentido ficar colocando dados esparsos dentro do objeto TDG. Mesmo se o fizéssemos, o objeto TDG ficaria extremamente pesado.
Além disso, pela definição de ambos, TDG é stateless (não possui "estados"), enquanto RDG é stateful (possui "estados", ou seja, armazena informações que podem variar).
Chega de lero-lero e vamos ao código.
Driver.php (487 linhas, inclusos os comentários):
<?php
class Driver {
const INSERT = 0;
const UPDATE = 1;
const DELETE = 2;
const SELECT = 3;
const FETCH_ASSOC = 10;
const FETCH_NUM = 20;
const FETCH_ARRAY = 30;
const FETCH_OBJ = 40;
/**
* Armazena o próximo statement a ser executado
* @var MySQLi_stmt
*/
private $stmt;
/**
* Armazena o objeto de conexão com o banco de dados.
* @var MySQLi
*/
private $connection;
/**
* Armazena a operação atualmente sendo executada.
* @var int : veja as constantes de classe
*/
private $currOp;
/**
* O modo de fetch dos dados no banco.
* @var int : veja as constantes de classe
*/
private $fetchMode = self::FETCH_ASSOC;
/**
* Armazena os nomes das colunas de tabelas
* @var array
*/
private $keys = array();
/**
* Armazena os valores das colunas de tabelas
* @var array
*/
private $values = array();
/**
* Construtor.
*
* @param MySQLi $conn : um objeto MySQLi para a conexão com o banco
*/
public function __construct(MySQLi $conn) {
$this->connect($conn);
}
/**
* Seta o modo de fetch para o objeto.
*
* @param int $mode : veja as constantes de classe
* @return Driver : fluent interface
*/
public function setFetchMode($mode) {
$this->fetchMode = $mode;
return $this;
}
/**
* Conecta-se ao banco de dados através de um objeto MySQLi.
*
* @param MySQLi $conn : o objeto para conexão com o banco
* @return Driver : fluent interface
*/
public function connect(MySQLi $conn = null) {
if(!$this->isConnected() && $conn === null) {
throw new Exception('Conexão com o banco de dados fechada!
Precisamos de um conector.');
}
if(!$this->isConnected()) {
$this->connection = $conn;
}
return $this;
}
/**
* Retorna o objeto de conexão com o banco de dados.
*
* @return MySQLi
*/
public function getConnection() {
$this->connect();
return $this->connection;
}
/**
* Inicia uma transação, deligando o autocommit.
*
* @return Driver : fluent interface
*/
public function begin() {
$this->connection->autocommit(false);
return $this;
}
/**
* Cancela a transação em andamento.
*
* @return Driver : fluent interface
*/
public function rollback() {
$this->connection->rollback();
return $this;
}
/**
* Salva a transação em andamento.
*
* @return Driver : fluent interface
*/
public function commit() {
$this->connection->commit();
return $this;
}
/**
* Verifica se o Driver está conectado ao banco.
*
* @return bool
*/
public function isConnected() {
return $this->connection instanceof MySQLi;
}
/**
* Fecha a conexão com o banco de dados.
*
* @return void
*/
public function close() {
if($this->isConnected()) {
$this->connection->close();
unset($this->connection);
$this->connection = null;
}
}
/**
* Prepara um statement.
*
* @param string $sql : um statement SQL válido
* @return MySQLi_stmt
*/
public function prepare($sql) {
return ($this->stmt = $this->getConnection()->prepare($sql));
}
/**
* Insere dados em uma tabela.
*
* @param string $tableName: o nome da tabela
* @param array $data : os dados para inserir
* @return int : o número de linhas inseridas com sucesso
*/
public function insert($tableName, array $data) {
$this->currOp = self::INSERT;
$cols = array_keys($data);
$sql = 'INSERT INTO ' . $tableName
. '(' . join(', ', $cols ) . ')'
. ' VALUES (' . join(', ', array_fill(0, count($data), '?')) . ')';
$this->stmt = $this->connection->prepare($sql);
$this->bindParams($data);
$this->execute();
return $this->stmt->affected_rows;
}
/**
* Atualiza dados em uma tabela, com base numa condicional.
*
* @param string $tableName: o nome da tabela
* @param array $data : os dados para inserir
* @param string $cond : uma condicional colocada em uma cláusula WHERE
* @param array $condParams : parâmetros da condição, caso a mesma
* faça parte de um prepared statement
* @return int : o número de linhas atualizadas
*/
public function update($tableName, array $data, $cond, array $condParams = array()) {
$this->currOp = self::UPDATE;
$cols = array_keys($data);
$set = array();
foreach($data as $key => $value) {
$set[] = $key . ' = ?';
}
$sql = 'UPDATE ' . $tableName
. ' SET ' . join(', ', $set)
. ' WHERE ' . $cond;
$params = array_merge($data, $condParams);
$this->stmt = $this->connection->prepare($sql);
$this->bindParams($params);
$this->execute();
return $this->stmt->affected_rows;
}
/**
* Deleta dados de uma tabela, com base numa condicional.
*
* @param string $tableName: o nome da tabela
* @param string $cond : uma condicional colocada em uma cláusula WHERE
* @param array $condParams : parâmetros da condição, caso a mesma
* faça parte de um prepared statement
* @return int : o número de linhas deletadas
*/
public function delete($tableName, $cond, array $condParams = array()) {
$this->currOp = self::DELETE;
$sql = 'DELETE FROM ' . $tableName
. ' WHERE ' . $cond;
$this->stmt = $this->connection->prepare($sql);
$this->bindParams($condParams);
$this->execute();
return $this->stmt->affected_rows;
}
/**
* Realiza uma seleção de dados na tabela, guiada por alguns parâmetros.
*
* @param string $tableName: o nome da tabela
* @param string $order : como a busca deve ser ordenada, na forma
* {<col_name> ASC | DESC}, {<col_name> ASC | DESC}, ...
* @param int $count : o número de linhas para selecionar
* @param int $offset : a linha a partir do qual começar a busca (0-based)
* @param array $fields : os campos da tabela a selecionar.
Caso este parâmetro não estaja setado, utilizamos o SQL wildcard ''
* @return boolean
*/
public function select($tableName, $order = null, $count = null, $offset = null, $fields = array()) {
$this->currOp = self::SELECT;
$fields = (array) $fields;
$selFields = array();
if(empty($fields)) {
$selFields[] = '*';
} else {
foreach($fields as $alias => $colName) {
if(is_string($alias)) {
$selFields[] = $colName . ' AS ' . $alias;
} else {
$selFields[] = $colName;
}
}
}
$sql = 'SELECT ' . join(', ', $selFields)
. ' FROM ' . $tableName;
if($order !== null) {
$sql .= ' ORDER BY ' . $order;
}
if(($count === null || $count === 0) && $offset !== null) {
$count = PHP_INT_MAX;
}
if((int) $count > 0) {
if($offset === null) {
$offset = 0;
}
$sql .= ' LITMIT ' . $count . ' OFFSET ' . $offset;
}
$this->stmt = $this->connection->prepare($sql);
return $this->execute();
}
/**
* Executa uma query.
*
* @param string $sqlStmt : um statement ou prepared statement válidos
* @param array $params : os parâmetros caso $sqlStmt seja um prepared statement
* @return bool
*/
public function query($sqlStmt, array $params = array()) {
if(!is_string($sqlStmt)) {
throw new Exception('O parametro #1 do método Driver::query() deve ser uma string');
}
if(preg_match('/^select/i', $sqlStmt)) {
$this->currOp = self::SELECT;
} else if(preg_match('/^insert/i', $sqlStmt)) {
$this->currOp = self::INSERT;
} else if(preg_match('/^update/i', $sqlStmt)) {
$this->currOp = self::UPDATE;
} else {
$this->currOp = self::DELETE;
}
$this->stmt = $this->connection->prepare($sqlStmt);
if($this->stmt === false) {
throw new Exception($this->connection->error);
}
return $this->execute($params);
}
/**
* Retorna todas as linhas buscadas a partir de um SELECT statement.
*
* @return array
*/
public function fetchAll() {
$data = array();
while($row = $this->doFetch()) {
$data[] = $row;
}
return $data;
}
/**
* Retorna a primeira linha buscada a partir de um SELECT statement.
*
* @return Row|null
*/
public function fetchOne() {
return $this->doFetch();
}
/**
* Faz de fato a busca dos dados no banco.
*
* @return Row|null
*/
private function doFetch() {
if($this->currOp !== self::SELECT) {
throw new Exception('Fetch só pode ser executado com operações do tipo SELECT');
}
// Faz o fetch no banco de dados...
$ret = $this->stmt->fetch();
// Caso tenhamos chegado ao fim...
if(!$ret) {
// O statement agora passa a apontar para o início da tabela...
$this->stmt->reset();
return null;
}
// Faz uma cópia dos valores, pois eles são referências
// Caso não façamos isso, ao fazer um novo fetch, os dados são sobrescritos
$values = array();
foreach($this->values as $val) {
$values[] = $val;
}
$row = null;
switch($this->fetchMode) {
case self::FETCH_NUM:
return $values;
case self::FETCH_ASSOC:
return array_combine($this->keys, $values);
case self::FETCH_ARRAY:
$assoc = array_combne($this->keys, $values);
return array_merge($this->keys, $values);
case self::FETCH_OBJ:
return (object) array_combine($this->keys, $values);
default:
throw new Exception('Modo de fetch inválido');
}
}
/**
* Executa o statement atual
*
* @param array $params : parâmetros de statement para binding
* @return bool
*/
private function execute(array $params = array()) {
if($this->stmt === null) {
return null;
}
$this->bindParams($params);
$ret = $this->stmt->execute();
if($ret === false) {
throw new Exception('Error # ' . $this->stmt->errno .
': ' . $this->stmt->error);
}
// Obtenção dos metadados do select, tais como as colunas retornadas
$metaData = $this->stmt->result_metadata();
if($this->stmt->errno) {
throw new Exception('Erro na obtenção dos metadados:' . $this->stmt->error);
}
// Metadados só são retornados em operações de SELECT
if($metaData !== false) {
// Definindo as chaves do array (nome dos campos da tabela)
$this->keys = array();
foreach($metaData->fetch_fields() as $col) {
$this->keys[] = $col->name;
}
// Criamos um array do tamanho do array $keys, com valores NULL
$aux = array_fill(0, count($this->keys), null);
$refs = array();
// MySQLi_stmt::bind_result precisa de referências para funcionar
foreach($aux as $i => &$f) {
// Dessa forma $refs passa a referenciar $aux
$refs[$i] = &$f;
}
$this->stmt->store_result();
// Associa os resultados ao array $values
call_user_func_array(
array(
$this->stmt, 'bind_result'
),
$refs
);
$this->values = $aux;
}
return $ret;
}
/**
* Faz o binding de parâmetros para o statement atual.
*
* @param array $params : os parâmetros de binding
* @return bool
*/
private function bindParams(array $params) {
if(empty($params)) {
return;
}
// Precisamos indicar o tipod os parâmetros de binding...
$bindStr = '';
foreach($params as $col => $value) {
// São 3 possíveis...
if(is_int($value)) {
$bindStr .= 'i';
} elseif(is_float($value)){
$bindStr .= 'd';
} else {
$bindStr .= 's';
}
}
// Adiciona a string para indicar os formatos dos dados
array_unshift($params, $bindStr);
// Para bind_param funcionar, ele precisa de referências.
// A única maneira de associar referências de arrays em PHP é a abaixo
$stmtParams = array();
foreach($params as $key => &$value) {
$stmtParams[$key] = &$value;
}
// Chamamos a função MySQLi_stmt::bind_param com os argumentos apropriados
$ret = call_user_func_array(
array($this->stmt, 'bind_param'),
$params
);
if($ret === false) {
throw new Exception('Erro ao executar bind_param no statement: ', $this->stmt->error);
}
}
/**
* Retorna o último ID auto_increment inserido no banco, na sessão atual.
*
* @return int
*/
public function lastInsertId() {
return $this->connection->insert_id;
}
}
Table.php (303 linhas, com comentários)
<?php
class Table {
/**
* O nome da tabela
* @var string
*/
private $name;
/**
* O driver MySQLi para a execução dos statements.
* @var MySQLi
*/
private $driver;
/**
* O statement a ser executado no banco de dados.
* @var MySQLi_Stmt
*/
private $stmt;
/**
* Mapa dos campos da tabela.
* Cada campo é da seguinte forma:
* array (
* name => 'nome do campo',
* type => 'tipo SQL do campo',
* is_primary => 'se o campo é parte da chave primária da tabela (boolean)'
* )
*
* Por simplicidade, não consideraremos tabelas com chaves compostas.
* @var array
*/
private $colMap = array();
/**
* Armazena os nomes dos campos da tabela.
* É obtido a partir de $colMap.
* @var array
*/
private $cols = array();
/**
* O nome da coluna que é a chave primária da tabela
* @var string
*/
private $idColName;
/**
* Checar ou não a integridade dos dados ao criar objetos Row desta tabela.
* Desabilitar a checagem de integridade é útil para JOINs.
*/
private $integrityCheck = true;
/**
* Construtor.
*
* @param string $name: o nome da tabela
* @param Driver $driver : o driver para o banco de dados
* @param array $map : um "mapa" para os campos da tabela, da forma:
* array (
* name => 'nome do campo',
* type => 'tipo SQL do campo',
* is_primary => 'se o campo é parte da chave primária da tabela (boolean)'
* )
*/
public function __construct($name, Driver $driver, array $map) {
$this->setName($name);
$this->setDriver($driver);
$this->setColMap($map);
}
private function setName($name) {
$this->name = $name;
}
public function getName() {
return $this->name;
}
public function setDriver(Driver $driver) {
$this->driver = $driver;
return $this;
}
public function getDriver() {
return $this->driver;
}
/**
* Seta um mapa de colunas da tabela.
* Adicionalmente, com base no mapa, seta o array de colunas da tabela.
*
* @param array $map : um "mapa" para os campos da tabela, da forma:
* array (
* name => 'nome do campo',
* type => 'tipo SQL do campo',
* is_primary => 'se o campo é parte da chave primária da tabela (boolean)'
* )
* @throws Exception : caso nenhuma chave primária seja informada
*/
private function setColMap(array $map) {
$this->colMap = $map;
$hasPrimary = false;
// Identifica o campo de ID da tabela
foreach($map as $column){
$this->cols[] = strtolower($column['name']);
if(array_key_exists('is_primary', $column) &&
(bool) $column['is_primary'] == true) {
$this->idColName = $column['name'];
$hasPrimary = true;
}
}
if($hasPrimary === false) {
throw new Exception(sprintf('Não é possível criar a tabela "%s" sem uma chave primária!', $this->name));
}
}
/**
* Setando a checagem de integridade para false, é possível utilizar
* o método createRow com dados não pertencentes à tabela. (Ex.: JOINs e alias).
*
* @param bool $opt
* @return Table : fluent interface
*/
public function setIntegrityCheck($opt) {
$this->integrityCheck = (bool) $opt;
return $this;
}
/**
* Retorna o nome da coluna que é a chave primária da tabela;
*
* @return string
*/
public function getIdColName() {
return $this->idColName;
}
/**
* Retorna um array contendo os nomes das colunas da tabela.
*
* @return array
*/
public function getCols() {
return $this->cols;
}
/**
* Salva os dados em $data no banco de dados.
* A decisão por inserção ou atualização é feita automaticamente.
*
* @param $data : os dados a inserir ou atualizar
* @return int : o número de linhas afetadas
*/
public function save(array $data) {
if(!array_key_exists($this->idColName, $data)
|| $data[$this->idColName] === null) {
return $this->insert($data);
} else {
return $this->update($data);
}
}
/**
* Insere os dados em $data no banco de dados.
*
* @param $data : os dados a inserir
* @return int : o número de linhas inseridas
*/
public function insert(array $data) {
$data = $this->intersectData($data);
return $this->driver->insert($this->getName(), $data);
}
/**
* Atualiza os dados em $data no banco de dados.
*
* @param $data : os dados para atualizar
* @param $cond : uma condicional para a atualização
* @param array $condParams : caso a condição seja parte de um
* prepared statement, estes são os parâmetros
* @return int : o número de linhas atualizadas
*/
public function update(array $data, $cond, array $condParams = array()) {
$data = $this->intersectData($data);
return $this->driver->update($this->getName(),$data, $cond, $condParams);
}
/**
* Deleta dados do banco.
*
* @param $cond : uma condicional para a atualização
* @param array $condParams : caso a condição seja parte de um
* prepared statement, estes são os parâmetros
* @return int : o número de linhas deletadas
*/
public function delete($cond, array $condParams = array()) {
return $this->driver->delete($this->getName(), $cond, $condParams);
}
/**
* Retorna dados pela chave primária.
*
* @param mixed $id : o valor da chave primária do registro desejado
* @return Row|null
*/
public function getById($id) {
$sql = 'SELECT * FROM ' . $this->getName()
. ' WHERE ' . $this->idColName . ' = ?';
$params = array($this->idColName => $id);
$this->driver->query($sql, $params);
$data = $this->driver->fetchOne();
if($data !== null) {
return $this->doCreateRow($data, true);
} else {
return null;
}
}
/**
* Retorna um conjunto de linhas da tabela.
*
* @param string|array|null $fields : os campos da tabela a serem retornados.
Caso seja nulo, utilizamos o SQL wildcard '', que retornará todos os campos.
@param string $order : a ordenação da busca "{<nome_campo><ASC|DESC>}"
* @param int count : a quantidade de linhas a retornar
* @param int offset : a linha inicial a partir da qual começar a buscar (0-based)
* @return array
*/
public function getAll($fields = null, $order = null, $count = null, $offset = null) {
$this->driver->select($this->getName(), $order, $count, $offset, $fields);
$ret = array();
$result = $this->driver->fetchAll();
foreach($result as $rowData) {
$ret[] = $this->doCreateRow($rowData, true);
}
return $ret;
}
/**
* Método público para a criação de um objeto Row a partir de $data.
*
* @param $data : os dados para a criação do objeto
* @return Row
*/
public function createRow(array $data = array()) {
return $this->doCreateRow($data, false);
}
/**
* Internamente, é possível configurar os dados como 'stored',
* o que indica que os dados são proveniente do banco, logo, são válidos.
* Em chamadas externas, é não é possível garantir isso.
*
* @param array $data : os dados para a criação do objeot
* @param bool $stored : indica se os dados são ou não provenientes do banco.
*/
private function doCreateRow(array $data, $stored) {
if($this->integrityCheck === false && !empty($data)) {
// Se a checagem de integridade está desabilitada, removemos a permissão de escrita
$newRow = new Row($this, array(
'readOnly' => true,
'stored' => $stored,
'data' => $data
));
} else {
$cols = $this->cols;
$defaults = array_combine($cols, array_fill(0, count($cols), null));
$rowData = array_intersect_key(array_replace($defaults, $data),$defaults);
$newRow = new Row($this, array(
'readOnly' => false,
'stored' => $stored,
'data' => $rowData
));
}
return $newRow;
}
/**
* Para operações de inserção e atualização, temos que garantir que
* entre os dados não haja campos inexistentes na tabela,
* o que geraria um erro na execução do statement.
*
* @param array $data : os dados para a operação
* @return array : os dados filtrados, contendo apenas campos da tabela
*/
private function intersectData(array $data) {
$dataCols = array_change_key_case($data, CASE_LOWER);
$tableCols = array();
foreach($this->colMap as $column) {
$tableCols[strtolower($column['name'])] = $column['name'];
}
return array_intersect_key($dataCols, $tableCols);
}
}
Row.php (375 linhas, com comentários)
<?php
class Row implements IteratorAggregate, ArrayAccess {
/**
* Armazena o objeto Table ao qual este objeto pertence.
* @var Table
*/
private $table;
/**
* Armazena os dados do objeto.
* @var array
*/
private $data = array();
/**
* Armazena os dados "limpos" do objeto, ou seja,
* os provenientes de uma operação SELECT no banco de dados.
* Ao utilizar set or unset, apenas a propriedade $data é alterada,
* $cleanData mantém-se da mesma forma até a execução de um refresh.
* @var array
*/
private $cleanData = array();
/**
* Array de flags indicando de o campo foi alterado.
* Útil para fazer updates sob demanda, economizando
* um pouco no acesso ao banco de dados.
* @var array
*/
private $dirtyData = array();
/**
* Indica se o objeto Row é somente leitura
* @var bool
*/
private $readOnly = false;
/**
* Construtor.
* Aqui define-se a tabela à qual o objeto Row se refere.
* Além disso, definimos também os dados existentes no mesmo.
* Os únicos índices permitidos para os dados do objeto são
* os que são informados no construtor.
*
* @param Table $table : o objeto Table ao qual este objeto Row pertence.
* @param array $info : informações sobre a criação do objeto Row.
*
* Parâmetros válidos para $info:
* - data => (array) os dados das colunas neste objeto Row
* - stored => (boolean) se os dados são provindos do banco de dados ou não
* - readOnly => (boolean) se é permitido ou não alterar os dados desta linha
*/
public function __construct(Table $table, array $info){
$this->setTable($table);
$this->setUp($info);
}
/**
* Checa as informações passadas ao construtor.
* É obrigatório informar ao menos os dados do objeto Row.
*
* @return void
* @throws Exception : caso não exista o índice 'data' em $info
*/
private function checkInfo(array $info){
if(!array_key_exists('data', $info)) {
throw new Exception('Nenhum dado para ser armazenado no objeto Row.');
}
}
/**
* Configura o objeto Row.
*
* @param array $info : as informações sobre o objeto
* @return void
*/
private function setUp(array $info) {
$this->checkInfo($info);
$this->data = (array) $info['data'];
foreach($info as $k => $val) {
switch($k) {
case 'readOnly':
$this->readOnly = (bool) $val;
break;
case 'stored':
if($val === true) {
$this->cleanData = $this->data;
}
break;
default:
// sem ação...
}
}
}
/**
* Seta uma tabela para o objeto Row.
*
* @param Table $table
* @return Row : fluent interface
*/
public function setTable(Table $table) {
$this->table = $table;
// Fluent interface
return $this;
}
/**
* Retorna o objeto Table deste objeto.
*
* @return Table
*/
public function getTable(){
return $this->table;
}
/**
* Seta os dados para o objeto Row.
*
* @param array $data
* @return void
*/
private function setData(array $data) {
$this->data = $data;
}
/**
* Retorna os dados deste objeto Row.
*
* @return array
*/
public function getData() {
return $this->data;
}
/**
* Retorna se o objeto Row é somenta para leitura
*
* @return bool
*/
public function isReadOnly() {
return $this->readOnly;
}
/**
* Converte um objeto Row em um array, retornando apenas seus campos.
*
* @return array
*/
public function toArray() {
return $this->getData();
}
/**
* Retorna a chave primária do objeto Row.
*
* @return mixed
*/
protected function getPk($useDirty = true) {
$pkCol = $this->table->getIdColName();
$array = $useDirty === true ? $this->data : $this->cleanData;
return array_key_exists($pkCol, $array) ? $array[$pkCol] : null;
}
/**
* Salva os dados do objeto atual no banco de dados (insere ou atualiza).
*
* @return int : o número de linhas afetadas.
*/
public function save() {
if($this->isReadOnly()) {
throw new Exception('Este objeto Db_Table_Row está marcado como somente-leitura.');
}
if(empty($this->cleanData)) {
return $this->doInsert();
} else {
return $this->doUpdate();
}
}
/**
* Faz a inserção no banco de dados dos dados no objeto Row.
*
* @return int : o número de linhas inseridas
*/
private function doInsert() {
$data = array_intersect_key($this->data, $this->dirtyData);
$result = $this->getTable()->insert($data);
$pk = $this->getTable()->getIdColName();
if(!array_key_exists($pk, $data) || $data[$pk] === null) {
$this->data[$pk] = $this->getTable()->getDriver()->lastInsertId();
}
$this->refresh();
return $result;
}
/**
* Faz a atualização no banco de dados dos dados no objeto Row.
*
* @return int : o número de linhas atualizadas
*/
private function doUpdate() {
$diffData = array_intersect_key($this->data, $this->dirtyData);
if(!empty($diffData)) {
$pk = $this->getPk(false);
if($pk === null) {
throw new Exception('Impossível atualizar uma linha sem chave primária!');
}
$pkCol = $this->getTable()->getIdColName();
$result = $this->getTable()->update($diffData, $pkCol . ' = ?', array($pk));
$this->refresh();
return $result;
}
return 0;
}
/**
* Atualiza os dados da linha após uma operação no banco de dados.
*
* @return void
*/
private function refresh() {
$pk = $this->getPk(false);
if($pk !== null) {
$row = $this->getTable()->getById($pk);
if($row === null) {
throw new Exception('Não foi possível atualizar a linha da tabela.
Erro ao buscá-la no banco de dados');
}
$this->data = $row->toArray();
$this->cleanData = $this->data;
$this->dirtyData = array();
}
}
/**
* Remove os dados correspondente ao objeto Row do banco de dados.
*
* @return int: o número de linhas afetadas.
*/
public function delete() {
if($this->readOnly) {
throw new Exception('Impossível remover uma linha somente-leitura!');
}
$pk = $this->getPk();
if($pk === null) {
throw new Exception('Impossível remover uma linha sem chave primária!');
}
$pkCol = $this->getTable()->getIdColName();
$result = $this->getTable()->delete($pkCol . ' = ?', array($pk));
$this->data = array_combine(
array_keys($this->data),
array_fill(0, count($this->data), null)
);
return $result;
}
/**
* Seta os dados do objeto a partir de um array.
*
* @param array $data
* @return Row : fluent interface
*/
public function setFromArray(array $data) {
foreach($data as $key => $val) {
$this->set($key, $val);
}
return $this;
}
/**
* Seta um valor para uma coluna do banco de dados.
*
* @param string $column : o nome da coluna a ser alterada
* @param mixed $value: o novo valor da coluna
* @return Row : fluent interface
* @throws Exception : caso a coluna não exista
*/
public function set($column, $value) {
// Irá lançar uma exceção caso a coluna não exista...
$this->verifyColumn($column);
if($value !== $this->data[$column]) {
$this->data[$column] = $value;
$this->dirtyData[$column] = true;
}
// Fluent interface, permite o encadeamento de métodos: $row->set('bla', 'foo')->set('baz', 'bazzinga');
return $this;
}
/**
* Retorna o valor de uma coluna.
*
* @param string $column : o nome da coluna
* @return mixed : o valor da coluna
* @throws Exception : caso a coluna não exista
*/
public function get($column) {
// Irá lançar uma exceção caso a coluna não exista...
$this->verifyColumn($column);
return $this->data[$column];
}
/**
* Verifica se a coluna existe no objeto.
*
* @param sting $column : o nome da coluna
* @return boolean
* @throws Exception : caso a coluna não exista
*/
public function has($column) {
return array_key_exists($column, $this->data);
}
/**
* Verifica se a coluna existe, lançando uma exceção caso não exista.
*
* @param $column : o nome da coluna
* @throws Exception : caso a coluna não exista
*/
private function verifyColumn($column) {
if(!$this->has($column)) {
throw new Exception(sprintf('A coluna "%s" não existe neste objeto Row!', $column));
}
}
/**
* @see ArrayAccess::offsetSet()
*/
public function offsetSet($offset, $value) {
$this->set($column, $value);
}
/**
* @see ArrayAccess::offsetGet()
*/
public function offsetGet($offset) {
return $this->get($column);
}
/**
* @see ArrayAccess::offsetExists()
*/
public function offsetExists($offset) {
return $this->has($column);
}
/**
* @see ArrayAccess::offsetUnset()
*/
public function offsetUnset($offset) {
$this->verifyColumn($column);
$this->set(offset, null);
}
/**
* @see IteratorAggregate::getIterator()
*/
public function getIterator() {
return new ArrayIterator($this->data);
}
}
Para dar uma brincada, utilize este banco de dados:
-- --------------------------------------------------------
--
-- Estrutura da tabela clients
--
CREATE TABLE IF NOT EXISTS `clients` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(30) COLLATE latin1_bin DEFAULT NULL,
`registered_by` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_reg_client_fk` (`registered_by`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin AUTO_INCREMENT=6 ;
--
-- Extraindo dados da tabela clients
--
INSERT INTO clients (id, name, registered_by) VALUES
(1, 'Seu Madruga', 1),
(2, 'Chapolin', 2),
(3, 'Bob Esponja', 1),
(4, 'Lula Molusco', 1),
(5, 'Patrick Estrela', 3);
-- --------------------------------------------------------
--
-- Estrutura da tabela users
--
CREATE TABLE IF NOT EXISTS `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(30) COLLATE latin1_bin DEFAULT NULL,
`login` varchar(30) COLLATE latin1_bin DEFAULT NULL,
`password` char(32) COLLATE latin1_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin AUTO_INCREMENT=22 ;
--
-- Extraindo dados da tabela users
--
INSERT INTO users (id, name, login, password) VALUES
(1, 'Zezinho', 'zezinho_123', '588029867c884e84dfbdb78c5ec6f3a4'),
(2, 'Manuelzinho', 'manololoko', 'd8578edf8458ce06fbc5bb76a58c5ca4'),
(3, 'Luluzinha', 'lugatinha', '912ec803b2ce49e4a541068d495ab570');
--
-- Restrições para as tabelas dumpadas
--
--
-- Restrições para a tabela clients
--
ALTER TABLE `clients`
ADD CONSTRAINT `user_reg_client_fk` FOREIGN KEY (`registered_by`) REFERENCES `users` (`id`);
Exemplo:
<?php
require_once 'Driver.php';
require_once 'Table.php';
require_once 'Row.php';
$connector = new MySQLi('SEU_SERVER', 'SEU_USUARIO', 'SUA_SENHA', 'SEU_DB');
$driver = new Driver($connector);
$table = new Table('users', $driver, array(
array('name' => 'id', 'type' => 'integer', 'is_primary' => true),
array('name' => 'name', 'type' => 'varchar'),
array('name' => 'login', 'type' => 'varchar'),
array('name' => 'password', 'type' => 'char'),
));
O código acima é a base pra brincar com a tabela usuários. Note que o "mapa" de colunas não é muito bem detalhado e nem precisa.
Inserir uma nova linha
$row = $table->createRow();
$row->set('name', 'Juquinha');
$row->set('login', 'jukaloko');
$row->set('senha', md5('jjjj'));
$row->save(); // Irá inserir os dados na tabela
Atualizando dados
$row = $table->getById(1);
$row->set('name', 'Mariazinha');
$row->set('login', 'mary_gatinha_cam_21');
$row->set('senha', md5('mary'));
$row->save(); // Irá atualizar os dados na tabela
Removendo dados
$row = $table->getById(1);
$row->delete();
A estrutura é bem flexível. Desativando o integrity check de Table, é possível criar Rows a partir de JOINS:
$sql = 'select c.name as client_name, u.name as user_name
from users as u
join clients as c
on c.registered_by = u.id
WHERE u.name = ?';
$driver->query($sql, array('Zezinho'));
$data = $driver->fetchAll();
$row = $table->setIntegrityCheck(false)->createRow($data);
var_dump($row);
Pois é, tudo junto dá aproximadamente 1000 linhas de código, e olha que eu simplifiquei o máximo possível.
O que eu realmente uso para as minhas aplicações tem no mínimo umas 4000, isso porque só tem suporte pra MySQL no momento, pretendo incluir suporte ao Postgre assim que tiver um tempinho.
Não tive tempo de testar tudo, pode haver algum erro ou outro aí. Se alguém encontrar, por favor, me avise. Eu desconfio que vai dar pau na se você inventar de alterar e atualizar o ID da linha.
Provavelmente você não vai entender de primeira, mas é assim mesmo, volte e leia quantas vezes for necessário, pergunte, só assim você vai conseguir entender.
[]s
A ideia é criar Tables e Rows genéricos, que são definidos em tempo de execução. Não é difícil encontrar exemplos por aí que te dizem se tratar de um RDG, mas na verdade, é um DTO (VO) ou DataMapper, dos quais eu pessoalmente não sou muito fã pois eles amarram sua implementação. Para cada entidade na minha aplicação eu preciso criar uma classe Mapper diferente. Se somar isso ao fato de ao utilizar MVC eu já ter que criar no mínimo 1 Model e alguns Controllers, além de ter alguns arquivos Views, cada inserção de entidade no sistema é um pé no saco.
Penso o mesmo.
De forma resumida, o DataMapper viola o MVC, por isso prefiro não usá-lo.
Depois de ler este excelente post do Henrique, eu percebi que o meu Model está uma m*****!
Puuts, gostei bastante,meus parabéns. Só me tira uma dǘvida: Este é o uso do Row Data Gateway e Table Data Gateway?
Vou pegar este exemplo para estudar!
Este é o uso do Row Data Gateway e Table Data Gateway?
Sim, ambos só existem em conjunto...
Na verdade, até pode haver o TDG sem o RDG, mas nunca vi fazerem isso...
>
Sim, ambos só existem em conjunto...
Na verdade, até pode haver o TDG sem o RDG, mas nunca vi fazerem isso...
Como assim só existem em conjunto? Eu pensava até agora que eles eram aplicados de forma separado,ou seja,posso usar um RDG quanto o TDG, sem a junção dos dois.
Agora eu fiquei confuso, além de estar estudando o conceito errado!
Veja só como está meu sistema atualmente.
GatewayDataTransfer
<?
final class GatewayDataTransfer extends FConnection implements IGateway{
final public function crud($cond, FCrud $crud, array $data){
switch(strtolower($cond)):
case 'insert':
return parent::execQuery($crud->insert($data));
break;
case 'update':
return parent::execQuery($crud->update($data));
break;
case 'delete':
return parent::execQuery($crud->delete($data));
break;
case 'select_all':
return parent::fetchAll($crud->selectAll($data));
break;
case 'select_one':
return parent::fetchOne($crud->selectOne($data));
break;
default:
MessageHelper::getErrorException('Operação inválida.');
break;
endswitch;
}
}
?>
UserGateway
<?
final class UserGateway extends Model{
private static $table = 'users';
public static function dataSelectAll($start,$limit){
$data = array(
"table" => self::$table,
"fields" => array("*"),
"start" => $start,
"limit" => $limit,
"orderBy" => "nome",
"direction" => "ASC"
);
return parent::getGateway()->crud('SELECT_ALL', new FCrud(), $data);
}
public static function dataSelectOne(UserBean $bean){
$data = array(
"table" => self::$table,
"fields" => array("*"),
"where" => "idUser = " .$bean->getId()
);
return parent::getGateway()->crud('SELECT_ONE', new FCrud(), $data);
}
public static function dataInsert(UserBean $bean){
$data = array(
"table" => self::$table,
"values" => array(
"nome" => $bean->getNome(),
"email" => $bean->getEmail()
));
parent::getGateway()->crud('INSERT', new FCrud(), $data);
}
public static function dataUpdate(UserBean $bean){
$data = array(
"table" => self::$table,
"values" => array(
"nome" => $bean->getNome(),
"email" => $bean->getEmail()
),
"where" => "idUser = " .$bean->getId()
);
parent::getGateway()->crud('UPDATE', new FCrud(), $data);
}
public static function dataDelete(UserBean $bean){
$data = array(
"table" => self::$table,
"where" => "idUser = " .$bean->getId()
);
parent::getGateway()->crud('DELETE', new FCrud(), $data);
}
}
?>
UserController
<?
class User extends Controller{
private static $bean;
function __construct(){
$this->_title = 'Usuarios';
self::$bean = parent::getBean('UserBean');
}
public static function run(){
return parent::setRun();
}
public function index(){
$users = UserGateway::dataSelectAll(0,10);
$i = 0;
while($i <= count($users)):
$data['data'] = $users;
$i++;
endwhile;
parent::_show(new View('index', $data));
}
public function select(){
self::$bean->setId(28);
$data = UserGateway::dataSelectOne(self::$bean);
parent::_show(new View('select', $data));
}
public function insert(){
self::$bean->setNome('Guilherme Pereira Nogueira');
self::$bean->setEmail('guilherme.std1@gmail.com');
UserGateway::dataInsert(self::$bean);
parent::_show(new View('insert', null));
}
public function update(){
self::$bean->setId(27);
self::$bean->setNome('Guilherme');
self::$bean->setEmail('guilherme.std1@gmail.com');
UserGateway::dataUpdate(self::$bean);
parent::_show(new View('update', null));
}
public function delete(){
self::$bean->setId(27);
UserGateway::dataDelete(self::$bean);
parent::_show(new View('delete', null));
}
}
?>
Sei que coloquei uma montueira de código, mas analisa bem e me diz: Eu tentei implementar um Gateway Data Transfer,pois pelo que li deste padrão,ele faz uma composição de objeto no Model, porém isto também pode parecer um Table Data Gateway, não? Veja que, no meu UserGateway, eu faço uma composição. O que tem de errado aí, o que pode melhorar? Ao seu ver, minha implementação está muito amarrada? Não coloquei outras classes por que não precisa, só quis ser mais objetivo neste post.
Como assim só existem em conjunto? Eu pensava até agora que eles eram aplicados de forma separado,ou seja,posso usar um RDG quanto o TDG, sem a junção dos dois.
Pode existir um TDG sem um RDG, mas o oposto não é verdade.
Por quê?
Porque um RDG faz uso de um objeto TDG para acessar o banco de dados.
Se você colocar a função de persistência dentro do RDG ele já não é mais um RDG, é um Active Record.
Observe que se eu retirar o RDG do exemplo, eu ainda posso utilizar o TDG, retornando somente arrays...
Algumas observações sobre o seu código:
GatewayDataTransfer é final, logo, você não precisa declarar os métodos como finais...
Outra coisa:
case 'insert':
return parent::execQuery($crud->insert($data));
break;
case 'update':
return parent::execQuery($crud->update($data));
break;
case 'delete':
return parent::execQuery($crud->delete($data));
break;
case 'select_all':
return parent::fetchAll($crud->selectAll($data));
break;
case 'select_one':
return parent::fetchOne($crud->selectOne($data));
break;
default:
MessageHelper::getErrorException('Operação inválida.');
break;
Tem certeza que essas são as únicas operações possíveis sobre o banco de dados?
Caso apareça mais alguma você terá que ir e editar esse switch... Por isso, sua implementação está "amarrada".
UserGateway é o que eu sou totalmente contra. As operações sobre o banco de dados são basicamente as mesmas para qualquer tabela. O que muda é somente a estrutura da mesma.
Se você usa MVC, precisa de 1 Model, 1 ou mais Controllers e algumas Views para cada entidade. Além disso, agora tem que se preocupar com um Gateway específico para cada uma?
No meu exemplo, eu faço uma descrição manual da tabela, mas é possível fazer isso automaticamente. No MySQL existe o comando DESCRIBE que retorna todas as informações que você precisa sobre qualquer elemento do banco, inclusive tabelas. Em outros SGBDs é um pouco mais complicado, precisamos consultar as "meta-tabelas" selecionando alguns campos, mas também é possível.
[...] pois pelo que li deste padrão,ele faz uma composição de objeto no Model, [...]
final class UserGateway extends Model
Composição, mas você está utilizando herança... :ermm:
É algo que muita gente confunde. Uma tabela NÃO É um Model, ela FAZ PARTE de um. O Model agrega muito mais coisas, como a lógica do negócio, validação, etc...
No meu mundinho perfeito, eu faria algo assim:
class UserModel extends AbstractModel {
private $dal;
public function __construct(DataAccessLayer $dal) {
$this->dal = $dal;
}
}
class DbTable implements DataAcessLayer {
}
class DbXML implements DataAccessLayer {
}
class TextFileFacade implements DataAccessLayer {
}
Ou seja, meu Model receberia uma camada de acesso a dados. Hoje pode ser uma tabela no banco de dados, amanhã eu posso mudar para o banco de dados baseado em XML, ou, para exemplos simples, um simples arquivo de texto.
Para alterar isso, basta eu alternar entre as instâncias que eu passo ao Model na hora de instanciá-lo:
$model1 = new UserModel(new DbTable('usuarios'));
$model2 = new UserModel(new DbXML('/path/to/usuarios.xml'));
$model3 = new UserModel(new TextFileFacade('/path/to/usuarios.txt'));
A dificuldade de trazer isso pra realidade é: como implementar consultas complexas tão poderosas quando um SELECT SQL em arquivos de texto? Bem complicado.
Ou seja, teria que limitar por baixo a funcionalidade e meu Model poderia fazer apenas operações básicas de CRUD, mas isso é impraticável, precisamos da complexidade.
É possível abstrair as consultas SQL, transformá-las em objetos (Ex.: Zend_Db_Select) e tentar expandir esse conceito para as demais camadas, entretanto, traduzir comandos SQL em operações sobre arquivos não é exatamente trivial...
Por últmo, em UserController eu não entendi porque a propriedade $bean é estática...
Pensando melhor, não é nem o Controller que lida com isso, é o Model que é responsável por criar e alterar Beans.
Mas a certeza que tenho é que ela não pode ser estática. Você nem sempre vai tratar um Bean de cada vez, ou vai?
Tente criar uma implementação com base no exemplo que eu dei e vê o que sai...
Tem certeza que essas são as únicas operações possíveis sobre o banco de dados?Caso apareça mais alguma você terá que ir e editar esse switch... Por isso, sua implementação está "amarrada".
Não, porém não sei por que não pensei nessa possibilidade! :o
Ôoo negócio complicado!
Composição, mas você está utilizando herança...
Veja bem o código abaixo. Estou usando composição para obter os dados do meu bean.
public static function dataInsert(UserBean $bean){
$data = array(
"table" => self::$table,
"values" => array(
"nome" => $bean->getNome(),
"email" => $bean->getEmail()
));
parent::getGateway()->crud('INSERT', new FCrud(), $data);
}
Tá errado, no conceito do Gateway Data Transfer?
É algo que muita gente confunde. Uma tabela NÃO É um Model, ela FAZ PARTE de um. O Model agrega muito mais coisas, como a lógica do negócio, validação, etc...
Sim, uma tabela faz parte do model, mas não é o model. Você me confundiu ainda mais, pois como que validação faz parte do Model? Isso não seria a responsabilidade do meu Controller?
>
Por últmo, em UserController eu não entendi porque a propriedade $bean é estática...
Pensando melhor, não é nem o Controller que lida com isso, é o Model que é responsável por criar e alterar Beans.
Mas a certeza que tenho é que ela não pode ser estática. Você nem sempre vai tratar um Bean de cada vez, ou vai?
Ah, não tenho nenhum motivo assim e nem explicação do que por que usei estático,só usei. Tá, eu fiz isso sem pensar muito, mas isso acontece, vou rever este conceito.
O meu Model pega os valores de um determinado Bean, porém a responsabilidade de setar os valores não é do Controller?
Por que não pode ser estática? Não sei se vou ficar tratando sempre os meus Beans, mas se caso isso ocorra, já está implementado, ou isso é um erro?
Tente criar uma implementação com base no exemplo que eu dei e vê o que sai...
Farei isso, sem dúvidas!
No mais, preciso de ajuda! :thumbsup:
Sim, uma tabela faz parte do model, mas não é o model. Você me confundiu ainda mais, pois como que validação faz parte do Model? Isso não seria a responsabilidade do meu Controller?
Como diz a minha avó:
De jeeeeito, manêrrrrra!
A validade dos dados é uma característica que deve ser garantida pelo modelo, não pelo Controller.
O Controller processa requisições e SÓ. É o conceito de "Fat Model, Skinny Controller". Dê uma pesquisada... Alguns são contra, mas não acho os argumentos deles muito válidos.
Veja bem o código abaixo. Estou usando composição para obter os dados do meu bean.
public static function dataInsert(UserBean $bean){
$data = array(
"table" => self::$table,
"values" => array(
"nome" => $bean->getNome(),
"email" => $bean->getEmail()
));
parent::getGateway()->crud('INSERT', new FCrud(), $data);
}
Tá errado, no conceito do Gateway Data Transfer?
Olha, errado é uma palavra muito forte... O problema maior é que, uma operação de INSERT é essencialmente igual para qualquer tabela, o que varia são os dados. Então pra que criar um método de inserção para cada entidade se eu posso criar um genérico e variar somente os dados.
Eu perco um pouco do controle sobre os dados, beleza, mas ao tentar inserir dados que não condizem com a tabela, um erro será lançado, ou seja, não dá pra você fazer besteira, nem se quiser.
O meu Model pega os valores de um determinado Bean, porém a responsabilidade de setar os valores não é do Controller?Por que não pode ser estática? Não sei se vou ficar tratando sempre os meus Beans, mas se caso isso ocorra, já está implementado, ou isso é um erro?
Mas por que o Model precisa guardar instâncias do Bean? Ele age sobre eles, sim, mas ele não POSSUI beans.
Note no meu exemplo:
/**
* Salva os dados em $data no banco de dados.
* A decisão por inserção ou atualização é feita automaticamente.
*
* @param $data : os dados a inserir ou atualizar
* @return int : o número de linhas afetadas
*/
public function save(array $data) {
if(!array_key_exists($this->idColName, $data)
|| $data[$this->idColName] === null) {
return $this->insert($data);
} else {
return $this->update($data);
}
}
O objeto Table não possui nenhum dado armazenado na tabela que ele diz respeito. Eu uso arrays para as operações e isso basta. Nada além do estritamente necessário para que as operações ocorram.
Aí você pensa assim:
Ah, mas aí eu posso incluir qualquer besteira aí que ele vai inserir...
Sim, o objeto Table não tem responsabilidade de saber se o dado é válido ou não, ele simplesmente faz as operações.
Para garantir a integridade dos dados, eu preciso passar dados válidos para a mesma. É aí que entra o Model, que vai fazer toda a lógica do negócio e as validações necessárias antes de acionar Table.
Isso é separação de responsabilidades!
>
Sim, uma tabela faz parte do model, mas não é o model. Você me confundiu ainda mais, pois como que validação faz parte do Model? Isso não seria a responsabilidade do meu Controller?
Depende do tipo de validação, validações referentes aos dados enviados pelo usuário, ou seja, validar a requisição, deve ser feita no Controller, como por exemplo verificar se determinada requisição foi feita através de POST, se os dados não estão vazios, prevenir XSS Injection, etc.
Agora se essa validação for referente às regras de negócio da tua aplicação, ela deve ser feita na camada de Model. Digamos que você esteja com uma aplicação de e-commerce, e que você não permite novos usuários efetuarem uma compra superior à X Reais, essa validação deve ser efetuada na Model, e não no Controller.
Depende do tipo de validação, validações referentes aos dados enviados pelo usuário, ou seja, validar a requisição, deve ser feita no Controller, como por exemplo verificar se determinada requisição foi feita através de POST, se os dados não estão vazios, prevenir XSS Injection, etc.
Perfeito!!! :clap:
Eu realmente me referia às validadações dos dados como objetos das regras de negócio...
vo e dto são a mesma coisa, so muda o autor. se n me engano quem usa o termo dto é o martin fwoler.
seu entendimento sobre vo/dto esta certo.