Essa é uma das armas mais poderosas de um programador, pois, dá a possibilidade de adicionar funcionalidades no seu sistema sem impactos no código existente. E se você ainda não usa, então, tenho certeza que sua arquitetura pode ir bem mais longe.
Criei algumas classes para conseguir utilizar interceptadores em PHP e vou apresentá-las no decorrer do artigo.
Vou mostrar como utilizei essas classes para poder abrir e fechar a transação com o banco de dados de forma automática. A ideia é que, no contexto de uma requisição, ou todas as operações terminem com sucesso ou então todas sejam abortadas.
Quais métodos serão interceptados
Faremos a interceptação dos nossos métodos de controle (o C do MVC). Para isso, criei uma configuração onde todas as URLs serão jogadas para o arquivo “index.php”. Essa configuração está no “.htaccess”. Veja:
RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . index.php
Dessa forma, toda a lógica de gerenciamento de requisições pode ser controlada a partir do arquivo “index.php”.
O arquivo ficou da seguinte forma:
try{ $ctrl = Router::resolve(); $controller = Proxy::create($ctrl->newInstance(), array(new Transaction())); $action = $ctrl->getMethodName(); $controller->$action(); } catch (Exception $e){ print_r($e); }
Como saber qual controle será utilizado
Repare, no arquivo “index.php” a seguinte linha:
$ctrl = Router::resolve();
O método estático “resolve”, da classe “Router”, devolve um objeto que guarda a informação de qual é o controle e qual a ação (método do controle) que será invocada. Veja a classe “Router”.
class Router { static $routes = array( 'GET' => array( '/' => 'home.index', '/tasks/new' => 'tasks.neww', '/tasks/edit' => 'tasks.edit' ), 'POST' => array( '/tasks/save' => 'tasks.save', '/tasks/remove' => 'tasks.remove' ) ); public static function resolve(){ $method = $_SERVER['REQUEST_METHOD']; $path = $_SERVER['REDIRECT_URL']; if(empty($path)){ return self::controller( self::$routes['GET']['/'] ); } if(Util::endsWith($path, '/')){ $path = substr($path, 0, (strlen($path) - 1)); } $rts = self::$routes[ $method ]; $controllerConfiguration = $rts[ $path ]; return self::controller( $controllerConfiguration ); } static function controller($config){ $conf = explode('.', $config); return new ControllerConfiguration($conf[ 0 ] . 'Controller', $conf[ 1 ]); } }
O objeto mencionado acima, que guarda a informação sobre o controle e a ação sendo executada, é do tipo “ControllerConfiguration”. É uma classe bem simples e vou inclui-la aqui também:
class ControllerConfiguration { public function construct($className, $methodName) { $this->className = $className; $this->methodName = $methodName; } public function newInstance(){ return new $this->className; } public function getMethodName(){ return $this->methodName; } }
A classe para conexão
Quero apresentar essa classe, pois, é a utilizada pelo interceptador (que vou mostrar abaixo) para abrir e fechar transações. Essa classe basicamente encapsula as funcionalidade do PDO. Não vou explicá-la método por método porque não é o foco desse artigo. Mas, caso tenha dúvidas, deixe um comentário abaixo.
Dessa classe quero destacar 3 métodos que serão usados pelo nosso interceptador:
- beginTransaction
- commit
- rollBack
class Connection { private static $instance; private $pdo; /** * Inicia o PDO. */ private function construct(){ $HOST = 'localhost'; $PORT = '3306'; $USER = 'root'; $PASS = '123'; $NAME = 'teste'; try{ $this->pdo = new PDO( 'mysql:host=' . $HOST . ';port=' . $PORT . ';dbname=' . $NAME, $USER, $PASS, array(PDO::ATTR_PERSISTENT => false, PDO::ATTR_AUTOCOMMIT => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION) ); }catch(PDOException $e){ $this->exceptionHandeling($e); } register_shutdown_function(array($this, 'close')); } /** * Retorna a instancia da classe. */ public static function getInstance(){ if (!isset(self::$instance)) { $c = CLASS ; self::$instance = new $c; } return self::$instance; } /** * Abre a transacao. */ public function beginTransaction(){ $this->pdo->beginTransaction(); } /** * Comita a transacao. */ public function commit(){ $this->pdo->commit(); } /** * Desfaz a transacao em caso de erros. */ public function rollBack(){ $this->pdo->rollBack(); } /** * Executa uma consulta na base. */ public function query($query){ if(func_num_args() > 1){ $params = func_get_arg(1); if(!is_array($params)){ $params = func_get_args(); array_shift($params); } }else{ $params = array(); } try{ $stmt = $this->pdo->prepare($query); $stmt->execute($params); $returned = $stmt->fetchAll(PDO::FETCH_ASSOC); $stmt->closeCursor(); $stmt = null; return $returned; }catch(PDOException $e){ $this->exceptionHandeling($e, $query, $params); } } /** * Executa uma atualizacao na base (insert ou update) */ public function execute($query){ $params = func_get_arg(1); if(!is_array($params)){ $params = func_get_args(); array_shift($params); } try{ $stmt = $this->pdo->prepare($query); $stmt->execute($params); $stmt->closeCursor(); $stmt = null; return $this->pdo->lastInsertId(); }catch(PDOException $e){ $this->exceptionHandeling($e, $query, $params); } } public function close(){ $this->pdo = null; } /** * Loga os erros em um arquivo txt. */ private function exceptionHandeling($e, $sql = null, $params = null) { $d = date('Ymd H:i:s', time()); $sqlMsg = isset($sql) ? ('. SQL: ' . $sql) : ''; $sqlParams = isset($params) ? ('. SQL Params: ' . str_replace("\n", '', print_r($params, true))) : ''; $line = $d . ' [ ERROR ] ' . $e->getMessage() . $sqlMsg . $sqlParams; file_put_contents('/tmp/PDOErrors.txt', $line . "\n", FILE_APPEND); throw $e; } }
Criação do proxy
É qqui que a mágica começa! Voltando para o arquivo “index.php”, repare na linha:
$controller = Proxy::create($ctrl->newInstance(), array(new Transaction()));
Repare que o método “create” da classe “Proxy” recebe uma nova instância do nosso controle e um array de objetos com os métodos interceptadores (que explicarei mais abaixo).
Essa instância do nosso controle é repassada para uma outra classe. A classe “InvocationHandler”. Veja:
class Proxy { public static function create($obj, $interceptors){ return new InvocationHandler($obj, $interceptors); } }
A classe “InvocationHandler” não possui métodos próprios. Ela somente implementa o método ” call” (que é um Magic Method). Esse método responde a chamada por métodos que não existem. E como a classe “InvocationHandler” não possui métodos, ela delega essas invocações para a instância da nossa classe de controle.
class InvocationHandler { private $obj; private $interceptors; /** * @param object $obj Instancia que possui o metodo que esta sera interceptado * @param array $interceptors E um array de objetos com os metodos interceptadores */ function construct($obj, $interceptors) { $this->obj = $obj; $this->interceptors = $interceptors; } /** * @param string $methodName Nome do metodo invocado (e que sera interceptado) * @param $args */ function call($methodName, $args) { $ctx = new InvocationContext($this->obj, $methodName, $args, $this->interceptors); return $ctx->proceed(); } }
Mas não é delegado de qualquer forma… Antes de realmente chamar o método do nosso controle, a instancia é passada para a classe “InvocationContext”. E é essa classe que gerencia a invocação de cada interceptor e, ao final, do método do nosso controle.
class InvocationContext { private $methodsIntercept; private $obj; private $args; private $method; private $index = 0; /** * @param object $obj Instancia que possui o metodo que esta sera interceptado * @param string $method Nome do metodo que sera interceptado * @param array $args Parametros que serao passados ao metodo que esta sendo interceptado * @param array $methodsIntercept E um array de objetos com os metodos interceptadores */ public function construct($obj, $method, $args, $methodsIntercept) { $this->obj = $obj; $this->method = $method; $this->args = $args; $this->methodsIntercept = $methodsIntercept; } public function proceed() { if ($this->index < count($this->methodsIntercept)) { $intercept = $this->methodsIntercept[$this->index++]; return $intercept->intercept($this); } $this->index = 0; return call_user_func_array(array($this->obj, $this->method), $this->args); } public function getObject(){ return $this->obj; } public function getObjectClassName(){ return get_class($this->obj); } public function getMethod(){ return $this->method; } public function getArgs(){ return $this->args; } }
Repare que todo o segredo está no método “proceed”. Esse método gerencia a invocação de cada interceptador para, ao final, invocar o objeto interceptado (no nosso caso, os controles).
O interceptor
O nosso interceptor é instanciado logo na criação do proxy. Eu já mostrei acima, mas, vou colocar novamente o trecho onde isso acontece.
$controller = Proxy::create($ctrl->newInstance(), array(new Transaction()));
Veja que o segundo parâmetro do método “create” é um array. E esse é um array de classes que devem implementar a interface “MethodInterceptor”. No momento utiliza-se somente um interceptor (o referente a classe “Transaction”), mas, poderiam ser vários.
interface MethodInterceptor { public function intercept($context); }
Veja que a classe interceptadora (a classe “Transaction”, no caso) não tem segredo algum:
require_once 'libs/proxy/methodInterceptor.class.php'; class Transaction implements MethodInterceptor { public function intercept($context){ $inTransaction = 'GET' != $_SERVER['REQUEST_METHOD'];//A transacao nao eh aberta para o metodo HTTP do tipo GET. if($inTransaction){ Connection::getInstance()->beginTransaction(); } try{ $returned = $context->proceed(); } catch(Exception $e) { if($inTransaction){ Connection::getInstance()->rollBack(); } throw $e; } if($inTransaction){ Connection::getInstance()->commit(); } return $returned; } }
Repare que, antes de continuar na pilha de invocações, a transação é aberta (somente para os métodos HTTP que não sejam do tipo GET – isso é um padrão que gosto de adotar para não ter que ficar abrindo transação a todo momento). Logo após, deixamos a invocação continuar. Caso seja lançada alguma exceção, é executado um rollback. E se o método for executado com sucesso, nós comitamos a transação.
O trecho mais importante (e essencial) do método “intercept” é:
$context->proceed();
Se você não invocar o método “proceed”, o objeto interceptado simplesmente não é executado. Perceba que com isso, o seu interceptador tem o poder de decidir se quer executar o método ou não (para esse exemplo eu sempre executo).
Outra coisa a se notar é que temos acesso a instância do objeto (o nosso controle, no caso), ao nome do método que será invocado e aos argumentos que esse método irá receber. Tudo isso através do parâmetro “$context”:
public function getObject(){ return $this->obj; } public function getObjectClassName(){ return get_class($this->obj); } public function getMethod(){ return $this->method; } public function getArgs(){ return $this->args; }
Ou seja, para ter acesso a instância do objeto, basta utilizarmos:
$context->getObject();
Conclusão
O controle de transação é somente um exemplo dentre os muitos em que se pode aplicar essa técnica. Para mim, os mais comuns são:
- Fazer cache do retorno de um método;
- Disparar eventos;
- Aplicar segurança;
- Controlar a transação do banco de dados.
Essa lista pode aumentar muito – vai depender da sua necessidade e/ou criatividade.
Não esqueça de deixar um comentário abaixo. Diga se gostou, se não gostou, deixe suas dúvidas…
Obs: Você pode baixar o código fonte aqui.