PHP-FIG e as PSR: Parte 3 - Blog do Dave
Voltando a falar sobre as PSR: PHP Standard Recommendations, ainda temos alguns desses conjuntos de regras para conhecer, e hoje vamos falar sobre a PSR-3, que descreve uma interface padrão para classes de log.
Caso você não tenha lido os posts anteriores, recomendo fortemente que leia:
Parte 1: Apresentação do PHP-FIG e explicação da PSR-1
Parte 2: Explicação da PSR-2
"O principal objetivo desta PSR e permitir que bibliotecas recebam um objeto Psr\Log\LoggerInterface e escreva logs de uma forma simples e universal. Frameworks e CMSs que possuem necessidades especiais PODEM estender a interface como precisarem, mas DEVEM permanecer compatíveis com este documento. Isso garante que bibliotecas de terceiros utilizados em uma determinada aplicação possam usar a aplicação centralizada de logs."
Sabemos que não é toda aplicação que possui um sistema de logs, mas esse ponto é extremamente importante: os logs facilitam (e muito) a detecção e correção de problemas gerais com a aplicação e, por isso, em boa parte dos casos, é interessante implementarmos um sistema de logs dentro das nossas aplicações.
A PSR-3 funciona exatamente como todas as outras PSR: definindo determinadas regras com chaves fixas.
Antes de conhecer essas regras, precisamos ter em mente que vamos trabalhar com uma interface, e nesta interface vamos criar assinaturas de nove métodos, oito deles relativos a um nível de log: debug, info, notice, warning, error, critical, alert e emergency, e nono método, log, aceita como primeiro parâmetro um dos níveis de log mencionados acima. Sendo assim, essa seria a interface que nosso sistema de logs deve implementar:
Tenha em mente que você pode criar uma interface que estenda a interface acima e criar níveis de log específicos para sua aplicação, caso necessário.
Chamar o método log(), especificando um nível de log, DEVE ter o mesmo efeito que chamar um método específico
O método log() deve ser apenas uma forma alternativa de se executar os métodos de log, ou seja: se você executar o método log() com o nível warning, e logo após executar o método warning, o resultado deve ser exatamente o mesmo. Concluímos então que o método log() deve identificar o nível de log desejado e chamar o método em questão.
Chamar o método log() informando um nível de log não especificado DEVE lançar uma exceção do tipo Psr\Log\InvalidArgumentException; Usuários (desenvolvedores) NÃO DEVEM utilizar um nível de log personalizado sem ter certeza de que a implementação atual suporte este nível.
Se o desenvolvedor chamar o método log(), passando urgent como primeiro parâmetro, o sistema de logs deverá lançar a exceção mencionada acima como forma de alertar o desenvolvedor de que está tentando gravar um log com um nível que não existe. Isso, claro, caso não exista uma extensão da interface LoggerInterface que defina o nível de log urgent.
Todos o métodos de log DEVEM aceitar uma string ou um objeto com o método __toString() como mensagem de log; Métodos PODEM ter tratamentos especiais para objetos recebidos. Se não for o caso, métodos DEVEM transformar o objeto em string.
Além de uma string, nós podemos passar para um método de log (warning, por exemplo), um objeto que possua o método __toString() implementado. Assim, na hora de escrever o log, esse objeto será automaticamente convertido em string. Além disso, o mesmo método de log pode tratar objetos recebidos de forma diferenciada, como por exemplo disparar um outro método. Mas caso não haja esse tratamento diferenciado, o método precisa converter o objeto recebido em string.
A mensagem de log PODE possuir placeholders que PODEM ser substituídos por elementos do argumento $context; placeholders DEVEM corresponder a uma chave dentro do argumento $context; placeholders DEVEM estar dentro de chaves ({}); NÃO DEVE haver nenhum espaço em branco entre as chaves e o placeholder; placeholders DEVERIAM ser compostos apenas de caracteres alfanuméricos (A-Za-z0-9), underscore (_) e ponto (.).
Digamos que em um determinado momento, você queira que seus logs armazenem o nome do usuário logado (caso exista). Você pode fazer isso colocando um placeholder na mensagem de log, e definindo o valor desse placeholder no array $context, para uma substituição posterior. Nesse caso, os placeholders devem respeitar as regras acima, assim, chamar um método de log com uma mensagem que contenha placeholders deve ser, por exemplo, desta forma:
$message = "Gravando log do usuário {nomedousuario}";
'nomedousuario' => $user->name
$meuLogger->debug($message, $context);
Métodos PODEM implementar estratégias para escapar valores de placeholders e tradução de logs para exibição. Desenvolvedores (usuários do seu sistema) NÃO DEVERIAM pré-escapar valores de placeholders, uma vez que eles não sabem em que contextos esses valores podem ser exibidos.
Isso significa que, na hora de criar o sistema de logs, você pode tratar as informações contidas nos placeholders da forma que for necessário. Mas, na hora de gerar uma mensagem de log, os valores de placeholders devem ser enviados como estão. Não há necessidade de escapar ou tratar esses valores.
Todos os métodos do logger aceita um array como parâmetro de contexto. Neste array, qualquer informação adicional sobre o log pode ser incluída. Os métodos DEVEM tratar esse array da melhor forma possível. Os valores contidos no parâmetros de contexto NÃO DEVEM provocar qualquer tipo de erro ou disparo de exceção.
Basicamente, o logger deve ler todo o array de contexto e fazer o melhor uso possível dos parâmetros. No caso de não haver um tratamento/uso adequado para um determinado valor nesse array, entende-se que podemos apenas armazenar a informação bruta quando aplicável ou ignorar o valor.
Se um objeto do tipo Exception for passado dentro do array de contexto, ele DEVE estar armazenado na chave "exception" do array. Salvar log de exceções é um bem comum e ajuda os desenvolvedores a fazer o rastreamento de pilha (stack trace) da exceção. Quando existir a chave "exception" no array de contexto, os métodos DEVEM verificar se realmente se trata de uma exceção antes de fazer uso deste valor, visto que essa chave, assim como todas dentro do parâmetro de contexto PODE conter qualquer tipo de dado.
Se formos salvar log de uma exceção, precisamos jogar o objeto Exception dentro da chave exception, no array de contexto. E no caso de um método de log precisar acessar essa Exception, precisa, primeiro ter certeza de que o valor dentro da chave "exception" é de fato, uma Exception. Dê uma olhada no exemplo abaixo de como isso é feito na prática:
public function warning($message, array $context = array()){
if(isset($context['exception']) && $context['exception'] instanceof \Exception){
/* A chave "exception" contem um objeto do tipo Exception. Qualquer manuseio desse objeto deve ser feito dentro desse bloco */
Essas foram as regras da PSR-3, regras que devemos seguir sempre que formos escrever um sistema de logs para garantir que qualquer aplicação (que respeite a PSR-3) possa fazer uso do mesmo sistema, evitando assim a duplicidade de classes dentro da aplicação. Mas tem mais: para facilitar a vida dos desenvolvedores (e garantir que a PSR-3 seja seguida corretamente), o pessoal do PHP-FIG disponibiliza no GitHub um pacote que seria o "esqueleto perfeito" para qualquer sistema de logs que siga a PSR-3. Esse pacote consiste de 4 classes, 2 interfaces e 2 traits que garantem um bom direcionamento para sistemas de logs. Abaixo, a descrição dos elementos desse pacote:
A classe Psr\Log\AbstractLogger é uma implementação abstrata da interface LoggerInterface, já contendo oito métodos (referente aos níveis de log), todos eles apontando para o método genérico log(). Sendo assim, seu sistema de logs pode simplesmente estender a AbstractLogger e implementar o método log().
A trait Psr\Log\LoggerTrait funciona exatamente como a classe AbstractLogger, descrita acima, com a diferença de que ela não implementa a interface LoggerInterface (Traits não podem implementar interfaces), e por consequência, ela já tem o método abstrato "log()". No final, o resultado do uso da classe AbstractLogger e da trait LoggerTrait tem o mesmo resultado.
A classe Psr\Log\NullLogger estende a classe AbstractLogger e deve ser usada como um fallback no caso de você não definir nenhuma classe responsável por salvar logs no seu sistema. Assim, quando algum módulo do sistema tentar chamar o logger, ele cairá direto no NullLogger, que por sua vez, não fará nada. Entretanto, é recomendado utilizar uma condicional nos casos em que os dados de contexto do log forem grandes, para evitar gerar um grande montante de dados e jogá-los direto para o NullLogger, perdendo desempenho do sistema. Isso funcionaria mais ou menos assim:
A interface Psr\Log\LoggerAwareInterface contém um único método: setLogger(). Este método pode ser utilizado por frameworks para escolher uma determinada classe de logs (logger).
A trait Psr\Log\LoggerAwareTrait funciona como uma implementação da interface LoggerAwareInterface. Ela pode ser utilizada para prover o método setLogger() e acesso a $this->logger.
A classe Psr\Log\LogLevel contem 8 constantes, uma para cada nível de log.
A classe Psr\Log\InvalidArgumentException nada mais é que uma extensão da Exception. Esta é a exceção que deve ser lançada quando um nível de log desconhecido é utilizado com o método log().
A interface Psr\Log\LoggerInterface é a interface base que o sistema de logs deve implementar.
Vale ressaltar que este pacote não é um sistema de logs, mas sim um esqueleto para que você crie um sistema de logs compatível com a PSR-3.
Paralelo a isso, eu estou iniciando um projeto aberto de um sistema de logs genérico baseado neste pacote da PSR-3, o que vocês acham? Se vocês acharem a ideia interessante, deixe nos comentários pra eu saber! :)
E aqui finalizo o terceiro artigo da série. No próximo falaremos sobre a PSR-4 (e a PSR-0), então fiquem ligados :)
E como sempre: qualquer dúvida, comentário ou sugestão, pode comentar aí embaixo que eu vou ler e responder com o maior prazer!