`trigger_error` vs` throw Exception` en el contexto de los métodos mágicos de PHP

13

Estoy teniendo un debate con un colega sobre el uso correcto (si lo hay) trigger_erroren el contexto de los métodos mágicos . En primer lugar, creo que trigger_errordebería evitarse a excepción de este caso.

Digamos que tenemos una clase con un método foo()

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

Ahora digamos que queremos proporcionar exactamente la misma interfaz pero usar un método mágico para capturar todas las llamadas a métodos

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 clases son iguales en la forma en que responden, foo()pero difieren cuando se llama a un método no válido.

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

Mi argumento es que los métodos mágicos deberían llamar trigger_errorcuando se detecta un método desconocido

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 clases se comporten (casi) de manera 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

Mi caso de uso es una implementación de ActiveRecord. Utilizo __callpara capturar y manejar métodos que esencialmente hacen lo mismo pero que tienen modificadores como Distincto Ignore, por ejemplo

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

o

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

Métodos como where(), from(), groupBy(), etc. están codificadas.

Mi argumento se resalta cuando accidentalmente llamas insret(). Si mi implementación de registro activa codificara todos los métodos, sería un error.

Como con cualquier buena abstracción, el usuario debe desconocer los detalles de implementación y confiar únicamente en la interfaz. ¿Por qué la implementación que usa métodos mágicos debería comportarse de manera diferente? Ambos deberían ser un error.

chriso
fuente

Respuestas:

7

Tomar dos implementaciones de la misma interfaz ActiveRecord ( select(), where(), etc.)

class ActiveRecord1 {
    //Hardcodes all methods
}

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

Si llama a un método no válido en la primera clase, por ejemplo ActiveRecord1::insret(), el comportamiento predeterminado de PHP es desencadenar un error . Una llamada de función / método no válida no es una condición que una aplicación razonable quiera capturar y manejar. Claro, puede detectarlo en lenguajes como Ruby o Python donde un error es una excepción, pero otros (JavaScript / algún lenguaje estático / más) fallarán.

Volver a PHP: si ambas clases implementan la misma interfaz, ¿por qué no deberían exhibir el mismo comportamiento?

Si __callo __callStaticdetectar un método válido, que debe dar lugar a un error de imitar el comportamiento por defecto de la lengua

$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);

No estoy discutiendo si los errores deberían usarse sobre las excepciones (no deberían hacerlo al 100%), sin embargo, creo que los métodos mágicos de PHP son una excepción (juego de palabras :)) a esta regla en el contexto del lenguaje

chriso
fuente
1
Lo siento, pero no lo compro. ¿Por qué deberíamos ser ovejas porque no existían excepciones en PHP hace más de una década cuando las clases se implementaron por primera vez en PHP 4.something?
Matthew Scharley
@Matthew para mayor consistencia. No estoy debatiendo excepciones versus errores (no hay debate) o si PHP tiene fallas de diseño (lo hace), estoy argumentando que en esta circunstancia única, por coherencia , es mejor imitar el comportamiento del lenguaje
chriso 01 de
La consistencia no siempre es algo bueno. Un diseño consistente pero pobre sigue siendo un diseño pobre que es difícil de usar al final del día.
Matthew Scharley
@Matthew es cierto, pero IMO desencadenar un error en una llamada de método no válida no es un diseño deficiente 1) muchos (¿la mayoría?) Idiomas lo tienen incorporado, y 2) No puedo pensar en un solo caso en el que alguna vez desearías para atrapar una llamada de método no válida y manejarla?
chriso 01 de
@chriso: El universo es lo suficientemente grande como para que haya casos de uso que ni tú ni yo soñamos que suceda. Está utilizando __call()para hacer un enrutamiento dinámico, ¿es realmente tan irracional esperar que en algún lugar del camino alguien quiera manejar el caso donde eso falla? En cualquier caso, esto va en círculos, así que este será mi último comentario. Haga lo que quiera, al final del día, todo se reduce a una decisión: mejor soporte frente a consistencia. Ambos métodos tendrán el mismo efecto en la aplicación en el caso de que no haya un manejo especial.
Matthew Scharley
3

Voy a lanzar mi opinión obvia, pero si la usas en trigger_errorcualquier lugar, entonces estás haciendo algo mal. Las excepciones son el camino a seguir.

Ventajas de las excepciones:

  • Pueden ser atrapados. Esta es una gran ventaja, y debería ser la única que necesita. Las personas pueden probar algo diferente si esperan que exista la posibilidad de que algo salga mal. Los errores no te dan esta oportunidad. Incluso configurar un controlador de errores personalizado no tiene una vela para simplemente detectar una excepción. Con respecto a sus comentarios en su pregunta, lo que es una aplicación 'razonable' depende completamente del contexto de la aplicación. Las personas no pueden atrapar excepciones si piensan que nunca sucederá en su caso. No darles a las personas una opción es algo malo.
  • Apilar rastros. Si algo sale mal, usted sabe dónde y en qué contexto ocurrió el problema. ¿Alguna vez ha tratado de rastrear dónde proviene un error de algunos de los métodos principales? Si llama a una función con muy pocos parámetros, obtiene un error inútil que resalta el inicio del método al que llama y deja totalmente de lado desde dónde está llamando.
  • Claridad. Combine los dos anteriores y obtendrá un código más claro. Si intenta utilizar un controlador de errores personalizado para manejar los errores (por ejemplo, para generar seguimientos de pila para errores), todo su manejo de errores se realiza en una función, no donde se generan realmente los errores.

Abordar sus inquietudes, llamar a un método que no existe puede ser una posibilidad válida . Esto depende completamente del contexto del código que está escribiendo, pero hay algunos casos en los que esto puede suceder. Al abordar su caso de uso exacto, algunos servidores de bases de datos pueden permitir alguna funcionalidad que otros no. Usar try/ catchy excepciones en __call()vs. una función para verificar capacidades es un argumento completamente diferente.

El único caso de uso que se me ocurre para usar trigger_errores para E_USER_WARNINGo más bajo. Disparar un pensamiento E_USER_ERRORes siempre un error en mi opinión.

Matthew Scharley
fuente
Gracias por la respuesta :) - 1) Estoy de acuerdo en que las excepciones deben usarse casi siempre sobre los errores; todos sus argumentos son puntos válidos. Sin embargo .. Creo que en este contexto el argumento falla ..
Chriso
2
" Llamar a un método que no existe puede ser una posibilidad válida " - Estoy completamente en desacuerdo. En todos los idiomas (que he usado), llamar a una función / método que no existe dará como resultado un error. Es no una condición que debe ser capturados y manipulados. Los idiomas estáticos no le permitirán compilar con una llamada de método no válida y los idiomas dinámicos fallarán una vez que lleguen a la llamada.
chriso 01 de
Si lees mi segunda edición, verás mi caso de uso. Supongamos que tiene dos implementaciones de la misma clase, una con __call y otra con métodos codificados. Ignorando los detalles de implementación, ¿por qué ambas clases deberían comportarse de manera diferente cuando implementan la misma interfaz? PHP activará un error si llama a un método no válido con la clase que tiene métodos codificados. El uso trigger_erroren el contexto de __call o __callStatic imita el comportamiento predeterminado del lenguaje
chriso
@chriso: los lenguajes dinámicos que no son PHP fallarán con una excepción que puede detectarse . Ruby, por ejemplo, arroja uno NoMethodErrorque puedes atrapar si lo deseas. Los errores son un error gigante en PHP en mi opinión personal. El hecho de que el núcleo utilice un método roto para informar errores no significa que su propio código deba hacerlo.
Matthew Scharley 01 de
@chriso Me gusta creer que la única razón por la que Core todavía usa errores es la compatibilidad con versiones anteriores. Esperemos que PHP6 sea otro salto adelante como lo fue PHP5 y elimine los errores por completo. Al final del día, tanto los errores como las excepciones generan el mismo resultado: una terminación inmediata del código de ejecución. Sin embargo, puede diagnosticar por qué y dónde es mucho más fácil.
Matthew Scharley 01 de
3

Los errores estándar de PHP deben considerarse obsoletos. PHP proporciona una clase incorporada ErrorException para convertir errores, advertencias y avisos en excepciones con un seguimiento de pila completo y adecuado. Lo usas así:

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 eso, esta pregunta se vuelve discutible. Los errores incorporados ahora generan excepciones, por lo que su propio código también debería hacerlo.

Wayne
fuente
1
Si usa esto, debe verificar el tipo de error, para que no se conviertan E_NOTICEen excepciones. Eso sería malo.
Matthew Scharley 01 de
1
No, ¡convertir E_NOTICES en Excepciones es bueno! Todos los avisos deben considerarse errores; es una buena práctica si los estás convirtiendo en excepciones o no.
Wayne
2
Normalmente estaría de acuerdo con usted, pero en el momento en que comienza a usar cualquier código de terceros, esto tiende a caer rápidamente.
Matthew Scharley 01 de
Casi todas las bibliotecas de terceros son E_STRICT | E_ALL seguro. Si estoy usando un código que no es, entraré y lo arreglaré. He trabajado de esta manera durante años sin problemas.
Wayne
obviamente no has usado Dual antes. El núcleo es realmente bueno así, pero muchos, muchos módulos de terceros no lo son
Matthew Scharley
0

OMI, este es un caso de uso perfectamente 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 esta estrategia, obtienes el $errcontextparámetro si lo haces $exception->getTrace()dentro de la función handleException. Esto es muy útil para ciertos propósitos de depuración.

Desafortunadamente, esto funciona solo si lo usa trigger_errordirectamente desde su contexto, lo que significa que no puede usar una función / método de envoltura para crear un alias de la trigger_errorfunción (por lo que no puede hacer algo como function debug($code, $message) { return trigger_error($message, $code); }si desea los datos de contexto en su seguimiento).

He estado buscando una mejor alternativa, pero hasta ahora no he encontrado ninguna.

John Slegers
fuente