Ir para conteúdo

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

Bruno Augusto

[Resolvido] Interfaces...

Recommended Posts

Estava pensando em fazer a família crescer... Não, não é nesse sentido que estão pensando. :lol:

 

Recentemente resolvi reescrever de novo meu conjunto de classes para DB. Ficou bem melhor e mais lógico do que antes mas... nessa refatoração eu introduzi duas interfaces novas: Driver e Statement

 

A primeira com métodos públicos referentes ao Driver de conexão (MySQL, SQLite...). Essa interface é implementada por uma classe abstrata "muito criativamente" chamada de AbstractDriver

 

Perfeito, mas por enquanto elas estão meio vazias.

 

interface Driver {

   public function connect();

   public function disconnect();

   public function isConnected();

   public function prepare( $statement );
}

interface Statement {

   public function execute( $parameters = NULL );
}

Daí eu queria saber quais outros métodos seriam interessantes de serem acrescentados em quais das interfaces.

 

Isso porque eu queria uma coisa uniforme o suficiente para os principais SGBD's disponíveis.

 

De início pensei de adicionar query() e lastInsertId() em Driver e fetch()/fetchAll() e rowCount() em Statement.

 

O quê mais?

Compartilhar este post


Link para o post
Compartilhar em outros sites

Huuum interessante. Também ficarei na espera de uma resposta!

Compartilhar este post


Link para o post
Compartilhar em outros sites

Em Driver:

public void beginTransaction();
public void commitTransaction();
public void rollBackTransaction();
public mixed query();
public mixed lastInsertId();
public int insert($table, $data);
public int update($table, $data, $where);
public int delete($table, $where);
public array listTables();
public array describeTable($table);
public string quote($str);

 

As operações de INSERT, UPDATE e DELETE são bastante simples e tem muito em comum em qualquer SGDB que utiliza SQL, então não vejo necessidade de criar objetos separados para tal.

Já a operação de SELECT é bem complexa, aí você precisa de um objeto separado.

 

Seria bacana também poder deixar o Statement interno ao Driver, criando um tipo de ponte entre ambos, nesse caso, precisaria também dos fetches em Driver.

Com isso você tem liberdade de tanto criar o statement fora do driver, quando deixar para criá-lo automaticamente.

Se for fazer isso, seria bom ter métodos para alternar os modos de fetch.

 

Em Statement:

public void bindParams(array $params); //para prepared statements
public Driver getDriver(); //para retornar o driver ao qual aquele statement diz respeito. 

 

Só questão de nomenclatura, seu 'Driver' está mais para um 'Adapter' do que propriamente para um driver. O driver em si você utilizaria DENTRO dessa classe.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Bom deixar eu fazer umas considerações.

 

Tristemente eu confesso que é a quinta vez que eu refato meus objetos relativos à banco de dados. Vô te falar, viu, que negócio complicado! :o

 

Sobre alguns métodos da Interface Driver:

 

public void beginTransaction();

public void commitTransaction();

public void rollBackTransaction();

Nunca trabalhei com transações. Não sei bem o que são, mas o pouco que sei é que, principal, mas não exclusivamente com cadastros por "etapas", que garantem formulários menores, se algo falhar em algums dos INSERT's a qualquer momento, será possível desfazer tudo, até a primeira etapa.

 

Esse conceito pode estar equivocado, mas infelizmente, é o que tenho para comigo nesse momento.

 

public mixed query();

public mixed lastInsertId();

Gotcha! Bem como eu pensava. :P

 

public int insert($table, $data);

public int update($table, $data, $where);

public int delete($table, $where);

Discordo nesse sentido.

 

Pretendo implementar um Data Mapper, assim que aprender melhor tudo aquilo que ele abrange.

 

Eu pensei assim. Essas operações de CRUD estão intimamente ligadas às Entidades, individuais, porém, como são operações idênticas entre todas as Entidades, vai numa classe em separado.

 

Para não ser dependente de herança e criar uma classe abstrata, crie um Entity Manager que recebe no construtor um Driver, caracterizado pela Interface supra e uma Entidade, caracterizada pela Interface Entity.

 

E nessa classe tenho insert(), update() e delete() para as operações características, além de um select(), que instãncia um objeto Select, dada sua complexidade e o retorna após passar por um de seus métodos que define quais colunas serão selecionadas.

 

Tudo bem transparente. :grin:

 

public array listTables();

public array describeTable($table);

Qual a necessidade desses métodos? Teriam ele algo a ver com as meta-informações ligadas ao Data Mapper?

 

public string quote($str);

Aqui eu também discordo em partes.

 

No meu caso pelo menos, as strings SQL são renderizadas em separado por um renderizador léxico inerente à cada driver, individualmente.

 

Os métodos renderizadores, são invocados, através do escopo de Driver, pelo Entity Manager, nos métodos insert(), delete() e update() E pelo método assemble() presente na classe Query\Builder, que tem por função montar as cláusulas comuns do SQL, como where(), order(), limit()...

 

Sendo assim, e como posso ter dezenas de renderizadores diferentes, tenho um AbstractRenderer e nele tenho esse método quote()

 

Ao meu ver a única possibilidade de uso desse método fora de um renderizardor, seria por parte de query() que garantiria a execução de queries "cruas e tunadas manualmente".

 

Já para a Interface Statement:

 

public void bindParams(array $params); //para prepared statements

No meu sistema esse método é completamente desnecessário. A operação de binding (já que a tradução é ridícula) é feita automatica e transparentemente.

 

public Driver getDriver(); //para retornar o driver ao qual aquele statement diz respeito.

Até o presente momento não vi nenhuma necessidade de o objeto Statement conhecer o objeto Driver. Teria alguma razão para isso? Com exemplo seria melhor ainda. ^_^

 

Por fim, com respeito à sua edição do Adapter, digo que inicialmente tudo que hoje é driver já foi Adapter um dia. Mas depois ver e rever o exemplo do Adaptador de Tomada, refleti que na realidade, conectar, desconectar e executar tais operações não são características de um adaptador.

 

Porém, como parte dos SGBD's suportados pelo meu sistema utiliza da PDO como base, tenho dentro do namespace PDO uma classe abstrata "muito criativamente" chamada de AbstractAdapter a qual define, através de um método abstrato, uma nova interface para cada adaptador suportado pela biblioteca.

 

E pelo que entendi, é bem isso que esse Padrão é/faz: Alterar uma interface através de outra, mesmo que essa outra não seja uma Interface propriamente dita.

 

Ufa! :P

Compartilhar este post


Link para o post
Compartilhar em outros sites
Nunca trabalhei com transações. Não sei bem o que são, mas o pouco que sei é que, principal, mas não exclusivamente com cadastros por "etapas", que garantem formulários menores, se algo falhar em algums dos INSERT's a qualquer momento, será possível desfazer tudo, até a primeira etapa.

 

Esse conceito pode estar equivocado, mas infelizmente, é o que tenho para comigo nesse momento.

É quase isso. Transações garantem a ATOMICIDADE de uma operação complexa no banco de dados. Ou ela ocorre por inteiro, ou não ocorre.

Aí você fala: nunca vou precisar disso! Um belo dia você precisa e lembra que não tem isso implementado. Não é algo tão complicado, são só 3 métodos que executam 1 sentença SQL cada, então é melhor ter logo.

 

Discordo nesse sentido.

 

Pretendo implementar um Data Mapper, assim que aprender melhor tudo aquilo que ele abrange.

 

Eu pensei assim. Essas operações de CRUD estão intimamente ligadas às Entidades, individuais, porém, como são operações idênticas entre todas as Entidades, vai numa classe em separado.

 

Tá, mas e quando chegar no nível mais baixo, onde NECESSARIAMENTE você terá que chamar alguma função de inserção/atualização/deleção a nível de Driver?

 

Qual a necessidade desses métodos? Teriam ele algo a ver com as meta-informações ligadas ao Data Mapper?

Eu não utilizo DataMapper. Com describeTable, obtenho todos os dados que necessito sobre a tabela, dessa forma, não preciso definir no PHP algo que já está definido na banco de dados. É só retrabalho (até muito pouco tempo atrás eu fazia assim).

 

listTables pode vir a ser interessante, vai que um dia você está implementando algum tipo de CMS onde é possível editar dados de algumas tabelas. Qual o jeito mais simples de obter a lista de tabelas do banco de dados? listTables

 

No meu caso pelo menos, as strings SQL são renderizadas em separado por um renderizador léxico inerente à cada driver, individualmente.

 

Os métodos renderizadores, são invocados, através do escopo de Driver, pelo Entity Manager, nos métodos insert(), delete() e update() E pelo método assemble() presente na classe Query\Builder, que tem por função montar as cláusulas comuns do SQL, como where(), order(), limit()...

 

Sim, fica exatamente separado.

interface Driver {
public function quote($str);
}

class Driver_Mysql implements Driver {
public function quote($str) {
   	return $this->connection->real_escape_string($str);
}
}

class Driver_Pgsql implements Driver {
public function quote($str) {
   	return $this->connection->escape($str);
}
}

 

Você agora pode invocar o método quote em qualquer lugar.

 

Até o presente momento não vi nenhuma necessidade de o objeto Statement conhecer o objeto Driver. Teria alguma razão para isso? Com exemplo seria melhor ainda.

Eu não sei exatamente o que você chama de Statement, tem alguns significados diferentes.

 

Na minha implementação, ele representa um prepared statement SQL. Ele possui métodos como execute, fetch, fetchAll, rowCount

Todas as operações sobre o banco passam pelo statement. Na hora de inserir dados, por exemplo, é criado um statement de inserção.

Na hora de montar a query, eu preciso quotar os valores inseridos para evitar problemas, então eu faço assim:

$value = $this->stmt->getAdapter()->quote($value);

 

Por fim, com respeito à sua edição do Adapter, digo que inicialmente tudo que hoje é driver já foi Adapter um dia. Mas depois ver e rever o exemplo do Adaptador de Tomada, refleti que na realidade, conectar, desconectar e executar tais operações não são características de um adaptador.

 

Porém, como parte dos SGBD's suportados pelo meu sistema utiliza da PDO como base, tenho dentro do namespace PDO uma classe abstrata "muito criativamente" chamada de AbstractAdapter a qual define, através de um método abstrato, uma nova interface para cada adaptador suportado pela biblioteca.

 

E pelo que entendi, é bem isso que esse Padrão é/faz: Alterar uma interface através de outra, mesmo que essa outra não seja uma Interface propriamente dita.

Talvez seja só questão de nomenclatura mesmo, mas o Driver é algo mais baixo nível, que "conversa" diretamente com o banco de dados. O Adapter surge justamente para suprimir as diferenças de implementação dos Drivers existentes, como MySQLi, PDO, PgSQL.

 

Usando o exemplo da tomada, eu posso trocar a tomada e o adaptador, mas manter o mesmo plug.

A tomada seria o SGBD, o adaptador, o Adapter (ah vá?) e o plug seria a sua aplicação.

Compartilhar este post


Link para o post
Compartilhar em outros sites
Nunca trabalhei com transações. Não sei bem o que são, mas o pouco que sei é que, principal, mas não exclusivamente com cadastros por "etapas", que garantem formulários menores, se algo falhar em algums dos INSERT's a qualquer momento, será possível desfazer tudo, até a primeira etapa.

 

Esse conceito pode estar equivocado, mas infelizmente, é o que tenho para comigo nesse momento.

É quase isso. Transações garantem a ATOMICIDADE de uma operação complexa no banco de dados. Ou ela ocorre por inteiro, ou não ocorre.

Aí você fala: nunca vou precisar disso! Um belo dia você precisa e lembra que não tem isso implementado. Não é algo tão complicado, são só 3 métodos que executam 1 sentença SQL cada, então é melhor ter logo.

Vou estudart melhor a respeito. Posso até implementar, mas vou segurar por um tempo.

 

Discordo nesse sentido.

 

Pretendo implementar um Data Mapper, assim que aprender melhor tudo aquilo que ele abrange.

 

Eu pensei assim. Essas operações de CRUD estão intimamente ligadas às Entidades, individuais, porém, como são operações idênticas entre todas as Entidades, vai numa classe em separado.

Tá, mas e quando chegar no nível mais baixo, onde NECESSARIAMENTE você terá que chamar alguma função de inserção/atualização/deleção a nível de Driver?

Pode citar um exemplo? Ainda não consigo enxergar algo que justifique.

 

Se houver uma inserção, uma atualização ou uma deleção não atrelada à uma Entidade, ela seria construída manualmente, e para isso há o query(), não?

 

Qual a necessidade desses métodos? Teriam ele algo a ver com as meta-informações ligadas ao Data Mapper?

Eu não utilizo DataMapper. Com describeTable, obtenho todos os dados que necessito sobre a tabela, dessa forma, não preciso definir no PHP algo que já está definido na banco de dados. É só retrabalho (até muito pouco tempo atrás eu fazia assim).

 

listTables pode vir a ser interessante, vai que um dia você está implementando algum tipo de CMS onde é possível editar dados de algumas tabelas. Qual o jeito mais simples de obter a lista de tabelas do banco de dados? listTables

Dentre os padrões relacionados à banco de dados, o Data Mapper me pareceu o menos complicado mais simples e flexível.

 

Mas pelo que entendi do Padrão, pela explicação do Martin Fowler (que é não muito clara), é uma classe que mapeia o schema do banco.

 

Pensando programaticamente, não seria inteligente fazer esse mapeamento toda vez, então haveria um "cache" dessas meta-informações.

 

Como que seria de outra forma?

 

Eu vejo que o PHP saberia que tal campo só aceita números inteiros e trataria os dados com is_int() por interpretar tais meta-informações. E se existem meta-informações elas vieram do mapeamento da entidade referente à tabela.

 

E se há o mapeamento, é um Data Mapper, não?

 

Até o presente momento não vi nenhuma necessidade de o objeto Statement conhecer o objeto Driver. Teria alguma razão para isso? Com exemplo seria melhor ainda.

Eu não sei exatamente o que você chama de Statement, tem alguns significados diferentes.

 

Na minha implementação, ele representa um prepared statement SQL. Ele possui métodos como execute, fetch, fetchAll, rowCount

Todas as operações sobre o banco passam pelo statement. Na hora de inserir dados, por exemplo, é criado um statement de inserção.

Na hora de montar a query, eu preciso quotar os valores inseridos para evitar problemas, então eu faço assim:

$value = $this->stmt->getAdapter()->quote($value);

Haveria outra possibilidade para o significado de um Statement. Também tenho para comigo que um Statement é o resultado de um SQL prepradao, do qual se extrai os resultados, sejam os dados ou o número de linhas afetadas.

 

Agora fiquei curioso com teu comentário.

Compartilhar este post


Link para o post
Compartilhar em outros sites
Pode citar um exemplo? Ainda não consigo enxergar algo que justifique.

 

Se houver uma inserção, uma atualização ou uma deleção não atrelada à uma Entidade, ela seria construída manualmente, e para isso há o query(), não?

Mas justamente estando atrelado a uma entidade, você quer ter que gerar statements de inserção para cada entidade que você tiver?

Eu tenho um sistema aqui com 40 tabelas, das quais umas 35 podem ser consideradas entidades, como é algo antigo, sim, eu tenho um monstrinho em mãos.

 

Imagine que você está lá no seu modelo de aplicação, você pode ter uma inserção genérica assim:

abstract class ApplicationModelAbstract {
protected $driver;
protected $tableName;
public function save(array $data) {
     	if($data['id'] == null){
         	$this->driver->insert($this->tableName, $data);
	  } else {
	  	$this->driver->update($this->tableName, $data, 'id = ?', $data['id']);
     	}
}
}

 

Ou gerar um um insert statement para cada classe saparadamente, o que eu acho loucura huehueuehuehue.

 

Dentre os padrões relacionados à banco de dados, o Data Mapper me pareceu o menos complicado mais simples e flexível.

 

Mas pelo que entendi do Padrão, pela explicação do Martin Fowler (que é não muito clara), é uma classe que mapeia o schema do banco.

 

Pensando programaticamente, não seria inteligente fazer esse mapeamento toda vez, então haveria um "cache" dessas meta-informações.

Então, é justamente o que eu acho desnecessário. E digo por que já fiz dessa maneira. Tinha algo como:

class Article extends AbstractDomainObject {
public function __construct() {
	parent::__construct('article');
	$this->setField(new TableColumn(new Integer(), 'id'));
	$this->setField(new TableColumn(new Integer(), 'user_id'));
	$this->setField(new TableColumn(new String(), 'title'));
	$this->setField(new TableColumn(new String(), 'text'));
	$this->setField(new TableColumn(new String(), 'source'));
	$this->setField(new TableColumn(new TimeStamp(), 'tmstp'));

	$this->setAdditionalField(new TableColumn(new String(), 'user_name'));
	$this->setAdditionalField(new TableColumn(new CompositeDataType(), 'pictures'));

	$this->setPrimary('id');
	$this->addForeign('user_id');

	$this->setInputFilter('text', new Trim(), false)
		 ->setOutputFilter('text', new Trim(), false);
}
}

Com essa classe, eu fazia o mapeamento da tabela 'article' dentro do DomainObject 'Article'.

Toda alteração que eu fizer na tabela, tenho que fazer nessa classe também.

 

Agora, por que fazer isso se o próprio banco de dados me fornece todas as informações que preciso?

Os únicos tipos de dados que precisam de um tratamento diferente são INT, FLOAT e seus derivados.

Para cada adapter, eu apenas armazeno esses tipos:

protected $_numericDataTypes = array(
		Db::INT_TYPE	=> Db::INT_TYPE,
		Db::BIGINT_TYPE => Db::BIGINT_TYPE,
		Db::FLOAT_TYPE  => Db::FLOAT_TYPE,
    	'INT'            	=> Db::INT_TYPE,
    	'INTEGER'        	=> Db::INT_TYPE,
    	'MEDIUMINT'      	=> Db::INT_TYPE,
    	'SMALLINT'       	=> Db::INT_TYPE,
    	'TINYINT'        	=> Db::INT_TYPE,
    	'BIGINT'         	=> Db::BIGINT_TYPE,
    	'SERIAL'         	=> Db::BIGINT_TYPE,
    	'DEC'            	=> Db::FLOAT_TYPE,
    	'DECIMAL'        	=> Db::FLOAT_TYPE,
    	'DOUBLE'         	=> Db::FLOAT_TYPE,
    	'DOUBLE PRECISION'   => Db::FLOAT_TYPE,
    	'FIXED'          	=> Db::FLOAT_TYPE,
    	'FLOAT'          	=> Db::FLOAT_TYPE
);

 

E se existem meta-informações elas vieram do mapeamento da entidade referente à tabela.

O único tipo de informação que você não consegue obter utilizando DESCRIBE é são as referências feitas por chave estrangeira.

Na minha implementação, existe também o objeto Db_Table, que é uma abstração sobre a tabela do banco de dados (jura???).

 

Db_Table possui uma propriedade chamada referenceMap.

/**
 * Array associativo contendo informações sobre regras de integridade.
 * Existe uma entrada para cada chave estrangeira da tabela.
 * Cada chave é um mnemônico para a regra de referência.
 *
 * Cada entrada é um array associativo contendo os seguintes índices:
 * - columns		=>	array contendo os nomes das colunas na tabela-filha
 * - refTable	=>	nome da classe na tabela-pai
 * - refColumns		=>	array de nomes contendo os nomes das colunas na
 * 						tabela-pai na mesma ordem que no índice 'columns'
 * - onDelete		=>	"cascade" significa que deletar uma linha na
 * 						tabela-pai causa uma deleção das linhas
 * 						referenciadas na tabela-filha
 * - onUpdate		=>	"cascade" significa que uma atualizar uma linha na
 * 						tabela-pai c""ausa uma atualização das linhas
 * 						referenciadas na tabela-filha
 *
 * @var array
 */
private $_referenceMap = array();

 

E para poder simplificar mais ainda o uso, tenho um outro objeto Db_Table_Definition, que armazena, entre outras coisas o mapa de referência de cada tabela.

$definition = new Db_Table_Definition(
array(
	'comments' => array(
		'referenceMap' => array(
			'ownerUser' => array(
				Db_Table::COLUMNS => 'user_id',
				Db_Table::REF_COLUMNS => 'id',
				Db_Table::REF_TABLE => 'users'
			)
		)
	)
)
);

Aqui eu informo que comments possui uma referência que eu chamei de ownerUser, cuja coluna user_id faz referência à coluna id da tabela user.

 

Como você já deve estar imaginando, Db_Table é genérica e recebe um Adapter por injeção de dependência.

Ao invés de chamar o método describeTable toda hora, eu crio um cache automático das informações da tabela.

000001357964531
a:4:{s:2:"id";a:14:{s:11:"SCHEMA_NAME";N;s:10:"TABLE_NAME";s:8:"comments";s:11:"COLUMN_NAME";s:2:"id";s:15:"COLUMN_POSITION";i:1;s:9:"DATA_TYPE";s:3:"int";s:7:"DEFAULT";N;s:8:"NULLABLE";b:0;s:6:"LENGHT";N;s:5:"SCALE";N;s:9:"PRECISION";N;s:8:"UNSIGNED";b:1;s:7:"PRIMARY";b:1;s:16:"PRIMARY_POSITION";i:1;s:8:"IDENTITY";b:1;}s:7:"user_id";a:14:{s:11:"SCHEMA_NAME";N;s:10:"TABLE_NAME";s:8:"comments";s:11:"COLUMN_NAME";s:7:"user_id";s:15:"COLUMN_POSITION";i:2;s:9:"DATA_TYPE";s:3:"int";s:7:"DEFAULT";N;s:8:"NULLABLE";b:1;s:6:"LENGHT";N;s:5:"SCALE";N;s:9:"PRECISION";N;s:8:"UNSIGNED";b:1;s:7:"PRIMARY";b:0;s:16:"PRIMARY_POSITION";N;s:8:"IDENTITY";b:0;}s:12:"comment_text";a:14:{s:11:"SCHEMA_NAME";N;s:10:"TABLE_NAME";s:8:"comments";s:11:"COLUMN_NAME";s:12:"comment_text";s:15:"COLUMN_POSITION";i:3;s:9:"DATA_TYPE";s:8:"tinytext";s:7:"DEFAULT";N;s:8:"NULLABLE";b:0;s:6:"LENGHT";N;s:5:"SCALE";N;s:9:"PRECISION";N;s:8:"UNSIGNED";N;s:7:"PRIMARY";b:0;s:16:"PRIMARY_POSITION";N;s:8:"IDENTITY";b:0;}s:13:"comment_tmstp";a:14:{s:11:"SCHEMA_NAME";N;s:10:"TABLE_NAME";s:8:"comments";s:11:"COLUMN_NAME";s:13:"comment_tmstp";s:15:"COLUMN_POSITION";i:4;s:9:"DATA_TYPE";s:9:"timestamp";s:7:"DEFAULT";s:17:"CURRENT_TIMESTAMP";s:8:"NULLABLE";b:0;s:6:"LENGHT";N;s:5:"SCALE";N;s:9:"PRECISION";N;s:8:"UNSIGNED";N;s:7:"PRIMARY";b:0;s:16:"PRIMARY_POSITION";N;s:8:"IDENTITY";b:0;}}

Meu sistema de cache é bem simples, tem apenas um vencimento e os dados armazenados.

Para facilitar (ou não) apenas serializo o array de informações e coloco no cache.

Se você não quiser ler do cache toda vez que utilizar a tabela (se bem que na maioria dos casos, você faz uma consulta por tabela a cada requisição), você pode colocá-lo dentro do próprio objeto.

 

Haveria outra possibilidade para o significado de um Statement. Também tenho para comigo que um Statement é o resultado de um SQL prepradao, do qual se extrai os resultados, sejam os dados ou o número de linhas afetadas.

 

Agora fiquei curioso com teu comentário.

Também chamam de statement somente a sentença SQL, é um tanto confuso mesmo.

Veja que a única necessidade do statement conhecer o Driver seria a aplicação da função quote(), que é de nível de Driver, mas é responsabilidade do prepared statement garantir esse tipo de segurança. Como objetos em PHP são passados por referência, não por cópia, não há gasto de memória maior.

 

É realmente bastante confuso essa história de DbPatterns, eu fiquei uns 4 meses estudando só isso para implementar o meu e ainda dei umas bisbilhotadas no código dos frameworks mais conhecidos para ver se não estava me esquecendo de nada.

 

Se te ajudar um pouco, posso tentar montar o Diagrama de Classes da minha implementação.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Eu não entendo esses diagramas :cry:

 

Vou temporariamente suspender essa parte de mapeamento de tipos, como os adaptadores suportados se baseiam n PDO, deixo ela cuidar por mim, por enquanto.

 

Quanto aos métodos de JOIN, também não os tenho xD

 

Vou implementar um pouco do que rolou nesse tópico e depois encho mais um pouco as paciências. :grin:

Compartilhar este post


Link para o post
Compartilhar em outros sites
Eu não entendo esses diagramas cry.gif

Cara, não é muito complicado não, em 2 horinhas de estudo você entende a notação.

 

Vou implementar um pouco do que rolou nesse tópico e depois encho mais um pouco as paciências. grin.gif

E vamo que vamo... ehuuhehuehu...

Eu to com umas ideias bacanas sobre HMVC, só preciso amadurecê-las um pouco mais e posto aqui =]

Compartilhar este post


Link para o post
Compartilhar em outros sites

Pode parecer um assunto diferente do original do tópico, mas para o que eu preciso se aplica.

 

O que você(s) acha(m) de mesclar as funcionalidades do Data Mapper com o DomainObject para se ter uma única classe que trabalhe de forma automatizada?

 

Baseando por esse e esse artigos (que aparentemente são complementos), o Domain Object não tem nada mais do que as propriedades e o setters/getters.

 

Qual o problema se fosse incluída a tabela à qual o Domain Object se refere e fazer todos os métodos de CRUD numa mesma classe?

 

Eu sei que tem a questão da responsabilidade única, mas não vejo problemas nesse caso. Eu estaria suprimindo uma classe e melhorando a performance, não?

 

Algo assim (vou escrever de cabeça):

 

class User extends AbstractMapper {    protected $_table = 'users';    protected $id;    protected $username;    protected $password;    public function findUserByName() {}    public function findUserById() {}}

abstract class AbstractMapper extends QueryBuilder {    public function select() {        /** Retorna um objeto Select, com métodos próprios para         * lidar com a complexidade de um SELECT Statement separadamente         */    }    public function insert() {        /**         * Lista os campos, seleciona o renderizador, insere e         * retorna o ID do último registro, tudo automaticamente         */    }    public function update() {        /**         * Faz o mesmo que acima, mas renderiza um UPDATE Statement         * e retorna uma instância do próprio Mapper, para que         * o método where(), por exemplo, possa ser usado em cadeia         */    }    public function delete() {        // Idem ao de cima    }}

abstract class QueryBuilder {    protected $driver;    public function __construct( Driver $driver ) {        $this -> driver =& $driver;    }    public function where() {}    public function order() {}    // E etc.}

$user = new User( new PDOSQliteAdapter( 'path/to/sqlite/file' ) );$user -> username = 'Bruno Augusto';$user -> password = '12345';$lastID = $user -> insert(); // Não vai funionar porque não implementei o __set()

Não me preocupei com conceitos como herança reduzida nem nada, mas a ídéia seria essa.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Supondo que tenha entendido certo e o que ele disse com data são as informações atribuídas às propriedades e behavior as ações de CRUD, ainda é um ActiveRecord mesmo que as ditas operações estejam numa classe em separado?

 

Porque pelo restante do capítulo do livro, a utilização desse padrão é para quando as operações de CRUD não são tão complexas, mas a estrutura que estou me permitindo montar pode realizar operações simples ou complexas.

 

Se, por exemplo, alguma classe de alguma Entidade realizar um procedimento complexo pós-update, por exemplo, eu sobrescreveria o método update(), invocaria à nível de herança e continuaria com outras operações:

 

abstract class AbstractMapper {

   public function update() {}
}

class User extends AbstractMapper {

   public function update() {

       $affected = parent::update();

       // Do something more

       return $affected;
   }
}

Compartilhar este post


Link para o post
Compartilhar em outros sites

Olha, sua implementação no post #10 amarra demais o código.

Toda vez que você alterar qualquer coisa na tabela, terá que refleti-lo na classe.

 

Como falei antes, a própria tabela se auto-define, não precisa fazer essa definição no PHP também, é só retrabalho.

No máximo, você precisa mapear os relacionamentos entre as tabelas, o que e feito como eu falei no post #7.

Mesmo que você não utilize banco de dados e sua fonte seja, sei lá, um RSS por exemplo, seus dados são definidos pela própria estrutura do arquivo RSS.

 

Pensando em outros tipos de armazenamento, não banco de dados, eu idealizei desenvolver uma camada a mais, a qual eu chamaria de DAL ou DAO (Data Access Layer ou Object).

O que seria essa camada?

Seria um conjunto de classes com apenas métodos de CRUD, sob uma interface comum.

Se um dia eu "cansasse" do banco de dados, poderia salvar meus dados em arquivos de texto (é, eu posso ficar louco um dia, quem garante que não?) apenas alterando a instância dessa interface passada para o Model.

 

Muito lindo na teoria, mas é mais complicado quando temos consultas complexas aos dados, forçando-nos a usar SQL no próprio model, então acabei desistindo dessa ideia.

 

Enfim, voltando um pouco aos patterns, eu pessoalmente gosto bastante do par Table Data Gateway e Row Data Gateway.

Como os próprios nomes devem indicar, o primeiro é um 'porta para os dados da tabela'. Objeto TDG são stateless, ou seja, não armazenam entradas da tabela, seus dados são, em tese, imutáveis.

O RDG representa uma linha, logo, objetos RGD são stateful, armazenam os dados da linha, pode-se alterá-los e salvá-los no banco de dados.

Fala-se muito desse par pois normalmente são utilizados em conjunto.

 

Eu tenho apenas uma classe TableGateway que recebe como parâmetro o nome da tabela e um Adapter (no seu caso, chamado de Driver), entre outras coisas.

Essa classe obtém os dados da tabela a partir do método describeTable do Adapter. Esses dados são armazenados em cache após a primeira consulta.

 

Além disso, posso lhe informar um objeto TableDefinition para o mapeamento de relacionamentos.

Métodos de CRUD recebem arrays de dados e condições, no caso de UPDATE e DELETE. No caso dos métodos de busca (fetch), eu passo um objeto Db_Select para o mesmo.

Existem também os métodos getBy($field, $value) e getById($id), que retornam os dados que eu desejo.

Note que ambos podem ser baseados na definição da tabela obtida anteriormente com describeTable.

 

Também só há necessidade para uma classe RowGateway, que recebe como parâmetro um objeto TableGateway.

Os campos são configurados de acordo com a definição do objeto TableGateway.

O mais bacana dessa classe é o que eu chamo de 'simplifica minha vida, meu filho'.

 

Muitas vezes, temos 2 tabelas com um relacionamento "pai/filho", onde partindo do filho, eu precise chegar ao pai, ou vice-versa.

Desde que eu tenha mapeado certinho os relacionamentos no objeto TableDefinition passado para o TableGateway, posso fazer isso através de 2 métodos:

TableRow::findParentRow($ruleName);
TableRow::findDependentRows($ruleName);

 

$ruleName se faz necessário, porque uma tabela pode ser relacionada a várias outras. Cada relacionamento (regra) recebe um nome unívoco. Normalmente, eu uso o nome o índice criado na tabela para fazer isso.

 

Não sei se ficou claro, se não ajudou muito, posso tentar postar um esqueleto da implementação aqui...

Compartilhar este post


Link para o post
Compartilhar em outros sites

Amarra em que sentido, Henrique?

 

Não consigo enxergar tão além ainda. Eu achei bem simples, tão simples que, pensando bem, até parece Active Record.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Pelo que entendi esse meu Query Builder é um Query Object. Só não o nomeei como Query Object por causa dos namespaces pois também tenho uma classe-base Object e não queria usar aliases. :closedeyes:

 

Eu dei uma mega-resumida do que tenho no post #10. A estrutura real me permite fazer:

 

$affectedRows = $entity -> update() -> where( 'field1 = ?', 'value 1' )                                    -> where( 'field2 = ?', 'value 2' )                                    -> where( 'field3 = ?', 'value 3' )                                    -> limit( 2 );

E a query vai ser construída direitinho, desde o prefix imutável do Statement até a última cláusula existente. E mais, ainda é Driver-aware, já que quem de fato monta a query são renderizadores léxicos em separado.

 

Sobre os find's, que que eu posso dizer, fiquei sem idéia do que escrever na hora. >.<

 

mas às vezes é bom uns métodos que realizam determinadas operações. Tais métodos justificam a aparente repetição por parte do SELECT, pois evitariam que eu fizesse toda santa vez algo como?

 

$openedTickets = $tickets -> select() -> where( 'status = ?', 1 ) -> fetchAll();

Sendo que poderia fazer:

 

public function findOpenedTickets() {    return $this -> select(0 -> where( 'status = ?', 1 ) -> fetchAll();}//...$openedTickets = $tickets -> findOpenedTickets()

Compartilhar este post


Link para o post
Compartilhar em outros sites

Tive que dar uma pesquisada pra me lembrar da diferença entre RDG e Active Record.

A diferença é: o Active Record mantém a estrutura da tabela dentro do objeto, o que para muitos, é muita responsabilidade e também um desperdício de memória, enquanto que o RDG usa um ÚNICO objeto TDG para se obter a estrutura da tabela.

 

Amarra em que sentido, Henrique?

 

Veja seu exemplo:

class User extends AbstractMapper {

   protected $_table = 'users';

   protected $id;
   protected $username;
   protected $password;

   public function findUserByName() {}

   public function findUserById() {}
}

No sentido de que o objeto que fizer uso de um objeto User, precisa conhecer parte de sua implementação.

findUserByName é um método que vai buscar o usuário pelo seu nome, logo, para eu utilizá-lo, eu preciso saber que um usuário tem um nome.

 

Você também teria que criar métodos setters e getters para as propriedades $id, $username, etc.

Na hora de utilizar, você faz:

$user = new User();
$password = $user->getPassword();

 

Novamente, você precisa conhecer a implementação.

 

Fazendo uma objeto geral como te indiquei, tudo fica sob uma mesma interface.

Você tem os métodos:

getBy($fieldName, $value);
getById($value);

 

Agora você não precisa conhecer a implementação, precisa conhecer apenas a estrutura da tabela, o que é realmente necessário.

 

Aí você pode pensa: bom, posso usar os métodos mágicos para contornar o meu problema e continuar implementando desse jeito.

Poder você pode, mas métodos mágicos são ruins, lembre-se disso...

 

Bom, aqui estão alguns fragmentos da implementação:

 

Db_Table

 

 

class Db_Table {
/**
 * Armazena um Db_Adapter padrão para os objetos Db_Table
 * @var Db_Adapter_Abstract
 */
private static $_defaultAdapter;


/**
 * Armazena um Db_Adapter para o objeto
 * @var Db_Adapter_Abstract
 */
private $_adapter;

/**
 * Armazena o schema da tabela
 * @var string
 */
private $_schema;

/**
 * Armazena o nome da tabela
 * @var string
 */
private $_name;

/**
 * Armazena as colunas da tabela, obtidas a
 * partir do método Db_Adapter::describeTable()
 * @var array
 */
private $_cols;

/**
 * Armazena a chave primária da tabela
 * @var mixed
 */
private $_primary = null;

/**
 * Se a chave primária é composta e uma das colunas
 * usa auto-incremnto ou sequência-gerada, setamos
 * $identity para o índice ordinal do campo no
 * array $_primary. O array $_primary começa em 1.
 * @var integer
 */
private $_identity = 1;


/**
 * Define a lógica para novos valores na chave
 * primária. Pode ser uma string ou booleano.
 * @var mixed
 */
private $_sequence = true;

/**
 * Informação fornecida pelo método describeTable() do Adapter
 * @var array
 */
private $_metadata = array();

/**
 * Flag: informa se devemos ou não cachear os metadados na classe
 * @var boolean
 */
private $_metadataCacheInClass = true;

/**
 * Array associativo contendo informações sobre regras de integridade.
 * Existe uma entrada para cada chave estrangeira da tabela.
 * Cada chave é um mnemônico para a regra de referência.
 *
 * Cada entrada é um array associativo contendo os seguintes índices:
 * - columns		=>	array contendo os nomes das colunas na tabela-filha
 * - refTable	=>	nome da classe na tabela-pai
 * - refColumns		=>	array de nomes contendo os nomes das colunas na
 * 						tabela-pai na mesma ordem que no índice 'columns'
 * - onDelete		=>	"cascade" significa que deletar uma linha na
 * 						tabela-pai causa uma deleção das linhas
 * 						referenciadas na tabela-filha
 * - onUpdate		=>	"cascade" significa que uma atualizar uma linha na
 * 						tabela-pai c""ausa uma atualização das linhas
 * 						referenciadas na tabela-filha
 *
 * @var array
 */
private $_referenceMap = array();

/**
 * Array contendo os nomes das tabelas "filhas" da atual, ou seja,
 * aquelas que contém uma chave estrangeira para esta.
 * @var array
 */
private $_dependentTables = array();

/**
 * A definição da tabela.
 * @var Db_Table_Definition
 */
private $_definition;

/**
 * A definição padrão para os objetos Db_Table
 * @var Db_Table_Definition
 */
private static $_defaultDefinition;

/**
 * Construtor.
 *
 * Parâmetros de configuração:
 * - db				 =>	instância de Db_Adapter
 * - name			 =>	o nome da tabela
 * - schema			 =>	o schema da tabela
 * - primary		 =>	a chave primária da tabela (string | array)
 * - rowClass		 =>	nome da classe TableRow
 * - rowsetClass	 =>	nome da classe TableRowset
 * - referenceMap	 =>	declaração das relações de integridade da tabela
 * - dependentTables =>	array de tabelas-filhas
 * - metadataCache	 =>	cache dos metadados
 * - integrityCheck	 => se o objeto deve ou não verificar a integridade
 * 						da tabela em remoções e atualizações.
 *
 * @param mixed $config : array de configurações, nome da tabela ou somente um Db_Adapter
 */
public function __construct($config = array()) {
	if($config instanceof Db_Adapter_Abstract) {
		$config = array(self::ADAPTER => $config);
	} else if(is_string($config)) {
		$config = array(self::NAME => $config);
	}

	if($config) {
		$this->setOptions($config);
	}

	$this->_setup();
	$this->init();
}

public function setOption(array $options) {
	// Código omitido...
}

/**
 * Adiciona uma tabela dependente desta.
 *
 * @param string $table : o nome da tabela dependente
 * @return Db_Table : fluent interface
 */
public function addDependentTable($table) {
	if(!in_array($table, $this->_dependentTables)) {
		$this->_dependentTables[] = $table;
	}
	return $this;
}

/**
 * @param array $depTabless
 */
public function setDependentTables(array $depTables) {
	$this->_dependentTables = $depTables;
	return $this;
}

/**
 * @return array
 */
public function getDependentTables() {
	return $this->_dependentTables;
}

/**
 * Inicializa os metadados da tabela.
 *
 * Se os metadados não puderem ser carregados do cache, o método
 * describeTable() do adapter é chamado para buscar essa informação.
 * Retorna true se e somente se os metadados forem carregados do cache
 *
 * @return boolean
 * @throws Db_Table_Exception
 */
private function _setupMetadata() {
	if($this->isMetadataCacheInClass() && (count($this->_metadata) > 0)) {
		return true;
	}

	$cacheName = $this->_getCacheName();
	try {
		$cacheContents = Cache::get(self::TABLE_CACHE_DIR, $cacheName);
	} catch (Exception $e) {
		$cacheContents = null;
	}

	//Se o cache não existe...
	if($cacheContents === null) {
		$isMetadataFromCache = false;
		$this->_metadata = $this->_adapter->describeTable($this->_name);
		try {
			Cache::set(self::TABLE_CACHE_DIR, $cacheName, $this->_metadata, '+ 1 YEAR');
		} catch(Cache_Exception $e) {
			trigger_error(sprintf('Impossível salvar o arquivo de cache de metadados da tabela  "%s"', $this->_name), E_USER_NOTICE);
		} catch (Exception $e) {
			// Uma exceção do tipo Exception é lançada quando o cache está desabilitado.
		}
	} else {
		$this->_metadata = $cacheContents;
		$isMetadataFromCache = true;
	}

	return $isMetadataFromCache;
}

/**
 * Retorna o nome do arquivo de cache da tabela.
 *
 * @return string
 */
private function _getCacheName() {
	// Código omitido...
}

/**
 * Retorna as informações da tabela.
 *
 * @param string|null $key : qual informação retornar ou NULL
 * @return mixed : array ou string
 */
public function info($key = null) {
	$this->_setupPrimaryKey();

	$info = array(
	self::SCHEMA       	=> $this->_schema,
	self::NAME         	=> $this->_name,
	self::COLS         	=> $this->_getCols(),
	self::PRIMARY      	=> (array) $this->_primary,
	self::METADATA     	=> $this->_metadata,
	self::ROW_CLASS    	=> $this->getRowClass(),
	self::ROWSET_CLASS 	=> $this->getRowsetClass(),
	self::REFERENCE_MAP	=> $this->_referenceMap,
	self::DEPENDENT_TABLES => $this->_dependentTables,
	self::SEQUENCE     	=> $this->_sequence
	);

	if ($key === null) {
		return $info;
	}

	if (!array_key_exists($key, $info)) {
		return null;
	}

	return $info[$key];
}

/**
 * Cria e retorna uma instâncida de Db_Table_Select;
 * @param mixed $cols : array ou string
 * @return Db_Table_Select
 */
public function select($cols = array()) {
	$select = new Db_Table_Select($this);
	$cols = empty($cols) || is_array($cols) ? $cols : array($cols);
	$select->setTable($this);
	if(!empty($cols)) {
		$select->columns($cols, $this->_name);
	}
	return $select;
}

/**
 * Insere uma nova linha.
 *
 * @param array $data : valores para inserir (pares coluna => valor)
 * @return mixed : a chave primária da linha inserida
 */
public function insert(array $data) {
	$this->_setupPrimaryKey();

	$primary = (array) $this->_primary;
	$pkIdentity = $primary[(int) $this->_identity == 0 ? 1 : $this->_identity];

	$pkSuppliedBySequence = false;
	if(is_string($this->_sequence) && !isset($data[$pkIdentity])) {
		$data[$pkIdentity] = $this->_adapter->nextSequenceId();
		$pkSuppliedBySequence = true;
	}

	if($pkSuppliedBySequence === false && isset($data[$pkIdentity])) {
		$pkValue = $data[$pkIdentity];
		if(empty($pkValue) || is_bool($pkValue)) {
			unset($data[$pkIdentity]);
		}
	}

	$tableSpec = $this->_getTableSpec();
	$this->_adapter->insert($tableSpec, $data);

	// Busca o último id inserido na tabela que foi gerado por
	// auto-incremento, a menos que seja especificado um valor
	// sobrescrevendo o valor do auto-incremento
	if($this->_sequence === true && !isset($data[$pkIdentity])) {
		$data[$pkIdentity] = $this->_adapter->lastInsertId();
	}

	$pkData = array_intersect($data, array_flip($primary));

	//Se a chave primária não é composta, retorna o próprio valor
	if(count($pkData) == 1) {
		reset($pkData);
		return current($pkData);
	}

	return $pkData;
}

/**
 * Atualiza as linhas da tabela que satifazem a condição $cond.
 *
 * @param array $data : os dados para atualização
 * @param array|string $cond : a condição para atualização
 * @return integer : o número de linhas afetadas
 */
public function update(array $data, $cond) {
	$rowsAffected = 0;
	$tableSpec = $this->_getTableSpec();

	if($this->_integrityCheck === true && !empty($this->_dependentTables)) {
		$select = new Db_Table_Select($this);
		$oldData = (array) $this->_adapter->fetchAll($select);
		$pk = $this->info('PRIMARY');

		$oldPkData = array_intersect($oldData, array_flip($pk));
		$newPkData = array_intersect($data, array_flip($pk));

		foreach($this->_dependentTables as $depTable) {
			$childTable = new self(array(
			self::ADAPTER	=>	$this->_adapter,
			self::NAME		=>	$depTable,
			self::SCHEMA	=>	$this->_schema
			));
			$rowsAffected += $childTable->_cascadeUpdate($tableSpec, $oldPkData, $newPkData);
		}
	}

	$ret += $this->_adapter->update($tableSpec, $data, $cond);

	return $ret;
}

/**
 * Verifica se a coluna $column é identidade da tabela.
 *
 * @param string $column
 */
public function isIdentity($column) {
	$this->_setupPrimaryKey();
	if(!isset($this->_metadata[$column])) {
		throw new Db_Table_Exception(sprintf('Coluna "%s" não encontrada na tabela "%s".', $column, $this->_name));
	}

	return (bool) $this->_metadata[$column]['IDENTITY'];
}

/**
 * Chamado pela tabela-pai durante o método save().
 *
 * @param string $parentTableName
 * @param array $oldPrimaryKey
 * @param array $newPrimaryKey
 * @return int : o número de linhas afetadas
 */
private function _cascadeUpdate($parentTableName, array $oldPrimaryKey, array $newPrimaryKey) {
	// Código omitido...
}

/**
 * Remove as linhas da tabela que satisfaçam $cond.
 *
 * @param string $cond
 * @return integer : o número de linhas removidas
 */
public function delete($cond) {
	$rowAffected = 0;
	$tableSpec = $this->_getTableSpec();

	if($this->_integrityCheck === true && !empty($this->_dependentTables)) {
		$select = new Db_Table_Select($this);
		$data = (array) $this->_adapter->fetchAll($select);
		$pk = $this->info('PRIMARY');

		$pkData = array_intersect($data, array_flip($pk));
		foreach($this->_dependentTables as $depTable) {
			$childTable = new self(array(
			self::ADAPTER	=>	$this->_adapter,
			self::NAME		=>	$depTable,
			self::SCHEMA	=>	$this->_schema
			));
			$rowsAffected += $childTable->_cascadeDelete($tableSpec, $pkData);
		}
	}

	$rowsAffected += $this->_adapter->delete($tableSpec, $cond);
	return $rowsAffected;
}

/**
 * Chamado pela tabela-pai durante o método delete().
 *
 * @param string $parentTableName : o nome da tabela-pai
 * @param array $primaryKey : a chave primária da linha deletada
 * @return integer : o número de linhas removidas
 */
private function _cascadeDelete($parentTableName, array $primaryKey) {
	// Código omitido...
}

/**
 * Retorna uma string no formato <schema>.<table_name> se o schema
 * estiver setado ou no formato <table_name> caso contrário.
 *
 * @return string
 */
private function _getTableSpec() {
	return ($this->_schema ? $this->_schema . '.' : '') . $this->_name;
}

/**
 * Busca linhas pela chave primária.
 * Caso a chave seja composta, o argumento deve ser um array
 * contendo o mesmo número de elementos que a chave e na mesma ordem.
 *
 * @param mixed $pk : a chave primária do registro a ser buscado
 * @return Db_Table_Row
 */
public function getById($pk) {
	$this->_setupPrimaryKey();
	$keyNames = (array) $this->_primary;

	if(!is_array($pk)) {
		$pk = array($pk);
	}

	if(($n = count($pk)) != ($m = count($keyNames))) {
		throw new Db_Table_Exception(sprintf('A chave primária da tabela "%s" é composta por %d colunas.
														Foram passados %d valores para buscar.', $this->_name, $m, $n));
	}

	$condList = array();
	$numberTerms = 0;
	foreach($pk as $val) {
		$pos = key($keyNames);
		$col = current($keyNames);
		array_shift($keyNames);

		$type = $this->_metadata[$col]['DATA_TYPE'];
		$colName = $this->_adapter->quoteIdentifier($col);

		$condList[] = $this->_adapter->quoteInto($colName . ' = ?', $val, $type);
	}

	$cond = join(' AND ', $condList);
	return $this->fetchRow($cond);
}

/**
 * Busca todas as linhas da tabela que satisfaçam os critérios.
 *
 * @param string|array|Db_Select $where
 * @param string|array $order
 * @param int $count
 * @param int $offset
 * @return Db_Table_Rowset
 */
public function fetchAll($where = null, $order = null, $count = null, $offset = null) {
	if($where instanceof Db_Select) {
		$select = $where;
	} else {
		$select = $this->select();

		if($where !== null) {
			$this->_where($select, $where);
		}

		if($order !== null) {
			$this->_order($select, $order);
		}

		if ($count !== null || $offset !== null) {
			$select->limit($count, $offset);
		}
	}

	$rows = $this->_fetch($select);
	$readOnly = $select instanceof Db_Table_Select ? $select->isReadOnly() : false;

	$data = array(
			'table'		=>	$this,
			'data'		=>	$rows,
			'readOnly'	=>	$readOnly,
			'rowClass'	=>	$this->getRowClass(),
			'stored'	=>	true
	);

	$rowsetClass = $this->getRowsetClass();
	return new $rowsetClass($data);
}

/**
 * Busca uma linha na tabela que satisfaçãm os critérios.
 *
 * @param string|array|Db_Select $where
 * @param string|array $order
 * @param int $offset
 * @return Db_Table_Row|null : retorna a linha da tabela ou null caso não haja nenhuma.
 */
public function fetchRow($where = null, $order = null, $offset = null) {
	if($where instanceof Db_Select) {
		$select = $where->limit(1, $where->getPart(Db_Select::LIMIT_OFFSET));
	} else {
		$select = $this->select();

		if($where !== null) {
			$this->_where($select, $where);
		}

		if($order !== null) {
			$this->_order($select, $order);
		}

		$select->limit(1, (int) $offset);
	}

	$rows = $this->_fetch($select);
	if(empty($rows)) {
		return null;
	}

	$readOnly = $select instanceof Db_Table_Select ? $select->isReadOnly() : false;
	$data = array(
			'table'		=>	$this,
			'data'		=>	reset($rows),
			'readOnly'	=>	$readOnly
	);

	$rowClass = $this->getRowClass();
	return new $rowClass($data);
}

/**
 * Cria uma nova linha para a tabela.
 *
 * @param array $data : os dados para popular a nova linha
 * @param string $defaultSource : fonte dos valores padrão para as colunas
 */
public function createRow(array $data = array(), $defaultSource = null) {
	$cols = $this->_getCols();
	$defaults = array_combine($cols, array_fill(0, count($cols), null));

	if($defaultSource === null) {
		$defaultSource = $this->_defaultSource;
	}

	//TODO: verificar
	if($defaultSource == self::DEFAULT_ADAPTER) {
		$this->_setupMetadata();
		foreach($this->_metadata as $key => $data) {
			if($data['DEFAULT'] != null) {
				$defaults[$key] = $data['DEFAULT'];
			}
		}
	} else if($defaultSource == self::DEFAULT_CLASS && !empty($this->_defaultValues)) {
		foreach($this->_defaultValues as $key => $val) {
			if(isset($defaults[$key])) {
				$defaults[$key] = $val;
			}
		}
	}

	$config = array(
			'table'		=>	$this,
			'data'		=>	$defaults,
			'readOnly'	=>	false,
			'stored'	=>	false
	);

	$rowClass = $this->getRowClass();
	$row = new $rowClass($config);
	$row->setFromArray($data);

	return $row;
}

/**
 * Gera uma cláusula WHERE a partir de um array ou string.
 *
 * @param Db_Select $select
 * @param string|array $where
 * @return Db_Select
 */
private function _where(Db_Select $select, $where) {
	// Código omitido...
}

/**
 * Gera uma cláusula ORDER a partir de um array ou string.
 *
 * @param Db_Select $select
 * @param array|string $order
 * @return Db_Select
 */
private function _order(Db_Select $select, $order) {
	// Código omitido...
}

/**
 * Método de apoio para busca de linhas.
 *
 * @param Db_Select $select
 * @return array
 */
private function _fetch(Db_Select $select) {
	// Código omitido...
}
}

 

 

 

Db_Table_Row

 

 

<?php
class Db_Table_Row {
/**
 * Os dados das colunas da linha da tabela
 * @var array
 */
protected $_data = array();

/**
 * Este atributo é setado como uma cópia dos dados
 * quando estes são buscados na tabela do banco de dados
 * ou especificado como uma nova tupla no construtor ou
 * quando dados 'sujos' são enviados ao banco de dados.
 * @var array
 */
protected $_cleanData = array();

/**
 * Rastreia as colunas onde os dados foram atualizados,
 * para permitir operações de INSERT e UPDATE mais específicas
 * @var array
 */
protected $_modifiedFields = array();

/**
 * Instância do objeto Db_Table que criou este objeto
 * @var Db_Table
 */
protected $_table = null;

/**
 * Se TRUE, temos uma referência a um objeto Db_Table.
 * Será FALSE após a desserialização.
 * @var boolean
 */
protected $_connected = true;

/**
 * Uma linha pode ser marcada como somente leitura se ela contém
 * colunas que não são fisicamente representadas no schema da tabela
 * (ex.: colunas avaliadas/Db_Expressions).
 *
 * Isso também pode ser configurado em tempo de execução,
 * para proteger os dados da linha.
 * @var boolean
 */
protected $_readOnly = false;

/**
 * O nome da tabela do objeto Db_table
 * @var string
 */
protected $_tableName;

/**
 * As colunas que são chave primária da linha
 * @var array
 */
protected $_primary;

/**
 * Construtor.
 *
 * Parâmetros de configuração suportados:
 * - table		=>	(string) o nome da tabela ou a instância de Db_Table
 * - data		=>	(array) os dados das colunas nesta linha
 * - 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
 *
 * @param array $config
 * @throws Db_Table_Row_Exception
 */
public function __construct(array $config = array()) {
	if(isset($config['table'])) {
		if($config['table'] instanceof Db_Table) {
			$this->_table = $config['table'];
		} else if($config['table'] != null){
			$this->_table = $this->_getTableFromString($config['table']);
		}
		$this->_tableName = $this->_table->getName();
	}

	if(isset($config['data'])) {
		if(!is_array($config['data'])) {
			throw new Db_Table_Row_Exception('Os dados precisam ser um array.');
		}
		$this->_data = $config['data'];
	}

	if(isset($config['stored']) && $config['stored'] === true) {
		$this->_cleanData = $this->_data;
	}

	if (isset($config['readOnly']) && $config['readOnly'] === true) {
		$this->setReadOnly(true);
	}

	if (($table = $this->getTable())) {
		$info = $table->info();
		$this->_primary = (array) $info['primary'];
	}

	$this->init();
}

/**
 * Retorna o valor de um campo da tabela.
 *
 * @param string $columnName
 * @return mixed
 * @throws Db_Table_Row_Exception
 */
public function get($columnName) {
	if(!$this->exists($columnName)) {
		throw new Db_Table_Row_Exception(sprintf('A coluna "%s" não existe na tabela "%s"', $columnName, $this->_tableName));
	}
	return $this->_data[$columnName];
}

/**
 * Seta um valor para o campo da tabela.
 *
 * @param unknown_type $columnName
 * @param unknown_type $value
 * @return Db_Table_Row : fluent interface
 * @throws Db_Table_Row_Exception
 */
public function set($columnName, $value) {
	if(!$this->exists($columnName)) {
		throw new Db_Table_Row_Exception(sprintf('A coluna "%s" não existe na tabela "%s"', $columnName, $this->_tableName));
	}
	$this->_data[$columnName] = $value;
	$this->_modifiedFields[$columnName] = true;
	return $this;
}

/**
 * Verifica se o campo existe na tabela
 *
 * @param string $columnName
 * @return boolean
 */
public function exists($columnName) {
	return isset($this->_data[$columnName]);
}

/**
 * Remove o valor de um campo da tabela
 *
 * @param string $columnName
 * @return Db_Table_Row : fluent interface
 * @throws Db_Table_Row_Exception
 */
public function remove($columnName) {
	if(!$this->exists($columnName)) {
		throw new Db_Table_Row_Exception(sprintf('A coluna "%s" não existe na tabela "%s"', $columnName, $this->_tableName));
	}

	if($this->isConnected() && in_array($columnName, $this->_table->info(Db_Table::PRIMARY))) {
		throw new Db_Table_Row_Exception(sprintf('A coluna "%s" é chave primária na tabela "%s" e não pode ser removida', $columnName, $this->_tableName));
	}

	unset($this->_data[$columnName]);
	return $this;
}

/**
 * Retorna os dados para a serialização.
 * 
 * @return array
 */
public function __sleep() {
	return array('_tableName', '_primary', '_data', '_cleanData', '_modifiedFields', '_readOnly');
}

/**
 * Não é esperado que uma linha desserializada tenha
 * acesso a uma conexão ativa com o banco de dados
 * 
 * @return void
 */
public function __wakeup() {
	$this->_connected = false;
}

/**
 * Converte o objeto em um array.
 * 
 * @return array
 */
public function toArray() {
	return (array) $this->_data;
}

/**
 * Seta os dados do objeto a partir de um array.
 * 
 * @param array $data
 * @return Db_Table_Row : fluent interface
 */
public function setFromArray(array $data) {
	foreach($data as $key => $val) {
		if(isset($this->_data[$key])) {
			$this->set($key, $val);
		}
	}

	return $this;
}

/**
 * Inicializa o objeto.
 * É chamado no final do construtor {@link __construct()}
 */
public function init() {

}

/**
 * Retorna um objeto Db_Table ou null, se a linha está 'desconectada'.
 * 
 * @return Db_Table|null
 */
public function getTable() {
	return $this->_table;
}

/**
 * Seta uma tabela para o objeto para restabelecer a conexão
 * com o banco de dados para o objeto desserializado.
 * 
 * @param Db_Table $table
 * @return boolean
 * @throws Db_Table_Row_Exception
 */
public function setTable(Db_Table $table = null) {
	if($table === null) {
		$this->_table = null;
		$this->_connected = false;
		return false;
	}

	if($table->getName() != $this->_tableName) {
		throw new Db_Table_Row_Exception(sprintf('A tabela especificada "%s" não é a mesma configurada no objeto Db_Table_Row ("%s")', 
												$table->getName(), 
												$this->_tableName
										));
	}

	$this->_table = $table;
	$this->_tableName = $table->getName();
	$info = $this->_table->info();

	$tableCols = $info['cols'];
	$rowCols = array_keys($this->_data);

	if($tableCols != $rowCols) {
		throw new Db_Table_Row_Exception(sprintf('As colunas da tabela (%s) não são as mesmas colunas da linha (%s)',
												join(', ', $tableCols),
												join(', ', $rowCols)
										));
	}

	$tablePk = $info['primary'];
	$rowPk = (array) $this->_primary;
	if(!array_intersect($rowPk, $tablePk) == $rowPk) {
		throw new Db_Table_Row_Exception(sprintf('A chave primária da tabela (%s) não é a mesma da linha (%s)',
												join(', ', $tablePk),
												join(', ', $rowPk)
										));
	}

	$this->_connected = true;
	return true;
}

/**
 * Retorna uma instância do objeto Db_Table_Select
 * criado pelo objeto Db_Table pai deste objeto.
 * 
 * @param mixed $cols
 * @return Db_Table_Select
 * @throws Db_Table_Row_Exception
 */
public function select($cols = array()) {
	$table = $this->_getRequiredTable();
	return $table->select($cols);
}

/**
 * Salva as propriedades no banco de dados.
 * 
 * Realiza inserções e atualizações inteligentes e recarrega as 
 * propriedades com os valores atualizados da tabela em caso de sucesso.
 * 
 * @return mixed : a chave primária do registro
 * @throws Db_Table_Exception
 */
public function save() {
	if($this->isReadOnly()) {
		throw new Db_Table_Row_Exception('Este objeto Db_Table_Row está marcado como somente-leitura.');
	}
	/* Se _cleanData está vazio,
	 * temos uma operação de inserção,
	 * se não, temos uma atualização.
	 */
	if(empty($this->_cleanData)) {
		return $this->_doInsert();
	} else {
		return $this->_doUpdate();
	}
}

/**
 * Realiza a inserção dos dados da linha na tabela.
 * 
 * @return mixed : a chave-primária da linha inserida
 */
public function _doInsert() {
	// Lógica de pré-inserção
	$this->_preInsert();

	$data = array_intersect_key($this->_data, $this->_modifiedFields);
	$pk = $this->_getRequiredTable()->insert($data);

	if(is_array($pk)) {
		$newPk = $pk;
	} else {
		$tmpPk = (array) $this->_primary;
		$newPk = array(current($tmpPk) => $pk);
	}

	$this->_data = array_merge($this->_data, $newPk);

	// Lógica de pós-inserção
	$this->_postInsert();

	// Atualiza _cleanData para refletir os dados que foram inseridos
	$this->refresh();

	return $pk;
}

/**
 * Atualiza os dados da linha na tabela.
 * 
 * @return mixed: a chave primária da linha alterada
 */
protected function _doUpdate() {
	/* 
	 * Cria uma expressão para a cláusula WHERE
	 * com base no valor da chave primária
	 */
	$where = $this->_getWhereQuery(false);

	// Lógica de pré-atualização
	$this->_preUpdate();

	// Descobre quais colunas foram modificadas.
	$diffData = array_intersect_key($this->_data, $this->_modifiedFields);

	$table = $this->_getRequiredTable();

	// Atualiza apenas se houver dados alterados
	if(!empty($diffData)) {
		$table->update($diffData, $where);
	}

	// Lógica de pós-atualização
	$this->_postUpdate();

	/* Atualiza os dados caso triggers no SGBD 
	 * tenham alterado o valor de qualquer coluna.
	 * Também reseta _cleanData 
	 */		
	$this->refresh();

	$pk = $this->_getPrimaryKey(true);
	if(count($pk) == 1) {
		return current($pk);
	}

	return $pk;
}

/**
 * Remove a linha da tabela
 * 
 * @throws Db_Table_Row_Exception
 * @return int : o número de linhas removidas
 */
public function delete() {
	if($this->isReadOnly()) {
		throw new Db_Table_Row_Exception('Este objeto Db_Table_Row está marcado como somente-leitura.');
	}

	/* 
	 * Cria uma expressão para a cláusula WHERE
	 * com base no valor da chave primária
	 */
	$where = $this->_getWhereQuery(false);

	// Lógica de pré-remoção
	$this->_preDelete();

	$table = $this->_getRequiredTable();

	// Executa a remoção
	$result = $table->delete($where);

	// Lógica de pós-remoção
	$this->_postDelete();

	/*
	 * Seta todas as colunas com o valor NULL
	 */
	$this->_data = array_combine(
		array_keys($this->_data),
		array_fill(0, count($this->_data), null)
	);

	return $result;
}

/**
 * Se o objeto Db_Table for necessário para executar uma operação,
 * deve-se invocar este método para buscá-lo.
 * Ele lançará uma excessão caso a tabela não seja encontrada.
 *
 * @return Db_Table
 * @throws Db_Table_Row_Exception
 */
protected function _getRequiredTable() {
	$table = $this->getTable();
	if(!$table instanceof Db_Table) {
		throw new Db_Table_Row_Exception('O objeto Db_Table_Row não está associado a um objeto Db_Table.');
	}
	return $table;
}

/**
 * Retorna um array associativo contendo a chave primária da linha
 * 
 * @param boolean $useDirty
 * @throws Db_Table_Row_Exception
 */
protected function _getPrimaryKey($useDirty = false) {
	if(!is_array($this->_primary)) {
		throw new Db_Table_Row_Exception('A chave primária deve estar setada como um array.');
	}

	$pk = array_flip($this->_primary);
	if($useDirty) {
		$array = array_intersect_key($this->_data, $primary);
	} else {
		$array = array_intersect_key($this->_cleanData, $primary);
	}

	if(count($pk) != count($array)) {
		throw new Db_Table_Row_Exception(sprintf(
											'A tabela especificada "%s" não possui a mesma chave primária (%s) que a linha (%s).',
											$this->_tableName,
											join(', ', $pk),
											join(', ', $array)
										));
	}

	return $array;
}

/**
 * Gera uma cláusula WHERE com base na chave primária da linha.
 * 
 * @param boolean $useDirty
 * @return array : array de cláusulas WHERE
 */
protected function _getWhereQuery($useDirty = true) {
	$where = array();

	$adapter = $this->_table->getAdapter();

	$pk = $this->_getPrimaryKey($useDirty);

	$info = $this->_table->info();
	$tableName = $tableName = $adapter->quoteIdentifier($info[Db_Table::NAME]);
	$metadata = $info[Db_Table::METADATA];

	$where = array();
	foreach($pk as $col => $val) {
		$type = $metadata[$col]['DATA_TYPE'];
		$colName = $adapter->quoteIdentifier($col);
		$where[] = $adapter->quoteInto($tableName . '.' . $colName . ' = ?', $val);
	}
	return $where;
}

/**
 * Atualiza os dados do objeto com os dados da linha
 * da tabela no banco de dados.
 * 
 * @return void
 * @throws Db_Table_Row_Exception
 */
public function refresh() {
	$where = $this->_getWhereQuery();
	$row = $this->_getRequiredTable()->fetchRow($where);

	if($row === null) {
		throw new Db_Table_Row_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->_modifiedFields = array();
}

/**
 * Lógica de pré-inserção
 */
protected function _preInsert() {

}

/**
 * Lógica de pós-inserção
 */
protected function _postInsert() {

}

/**
 * Lógica de pré-atualização
 */
protected function _preUpdate() {

}

/**
 * Lógica de pós-atualização
 */
protected function _postUpdate() {

}

/**
 * Lógica de pré-remoção
 */
protected function _preDelete() {

}

/**
 * Lógica de pós-remoção
 */
protected function _postDelete() {

}

/**
 * Prepara uma referência para uma tabela.
 * 
 * Assegura que todas as referências estão setadas
 * e devidamente formatadas.
 * 
 * @param Db_Table $dependent
 * @param Db_Table $parent
 * @param string|null $ruleKey
 * @return array
 */
protected function _prepareReference(Db_Table $dependent, Db_Table $parent, $ruleKey = null) {
	$parentName = $parent->getName();
	$map = $dependent->getReference($parentName, $ruleKey);

	if(!isset($map[Db_Table::REF_COLUMNS])) {
		$parentInfo = $parent->info();
		$map[Db_Table::REF_COLUMNS] = array_values((array) $parentInfo['primary']);
	}

	$map[Db_Table::COLUMNS] = (array) $map[Db_Table::COLUMNS];
	$map[Db_Table::REF_COLUMNS] = (array) $map[Db_Table::REF_COLUMNS];

	return $map;
}

/**
 * Encontra o conjunto de linhas dependente deste objeto.
 * 
 * @param Db_Table|string $dependentTable
 * @param string|null $ruleKey
 * @param Db_Select $select
 * @return Db_Table_Rowset
 * @throws Db_Table_Row_Exception
 */
public function findDependentRowset($dependentTable, $ruleKey = null, Db_Select $select = null) {
	// Código omitido...
}

/**
 * Retorna a linha pai deste objeto.
 * 
 * @param Db_Table|string $parentTable
 * @param string $ruleKey
 * @param Db_Select $select
 * @return Db_Table_Row|null
 * @throws Db_Table_Row_Exception
 */
public function findParentRow($parentTable, $ruleKey = null, Db_Select $select = null) {
	// Código omitido...
}

/**
 * Retorna as linhas associadas ao objeto atual 
 * em um relacionamento N:N
 * 
 * @param Db_Table|string $matchTable
 * @param Db_Table|string $intersectionTable
 * @param string $callerRefRule
 * @param string $matchRefRule
 * @param Db_Select $select
 * @return Db_Table_Rowset
 * @throws Db_Table_Row_Exception
 */
public function findManyToManyRowset($matchTable, $intersectionTable, $callerRefRule = null, 
									 $matchRefRule = null, Db_Select $select = null) {
	// Código omitido...
}

/**
 * Retorna uma instância de Db_Table ou classe derivada
 * a partir de uma string contendo o nome da tabela ou classe.
 * @param string $tableName
 * @return Db_Table
 */
protected function _getTableFromString($tableName) {
	try {
		if(class_exists($tableName)){
			return new $tableName($options);
		}
	} catch (Exception $e) {
		$options = array();
		if($table = $this->getTable()) {
			$options['db'] = $table->getAdapter();
		}
		$options['name'] = $tableName;

		return new Db_Table($options);
	}
}
}

 

 

 

Henrique Barcelos, poste seus diagramas!

Vou tentar montá-los e posto...

Compartilhar este post


Link para o post
Compartilhar em outros sites

Acho que entendi. Toda essa confusão foi por causa dos métodos find...() que coloquei.

 

Posta um exemplo de uso dessas suas duas classes também?

Compartilhar este post


Link para o post
Compartilhar em outros sites

Pois é happy.gif

 

Só estou aguardando a resposta do Henrique com o que pedi para ver como seria SEM cada entidade estender o Mapper. Uma vez sem esses find...()'s, não aparenta ser um dano moral às leis e conceitos da OOP, mas se der pra fazer melhor...

 

Pois é happy.gif

 

Só estou aguardando a resposta do Henrique com o que pedi para ver como seria SEM cada entidade estender o Mapper. Uma vez sem esses find...()'s, não aparenta ser um dano moral às leis e conceitos da OOP, mas se der pra fazer melhor...

nao fere os conceitos do OOP, mas dos padroes de projeto.... sao coisas diferentes...

Compartilhar este post


Link para o post
Compartilhar em outros sites

Pois é, apesar de eu saber que são coisas diferentes, como ambos andam juntos, vez ou outra associo um como sendo o outro.

 

Mas já que tocou no assunto, por que fere? O que há de errado com essa abordagem? Por favor esqueça os find...()'s na sua resposta (se não vamos dar voltas e voltas à toa :P).

Compartilhar este post


Link para o post
Compartilhar em outros sites

Não entendi pq você disse que fere os princípios de OO...

 

Mas enfim, exemplo?

$newsGtw = new Db_Table('news');$news = $newsGtw->getById((int) $_GET['id']); // busca uma notícia por ID$newsPictures = $news->findDependentRowset('news_picture'); // Busca as imagens cadastradas para aquela notícia.$text = $news->get('text');$news->set('title', htmlentitydecode($text)); // O texto da notícia normalmente contém HTML

Percebeu que eu só interajo com Table e TabeRow?

$newsGtw = new Db_Table('news');$data = $newsGtw->fetchAll();$someNews = $newsGtw->find(1, 18, 500); // Retorna as notícias com ID 1, 18 e 500...

Adapters (Drivers), Statements, etc não ficam visíveis normalmente.

 

Agora na hora de criar os Application Models, aí sim você precisará de um para cada entidade e aí sim caberia a herança.

Normalmente, as operações de CRUD são idênticas para a maioria das entidades, então você pode definir os métodos save e delete padrão numa classe abstrata, por exemplo.

 

Caso alguma entidade envolva algum tipo de operação diferente, você simplesmente vai lá e sobrescreve o método save e/ou delete.

 

Qual a vantagem dessa abordagem?

Que o Domain Model se auto-define, você não precisa se preocupar com ele, mudanças no banco de dados automaticamente refletem na aplicação (ou quase isso, lembre-se do que eu falei sobre os mapeamentos de FKs).

 

No Application Model, onde estão validações, filtros, etc, aí sim é algo quase que completamente "manual", principalmente nas operações de Retrieve, que variam muito de uma entidade pra outra, aí sim você concentra seus esforços.

 

Já é bem trabalhoso ter que definir coisas no AppModel, no Controller e na View (no caso das duas últimas, é um par para cada entidade para cada módulo), você ter que se preocupar também com o Domain Model é sacanagem (acredite em mim, já fiz dessa forma, é tão ruim que eu só aguentei desenvolver 1 sistema nesse modelo).

 

Pois é, apesar de eu saber que são coisas diferentes, como ambos andam juntos, vez ou outra associo um como sendo o outro.

 

Mas já que tocou no assunto, por que fere? O que há de errado com essa abordagem? Por favor esqueça os find...()'s na sua resposta (se não vamos dar voltas e voltas à toa tongue.gif).

bom... mas tenho varias profissoes, e dentre elas esta a de musico, e sempre vejo algo com os varios olhares, nisto, me vem algo q aprendi com a musica: se nao souber o q vem antes, você nao sabe o q vem depois, ex. se você nao souber o q em tom e semitom você nao vai saber o q fazem os sustenisdos e bemois, se nao souber o q fazem os bemois e sustenidos, você nao sabera intervalos entre uma nota e outra, e concomitantemente nao sabera classificar os acordes e tons das escalas...ou seja, você esqueceu de algo muito basico: a diferenca entre os padroes de projeto e OO: OO eh uma forma de abstrair o mundo real à programacao, assim como padroes de projeto tem o objetivo de resolver determinados problemas e uniformizar a programacao (para o caso de 2 programadores pegar o mesmo projeto, mesmo sistema a desenvolver), com isto eu posso dizer q posso ter padroes de projeto sem OO e vice-versa, da certo? at certo ponto...mas na verdade sao 2 pontas de uma linha que formam um unico circulo...respondendo sua pergunta: pq fere? simples, fez tentou implementar um padrao, q nao resolveu um problema, sendo assim nao o implementou corretamente...f.oda eh q muitos exemplos sao em java ou C++( nao sei você , mas eu nao entendo estas linguagens), eu teria certa dificuldade...lembre-se um padrao de projeto tem um probema a resolver, um contexto, e uma implementacao...outra coisa q aprendi com a musica eh q a teoria nao eh nada sem a pratica, e mais importante a pratica nao eh nada sema a teoria...programacao nao eh tao importante quanto os conceitos...a teria...

Compartilhar este post


Link para o post
Compartilhar em outros sites

×

Informação importante

Ao usar o fórum, você concorda com nossos Termos e condições.