Back-End

14 jul, 2015

Melhore seu sistema sem alterar o código: interceptadores em PHP

Publicidade

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.

aa01

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.