Lidar com erro em PHP

Contexto

Recentemente eu estava vendo conteudo sobre Go e ví algo que achei interessante. Go lida de uma forma diferente com erros (uma forma que eu não conhecia).

Basicamente toda função de Go retorna "duas variáveis". Uma variável do tipo error - para verificar se houve algum erro na execução dessa função, e outra variável que retorna o dado em si.

Isso pra mim foi um pouco diferente porque estou acostumado com PHP e usar try catch para lidar com exceptions, e no Go não tem try catch.

Depois de pesquisar um pouco entendi o porque Go lida dessa forma com erros. A ideia é ter um comportamento prevísivel.

Ex.: Se eu implemento uma função e tipo o retorno como string, eu espero receber uma string todas as vezes que eu executar essa função. Em Go, por padrão toda função vai retornar uma variável dizendo se teve ou não erro e o meu retorno de fato, eu sempre sei qual vai ser o retorno.

Agora no caso do PHP não é exatamente assim, se eu implementar uma função que eu tipo o retorno como sendo uma string, o meu retorno vai ser string somente se uma exceção for lançada. Caso uma exceção aconteça, existem algumas possíblidades do que pode acontecer:

  • Se houver um try catch o fluxo é redirecionado para o catch, esse caso pode acontecer do catch estar em um arquivo diferente de onde a exception aconteceu. Por isso, nessa situação a função ao invés de retornar o que eu tipei, não vai retornar nada, só vai passar o erro pra cima até chegar em algum lugar com catch.
  • Caso nenhum catch tenha sido definido, o processo é encerrado com uma mensagem de erro.

O que quero mostrar é que linguagens que usam try catch para lidar com erros/exceptions podem acabar tendo um comportamento muito mais difícil de reproduzir. Como achei curioso o formato em que Go lida com erros, eu decidi tentar fazer algo parecido só que em PHP.

Reproduzindo o error handling de Go mas em PHP

Então, não tem como deixar de usar try catch no PHP. Mas tem como reproduzir algo minimamente parecido com Go. Aqui é onde vai precisar de um pouco de criativiade e de uns recursos que já tem na maioria dos IDEs.

Primeira coisa que acho importante lembrar é que, de forma simples, posso dizer que funções tem input e output. Lembre-se do output, ele vai ser importante daqui a pouco.

Pensei em duas formas de ter um retorno onde eu possa ter um valor que represente um erro e outro que represente ou seja o dado retornado. 1 - seria o caso de usar um array com duas posições, mas não acho q array seria uma boa coisa, por ser um pouco mais complexo tipar o array. 2 - Criar uma classe que representa um retorno, essa alternativa me agradou mais, e é ela que eu escolhi para realizar testes.

A ideia é criar uma classe que tenha 2 propriedades, uma representando o erro caso ele exista, e o tipo dessa propriedade pode ser algo como public ?Throwable $error = null. A outra propriedade seria o dado que eu de fato tenho a intenção de usar no retorno da função, algo como public mixed $data = null.

Agora o que preciso fazer é, sempre que criar uma função ou um metodo em qualquer classe, eu coloco o código dentro dessa função em um try catch. Ou seja, toda função ou método vai ter seu próprio try catch - toda função/metodo vai ter a responsabilidade de tratar possíveis exceções.

Se caso nenhuma exceção for lançada, eu vou retornar uma instancia daquele objeto de retorno mas somente preenchedo a propriedade $data e deixando a propriedade $error vazia. Mas se caso uma exceção for lançada, o fluxo será direcionado para o catch dentro da função/metodo e dai é só atribuir a exception para a variavel $errore retornar o objeto de retorno.

Com essa abordagem é possível reproduzir o comportamento de Go, eu pego o retorno e verifico se a variável $error está preenchida, se estiver, eu posso tratar a exceção da forma que eu achar melhor. Mas caso $error esteja vazio, significa que tudo deu certo e eu posso acessar a variável $data.

Essa abordagem tráz previsibilidade que temos em Go pra dentro do PHP.

Agora, tem duas coisas para ser resolvida nessa abordagem. 1 - o nome da classe usada para retorno e 2 - a tipagem da variável $data.

Sobre o nome da classe de retorno. Poderia ter o nome que o dev achar melhor. Mas, no meu caso, pensando no que disse lá em cima sobre funções terem input e outputs, pra mim faz sentido o nome da classe ser FunctionOutput. Não achei legal o nome da classe ser Return, acho que ficaria esquisito ver o código assim: return new Return($error, $data);. Por isso achei melhor o FunctionOutput.

Agora sobre a tipagem da propriedade $data do objeto de retorno (que agora podemos chamar de FunctionOutput), infelizmente o PHP não possui Generics até esse momento. Mas existe um recurso que é comumente usado pela comunidade PHP, que são os docblocks. Dockblocs são comentários em um formato específico onde as IDES conseguem extrair vários tipos de informações para recursos como por exemplo autocomplete.

Nos docblocks temos o @template onde podemos criar um tipo genérico. Com isso podemos adicionar um dockbloc na propriedade $data para tipar ela usando esse tipo genérico. O resultado disso é que quando criamos uma função/metodo, podemos usar um dockbloc para tipar o retorno com algo mais ou menos assim:


    /**
     * Summary of index
     * @return FunctionOutput<string>
     */
    public function metodo(): FunctionOutput
    {
        try {
            ... codigo qualquer
            return new Tupla(data: 'Valor qualquer');
        } catch (Throwable $e) {
            return new Tupla(error: $e);
        }
    }
    
    public function teste()
    {
        $retorno = $this->metodo();
        if ($retorno->error) {
            //
        }
        $data = $retorno->data;

        return $data;
    }

A própria IDE vai conseguir te fornecer as os recursos específicos de cada tipo informado para a variável $data. Isso é o que temos de mais próximo a generics no PHP hoje.

Considerações finais

Não é a solução perfeita, mas pretendo testar num projeto para saber se isso pode ajudar ou vai adicionar mais complexibilidade sem ter tantos benfícios.

Você que tá lendo, acha que esse tipo de abordagem é boa para linguagens que usam try catch ou isso não vai trazer tantos benefícios assim?

Legal sua abordagem, mas acredito que essa solução visa resolver um problema que simplesmente não existe. Enquanto dev PHP, não deixaria de utilizar esse procedimento de try catch para usar algo assim. Parabéns pelo post, mas não vejo aplicação para tal.

eu concordo com voce e vou alem, se quer nao retornar excessoes retorne um resuktado vazio. essa questão com as excessoes é justamente pra casos especificos. antes de falar depes precisa saber o que eles resolvem.
Realmente, pra mim a única coisa que me mantém longe do Go no momento é justamente não ter excessões