Dev (Back & Front)ARTIGO

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

Criei algumas classes para conseguir utilizar interceptadores em PHP e vou apresentá-las no decorrer do artigo.

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 PHPPHP44 conteúdosPadronizando seu código com PHP CS FixerDev (Back & Front) · mai 2019Docker de imagem. Para rodar projetos PHP em menos de 30 segundosDev (Back & Front) · jan 2026PHP 7.3: conheça as novidades desta versãoDev (Back & Front) · abr 2019Ver tudo em Dev (Back & Front) 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 dadosBanco de dados134 conteúdosSQL ou NoSQL: eis a questão!!Data · mar 2020Banco de dados: como organizar e dar segurança para milhões de dados de loteriasData · mai 20215 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data 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.

É graduado em Sistemas de Informação, com mais de 7 anos de experiência em desenvolvimento, principalmente, em Java. Acredita que o estado da arte em desenvolvimento de software está na Arquitetura. Gosta de buscar por soluções sempre imaginando como seriam aplicadas independente da linguagem utilizada.

Ver perfil