Ir para conteúdo

Arquivado

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

brhue

Como implementar SOLID sem violar seus principios ?

Recommended Posts

Estou estudando php e por consequência orientação objetos. Nesses últimos dias, me deparei com os princípios do SOLID (Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion) e tive algumas dúvidas. Como por exemplo, na tentativa de criar um "registration system", não consegui implementar os conceitos do SOLID, principalmente o SRP e OCP.

Com isso me deparei com algumas dúvidas, por exemplo:

 

Como escolher os nomes das classes e suas funções (objetivo, etc) ?

Como relacionar os objetos (banco de dados, usuario, etc) ?

Como deve ser a estrutura de classes(ex: DataBase, User, User Service) para um 'registration system' ou similar ?

Como saber se estou violando ou não os principios do SOLID (dicas, etc) ?

Como deveria ser implementada as classes abaixo, principalmente a DataBase ?

 

 

Segue a tentativa:

 

DataBase.class.php

<?php

require_once 'config.php';

class DataBase {

	private $DB;
	private $table = 'cadastro';
	
	public function __construct() {
		$this->DB = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME, DB_USER, DB_PASS);
	}

	public function insertUser($name, $lastname, $email, $password) {

		$sql  = "INSERT INTO {$this->table} (name, lastname, email, password) VALUES (:name, :lastname, :email, :password)";
		$stmt = $this->DB->prepare($sql);

		$stmt->bindParam(':name', $name, PDO::PARAM_STR);
		$stmt->bindParam(':lastname', $lastname, PDO::PARAM_STR);
		$stmt->bindParam(':email', $email, PDO::PARAM_STR);
		$stmt->bindParam(':password', $password, PDO::PARAM_STR);
	
		return $stmt->execute(); 
	}

	public function findUserByEmail($email) {
		//...
	}

	public function findAll() {
		//...
	}
	//...
}

?>

User.class.php

<?php

class User {

	private $id;
	private $name;
	private $lastname;
	private $email;
	private $password;

	public function __construct($name = null, $lastname = null, $email = null, $password = null) {
		$this->name = $name;
		$this->lastname = $lastname;
		$this->email = $email;
		$this->password = $password;
	}

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

	public function setId($id) {
		$this->id = $id;
	}

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

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

	public function getLastname() {
		return $this->lastname;
	}

	public function setLastname($lastname) {
		$this->lastname = $lastname;
	}

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

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

	public function getPassword() {
		return $this->password;
	}

	public function setPassword($password) {
		$this->password = $password;
	}
}

?>

UserAction.class.php

<?php

class UserAction {

	public function register($user, $database) {
		return $database->insertUser($user->getName(), $user->getLastname(), $user->getEmail(), $user->getPassword());
	}
        /*
	public function login($user, $database) {
		//...
	} */
	//...
}

?>

register.php

<?php

require_once 'autoload.php';

$name = (isset($_POST['name']))?$_POST['name']:null;
$lastname = (isset($_POST['lastname']))?$_POST['lastname']:null;
$email = (isset($_POST['email']))?$_POST['email']:null;
$password = (isset($_POST['password']))?$_POST['password']:null;


$user = new User($name, $lastname, $email, $password);
$database = new DataBase();
$action = new UserAction();

$action->register($user, $database);
//...

?>

Se puderem me indicar materias ou exemplos para prática ficarei grato.

Obrigado !

Compartilhar este post


Link para o post
Compartilhar em outros sites

Apesar do exemplo CRUD ser o mais demonstrado/estudado, é um dos mais difíceis para se iniciar no mundo da orientação à objetos. Isso pode ser visto/demonstrado através dos inúmeros modelos (vide Anemic Model) e outros patterns (DataMapper, DAO, TableDataGateway, etc..).

 

O problema fica no ponto que não consegue ser 100% abstrato, visto que cada SGBD possui suas particularidades e isso influencia muito no desenvolvimento. Dessa forma, cada proposta possui seus pontos positivos e negativos.

 

Como escolher os nomes das classes e suas funções (objetivo, etc) ?

Nomear classes/bibliotecas é um dos pontos mais difíceis da programação. Entretanto, alguns pontos devem ser considerados, os principais são o que a classe/biblioteca se propõe a realizar e qual o domínio dela.

 

Robert Martin escreve um capítulo inteiro sobre isso no seu livro Clean Code.

 

Como relacionar os objetos (banco de dados, usuario, etc) ?

É mais simples de responder do que entender. Na programação, existe dois tipos básicos de relacionamento: herança e associação.

 

No básico, se definem da seguinte forma:

- O objeto X é um objeto Y? Se sim, usa-se herança;

- O objeto X usa um objeto Y? Se sim, usa-se associação.

 

Após o básico, vem os diferentes tipos de associação (simples, agregação e composição). No link abaixo há um bom exemplo sobre os tipos de associações:

http://forum.imasters.com.br/topic/458278-como-pensar-em-orientacao-a-objetos/page-2#entry1815097

 

Outro ponto importante, é estudar a modelagem através de alguns diagramas, como o Diagrama de Classes para criar/entender o seu relacionamento sem uso e o Diagrama de Sequência que demonstra a interação entre os participantes através de uma linha do tempo.

 

Como deve ser a estrutura de classes(ex: DataBase, User, User Service) para um 'registration system' ou similar ?

Depende da abordagem. Eu, particularmente, gosto do DataMapper. Entretanto, o DataMapper peca pela granularidade, ele exige muitas classes, pois cada uma define um propósito (Mapper, Factory, Collection, Entity). Caso você for usar o DataMapper completamente, existem mais participantes (UnitOfWork, IdentityMap). A parte interessante do DataMapper, fica no completo desacoplamento da base de dados.

 

No link abaixo existe um exemplo interessante sobre DataMapper:

http://blog.tekerson.com/2008/12/17/data-mapper-pattern-in-php/

 

O TableDataGateway também é bem interessante. Ele é mais simples que o DataMapper, tem uma granularidade menor (o que é muito bom) e mantém a mesma finalidade, desacoplar o SGBD. O Henrique Barcelos escreveu um bom artigo sobre o padrão.

http://imasters.com.br/linguagens/php/padroes-tabledatagateway-e-tablerowgateway-teoria-e-pratica/

 

Como saber se estou violando ou não os principios do SOLID (dicas, etc) ?

Prática, somente a prática. Nos links abaixo, existem muitos exemplos sobre OO:

http://www.oodesign.com - http://www.oodesign.com/design-principles.html

https://sourcemaking.com - https://sourcemaking.com/refactoring

 

Apesar do SourceMaking não ter sobre Design Principles, ele possui sobre Refactoring, que é uma área aonde o SOLID se encaixa muito bem e vice-versa.

 

 

Como deveria ser implementada as classes abaixo, principalmente a DataBase ?

Depende da abordagem. Acredito que nos exemplos acima listados, você pode ter uma pequena noção de qual abordagem irá desejar.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Eu tinha começado a escrever um textão ontem, mas fiquei com sono e não terminei. O Gabriel falou basicamente tudo que eu iria falar (e de forma bem mais resumida :lol:).

 

Uma última dica que eu dou é a seguinte:

- http://br.phptherightway.com/

 

Use isso como guia.

Compartilhar este post


Link para o post
Compartilhar em outros sites

DataBase->insertUser
já é uma violação, responsabilidade no lugar errado.

 

A tua class User é como um VO (ValueObject) que eu considero um desperdício de memória, por estar indo para o lado do ActiveRecord.

 

Eu teria um DAO e pronto.

Compartilhar este post


Link para o post
Compartilhar em outros sites

O TableDataGateway também é bem interessante. Ele é mais simples que o DataMapper, tem uma granularidade menor (o que é muito bom) e mantém a mesma finalidade, desacoplar o SGBD. O Henrique Barcelos escreveu um bom artigo sobre o padrão.

http://imasters.com.br/linguagens/php/padroes-tabledatagateway-e-tablerowgateway-teoria-e-pratica/

 

- Gostei bastante desse padrão, depois vou tentar botar ele em prática.

 

 

Uma última dica que eu dou é a seguinte:

- http://br.phptherightway.com/

 

Use isso como guia.

 

- Bastante útil.

 

 

A tua class User é como um VO (ValueObject) que eu considero um desperdício de memória, por estar indo para o lado do ActiveRecord.

 

- Como assim desperdício de memória? desculpe a ignorância, não entendi muito bem.

- Não é necessário a classe User ?

 

 

Eu teria um DAO e pronto.

 

- Pesquisei sobre DAO e tentei implementar, segue a tentativa:

 

index.php

<?php 

require_once 'config.php';
require_once 'autoload.php';

$db = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME, DB_USER, DB_PASS);
$userDAO = new UserDAO($db);

$user = new User('Fulano', 'Silva', 'fulanosilva@email.com', '123456');

$userDAO->insert($user);

?>

IDAO.class.php

<?php 

interface IDAO {

	public function findById($pk);
	public function listAll();
	public function insert($object);
	public function update($object);
	public function delete($pk);
}

?>

obs: A interface aqui é necessária ? Me pareceu correto ser implementada....

 

UserDAO.class.php

<?php

require_once 'IDAO.class.php';

class UserDAO implements IDAO {

	private $db;
	private $table = 'cadastro';

	public function __construct($db) {
		$this->db = $db;
	}

	public function findByPk($pk) {
		//code
	}

	public function listAll() {
		//code
	}

	public function insert($object) {

		$sql  = "INSERT INTO {$this->table} (name, lastname, email, password) VALUES (:name, :lastname, :email, :password)";
		$stmt = $this->db->prepare($sql);

		$stmt->bindParam(':name', $object->getName());
		$stmt->bindParam(':lastname', $object->getLastname());
		$stmt->bindParam(':email', $object->getEmail());
		$stmt->bindParam(':password', sha1(md5($object->getPassword())));
	
		return $stmt->execute();
	}

	public function update($object) {
		//code
	}

	public function delete($pk) {
		//code
	}
}

?>

User.class.php

<?php

class User {

	private $id;
	private $name;
	private $lastname;
	private $email;
	private $password;

	public function __construct($name = null, $lastname = null, $email = null, $password = null) {
		$this->name = $name;
		$this->lastname = $lastname;
		$this->email = $email;
		$this->password = $password;
	}

	//gets e sets
        //...
}

?>

Ainda sobre DAO, para cada classe (User, Client, etc, etc...) terei que ter um DAO referente ?

 

 

 

Obrigado a todos por enquanto, estou lendo e revendo todas as informações !!!

Compartilhar este post


Link para o post
Compartilhar em outros sites

- Como assim desperdício de memória? desculpe a ignorância, não entendi muito bem.

- Não é necessário a classe User ?

Pelo o que eu entendi, nesse caso, o Willian Bruno está se referindo a Data Transfer Object (DTO), a comunidade Java fala Value Object (VO). Mas tome cuidado quanto a isso, VO pode ter outro significado, explicado aqui: http://forum.imasters.com.br/topic/541651-entidade-x-value-object/

 

O ActiveRecord "knows to much", ou seja, sabe demais. Ele tem conhecimento sobre a sua própria persistência, o que não está de acordo com SRP. Ele deve possuir apenas uma responsabilidade, representar uma entidade Usuário, mas acaba conhecendo regras de persistência.

 

Mas acredito que ele seja o mais indicado para responder o questionamento.

 

obs: A interface aqui é necessária ? Me pareceu correto ser implementada....

Não é necessária, mas dá "confiança" no uso de uma implementação, além de permitir o polimorfismo.

 

Mas é interessante fazer uso de outros recursos do PHP, como o phpDoc, pois consegue definir questões que o PHP por si só não implementa (no PHP 7 type hint já são realidade).

 

Alguns exemplos aqui:http://forum.imasters.com.br/topic/538625-classe-para-gerenciar-postagens/?p=2151886

 

Já no seu caso, a implementação não ira funcionar, visto que toda a interface não foi implementada:

 

IDAO::findById($pk) -> UserDAO::findByPk($pk);

Apesar de fortemente atrelado a um SGDB SQL (do tipo relacional), PK e ID não necessariamente necessitam ser a mesma coisa. PK refere-se ao identificador da tabela (id da tabela), mas o ID é o identificador do objeto. Um exemplo pode ser o código de referência, como o código de barras. Outro exemplo, em uma chave composta (com mais de uma PK), vocês possui N PK's, mas apenas um ID (que são todas as PK's juntas).

 

Ainda sobre DAO, para cada classe (User, Client, etc, etc...) terei que ter um DAO referente ?

Sim, para cada objeto (ao nível do sistema) e necessário um DAO. O DAO é uma camada de abstração entre a sua aplicação e o seu Storage. Se ocorrer uma mudança, a nível de SGBD, você só precisa mudar o DAO. Sua aplicação se torna independente de um Storage em específico e fica dependente de uma abstração (o que é muito bom).

 

Diferente do DataMapper/TDG, um DAO não necessita ser 1:1 com o SGBD, ou seja, cada tabela ser representada por um objeto. Mas, normalmente é 1:1.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Mas é interessante fazer uso de outros recursos do PHP, como o phpDoc, pois consegue definir questões que o PHP por si só não implementa (no PHP 7 type hint já são realidade).

 

Alguns exemplos aqui:http://forum.imasters.com.br/topic/538625-classe-para-gerenciar-postagens/?p=2151886

 

Obrigado pela dica, nos meus próximos códigos vou implementar phpDoc.

 

Nesses últimos dias estou desenvolvendo classe UserService e a classe Session, queria opinião de vocês a respeito dos principios do S.O.L.I.D.

 

UserService.class.php

class UserService implements IUserService {

	private $validator;

	public function __construct(UserServiceValidator $validator) {
		
		$this->validator = $validator;
	}

	public function login(IUserDAO $userDAO, $email, $password) {

		$this->validator->validateUser('email', $email);
		$this->validator->validateUser('password', $password);

		if ($this->validator->getError() > 0) {
			return false;
		}

		$result = $userDAO->checkLoginByCredentials($email, $password);
		
		if ($result > 0) {

			$this->setSession($userDAO->getUser($email));

			return true;
		}

		return false;
	}

	public function register(IUserDAO $userDAO, User $user) {

		$this->validator->validateUser('name', $user->getName());
		$this->validator->validateUser('lastname', $user->getLastname());
		$this->validator->validateUser('email', $user->getEmail());
		$this->validator->validateUser('password', $user->getPassword());

		if ($this->validator->getError() > 0) {
			return false;
		}

		$result = $userDAO->insert($user);

		return $result;
	}

	public function logoff() {
		Session::destroy();
	}

	private function setSession($user) {

		Session::create();
		Session::set('id', $user->id);
		Session::set('name', $user->name);
		Session::set('lastname', $user->lastname);
		Session::set('email', $user->email);
	}

	public function getCurrentUser() {

		$id = Session::get('id');
		$name = Session::get('name');
		$lastname = Session::get('lastname');
		$email = Session::get('email');

		return new User($name, $lastname, $email, null, $id);
	}

	public function isLogged() {

		Session::create();
		if (!is_null(Session::get('email'))) {
			return true;
		}

		return false;
	}
}

-> Nessa classe, não sabia como validar os dados do usuário então criei a classe UserServiceValidator para fazer as consistência.

A parte do Session esta correto? Não sei se fere o principio de SRP.

 

UserServiceValidator.class.php

class UserServiceValidator {

	private $error;

	public function validateUser($option, $field) {
		$this->error = 0;

		// code
	}

	public function getError() {
		return $this->error;
	}
}

-> Aqui ainda não foi implementada, mas a ideia é cada opção ('username, email, password, etc...') fazer uma consistencia.

 

Session.class.php

class Session {

	public static function create() {
		if (!isset($_SESSION)) {
			session_start();
		}	
	}

	public static function destroy() {

		self::create();
		$_SESSION  = array();
		session_destroy();
	}

	public static function set($key, $value) {
		$_SESSION[$key] = $value;
	} 

	public static function get($key) {
		
		self::create();
		if (isset($_SESSION[$key])) {
			return $_SESSION[$key];
		}

		return null;
	}

	public static function delete($key) {

		$_SESSION[$key] = null;
	}
}

-> Não sei se foi implementada corretamente essa classe na UserService, pois ela foi chamada estaticamente.

 

...

 

Caso vocês puderem me dizer o que esta errado...

 

Obrigado a todos...

Compartilhar este post


Link para o post
Compartilhar em outros sites

  • Conteúdo Similar

    • Por First
      Olá a todos!
       
      Eu estou criando um sistema do zero mas estou encontnrando algumas dificuldades e não estou sabendo resolver, então vim recorrer ajuda de vocês.
      Aqui está todo o meu código: https://github.com/PauloJagata/aprendizado/
       
      Eu fiz um sistema de rotas mas só mostra o conteúdo da '/' não sei porque, quando eu tento acessar o register nada muda.
      E eu também quero que se não estiver liberado na rota mostra o erro de 404, mas quando eu tento acessar um link inválido, nada acontece.
      Alguém pode me ajudar com isso? E se tiver algumas sugestão para melhoria do código também estou aceitando.
       
       
      Desde já, obrigado.
    • Por Rodrigo5468
      Boa tarde a todos.
       
      Estou desenvolvendo um sistema de registro para fins de estudos, mas tenho algumas dúvidas e dificuldades até. Estou usando "programação orientada a objetos", e quero validar alguns campos do meu registro, se puderem me auxiliar, será de grande ajuda.
       
      Meu Diretório:
      Projeto1/ ├── backend/ │ ├── classes/ │ │ ├── Register.php ├── index.php Em Register.php tenho o seguinte código para fazer a validação, mas acredito que estou fazendo algo de errado.
      public function setUsername($username) { $sql = "SELECT * FROM $this->table WHERE username = :username"; $stmt = Database::prepare($sql); $stmt->execute(array('username' => $_POST["username"])); if(empty($_POST["username"])) { return "O campo usuário não pode ser vázio."; }elseif(ctype_space($_POST["username"])) { return "Não pode usar apenas espaços no campo de usuário."; }elseif(strlen($_POST["username"] < 3)) { return "É necessário no mínimo 3 (três) caracteres no usuário."; }elseif(strlen($_POST["username"] > 15)) { return "O máximo é de 15 (quinze) caracteres no usuário."; }elseif(preg_match("/^[a-zA-Z0-9]*$/", $_POST["username"] == 0)) { return "O nome de usuário só pode conter letras e números. (sem espaços e sem caracteres epeciais)"; }elseif($stmt->num_rows !== 0) { return "O nome de usuário já está cadastrado em nossos bancos de dados."; }else { $this->username = $username; } } E no index.php tenho o seguinte código, acredito que está certo, mas eu gostaria de mostrar as mensagens de erros que estão no Register.php, como que posso fazer isso?
      $register = new Registers(); if(isset($_POST["cadastrar"])) { $username = $_POST["username"]; $email = $_POST["email"]; $password = $_POST["password"]; $register->setUsername($username); $register->setEmail($email); $register->setPassword($password); if($register->insert()) { return "Usuário cadastrado com sucesso."; } }  
       
      Obrigado pela atenção!
    • Por dayenne
      Galera então é o seguinte, tenho um trabalho da faculdade para fazer porém ainda não entendo quase nada de java, to meio perdida no trabalho.
      o trabalho propoe que eu faça uma agenda de contatos, onde eu possa armazenar contatos, excluir contatos, pesquisa-los, edita-los, tudo isso usando 
      arquivos txt, porém não consigo de jeito nenhum sair da estaca 0, queria que você me orientasse melhor para que eu consiga flluir melhor os codigos.
       
    • Por kalua
      Olá galera, eu estou tendo um problema que está me aflingindo muito, nunca vi algo assim, eu estou desenvolvendo algumas classes para trabalhar com WordPress e estou usando Output Buffering para gerar as saidas dos meus templates em meus plugins, porem, estou enfrentando um problema quando tento executar um método render de um mesmo tipo, o que acontece é que o PHP me retorna uma string vazia apos a chamada da primeira instancia.
       
      View.php:
      <?php namespace App\Core; class View { /** * Renderiza uma view * @param string $path * @param array $data */ public static function render($path, $data = []) { ob_start(); extract($data); include_once ROOT_PATH . '/views/' . $path . '.php'; return ob_get_clean(); } } Input.php
      <?php namespace App\Core\Fields; use App\Core\Field; use App\Core\View; class Input implements Field { ... public function render() { return View::render('fields/input', [ 'key' => $this->getKey(), 'label' => $this->getLabel(), 'type' => $this->getType(), 'value' => $this->getValue() ]); } } O código client:
       
      $f1 = new Input('athlete_address', 'Endereço', 'text'); $f2 = new Input('athlete_birthdate', 'Data de nascimento', 'date'); $f1->render(); // Retorna saida normal $f2->render(); // Retorna string vazia Porém se eu fizer assim...
      $select = (new Select('athlete_category', 'Categoria')) ->addOption('Open', 'Open') ->addOption('Master', 'Master'); $f1 = new Input('athlete_address', 'Endereço', 'text'); $f2 = new Input('athlete_birthdate', 'Data de nascimento', 'date'); $f1->render(); // Retorna saida normal $f2->render(); // Retorna string vazia $select->render(); // Retorna saida normal Codigo da classe Select.php
      <?php namespace App\Core\Fields; use App\Core\Field; use App\Core\View; class Select implements Field { public function render() { return View::render('fields/select', [ 'key' => $this->getKey(), 'label' => $this->getLabel(), 'options' => $this->getOptions(), 'value' => $this->getValue() ]); } } As classes Input.php e Select.php compartilham de uma interface comum, porem quando eu tento renderizar uma segunda instancia de Input o que eu recebo é uma string vazia, o que pode ser isso?
    • Por Gabriel Heming
      Muitas vezes durante o desenvolvimento em orientações a objetos, tive problemas usando coleções de objetos, principalmente quanto a integridade.

      Veja o exemplo de uma Pessoa que possui uma lista de Contatos.

      Aqui vemos a implementação simples de uma classe Pessoa com uma lista de contatos (demais métodos foram omitidos para simplificação):
      class Pessoa { private $contatoList = array(); public function setContatoList(array $contatoList) { $this->contatoList = $contatoList; } public function getContatoList() { return $this->contatoList; } } E, agora, nossa entidade de contato:
      class Contato { const EMAIL = 1; const TELEFONE = 2; private $contato; private $tipo; public function __construct($contato , $tipo) { $this->contato = $contato; $this->tipo = $tipo; } } Com nossas entidades implementadas, vamos usufruir de suas funcionalidades:
      $contatoList = array(); $contatoList[] = new Contato('(54) 9999-9999' , Contato::TELEFONE); $contatoList[] = new Contato('email@provedor.com.br' , Contato::EMAIL); $pessoa = new Pessoa(); $pessoa->setContatoList($contatoList); var_dump($pessoa->getContatoList()); Saída:

      Dessa forma implementamos rapidamente uma classe simples Pessoa com sua lista de Contatos. Entretanto, não é possível adicionar apenas um contato para a lista já existente e nem podemos garantir a integridade dos dados (algo muito importante na orientação a objetos). Veja Só:
      $contatoList = array(); $contatoList[] = new Contato('(54) 9999-9999' , Contato::TELEFONE); $contatoList[] = new Contato('email@provedor.com.br' , Contato::EMAIL); $contatoList[] = 'meu email é email@provedor.com.br'; $pessoa = new Pessoa(); $pessoa->setContatoList($contatoList); var_dump($pessoa->getContatoList()); Saída:
      Como podemos ver na saída acima, tivemos um falha na integridade da lista. Foi permitida a inserção de item que não é um contato, apesar de "parecer ser um".

      Como podemos garantir que sempre será inserido um Contato na lista de usuários e poder adicionar um usuário existente a lista de contatos?

      Vamos mudar um pouco a implementação da classe Pessoa:
      class Pessoa { private $contatoList = array(); public function setContatoList(array $contatoList) { /** verificar se existe algum elemento que não é do tipo contato **/ $callback = function($row) { return !$row instanceof Contato; }; if(array_filter($contatoList , $callback)) { throw new \RuntimeException('Somente valores do tipo Contato são permitidos'); } $this->contatoList = $contatoList; } public function getContatoList() { return $this->contatoList; } } E vamos ao novo teste:
      $contatoList = array(); $contatoList[] = new Contato('(54) 9999-9999' , Contato::TELEFONE); $contatoList[] = new Contato('email@provedor.com.br' , Contato::EMAIL); $contatoList[] = 'meu email é @provedor.com.br'; $pessoa = new Pessoa(); try { $pessoa->setContatoList($contatoList); } catch (\RuntimeException $exception) { echo $exception->getMessage(); } Saída:
      A lógica é simples, sempre que algum item da lista não for um Contato, o array retornado por array_filter será maior que zero. Nesse caso, será lançada uma exceção informando que a lista não está correta.

      Ok, garantimos, por enquanto e sem minha total satisfação, a integridade de adicionar apenas listas que contenham apenas contatos. Deixarei a implementação da lista parada por enquanto e vamos implementar a funcionalidade para adicionar um contato a lista já existente.
      class Pessoa { private $contatoList = array(); public function setContatoList(array $contatoList) { /** verificar se existe algum elemento que não é do tipo contato **/ $callback = function($row) { return !$row instanceof Contato; }; if(array_filter($contatoList , $callback)) { throw new \RuntimeException('Somente valores do tipo Contato são permitidos'); } $this->contatoList = $contatoList; } public function addContato(Contato $contato) { $this->contatoList[] = $contato; } public function getContatoList() { return $this->contatoList; } } Implementamos o método, na classe pessoa, para permitir que possamos adicionar apenas um contato a lista já existente, vamos ao teste:
      $contatoList = array(); $contatoList[] = new Contato('(54) 9999-9999' , Contato::TELEFONE); $pessoa = new Pessoa(); try { $pessoa->setContatoList($contatoList); } catch (\RuntimeException $exception) { echo $exception->getMessage(); } $pessoa->addContato(new Contato('email@provedor.com.br' , Contato::EMAIL)); var_dump($pessoa->getContatoList()); Saída:
       
      Ok, ok e ok. Está funcionando da forma esperada. Mas esse código não está, como diria Kent Beck, "fedendo"?

      Vamos a alguns pontos:
      - Por que a classe Pessoa está validando a lista?
      - Por que a classe Pessoa está implementando um método de lista?
      - Por que meu café... digo... por que a classe Pessoa está assumindo essas responsabilidades?

      A resposta é simples, o PHP não implementa nativamente essa integridade.

      A partir de então, e com muita pesquisa, percebe-se que é necessário implementar esta integridade. Neste momento partimos para o design pattern Iterator. Para que quiser saber mais sobre o Iterator como um Design Pattern, sugiro a leitura deste tópico.

      Vamos a implementação da classe generalizada para coleções. Classe a qual será nossa base para qualquer implementação de coleções de objetos.

      Obs: Essas implementações fazem parte do meu TCC da Pós e implementadas sobre o namespace Harbinger, nome o qual utilizo para o desenvolvimento das minhas API/Plugins e framework de estudos.
       
      namespace Harbinger\Iterator; /** * @author Gabriel Heming <gabriel.heming@hotmail.com> * @package Harbinger\Iterator **/ abstract class Collection implements \Iterator { /** * @var Object[] **/ protected $object = array(); /** * @var int **/ private $pointer = 0; /** * @var int **/ protected $total = 0; /** * add an Object into collection * @param Object $object * @return $this * @throws \UnexpectedValueException If Object isn't part of a object kind **/ public function add($object) { $class = $this->getTargetClass(); if(!$object instanceof $class) { throw new \UnexpectedValueException("This is a {$class} collection"); } $this->object[$this->total] = $object; $this->total++; return $this; } /** * retrieve the object from current position * @return Object * @throws \OutOfBound---ception If the collection not has any object **/ public function current() { if(isset($this->object[$this->key()])) { return $this->object[$this->key()]; } throw new \OutOfBound---ception("Index {$this->key()} not exists as a object index"); } /** * retrieve the current key * @return int **/ public function key() { return $this->pointer; } /** * move the pointer to next index position **/ public function next() { $this->pointer++; } /** * move the pointer to beginning **/ public function rewind() { $this->pointer = 0; } /** * check if the actual position is valid * @return boolean **/ public function valid() { return (isset($this->object[$this->key()])); } /** * return the object kind for collection * @return string **/ abstract public function getTargetClass(); } Dessa forma, desenvolvemos nossa base abstrata para a criação de uma coleção. Nossa abstração exige apenas uma implementação, o método getTargetClass. Este método é reponsável por informar qual objeto a coleção será responsável por manipular, não permitindo a inserção de nenhum outro tipo de objeto.

      A partir desse ponto, podemos prosseguir de duas formas: uma classe genérica para trabalhar com objetos especificados em tempo de execução ou uma classe especializada em um determinado tipo de objeto.

      Pois bem, vamos realizar das duas formas, iniciando com a nossa classe genérica:
      namespace Harbinger\Iterator\Collection; /** * @author Gabriel Heming <gabriel.heming@hotmail.com> * @package Harbinger\Iterator * @subpackage Collection **/ class Object implements \Harbinger\Iterator\Collection { /** * @var string **/ private $targetClass; public function __construct($targetClass) { $this->targetClass = $targetClass; } /** * {@inheritdoc} **/ public function getTargetClass() { return $this->targetClass; } } Vamos a modificação da classe pessoa:
      class Pessoa { private $contatoCollection; public function __construct() { $this->contatoCollection = new \Harbinger\Iterator\Collection(\Contato::class); } public function setContatoCollection(\Harbinger\Iterator\Collection $contatoCollection) { if($this->contatoCollection->getTargetClass() != $contatoCollection->getTargetClass()) { throw new \UnexpectedValueException( sprintf( "Expected a %s collection. %s given." $this->contatoCollection->getTargetClass(), $contatoCollection->getTargetClass() ) ); } $this->contatoCollection = $contatoCollection; } public function getContatoCollection() { return $this->contatoCollection; } } E o nosso uso:
      $contatoCollection = new \Harbinger\Iterator\Collection\Object('Contato'); $contatoCollection->add(new Contato('(54) 9999-9999' , Contato::TELEFONE)); $pessoa = new Pessoa(); try { $pessoa->setContatoCollection($contatoCollection); } catch (\RuntimeException $exception) { echo $exception->getMessage(); } $pessoa->getContatoCollection()->add(new Contato('email@provedor.com.br' , Contato::EMAIL)); foreach($pessoa->getContatoCollection() AS $contato) { var_dump($contato); } Saída:

      A implementação genérica está pronta e funcionando como o desejado. E caso eu tente utilizar uma classe que suporte outro tipo de objetos, teremos uma exception:
      $collection = new \Harbinger\Iterator\Collection\Object('Pessoa'); $pessoa = new Pessoa(); try { $pessoa->setContatoCollection($collection); } catch (\RuntimeException $exception) { echo $exception->getMessage(); } Saída:

      A nossa única baixa, neste tipo de implementação, é a necessidade de verificar o tipo de coleção adicionada a pessoa. Mas vamos a nossa implementação específica que resolve esse problema
      Neste caso, ao invés de utilizar uma coleção genérica, utilizarei uma coleção especializada para objetos do Contato.
      namespace Collection; class Contato extends \Harbinger\Iterator\Collection { /** * {@inheritdoc} **/ public function getTargetClass() { return \Contato::class; } Realizemos as modificações (ou exclusões) necessárias na classe Pessoa, class a qual ficará mais "enxuta".
      class Pessoa { private $contatoCollection; public function __construct() { $this->contatoCollection = new \Collection\Contato(); } public function setContatoCollection(\Collection\Contato $contatoCollection) { $this->contatoCollection = $contatoCollection; } public function getContatoCollection() { return $this->contatoCollection; } } E nossa utilização:
      $contatoCollection = new \Collection\Contato(); $contatoCollection->add(new Contato('(54) 9999-9999' , Contato::TELEFONE)); $pessoa = new Pessoa(); try { $pessoa->setContatoCollection($contatoCollection); } catch (\RuntimeException $exception) { echo $exception->getMessage(); } $pessoa->getContatoCollection()->add(new Contato('email@provedor.com.br' , Contato::EMAIL)); foreach($pessoa->getContatoCollection() AS $contato) { var_dump($contato); } Saída:

      Como está se tornando frequente, obtivemos exito quanto o sucesso da implementação.
      Uma classe específica nos da a vantagem de uma modelagem mais específica, utilizando o type hint da coleção necessária. Entretanto, nos gera granularidade, pois cada coleção especializada necessita de uma implementação e isso nos é traduzido através de uma nova classe.

      Voltando ao Iterator, estou sentindo falta de algumas implementações, por exemplo, Iterator não permite contar quantos objetos existem em sua coleção, veja:
      $collection = new \Collection\Contato(); $collection->add(new Contato('(54) 9999-9999' , Contato::TELEFONE)); $collection->add(new Contato('email@provedor.com.br' , Contato::EMAIL)); echo 'Quantidade de contatos: '.count($collection); Saída:

      Mas como 1? Se eu tenho dois elementos? Como podemos resolver esse problema?

      Para este fim, podemos utilizar a interface Countable. Para essa interface devemos implementar apenas um método denominado count. Vamos a implementação:
      namespace Harbinger\Iterator; abstract class Collection implements \Iterator , \Countable { /** demais métodos e propriedades omitidos **/ /** * retrieve the number of rows * @return int **/ public function count() { return (int)$this->total; } } E, agora, vamos novamente ao uso:
      $collection = new \Collection\Contato(); $collection->add(new Contato('(54) 9999-9999' , Contato::TELEFONE)); $collection->add(new Contato('email@provedor.com.br' , Contato::EMAIL)); echo 'Quantidade de contatos: '.count($collection); Saída:
       
      Neste momento, podemos contar quantos objetos nossa coleção possui.

      Uma outra implementação interessante, é a interface SeekableIterator. Essa interface define que podemos ir para qualquer registro em específico e não apenas iterar sobre nossa coleção. A interface SeekableIterator extende a interface Iterator, logo devemos substituir a interface Iterator e utilizar em seu lugar a interface SeekableIterator. Vamos a nossa implementação:
       
      namespace Harbinger\Iterator; abstract class Collection implements \SeekableIterator , \Countable { /** demais métodos e propriedades omitidos **/ /** * Seeks to a given position in the iterator. * @param int $position * @throws \OutOfBound---ception If an invalid position has been given **/ public function seek($position) { if (!isset($this->object[$this->key()])) { throw new \OutOfBound---ception("invalid seek position ($position)"); } $this->pointer = $position; } } Vamos a nosso teste recorrente:
      try { $collection = new \Collection\Contato(); $collection->add(new Contato('(54) 9999-9999' , Contato::TELEFONE)); $collection->add(new Contato('email@provedor.com.br' , Contato::EMAIL)); var_dump($collection->current()); $collection->seek(1); var_dump($collection->current()); $collection->seek(2); var_dump($collection->current()); } catch (\RuntimeException $exception) { echo $exception->getMessage(); }
      Saída:

      Com um pouco de implementação, conseguimos um grande avanço na integridade quanto a orientação à objetos. Outra interface interessante para se utilizar com Iterator é ArrayAccess que permite que objetos sejam acessados como arrays. Essa implementação, deixarei para os interessados :yes:

      No próximo artigo, quero demonstrar o uso de Iterator com dados retornados do banco de dados e a sua criação em tempo de execução.

      Dúvidas, sugestões e críticas são muito bem vindas!


      ------

      Código disponibilizado como pacote do composer:
      https://bitbucket.org/harbingerproject/iterator

      Para realizar a instalção do pacote, basta adicionar o repositório ao composer e adicionar como requerimento:

      composer.json
      { "repositories": [ { "type": "vcs", "url": "https://username@bitbucket.org/harbingerproject/iterator.git" } ], "require": { "harbinger/iterator": "^1.0.2", } } E rodar o comando composer install (pode ser executado em qualquer sistema operacional).
×

Informação importante

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