`trigger_error` vs` buttare l'eccezione` nel contesto dei metodi magici di PHP


13

Sto discutendo con un collega sull'uso corretto (se presente) di trigger_errornel contesto dei metodi magici . In primo luogo, penso che trigger_errordovrebbe essere evitato, tranne per questo caso.

Supponiamo che abbiamo una classe con un metodo foo()

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

Ora diciamo che vogliamo fornire la stessa identica interfaccia ma usare un metodo magico per catturare tutte le chiamate di metodo

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

Entrambe le classi sono uguali nel modo in cui rispondono foo()ma differiscono quando si chiama un metodo non valido.

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

La mia tesi è che i metodi magici dovrebbero chiamare trigger_errorquando viene catturato un metodo sconosciuto

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;
        }
    }
}

In modo che entrambe le classi si comportino (quasi) in modo identico

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

Il mio caso d'uso è un'implementazione di ActiveRecord. Uso __callper catturare e gestire metodi che essenzialmente fanno la stessa cosa ma hanno modificatori come Distincto Ignore, ad esempio

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

o

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

Metodi come where(), from(), groupBy(), ecc, sono hard-coded.

Il mio argomento viene evidenziato quando si chiama accidentalmente insret(). Se l'implementazione del mio record attivo codificasse tutti i metodi, sarebbe un errore.

Come per ogni buona astrazione, l'utente dovrebbe essere ignaro dei dettagli di implementazione e fare affidamento esclusivamente sull'interfaccia. Perché l'implementazione che utilizza metodi magici dovrebbe comportarsi diversamente? Entrambi dovrebbero essere un errore.

Risposte:


7

Prendere due implementazioni della stessa interfaccia ActiveRecord ( select(), where(), ecc)

class ActiveRecord1 {
    //Hardcodes all methods
}

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

Se chiami un metodo non valido sulla prima classe, ad esempio ActiveRecord1::insret(), il comportamento PHP predefinito è quello di innescare un errore . Una chiamata di funzione / metodo non valida non è una condizione che un'applicazione ragionevole vorrebbe catturare e gestire. Certo, puoi prenderlo in lingue come Ruby o Python dove un errore è un'eccezione, ma altri (JavaScript / qualsiasi linguaggio statico / altro?) Falliranno.

Torna a PHP - se entrambe le classi implementano la stessa interfaccia, perché non dovrebbero mostrare lo stesso comportamento?

Se __callo __callStaticrileva un metodo non valido, dovrebbero attivare un errore per imitare il comportamento predefinito della lingua

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

Non sto discutendo se gli errori debbano essere utilizzati rispetto alle eccezioni (al 100% non dovrebbero), tuttavia credo che i metodi magici di PHP siano un'eccezione - gioco di parole :) - per questa regola nel contesto del linguaggio


1
Scusa, ma non lo compro. Perché dovremmo essere pecore, perché le eccezioni non esistevano in PHP più di un decennio fa, quando le classi sono state attuate prima in PHP 4.something?
Matthew Scharley,

@Matthew per coerenza. Non sto discutendo delle eccezioni rispetto agli errori (non c'è dibattito) o se PHP non ha un design fallito (lo fa), sto sostenendo che in questa circostanza unica, per coerenza , è meglio imitare il comportamento del linguaggio
chriso

La coerenza non è sempre una buona cosa. Un design coerente ma scadente è ancora un design scadente che è un dolore da usare alla fine della giornata.
Matthew Scharley,

@Matthew vero, ma IMO che genera un errore in una chiamata di metodo non valida non è un progetto scadente 1) molte (la maggior parte?) Lingue lo hanno incorporato e 2) Non riesco a pensare a un singolo caso in cui vorresti mai per catturare un metodo di chiamata non valida e gestirlo?
Chriso

@chriso: L'universo è sufficientemente grande da esistere casi d'uso che né tu né io potremmo mai sognare di accadere. Stai usando __call()per fare il routing dinamico, è davvero così irragionevole aspettarsi che da qualche parte in fondo qualcuno possa voler gestire il caso in cui ciò fallisce? Ad ogni modo, questo sta andando in cerchio, quindi questo sarà il mio ultimo commento. Fai quello che vuoi, in fin dei conti questo si riduce a una richiesta di giudizio: migliore supporto vs coerenza. Entrambi i metodi comporteranno lo stesso effetto sull'applicazione in caso di mancata gestione.
Matthew Scharley,

3

Sto per lanciare la mia opinione supponente là fuori, ma se usi trigger_errorovunque, allora stai facendo qualcosa di sbagliato. Le eccezioni sono la strada da percorrere.

Vantaggi delle eccezioni:

  • Possono essere catturati. Questo è un enorme vantaggio e dovrebbe essere l'unico di cui hai bisogno. Le persone possono effettivamente provare qualcosa di diverso se si aspettano che ci sia una possibilità che qualcosa vada storto. L'errore non ti dà questa opportunità. Anche l'impostazione di un gestore errori personalizzato non regge il confronto con una semplice eccezione. Per quanto riguarda i tuoi commenti nella tua domanda, quale sia un'applicazione "ragionevole" dipende interamente dal contesto dell'applicazione. Le persone non possono cogliere eccezioni se pensano che non accadrà mai nel loro caso. Non dare alle persone una scelta è una cosa cattiva ™.
  • Pila di tracce. Se qualcosa va storto, sai dove e in quale contesto si è verificato il problema. Hai mai provato a rintracciare la provenienza di un errore da alcuni dei metodi principali? Se si chiama una funzione con troppi parametri si ottiene un errore inutile che evidenzia l'inizio del metodo che si sta chiamando e lascia completamente da dove sta chiamando.
  • Chiarezza. Combina i due precedenti e otterrai un codice più chiaro. Se si tenta di utilizzare un gestore errori personalizzato per gestire gli errori (ad es. Per generare tracce di stack per errori), tutta la gestione degli errori è in una funzione, non dove gli errori vengono effettivamente generati.

Affrontare le tue preoccupazioni, chiamare un metodo che non esiste può essere una possibilità valida . Questo dipende interamente dal contesto del codice che stai scrivendo, ma ci sono alcuni casi in cui ciò può accadere. Affrontando l'esatto caso d'uso, alcuni server di database potrebbero consentire alcune funzionalità che altri non lo fanno. L'uso di try/ catche le eccezioni __call()rispetto a una funzione per verificare le capacità è un argomento completamente diverso.

L'unico caso d'uso che mi viene in mente per l'utilizzo trigger_errorè per E_USER_WARNINGo inferiore. A E_USER_ERRORmio avviso, innescare un pensiero è sempre un errore.


Grazie per la risposta :) - 1) Concordo sul fatto che le eccezioni dovrebbero quasi sempre essere usate sugli errori - tutti i tuoi argomenti sono punti validi. Tuttavia .. Penso che in questo contesto la tua discussione fallisca ..
Chriso

2
" Chiamare un metodo che non esiste può essere una possibilità valida " - Non sono completamente d'accordo. In ogni lingua (che io abbia mai usato), chiamare una funzione / metodo che non esiste comporterà un errore. E ' non è una condizione che deve essere intercettato e gestito. Le lingue statiche non ti consentiranno di compilare con una chiamata di metodo non valida e le lingue dinamiche non riusciranno una volta raggiunta la chiamata.
Chriso

Se leggi la mia seconda modifica vedrai il mio caso d'uso. Supponi di avere due implementazioni della stessa classe, una che usa __call e una con metodi hardcoded. Ignorando i dettagli dell'implementazione, perché entrambe le classi dovrebbero comportarsi diversamente quando implementano la stessa interfaccia? PHP genererà un errore se si chiama un metodo non valido con la classe che ha metodi hardcoded. L'uso trigger_errornel contesto di __call o __callStatic imita il comportamento predefinito della lingua
chriso

@chriso: i linguaggi dinamici che non sono PHP falliranno con un'eccezione che può essere colta . Ruby, ad esempio, lancia una NoMethodErrorche puoi catturare se lo desideri. L'errore è un errore enorme in PHP secondo la mia opinione personale. Solo perché il core utilizza un metodo non funzionante per segnalare errori non significa che il tuo codice dovrebbe.
Matthew Scharley,

@chriso Mi piace credere che l'unica ragione per cui core utilizza ancora errori sia per la compatibilità con le versioni precedenti. Speriamo che PHP6 sia un altro salto in avanti come lo era PHP5 e che elimini del tutto gli errori. Alla fine della giornata, sia gli errori che le eccezioni generano lo stesso risultato: una risoluzione immediata dell'esecuzione del codice. Con un'eccezione puoi diagnosticare perché e dove molto più facilmente.
Matthew Scharley,

3

Gli errori PHP standard dovrebbero essere considerati obsoleti. PHP fornisce una classe ErrorException integrata per convertire errori, avvisi e avvisi in eccezioni con una traccia dello stack corretta e completa. Lo usi in questo modo:

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

Usandolo, questa domanda diventa discutibile. Gli errori integrati ora generano eccezioni e quindi anche il tuo codice dovrebbe.


1
Se si utilizza questo, è necessario verificare il tipo di errore, per non trasformare E_NOTICEs in eccezioni. Sarebbe male.
Matthew Scharley,

1
No, trasformare E_NOTICES in eccezioni è positivo! Tutti gli avvisi devono essere considerati errori; è una buona pratica se li stai convertendo in eccezioni o meno.
Wayne,

2
Normalmente sono d'accordo con te, ma nel momento in cui inizi a utilizzare qualsiasi codice di terze parti, questo tende rapidamente a cadere in faccia.
Matthew Scharley,

Quasi tutte le librerie di terze parti sono E_STRICT | E_ALL sicuro. Se sto usando un codice che non lo è, entrerò e lo riparerò. Ho lavorato in questo modo per anni senza problemi.
Wayne,

ovviamente non hai mai usato Dual prima. Il nucleo è davvero buono come questo, ma molti, molti moduli di terze parti non lo sono
Matthew Scharley,

0

IMO, questo è un caso d'uso perfettamente valido per 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 questa strategia, ottieni il $errcontextparametro se lo fai $exception->getTrace()all'interno della funzione handleException. Questo è molto utile per determinati scopi di debug.

Sfortunatamente, questo funziona solo se usi trigger_errordirettamente dal tuo contesto, il che significa che non puoi usare una funzione / metodo wrapper per aliasare la trigger_errorfunzione (quindi non puoi fare qualcosa come function debug($code, $message) { return trigger_error($message, $code); }se desideri i dati di contesto nella tua traccia).

Ho cercato un'alternativa migliore, ma finora non ne ho trovato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.