Ler um stack trace de PHP como um detetive
Os vinte frames entre a mensagem e o teu código
Uma mensagem de exceção diz-te que o motor se recusou a continuar. Não te diz como o programa chegou àquela linha. Num script pequeno isso não importa, porque vês o ficheiro todo de uma vez. Num projeto com framework e trinta frames no trace, essa diferença é o trabalho todo.
Cabeçalho contra frames
Cada stack trace de PHP tem um cabeçalho e uma lista de frames, e respondem a coisas diferentes. O cabeçalho dá o nome da exceção, a mensagem e o local onde foi lançada:
Uncaught TypeError: Cart::add(): Argument #1 ($item) must be of type Item, string given in /var/www/src/Cart.php:42
Os frames abaixo são a cadeia de chamadas, da mais recente para trás:
#0 /var/www/src/Order.php(31): Cart->add('sku-9')
#1 /var/www/src/Checkout.php(18): Order->addLine('sku-9', 2)
#2 /var/www/public/index.php(12): Checkout->run()
#3 {main}
Como se lê um frame
O frame zero lê-se assim: na linha 31 do Order.php, algo chamou Cart->add() e passou-lhe a string 'sku-9'. O ficheiro e a linha entre parênteses pertencem a quem fez a chamada, e a função depois dos dois pontos é a chamada que foi feita ali. É aqui que muita gente se engana. O Cart.php não aparece no frame zero, e a linha que precisas de abrir é a de quem chamou.
Neste trace, a falha está na linha 31 do Order.php, não no Cart.php, mesmo que o cabeçalho aponte para a linha 42 do Cart.php, que é onde o add() verificou o argumento e desistiu. O Cart.php está a fazer o que lhe compete.
Ler de baixo para cima
A numeração dos frames vai ao contrário da história. O frame zero é o ponto mais fundo, a chamada mais próxima da falha. A última linha, {main}, é o script de entrada. Ler de baixo para cima é ler a ordem em que as coisas aconteceram: um pedido chegou ao index.php, o index.php chamou o run(), o run() chamou o addLine(), o addLine() chamou o add(). Não falta nada no trace. O trace é o percurso.
Há uma razão prática para começar em baixo: numa aplicação real a maioria dos frames está em vendor. O último frame antes de o trace sair do teu namespace é o que tomou a decisão, e normalmente é o interessante.
Local do lançamento contra cadeia de chamadas
O cabeçalho é o único sítio que te diz onde o erro foi levantado. Os frames dizem-te como o programa lá chegou. Confundir os dois custa uma hora e produz uma alteração no ficheiro errado, porque uma biblioteca que lança uma exceção bem descrita por causa de input inválido está a funcionar bem. O erro está no código que construiu esse input.
É meu ou é deles
Normalmente percebe-se pelos valores dos argumentos nos frames.
- Se o valor que falhou tem o aspeto de dados que tu produziste, como um parâmetro do pedido em bruto, uma coluna da base de dados ou uma string onde devia estar um objeto, é teu.
- Se a biblioteca recebe exatamente o que a documentação dela promete e falha na mesma, confirma a versão que tens instalada e o changelog antes de assumir um bug. Versões desalinhadas parecem bugs de biblioteca e não são.
Mais dois sinais. Se o trace passa por um método de vendor duas vezes sem código teu entre os dois, a biblioteca está a falhar nas suas próprias entranhas. Se a exceção é um erro de tipo de argumento ou uma chave de array em falta, o input veio de fora da biblioteca, e o frame abaixo é o sítio de onde veio.
Quando os argumentos não aparecem
As configurações de produção muitas vezes retiram os valores dos argumentos dos traces com zend.exception_ignore_args, porque os argumentos podem levar passwords e dados pessoais. Se os teus frames mostram add() com parênteses vazios e sem valores, é por isso. Reproduz localmente com a configuração por omissão e os valores voltam.
Usa o ficheiro e a linha, e lê a função toda
Abre o ficheiro, vai à linha e lê a função que a contém em vez da linha isolada. A linha no trace é onde a execução parou, e a razão está normalmente uma linha acima ou numa condição anterior. Se o ficheiro for gerado, a partir de um template compilado ou de um container em cache, lê o template e não o resultado.
Desde o PHP 8.5, os erros fatais como um timeout ou falta de memória trazem também um backtrace, por isso a mesma leitura se aplica aos erros que antes te davam uma mensagem e mais nada.
O hábito
Pega no frame de baixo, sobe e para no último frame que é código teu. Esse frame ou contém o bug ou construiu o valor que o causou. É assim tão mecânico, e é mais rápido do que qualquer debugger nos primeiros cinco minutos de investigação.
Tens um site WordPress que não para de se partir? Vê o que faço e como trabalho.