Ir para conteúdo

Arquivado

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

João Batista Neto

O Que é MVC - A Model

Recommended Posts

Bom, como a primeira camada a ser separada foi a Model, vamos implementá-la primeiro também.

 

Para implementar a Model precisaremos pensar um pouco em como as coisas são feitas:

 

1. Um usuário faz uma requisição

2. Um Controller intersepta essa requisição

3. A Model apropriada é acionada

4. Os dados são recuperados de algum dispositivo de armazenamento (não necessariamente banco de dados)

5. Os dados são passados para a View

6. A View itera esses dados e cria uma estrutura lógica para ser renderizada.

7. O solicitante recebe os dados.

 

Como estamos falando da Model, e nossa aplicação usará um banco de dados, vamos primeiro diagramar nossas tabelas:

Imagem Postada

 

Com exceção da UserArticle que é InnoDB, as tabelas User e Article são MyISAM, o projeto do WorkBench está disponível para download em: Articles.mwb

 

Bom, precisaremos implementar o CRUD, é claro que poderíamos simplesmente sair criando as consultas e iterando os resultados, mas ai iríamos de encontro com uma das grandes vantagens da programação orientada a objetos que é a reutilização, veja:

 

class User {
private $idUser;
private $userEmail;
private $userName;
private $userPswd;

public function getId(){
	return $this->idUser;
}

public function getEmail(){
	return $this->userEmail;
}

public function getName(){
	return $this->userName;
}

public function setEmail( $email ){
	$this->userEmail =& $email;
}

public function setName( $name ){
	$this->userName =& $name;
}

public function setPswd( $pswd ){
	$this->userPswd =& $pswd;
}
}

 

$pdo = new PDO( $dsn , $user , $pswd );
$stm = $pdo->prepare( 'SELECT * FROM `User` WHERE `userEmail`=:userEmail AND `userPswd`=:userPswd LIMIT 1;' );
$stm->setFetchMode( PDO::FETCH_CLASS , 'User' );
$stm->bindParam( ':userEmail' , 'user' , PDO::PARAM_STR );
$stm->bindParam( ':userPswd' , 'pswd' , PDO::PARAM_STR );

if ( $stm->execute() ){
$user = $stm->fetch();
}

 

Essa implementação resolve muito bem um problema, mas cria um novo; Estamos presos em uma implementação, se basearmos nossa aplicação em uma implementação assim teremos que ter várias implementações para cada tipo de caso e com isso acabaremos nos repetindo.

 

É claro que podemos utilizar um objeto específico para acesso a dados na nossa Model:

 

class UserDAO {
private $pdo;

public function setPDO( PDO $pdo ){
	$this->pdo =& $pdo;
}

public function login( $user , $pswd ){
	$user = false;

	$stm = $this->pdo->prepare( 'SELECT * FROM `User` WHERE `userEmail`=:userEmail AND `userPswd`=:userPswd LIMIT 1;' );
	$stm->setFetchMode( PDO::FETCH_CLASS , 'User' );
	$stm->bindParam( ':userName' , $user , PDO::PARAM_STR );
	$stm->bindParam( ':userPswd' , $pswd , PDO::PARAM_STR );

	if ( $stm->execute() )
	$user = $stm->fetch();

	return $user;
}

public function userList(){
	$ret = array();
	$stm = $this->pdo->query( 'SELECT * FROM `User`;' );
	$stm->setFetchMode( PDO::FETCH_CLASS , 'User' );

	foreach ( $stm->fetchAll( PDO::FETCH_CLASS ) as $row )
	$ret[] = $row;

	return $ret;
}
}

class UserModel {
private $pdo;
private $dao;

public function __construct(){
	$this->dao = new UserDAO();
}

public function setPDO( PDO $pdo ){
	$this->pdo =& $pdo;
	$this->dao->setPDO( $this->pdo );
}

public function login( $user , $pswd ){
	return $this->dao->login( $user , $pswd );
}

public function userList(){
	return $this->dao->userList();
}
}

 

Melhorou um pouco, mas pelo fato da UserModel conhecer e ela própria instanciar a DAO continuamos presos em uma única implementação, sem contar que estamos pré-supondo que vamos sempre trabalhar com banco de dados e ainda, que utilizaremos sempre PDO para fazer esse trabalho, isso nos traz 2 problemas:

 

Problema:

1. Pode surgir uma situação em que não vamos trabalhar com um determinado banco de dados ou precisemos trocare sim com webservices.

2. Podemos estar diante de um servidor que ainda não tem PDO habilitado.

 

Solução:

1. Precisamos remover o objeto da conexão da Model, esse não é o lugar correto para ela

2. Precisamos criar uma interface para o caso de precisarmos trabalhar com banco de dados e não termos PDO habilitado, dessa forma, saberemos trabalhar com qualquer mecanismo de consulta ao banco de dados.

 

Bom, se a Model não é o lugar para o banco de dados, onde é ?

 

Como agora temos um objeto específico (Data Access Object) para esse fim, a DAO passa a ser o lugar adequado para a conexão, afinal, poderemos utilizar apenas quando necessário e se necessário; Mas com isso criamos um novo problema:

 

Problema:

Se tivermos 10 DAO teremos 10 conexões; Isso é errado porque a conexão com cada banco de dados deve ser única para toda a aplicação.

 

Solução:

Criamos um objeto de conexão de forma que, caso haja uma conexão aberta, nós a utilizemos e caso ainda não haja nós abrimos uma nova.

 

Podemos usar o padrão de projeto Singleton para isso:

 

class Singleton {
private static $instance;

protected function __construct(){
	echo 'Construindo Singleton.';
}

public static function instance(){
	if ( self::$instance == null ) self::$instance = new Singleton();

	return self::$instance;
}
}

$obj = Singleton::instance();
$obj = Singleton::instance();
$obj = Singleton::instance();
$obj = Singleton::instance();

 

A saída será apenas:

 

Construindo Singleton.

 

Mesmo que chamemos o método instance de Singleton 100 vezes, o objeto será construído apenas uma vez e, pelo fato de o construtor ser definido como protected, ele não poderá ser instanciado fora da classe. O Padrão Singleton como o próprio nome deixa claro, oferece acesso controlado a uma única instância de um objeto. Mas, se basearmos nossas operações com banco de dados em uma única instância poderemos ter problemas no futuro se precisarmos trabalhar com dois SGBD, afinal, só é necessário 1 conexão com cada banco de dados mas pode ocorrer de precisarmos ter 2 servidores de banco de dados e com isso teremos 1 conexão para cada servidor, resultando 2 conexões e com apenas uma instância isso não será possível.

 

Para resolver a questão da conexão única com o banco de dados e, caso necessário, ter mais uma conexão com um outro servidor podemos utilizar um padrão chamado Registry para manter nossas conexões únicas com banco de dados e as instâncias controladas com um acesso nomeado:

 

class Registry {
private static $storage = array();

public function get( $key ){
	$ret = null;

	if ( isset( self::$storage[ $key ] ) ) $ret =& self::$storage[ $key ];

	return $ret;
}

public static function register( $key , $value ){
	self::$storage[ $key ] = $value;
}

public static function unregister( $key ){
	if ( isset( self::$storage[ $key ] ) ) unset( self::$storage[ $key ] );
}
}

 

class DB {
private static $instances = 0;

private $instanceNum = 0;

public function __construct(){
	echo 'Contruindo DB' , PHP_EOL;
	$this->instanceNum = ++self::$instances;
}

public function getInstanceNum(){
	return $this->instanceNum;
}
}

 

Com a Registry conseguimos manter o acesso controlado a nossa instância de conexão e ainda manter a instância acessível de forma global:

Registry::register( 'db1' , new DB() );
Registry::register( 'db2' , new DB() );

echo Registry::get( 'db1' )->getInstanceNum() , PHP_EOL;
echo Registry::get( 'db2' )->getInstanceNum() , PHP_EOL;

 

Mas ainda não é suficiente, se utilizarmos Registry obrigaremos nossas DAO a verificarem se o registro existe e caso não exista elas terão que criá-lo. Isso é um grande problema porque, com as DAO tendo que criar um registro com o objeto, elas terão que conhecer a classe que a classe que irá instanciar, com isso teremos que ter várias implementações para cada caso ou então teremos que prever cada caso com switchs ou ifs. Se não nos repetirmos iremos com certeza ter que editar nosso código em cada nova situação.

 

Precisamos de uma solução mais abstrata, uma que reflita o que estamos precisando mas que não nos prenda a uma implementação, alguma coisa que permita que possamos conectar a qualquer SGBD e se necessário, termos mais de uma conexão; Mas que também nos permita recuperar os dados de um webservice ou qualquer que seja o mecanismo de armazenamento de dados.

 

Vamos abstrair o acesso a dados da DAO, criando uma interface que nos permita acessar qualquer coisa de qualquer lugar:

 

interface IDataAccessObject {
public function create( AbstractDataObject $do );
public function read( AbstractDataObject $do , $count = 25 , $offset = 0 );
public function update( AbstractDataObject $do );
public function delete();
public function setCriteria( ICriteria $criteria );
}

 

A interface IDataAccessObject oferecerá um acesso comum para qualquer tipo de mecanismo de armazenamento, seja ele um SGBD ou um webservice. Agora, precisaremos também de uma interface que nos permita criar objetos com os dados que serão gravados ou recuperados.

 

Vamos precisar também de uma interface para definir as condições do acesso às informações:

 

interface ICriteria extends IteratorAggregate {
const COND_EQUAL = 1;
const COND_MATCH = 2;

public function addCond( AbstractField $field , $type = self::COND_EQUAL );
}

 

A interface ICriteria ficou simples assim de propósito, na nossa aplicação de exemplo será apenas isso é suficiente.

 

Como estamos trabalhando com objetos e a única forma de sabermos como trabalhar com um determinado objeto é através de sua interface, vamos também criar uma para definir um perfil de conexão:

 

interface IConnectionProfile {
public function getHost();
public function getName();
public function getPswd();
public function getUser();
}

 

E uma implementação do perfil:

 

class ConnectionProfile implements IConnectionProfile {
private $host;
private $name;
private $pswd;
private $user;

public function __construct( $host , $name , $user , $pswd ){
	 $this->host =& $host;
	 $this->name =& $name;
	 $this->user =& $user;
	 $this->pswd =& $pswd;
}

public function getHost(){
	 return $this->host;
}

public function getName(){
	 return $this->name;
}

public function getPswd(){
	 return $this->pswd;
}

public function getUser(){
	 return $this->user;
}
}

 

Certo, agora, finalmente, criaremos a base da IDataAccessObject:

abstract class AbstractDatabaseAccess implements IDataAccessObject {
private $pdo;
private static $registry = array();

abstract protected function __construct( IConnectionProfile $profile );

final public static function createInstance( $name , IConnectionProfile $profile ){
	 $class = get_called_class();

	 $ret = new $class( $profile );
	 self::$registry[ $name ] =& $ret;

	 return $ret;
}

final public static function instance( $name = null ){
	 $ret = null;

	 if ( $name !== null ){
		 if ( isset( self::$registry[ $name ] ) ) $ret =& self::$registry[ $name ];
		 else throw new InvalidArgumentException( sprintf( 'O registro %s não foi encontrado.' , $name ) );
	 } else if ( count( self::$registry ) ){
		 $ret =& current( self::$registry );
	 } else {
		 throw new BadMethodCallException( 'Nenhuma instância do objeto de conexão com o banco de dados registrada' );
	 }

	 return $ret;
}
}

 

E a implementação para banco de dados MySQL que usaremos em nossa aplicação:

 

final class MySQLDatabaseAccess extends AbstractDatabaseAccess {
private $criteria;

protected function __construct( IConnectionProfile $profile ){
	 $this->pdo = new PDO(
		 sprintf( 'mysql:host=%s;dbname=%s' , $profile->getHost() , $profile->getName() ),
		 $profile->getUser(),
		 $profile->getPswd()
	 );
}

public function create( AbstractDataObject $do ){
}

public function read( AbstractDataObject $do , $count = 25 , $offset = 0 ){
	 $ret = array();
	 $sql = sprintf( 'SELECT `%s` FROM `%s`' , implode( '`,`' , $do->getFieldNames() ) , $do->getName() );

	 $stm = $this->parseCriteria( $sql , sprintf( ' LIMIT %d,%d' , $offset , $count ) );

	 if ( $stm->execute() ){
		 foreach ( $stm->fetchAll( PDO::FETCH_OBJ ) as $stmobj ){
			 $obj = clone( $do );

			 foreach ( get_object_vars( $stmobj ) as $name => $value ) $obj->$name = $value;

			 $ret[] = $obj;
		 }
	 }

	 return $ret;
}

public function update( AbstractDataObject $do ){

}
public function delete(){

}

public function setCriteria( ICriteria $criteria ){
	 $this->criteria =& $criteria;
}

private function parseCriteria( &$sql , $append = null ){
	 if ( $this->criteria ){
		 $criterias = array();

		 foreach ( $this->criteria as $cond ){
			 $name = $cond[ 0 ]->getName();
			 $criterias[] = sprintf( '`%s`=:%s' , $name , $name );
		 }

		 $sql .= sprintf( ' WHERE %s' , implode( ' AND ' , $criterias ) );
	 }

	 $stm = $this->pdo->prepare( sprintf( '%s%s' , $sql , $append ) );

	 foreach ( $this->criteria as $cond )
		 $stm->bindParam( sprintf( ':%s' , $cond[ 0 ]->getName() ) , $cond[ 0 ]->getValue() , $cond[ 0 ]->getType() );

	 return $stm;
}
}

 

Como é possível notar, utilizamos o padrão Registry na nossa AbstractDatabaseAccess, com isso, nosso banco de dados terá uma única conexão aberta para toda a aplicação, mas também permitirá que tenhamos mais de uma conexão caso necessário.

 

Os dados que iremos recuperar ou gravar possuirão sempre algumas informações fundamentais:

 

1. O nome do campo da informação.

2. O valor que esse campo armazena.

3. O tipo do dado armazenado.

4. A origem do campo.

5. É um campo de identidade ?

 

interface IField {
const FIELD_NULL = 0;
const FIELD_NUMERIC = 1;
const FIELD_STRING = 2;
const FIELD_BOOLEAN = 5;

public function getName();
public function getValue();
public function getDataObject();
public function getType();
public function isIdentity();
public function setName( $name );
public function setValue( $value );
public function setDataObject( AbstractDataObject $da );
public function setType( $type );
}

 

Para evitar ficarmos nos repetindo sempre, vamos criar uma base para criação de uma classe Field:

 

abstract class AbstractField implements IField {
protected $identity = false;
private $DataObject;

public function getDataObject(){
	 return $this->DataObject;
}

public function isIdentity(){
	 return (bool) $this->identity;
}

public function setDataObject( AbstractDataObject $do ){
	 $this->DataObject = & $do;
}
}

 

Agora, com a interface totalmente definida, podemos criar uma classe concreta para um campo normal:

 

class Field extends AbstractField {
private $name;
private $value;
private $type = self::FIELD_STRING;

public function __construct( $name , $type = self::FIELD_STRING , $value = null ){
	 $this->setName( $name );
	 $this->setType( $type );
	 $this->setValue( $value );
}

public function getName(){
	 return $this->name;
}

public function getValue(){
	 return $this->value;
}

public function getType(){
	 return $this->type;
}

public function setName( $name ){
	 $this->name =& $name;
}

public function setValue( $value ){
	 $this->value =& $value;
}

public function setType( $type ){
	 $this->type =& $type;
}
}

 

E mais uma para um campo identidade:

 

final class IdentityField extends Field {
protected $identity = true;
}

 

Agora que temos definidos nossos campos, precisamos criar alguma coisa que identifique uma tabela:

 

abstract class AbstractDataObject {
private $name;
protected $fields = array();

final public function __construct(){
	 $this->setName( get_called_class() );
	 $this->configure();
}

public function __get( $name ){
	 if ( !isset( $this->$name ) )
		 throw new UnexpectedValueException( sprintf( 'O campo %s não está definido' , $name ) );

	 $field = & $this->fields[ $name ];

	 return $field->getValue();
}

public function __isset( $name ){
	 return isset( $this->fields[ $name ] ) && ( $this->fields[ $name ] instanceof AbstractField );
}

public function __set( $name , $value ){
	 if ( !isset( $this->fields[ $name ] ) ){
		 throw new UnexpectedValueException( sprintf( 'O campo %s não está definido' , $name ) );
	 }

	 $field =& $this->fields[ $name ];
	 $field->setValue( $value );
}

abstract protected function configure();

protected function addField( AbstractField $field ){
	 if ( $field->getDataObject() )
		 throw new LogicException( 'Um campo pode estar relacionado com apenas 1 DataObject.' );

	 $this->fields[ $field->getName() ] =& $field;
	 $field->setDataObject( $this );
}

public function getFields(){
	 return new ArrayIterator( $this->fields );
}

public function getFieldNames(){
	 return array_keys( $this->fields );
}

public function getName(){
	 return $this->name;
}

public function setName( $name ){
	 $this->name =& $name;
}
}

 

Um recurso interessante do PHP e que vale algum estudo são os métodos mágicos, com eles é possível fazer bastante coisa: http://br2.php.net/manual/en/language.oop5.magic.php

 

Com as interfaces de campos e campos identidade criadas, poderemos escrever a tabela User da seguinte forma:

 

class User extends AbstractDataObject {
protected function configure(){
	 $this->addField( new IdentityField( 'idUser' , Field::FIELD_NUMERIC ) );
	 $this->addField( new Field( 'userName' , Field::FIELD_STRING ) );
	 $this->addField( new Field( 'userEmail' , Field::FIELD_STRING ) );
	 $this->addField( new Field( 'userPswd' , Field::FIELD_STRING ) );
}
}

 

A interface da Model (bem simples):

interface IModel {
public function setDataAccessObject( IDataAccessObject $dao );
}

 

E uma Model para a User:

 

class UserModel implements IModel {
private $dao;

public function setDataAccessObject( IDataAccessObject $dao ){
	 $this->dao =& $dao;
}

public function login( $email , $pswd ){
	 $criteria = new Criteria();
	 $criteria->addCond( new Field( 'userEmail' , FIELD::FIELD_STRING , $email ) , Criteria::COND_EQUAL );
	 $criteria->addCond( new Field( 'userPswd' , FIELD::FIELD_STRING , $pswd ) , Criteria::COND_EQUAL );

	 $this->dao->setCriteria( $criteria );

	 return $this->dao->read( new User() , 1 );
}
}

 

Nesse momento temos uma model que não sabe de onde vai recuperar seus dados. Tudo o que a Model sabe é que ela pode trabalhar com duas ferramentas, um objeto do tipo ICriteria e um outro objeto do tipo IDataAccessObject. Com isso, conseguimos abstrair o acesso a dados da Model e, se precisarmos mudar de banco de dados MySQL para um arquivo XML a Model não precisará de nenhuma modificação; Na verdade, ninguém precisará ser modificada e apenas uma implementação da IDataAccessObject que faça o CRUD em um XML será necessária.

 

$mysql = MySQLDatabaseAccess::createInstance( 'mysql' , new ConnectionProfile( '127.0.0.1' , 'Articles' , 'user' , 'pswd' ) );

$model = new UserModel();
$model->setDataAccessObject( $mysql );
$model->login( 'email@dominio.com' , 'senha' );

 

Nesse parte do tutorial, as classes não foram finalizadas e todos os comentários do código foram retirados devido ao espaço que temos, quando terminarmos de falar sobre todas as três camadas, publicarei todo o código e a documentação de cada classe para estudo.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Esses seus artigos têm continuação? Eu não achei. Se não tiver, continua ae fera. Não existe um exemplo prático sequer de MVC em PHP, o que não me deixa montar uma opinião sólida sobre o padrão e acaba levando todo mundo pro lado dos frameworks. Eu não queria ir pra esse lado também.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Parabéns cara...

Ficou fodão! =]

 

Eu programo php a um bom tempo... mais acabei de ver que não sei muita coisa quando vejo seus codigos...

Sei programar e usar as funções do php, só que essas "teorias" "padrões" para facilitar o serviço realmente ajuda... Vale a pena dar uma estudada nisso sim...

Vou acompanhar seus tutoriais ;]

Sempre é bom aprender mais :P

 

Abraços!

Sucesso!

Compartilhar este post


Link para o post
Compartilhar em outros sites

Só não entendi muito bem sobre o Criteria, a classe Criteria é nativa?

Compartilhar este post


Link para o post
Compartilhar em outros sites

Salve,

 

Meus parabéns pelo conteúdo. Esta bem escrito e detalhado.

 

Dúvida,

 

Qual a necessidade da classe `UserModel` ter um atributo (membro) do tipo PDO se o mesmo não é utilizado ?

 

Não seria muito mais prático fazer assim ?

 

$this->dao->setPDO( &$pdo );

Compartilhar este post


Link para o post
Compartilhar em outros sites

Só não entendi muito bem sobre o Criteria, a classe Criteria é nativa?

 

Queria saber também, pois ele definiu a interface ICriteria

Compartilhar este post


Link para o post
Compartilhar em outros sites
Hmm... ótimo tutorial, mas fico com o simples e poderoso CodeIgniter

CodeIgniter é porco, pseudo-OO. Meu 'framework' caseiro não perde em muito para ele.

 

Bacana que o exemplo do JBN tem exatamente mesma a trajetória que os meus códigos :D.

E não tem jeito, você só aprende com o tempo...

 

Até hoje não utilizo nenhum framework pois primeiro quero entender completamente o funcionamento da 'roda'.

Na minha opinião, começar a utilizar um framework PHP antes de saber como ele funciona é a mesma coisa que querer aprender jQuery sem saber Javascript.

 

A minha maior dificuldade com o MVC incrivelmente é a View, que teoricamente é a camada mais simples. Fico no aguardo de mais uma aula do grande JBN =D

Compartilhar este post


Link para o post
Compartilhar em outros sites

Estava relendo este post, algumas observações a fazer.

Não está faltando a classe Criteria no exemplo?

Por que utilizar referências nas atribuições? O código não é compatível com o PHP4 pois utiliza palavras-chave como private, protected e public.

É realmente necessário definir uma classe IdentityField? Não seria melhor termos apenas uma flag indicando o tipo de campo em Field e assim haver apenas uma classe field?

class Field  {
const FIELD_NULL = 1;
const FIELD_NUMERIC = 2;
const FIELD_STRING = 4;
const FIELD_BOOLEAN = 8;

const FIELD_COMMON = 16;
const FIELD_INDENTITY = 32;
const FIELD_FOREIGN = 64; 

private $name;
private $value;
private $modifier = self::FIELD_COMMON;
private $type = self::FIELD_STRING;
//...

public function __construct( $name , $type = self::FIELD_STRING, $modifier = self::FIELD_COMMON , $value = null ){
        $this->setName( $name );
        $this->setType( $type );
        $this->setValue( $value );
        $this->setModifier($modifier);
}

public function isIdentity(){
        return $this->modifier == self::FIELD_IDENTITY
}
}

Compartilhar este post


Link para o post
Compartilhar em outros sites

Muito bom, gostei bastante. Aguardando a View e Controller (2)

 

Não vai ter mais continuação? :cry:

 

Não vai ter mais continuação?

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.