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.