`trigger_error` vs` throw Exception` no contexto dos métodos mágicos do PHP

13

Estou tendo um debate com um colega sobre o uso correto (se houver) trigger_errorno contexto de métodos mágicos . Em primeiro lugar, acho que isso trigger_errordeve ser evitado, exceto neste caso.

Digamos que temos uma classe com um método foo()

class A {
    public function foo() {
        echo 'bar';
    }
}

Agora diga que queremos fornecer exatamente a mesma interface, mas use um método mágico para capturar todas as chamadas de método

class B {
    public function __call($method, $args) {
        switch (strtolower($method)) {
        case 'foo':
            echo 'bar';
            break;
        }
    }
}

$a = new A;
$b = new B;

$a->foo(); //bar
$b->foo(); //bar

Ambas as classes são iguais na maneira como respondem, foo()mas diferem ao chamar um método inválido.

$a->doesntexist(); //Error
$b->doesntexist(); //Does nothing

Meu argumento é que métodos mágicos devem chamar trigger_errorquando um método desconhecido é capturado

class B {
    public function __call($method, $args) {
        switch (strtolower($method)) {
        case 'foo':
            echo 'bar';
            break;
        default:
            $class = get_class($this);
            $trace = debug_backtrace();
            $file = $trace[0]['file'];
            $line = $trace[0]['line'];
            trigger_error("Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR);
            break;
        }
    }
}

Para que ambas as classes se comportem (quase) de forma idêntica

$a->badMethod(); //Call to undefined method A::badMethod() in [..] on line 28
$b->badMethod(); //Call to undefined method B::badMethod() in [..] on line 32

Meu caso de uso é uma implementação do ActiveRecord. Eu uso __callpara capturar e manipular métodos que essencialmente fazem a mesma coisa, mas têm modificadores como Distinctor Ignore, por exemplo

selectDistinct()
selectDistinctColumn($column, ..)
selectAll()
selectOne()
select()

ou

insert()
replace()
insertIgnore()
replaceIgnore()

Métodos como where(), from(), groupBy(), etc. são codificados.

Meu argumento é destacado quando você liga acidentalmente insret(). Se minha implementação de registro ativo codificasse todos os métodos, seria um erro.

Como em qualquer boa abstração, o usuário deve desconhecer os detalhes da implementação e confiar apenas na interface. Por que a implementação que usa métodos mágicos deve se comportar de maneira diferente? Ambos devem ser um erro.

chriso
fonte

Respostas:

7

Tomar duas implementações da mesma interface ActiveRecord ( select(), where(), etc.)

class ActiveRecord1 {
    //Hardcodes all methods
}

class ActiveRecord2 {
    //Uses __call to handle some methods, hardcodes the rest
}

Se você chamar um método inválido na primeira classe, por exemplo ActiveRecord1::insret(), o comportamento padrão do PHP é acionar um erro . Uma chamada de função / método inválida não é uma condição que um aplicativo razoável deseje capturar e manipular. Claro, você pode capturá-lo em idiomas como Ruby ou Python, onde um erro é uma exceção, mas outros (JavaScript / qualquer linguagem estática / mais?) Falharão.

De volta ao PHP - se ambas as classes implementam a mesma interface, por que não deveriam exibir o mesmo comportamento?

Se detectar __callou __callStaticdetectar um método inválido, eles devem acionar um erro para imitar o comportamento padrão do idioma

$class = get_class($this);
$trace = debug_backtrace();
$file = $trace[0]['file'];
$line = $trace[0]['line'];
trigger_error("Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR);

Não estou discutindo se os erros devem ser usados ​​sobre as exceções (eles 100% não deveriam), no entanto, acredito que os métodos mágicos do PHP são uma exceção - trocadilhos :) - a esta regra no contexto da linguagem

chriso
fonte
1
Desculpe, mas eu não compro. Por que deveríamos ser ovelhas porque as exceções não existiam no PHP há mais de uma década atrás, quando as classes foram implementadas pela primeira vez no PHP 4.something?
Matthew Scharley
@ Matthew por consistência. Não estou debatendo exceções x erros (não há debate) ou se o PHP tem design falho (sim), estou argumentando que nessa circunstância única, por consistência , é melhor imitar o comportamento da linguagem
chriso 01/04
Consistência nem sempre é uma coisa boa. Um design consistente, mas ruim, ainda é um design ruim, que é difícil de usar no final do dia.
Matthew Scharley
@ Matthew verdade, mas a IMO desencadear um erro em uma chamada de método inválida não é um projeto ruim 1) muitas (a maioria?) De linguagens o incorporam e 2) não consigo pensar em um único caso em que você desejaria para pegar uma chamada de método inválido e lidar com isso?
Chriso #
@chriso: O universo é suficientemente grande para haver casos de uso em que nem você nem eu jamais sonharíamos que isso acontecesse. Você está usando __call()para fazer roteamento dinâmico, é realmente tão irracional esperar que em algum ponto da pista alguém possa querer lidar com o caso em que isso falha? De qualquer forma, isso está circulando, então esse será meu último comentário. Faça o que quiser, no final do dia, tudo se resume a um julgamento: Melhor suporte versus consistência. Ambos os métodos resultarão no mesmo efeito na aplicação no caso de nenhum tratamento especial.
Matthew Scharley
3

Vou lançar minha opinião opinativa por aí, mas se você usar em trigger_errorqualquer lugar, estará fazendo algo errado. Exceções são o caminho a percorrer.

Vantagens das exceções:

  • Eles podem ser pegos. Essa é uma grande vantagem e deve ser a única de que você precisa. As pessoas podem realmente tentar algo diferente se esperam que haja uma chance de algo dar errado. Os erros não oferecem essa oportunidade. Mesmo a configuração de um manipulador de erros personalizado não impede uma simples exceção. Em relação aos seus comentários na sua pergunta, o que é um aplicativo 'razoável' depende inteiramente do contexto do aplicativo. As pessoas não podem capturar exceções se acharem que isso nunca acontecerá. Não dar uma escolha às pessoas é uma coisa ruim ™.
  • Rastreios de pilha. Se algo der errado, você sabe onde e em que contexto o problema aconteceu. Você já tentou rastrear a origem de algum erro de alguns dos métodos principais? Se você chamar uma função com poucos parâmetros, receberá um erro inútil, que destaca o início do método que você está chamando e deixa totalmente de fora de onde ele está chamando.
  • Clareza. Combine os dois acima e você obterá um código mais claro. Se você tentar usar um manipulador de erros personalizado para manipular erros (por exemplo, para gerar rastreamentos de pilha para erros), todo o seu tratamento de erros estará em uma função, não onde os erros estão realmente sendo gerados.

Resolvendo suas preocupações, chamar um método que não existe pode ser uma possibilidade válida . Isso depende inteiramente do contexto do código que você está escrevendo, mas há alguns casos em que isso pode acontecer. Dirigindo seu caso de uso exato, alguns servidores de banco de dados podem permitir algumas funcionalidades que outros não. Usar try/ catche exceções em __call()vs. uma função para verificar recursos é um argumento completamente diferente.

O único caso de uso que posso pensar em usar trigger_erroré para E_USER_WARNINGou mais baixo. Disparar um pensamento E_USER_ERRORé sempre um erro na minha opinião.

Matthew Scharley
fonte
Obrigado pela resposta :) - 1) Concordo que as exceções quase sempre devem ser usadas sobre erros - todos os seus argumentos são pontos válidos. No entanto .. Eu acho que, neste contexto o seu argumento falha ..
Chriso
2
" chamar um método que não existe pode ser uma possibilidade válida " - discordo completamente. Em todos os idiomas (que eu já usei), chamar uma função / método que não existe resultará em um erro. É não uma condição que deve ser capturada e tratada. Os idiomas estáticos não permitem que você compile com uma chamada de método inválida e os idiomas dinâmicos falharão assim que chegarem à chamada.
Chriso #
Se você ler minha segunda edição, verá meu caso de uso. Digamos que você tenha duas implementações da mesma classe, uma usando __call e outra com métodos codificados. Ignorando os detalhes da implementação, por que as duas classes devem se comportar de maneira diferente quando implementam a mesma interface? O PHP acionará um erro se você chamar um método inválido com a classe que possui métodos codificados. Usando trigger_errorno contexto de __call ou imita __callStatic o comportamento padrão da língua
Chriso
@chriso: linguagens dinâmicas que não são PHP falharão com uma exceção que pode ser detectada . Ruby, por exemplo, lança um NoMethodErrorque você pode pegar se desejar. Erros são um erro gigante em PHP na minha opinião pessoal. Só porque o núcleo usa um método quebrado para relatar erros não significa que seu próprio código deveria.
Matthew Scharley
@chriso Eu gosto de acreditar que a única razão pela qual o núcleo ainda usa erros é a compatibilidade com versões anteriores. Esperamos que o PHP6 seja outro salto adiante como o PHP5 e que elimine erros completamente. No final do dia, erros e exceções geram o mesmo resultado: um encerramento imediato do código em execução. Com uma exceção, você pode diagnosticar por que e onde muito mais fácil.
Matthew Scharley
3

Erros PHP padrão devem ser considerados obsoletos. O PHP fornece uma classe interna ErrorException para converter erros, avisos e avisos em exceções com um rastreamento de pilha adequado completo. Você o usa assim:

function errorToExceptionHandler($errNo, $errStr, $errFile, $errLine, $errContext)
{
if (error_reporting() == 0) return;
throw new ErrorException($errStr, 0, $errNo, $errFile, $errLine);
}
set_error_handler('errorToExceptionHandler');

Usando isso, essa questão se torna discutível. Os erros internos agora geram exceções e, portanto, seu próprio código também deve.

Wayne
fonte
1
Se você usar isso, precisará verificar o tipo do erro, para não transformar E_NOTICEs em exceções. Isso seria ruim.
Matthew Scharley
1
Não, transformar E_NOTICES em exceções é bom! Todos os avisos devem ser considerados erros; Essa é uma boa prática, esteja você convertendo-os em exceções ou não.
Wayne
2
Normalmente, eu concordo com você, mas no momento em que você começa a usar qualquer código de terceiros, isso tende a cair rapidamente.
Matthew Scharley
Quase todas as bibliotecas de terceiros são E_STRICT | E_ALL seguro. Se eu estiver usando um código que não é, eu irei corrigi-lo. Trabalho assim há anos sem problemas.
Wayne
você obviamente não usou o Dual antes. O núcleo é realmente bom como este, mas muitos, muitos módulos de terceiros não são
Matthew Scharley
0

IMO, este é um caso de uso perfeitamente válido para trigger_error:

function handleError($errno, $errstring, $errfile, $errline, $errcontext) {
    if (error_reporting() & $errno) {
        // only process when included in error_reporting
        return handleException(new \Exception($errstring, $errno));
    }
    return true;
}

function handleException($exception){
    // Here, you do whatever you want with the generated
    // exceptions. You can store them in a file or database,
    // output them in a debug section of your page or do
    // pretty much anything else with it, as if it's a
    // normal variable

    switch ($code) {
        case E_ERROR:
        case E_CORE_ERROR:
        case E_USER_ERROR:
            // Make sure script exits here
            exit(1);
        default:
            // Let script continue
            return true;
    }
}

// Set error handler to your custom handler
set_error_handler('handleError');
// Set exception handler to your custom handler
set_exception_handler('handleException');


// ---------------------------------- //

// Generate warning
trigger_error('This went wrong, but we can continue', E_USER_WARNING);

// Generate fatal error :
trigger_error('This went horrible wrong', E_USER_ERROR);

Usando essa estratégia, você obtém o $errcontextparâmetro se fizer $exception->getTrace()dentro da função handleException. Isso é muito útil para determinados fins de depuração.

Infelizmente, isso funciona apenas se você usar trigger_errordiretamente do seu contexto, o que significa que não é possível usar uma função / método de invólucro para alias a trigger_errorfunção (portanto, você não pode fazer algo como function debug($code, $message) { return trigger_error($message, $code); }se desejar os dados do contexto em seu rastreio).

Eu estava procurando uma alternativa melhor, mas até agora não encontrei nenhuma.

John Slegers
fonte