Existem duas maneiras de construir o design de um
software. Uma delas é tão simples que obviamente não existem deficiências. E o
outro jeito é fazer de uma maneira tão complicada que não existam deficiências
óbvias – Sir Charles Antony Richard Hoare
Como
você já sabe, o PHP é a linguagem dos apressados e espertinhos, em que a
tendência do desenvolvedor é sempre em direção à preguiça (eficiência).
Nós
nem sempre testamos nosso código. Às vezes, nós pegamos atalhos de design,
mesmo sabendo que eles vão nos atrapalhar mais tarde. As restrições dos
negócios atam nossas mãos, e nos impedem de fazer nosso trabalho de maneira
otimizada.
Se considerarmos o que Fred
Brooks tem a dizer, percebemos que o custo de tal miopia a longo prazo pesa
muito mais que suas vantagens. Em seu trabalho seminal, The Mythical Man-Month, Brooks explica, “mais de 90% dos custos de um sistema
típico aumentam na fase de manutenção, e qualquer peça do software que funciona
bem vai ser inevitavelmente mantida.”
Não
preste atenção no homem atrás da cortina
Normalmente,
“manutenção” significa escrever o relatório de um bug, entender como reproduzi-lo
e arrumar o código afetado. Ou, em casos mais sérios, refazer o design de parte
de sua arquitetura.
Felizmente, quando o assunto é
debugar, a faca suíça das ferramentas de um desenvolvedor PHP é o Xdebug.
Apesar de todo poder e utilidade inacreditáveis dessa extensão, o fato de
muitos desenvolvedores não o terem instalado – ou pior, nunca terem ouvido
falar dele – é incrível para mim.
Istalar
o Xdebug não poderia ser mais fácil. Se você está executando um sistema
operacional real, sua instalação é a mesma de qualquer outra extensão PECL:
execute pecl install xdebug. Se você está no Windows, você tem que achar o
binário correto, o que pode ser complicado. Felizmente, o autor foi gentil o
suficiente para fornecer uma ferramenta para ajudar. Uma vez instalado, você
pode ver os benefícios do Xdebug imediatamente. Ele substitui as mensagens
puras geradas pelos erros do PHP e uncaught exceptions com erros formatados,
incluindo stack traces. (Para ver mensagens de erro em sua total glória HTML,
garanta que html_errors esteja ativado).
Isso
é algo importante. A coisa mais relevante (difícil) do processo de debugar é
isolar, determinar a principal causa do bug e onde ele ocorreu. Ao tentar
isolar um bug, os stack traces são indispensáveis; eles permitem que você
descubra o caminho que seu código percorreu para chegar lá.
Em
soma aos traços de stack, o Xdebug tem duas configurações php.ini que são extremamente úteis. A primeira, xdebug.collect_params, especifica o nível de detalhe da função ou os
parâmetros do método que você gostaria de ver nos seus stack traces. O valor
padrão, 0, não mostra nenhuma informação de parâmetro, enquanto
o valor de 4 mostra detalhes completos de qualquer parâmetro. Isso
pode ser um pouco demais para a maioria das situações. Sugiro que você ajuste
esse valor de acordo com sua necessidade.
A
segunda configuração, xdebug.show_local_vars, garante maior facilidade no
isolamento por dumping das variáveis locais onde o erro ou a exceção ocorreram.
Estabeleça esse valor a On para ver um dump formatado
em todos as variáveis do escopo.
O
que você não vê
Ao
usar as ferramentas apresentadas acima, você agora deve estar completamente
equipado para seguir em frente e rasgar sua lista de bugs com impunidade.
Certo?
Errado!
Duas coisas que a coleção de
parâmetros e variáveis do Xdebug não vão te mostrar são o estado do escopo do
objeto atual (i.e. $this), e o acesso a quaisquer dados
que existam fora do método.
Em vez de considerar isso como
uma deficiência do Xdebug, considere as implicações de como você faz o design
de seus métodos. A principal causa lógica para os bugs de softwares é um estado
pobremente gerenciado; você tem mais chances, ao utilizar dados que podem mudar
imprevisivelmente, de produzir um código que não pode ser testado nem
sustentável.
Ao
pensar em códigos que são otimizados por previsibilidade, pensamos em coisas
simples, como manipulações de funções string. Considere str_replace(), uma função altamente testável. Dados quaisquer três
inputs, podemos esperar um output consistente, previsível. Se um call fosse
repetido com os mesmos três parâmetros, nós teríamos os mesmos resultados,
porque str_replace() não depende de nenhum state para fazer
seu trabalho.
Além
disso, códigos previsíveis minimizam ou eliminam efeitos colaterais, tais como
ecos no output, modificações em uma variável que é passada como referência, ou
designações de cabeçalhos. Efeitos colaterais se tornam problemáticos, porque –
além de isolar um método em particular – você também deve isolar o que aquele
método mudou em outro lugar do aplicativo.
Produzir
efeitos colaterais e depender do estado externo em um método não é sempre tão
óbvio quanto uma referência ao $GLOBALS (ou superglobais). Nós
também temos os singletons, a memorização das variáveis globais em designs orientados
a objetos.
Existem
também incursões sutis de estado, como usar informações de tempo e de data. Mesmo
não sendo tipicamente considerado um estado em um aplicativo, é uma peça de
informação que está constantemente mudando fora do escopo do seu código. Isso
para não dizer que é totalmente perigoso; apenas seja prudente ao usá-lo.
Para
códigos orientados a objetos, você não precisa de nada além de $this. Gerenciar o estado efetivamente pode ser difícil em
OOP, porque o OOP (diferentemente da programação funcional) não força você a
pensar sobre o estado. Em vesdisso, ele disponibiliza estados mutantes
imediatamente acessíveis, promovendo demora em volta de um dos mais críticos
preceitos do design do software.
Não
entenda errado: esta não é uma condenação universal de programação orientada a
objetos. É simplesmente uma sugestão para pensar antes de fazer seu código. Se
você somente aprendeu sobre design de software através de um ponto de vista
orientado pelo objeto, eu o aconselho a estudar a linguagem sob um outro
paradigma. Você abrirá seus horizontes e ganhará um melhor entendimento do seu
papel como desenvolvedor.
Finalizando
Uma questão final quando o
assunto é debugar com o Xdebug se refere ao novo recurso do PHP 5.3, closures.
Quando você grava um stack trace que contém closure, em vez de informações
úteis, o Xdebug apenas grava {closure}. Enquanto nós ainda vemos o
número do arquivo e a linha onde a closure foi invocada pelo stack, isso não é
suficiente para que você seja capaz de determinar rapidamente onde ele foi
definido.
Em vez disso, nós podemos criar
um pequeno script que irá converter um stack PHP normal para um que fornece um
pouco mais de informação sobre onde a closure foi definida. Esse código usa
componentes do Lithium framework com o objetivo de simplicar a introspecção de classes e métodos, mas a premissa
básica é simples, e nenhum framework é necessário. Você provavelmente optará por
usar o Reflection API – nativo do PHP – diretamente.
<?php
use lithiumanalysisDebugger;
use lithiumanalysisInspector;
class ClosureLocator {
public static function trace($stack = null) {
$stack = $stack ?: Debugger::trace(array('start' => 1, 'format' => 'array'));
foreach ($stack as $i => $frame) {
$prev = isset($stack[$i - 1]) ? $stack[$i - 1] : null;
unset($stack[$i]['args'], $stack[$i]['object']);
if (strpos($frame['function'], '{closure}') === false || !$prev) {
$stack[$i]['function'] = $frame['functionRef'];
continue;
}
if ($class = Inspector::classes(array('file' => $prev['file']))) {
foreach (Inspector::methods(key($class), 'extents') as $method => $extents) {
if (!($extents[0] <= $prev['line'] && $prev['line'] <= $extents[1])) {
continue;
}
$class = key($class);
$reference = "{$class}::{$method}";
$stack[$i]['function'] = "{$reference}()::{closure}";
break;
}
} else {
$reference = $prev['file'];
$stack[$i]['function'] = "{$reference}::{closure}";
}
}
return $stack;
}
}
?>
Primeiramente,
este código gera um stack trace especialmente formatado, que é essencialmente debug_backtrace() com algumas checagens e formatações.
Em seguida, ele repete
sobre o stack, procurando por frames contendo calls feitos por closures. Uma
vez que encontra um (e garante que exista um chamado anterior no stack para servir
de referência para o indicador de informação), ele tenta determinar se a
closure é definida em um método de classe ou em um arquivo processual PHP. Esta
parte do código está fazendo uma suposição, especificamente de que seu código
usa uma convenção nominal class-to-file (classe-para-arquivo).
Se
o closure é definido em uma classe, cada um dos métodos de classe é repetido e
inspecionado para ver se o chamado do stack subsequente ao número da linha
está entre o começo e o fim das linhas da definição do método. Quando o
correspondente é encontrado, nós temos nossa classe e métodos de origem.
Esse é um bom começo. Agora nós
somos capazes de ver o nome do arquivo ou o método de referência onde o closure
está definido em nosso stack trace. No entanto, para uma simples identificação,
nós também queremos ser capazes de ver em qual número de linha a closure foi
definida.
Para tanto, podemos adicionar os
seguintes métodos às nossas classes ClosureLocator :
public static function definition($reference, $callLine) {
if (file_exists($reference)) {
foreach (array_reverse(token_get_all(file_get_contents($reference))) as $token) {
if (!is_array($token) || $token[2] > $callLine) {
continue;
}
if ($token[0] === T_FUNCTION) {
return $token[2];
}
}
} else {
list($class, $method) = explode('::', $reference);
$classRef = new ReflectionClass($class);
$methodInfo = Inspector::info($reference);
$methodDef = join("n", Inspector::lines($classRef->getFileName(), range(
$methodInfo['start'] + 1, $methodInfo['end'] - 1
)));
foreach (array_reverse(token_get_all("<?php {$methodDef} ?>")) as $token) {
if (!is_array($token) || $token[2] > $callLine) {
continue;
}
if ($token[0] === T_FUNCTION) {
return $token[2] + $methodInfo['start'];
}
}
}
}
Este método pega um nome de
arquivo, ou o nome de uma classe e método, separados por dois pontos. Se $reference é um nome de arquivo, o arquivo está
anexando tokens em ordem contrária, seguindo em frente até que encontramos
onde o próximo chamado (vindo do closure) aconteceu.
Uma
vez que achamos a linha onde o chamado aconteceu, continuamos repetindo ao
contrário os tokens até que encontramos o T_FUNCTION, indicando o início da
definição do closure. O número da linha do token é então devolvido.
A
segunda parte do método funciona praticamente da mesma maneira. Ao utilizar
algumas das técnicas acima, nós podemos determinar os números das linhas no
arquivo onde o método é definido. Então, extraímos apenas o código para a
definição do método, o anexamos e começamos a repetir os tokens ao
contrário. Após passar por todos os tokens que aparecem depois do método, anexamos, e começamos a repetir a primeira instância do f T_FUNCTION. Quando encontrado, nós voltamos o número da linha
relativo ao trecho extraído mais o do início da definição do método (de onde
extraímos o código).
Finalmente,
adicionaremos as seguintes linhas ao método trace() , pouco antes da chave de fechamento do loop foreach anterior:
$line = static::definition($reference, $prev['line']) ?: '?';
$stack[$i]['function'] .= " @ {$line}";
Isso
invoca nosso novo método e acrescenta o número da linha no fim da definição de
referência do closure.
Agora
podemos “chamar” nosso método de qualquer lugar onde precisemos de uma stack
trace, e deveríamos poder ver algo um pouco mais informativo. Reduzido somente à
informação que geramos, nosso stack trace deve estar mais ou menos assim:
Array
(
[0] =>
appcontrollersHelloWorldController::test()::{closure} @ 24
[1] =>
lithiumnethttpRouter::parse()
[2] =>
lithiumnethttpRouter::process()
[3] =>
lithiumaction{closure}
[4] =>
lithiumactionDispatcher::run()::{closure} @ 108
[5] =>
/path/to/app/config/bootstrap/action.php::{closure} @ 42
[6] =>
lithiumcoreStaticobject::_filter()
[7] =>
lithiumactionDispatcher::run()
[8]
=> [main]
)
Conclusão
Aí
está. Com o bom uso das ferramentas e das práticas de design de software, você pode
se salvar de um mundo de dor e salvar sua empresa de um colapso sobre projetos
difíceis de manter. O que poderia ser maior motivo para celebração nessa temporada
de comemorações?
*
Texto
original de Nate Abele, disponível em
http://phpadvent.org/2010/debugging-by-nate-abele








