`trigger_error` vs` throw Exception` w kontekście magicznych metod PHP

13

Prowadzę debatę z kolegą na temat prawidłowego użycia (jeśli w ogóle) trigger_errorw kontekście magicznych metod . Po pierwsze, uważam, że trigger_errornależy tego unikać, z wyjątkiem tego jednego przypadku.

Powiedzmy, że mamy klasę z jedną metodą foo()

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

Powiedzmy teraz, że chcemy zapewnić dokładnie ten sam interfejs, ale użyj magicznej metody, aby przechwycić wszystkie wywołania metod

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

Obie klasy są takie same pod względem odpowiedzi, foo()ale różnią się przy wywoływaniu niepoprawnej metody.

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

Moim argumentem jest, że magiczne metody powinny wywoływać, trigger_errorgdy zostanie złapana nieznana metoda

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

Tak, aby obie klasy zachowywały się (prawie) identycznie

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

Mój przypadek użycia to implementacja ActiveRecord. Używam __calldo chwytania i obsługi metod, które zasadniczo robią to samo, ale mają modyfikatory takie jak Distinctlub Ignorenp

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

lub

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

Metody takie jak where(), from(), groupBy(), itd. Są zakodowane.

Mój argument jest podkreślony, gdy przypadkowo dzwonisz insret(). Gdyby moja aktywna implementacja rekordu zapisała wszystkie metody na stałe, byłby to błąd.

Jak w przypadku każdej dobrej abstrakcji, użytkownik powinien nie znać szczegółów implementacji i polegać wyłącznie na interfejsie. Dlaczego implementacja korzystająca z magicznych metod powinna zachowywać się inaczej? Oba powinny być błędem.

Chris
źródło

Odpowiedzi:

7

Weź dwie implementacje tego samego interfejsu ActiveRecord ( select(), where()etc.)

class ActiveRecord1 {
    //Hardcodes all methods
}

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

Jeśli wywołasz niepoprawną metodę w pierwszej klasie, np ActiveRecord1::insret(). Domyślnym zachowaniem PHP jest wyzwolenie błędu . Nieprawidłowe wywołanie funkcji / metody nie jest warunkiem, który rozsądna aplikacja chciałaby przechwycić i obsłużyć. Jasne, możesz go złapać w językach takich jak Ruby lub Python, w których błąd jest wyjątkiem, ale inne (JavaScript / dowolny język statyczny / więcej?) Nie powiedzie się.

Powrót do PHP - jeśli obie klasy implementują ten sam interfejs, dlaczego nie powinny wykazywać tego samego zachowania?

Jeśli wykryje __calllub __callStaticwykryje niepoprawną metodę, powinny one wywołać błąd naśladujący domyślne zachowanie języka

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

Nie spieram się, czy należy stosować błędy w stosunku do wyjątków (w 100% nie powinny), jednak uważam, że magiczne metody PHP są wyjątkiem - kalambur zamierzony :) - do tej reguły w kontekście języka

Chris
źródło
1
Przepraszam, ale nie kupuję tego. Dlaczego powinniśmy być owcami, ponieważ ponad 10 lat temu w PHP nie istniały wyjątki, kiedy klasy były po raz pierwszy wdrażane w PHP 4.something?
Matthew Scharley
@Matthew dla spójności. Nie dyskutuję o wyjątkach vs. błędach (nie ma debaty) ani o tym, czy PHP zawodzi (nie), argumentuję, że w tej bardzo wyjątkowej sytuacji, dla zachowania spójności , najlepiej naśladować zachowanie języka
Chris
Spójność nie zawsze jest dobra. Konsekwentna, ale kiepska konstrukcja jest nadal kiepską konstrukcją, której używanie jest trudne pod koniec dnia.
Matthew Scharley
@ Matthew prawda, ale IMO wyzwalające błąd w nieprawidłowym wywołaniu metody nie jest złym projektem 1) wiele (większość?) Języków ma to wbudowane i 2) Nie mogę wymyślić jednego przypadku, w którym kiedykolwiek chciałbyś aby złapać nieprawidłowy wywołanie metody i poradzić?
Chris
@chriso: Wszechświat jest wystarczająco duży, aby zdarzały się przypadki użycia, o których ani ty, ani ja nie marzylibyśmy, aby tak się stało. Używasz __call()do rutowania dynamicznego, czy naprawdę tak nierozsądne jest oczekiwanie, że gdzieś na ścieżce ktoś będzie chciał zająć się sprawą, w której się to nie powiedzie? W każdym razie dzieje się to w kółko, więc to będzie mój ostatni komentarz. Rób co chcesz, pod koniec dnia sprowadza się to do osądu: lepsze wsparcie kontra konsekwencja. Obie metody będą miały taki sam wpływ na aplikację w przypadku braku specjalnego obchodzenia się.
Matthew Scharley,
3

Zamierzam opublikować moją opinię, ale jeśli używasz trigger_errorgdziekolwiek, to robisz coś złego. Wyjątki to droga.

Zalety wyjątków:

  • Można je złapać. Jest to ogromna zaleta i powinna być jedyną, której potrzebujesz. Ludzie mogą spróbować czegoś innego, jeśli spodziewają się, że coś pójdzie nie tak. Błędy nie dają tej okazji. Nawet skonfigurowanie niestandardowej procedury obsługi błędów nie podtrzymuje świecy, aby po prostu złapać wyjątek. Jeśli chodzi o komentarze w pytaniu, to, co to jest „uzasadniona” aplikacja, zależy całkowicie od jej kontekstu. Ludzie nie mogą wychwycić wyjątków, jeśli myślą, że w ich przypadku tak się nigdy nie stanie. Nie dawanie ludziom wyboru to Bad Thing ™.
  • Układaj ślady. Jeśli coś pójdzie nie tak, wiesz, gdzie i w jakim kontekście wystąpił problem. Czy próbowałeś kiedyś wyśledzić, skąd pochodzi błąd niektórych z podstawowych metod? Jeśli wywołasz funkcję o zbyt małej liczbie parametrów, otrzymasz niepotrzebny błąd, który podkreśla początek metody, którą wywołujesz, i całkowicie pomija miejsce, z którego się ona wywołuje.
  • Przejrzystość. Połącz powyższe dwa, a otrzymasz wyraźniejszy kod. Jeśli spróbujesz użyć niestandardowej procedury obsługi błędów do obsługi błędów (np. Do generowania śladów stosu dla błędów), cała obsługa błędów odbywa się w jednej funkcji, a nie w miejscu, w którym błędy są generowane.

W odpowiedzi na Twoje obawy wywołanie metody, która nie istnieje, może być uzasadnioną możliwością . Zależy to całkowicie od kontekstu pisanego kodu, ale w niektórych przypadkach może się to zdarzyć. Reagując na konkretny przypadek użycia, niektóre serwery baz danych mogą oferować pewne funkcje, których inne nie. Używanie try/ catchi wyjątków w __call()funkcji w celu sprawdzenia możliwości to zupełnie inny argument.

Jedynym przypadkiem użycia, który mogę wymyślić, trigger_errorjest użycie E_USER_WARNINGlub niższe. Wywołanie E_USER_ERRORchoć jest zawsze moim zdaniem błędem.

Matthew Scharley
źródło
Dziękuję za odpowiedź :) - 1) Zgadzam się, że prawie zawsze należy stosować wyjątki w przypadku błędów - wszystkie argumenty są poprawnymi punktami. Jednak .. Myślę, że w tym kontekście twój argument się nie udaje ...
Chris
2
wywołanie metody, która nie istnieje, może być uzasadnioną możliwością ” - całkowicie się nie zgadzam. W każdym języku (którego kiedykolwiek używałem) wywołanie funkcji / metody, która nie istnieje, spowoduje błąd. To nie stan, który powinien być złapany i przetwarzane. Języki statyczne nie pozwolą na kompilację z niepoprawnym wywołaniem metody, a języki dynamiczne zakończą się niepowodzeniem po osiągnięciu wywołania.
Chris
Jeśli przeczytasz moją drugą edycję, zobaczysz mój przypadek użycia. Załóżmy, że masz dwie implementacje tej samej klasy, jedną z wykorzystaniem __call i jedną z metodami zakodowanymi na stałe. Ignorując szczegóły implementacji, dlaczego obie klasy powinny zachowywać się inaczej, gdy implementują ten sam interfejs? PHP wywoła błąd, jeśli wywołasz niepoprawną metodę z klasą, która ma metody zakodowane na stałe. Używanie trigger_errorw kontekście __call lub __callStatic naśladuje domyślne zachowanie języka
Chris
@chriso: Dynamiczne języki, które nie są PHP, zawiodą z wyjątkiem, który można złapać . Ruby na przykład rzuca, NoMethodErrorktóry możesz złapać, jeśli chcesz. Błędy są moim zdaniem ogromnym błędem w PHP. To, że rdzeń używa zepsutej metody do zgłaszania błędów, nie oznacza, że ​​powinien to zrobić Twój własny kod.
Matthew Scharley
@chriso Lubię wierzyć, że jedynym powodem, dla którego rdzeń nadal używa błędów, jest kompatybilność wsteczna. Mam nadzieję, że PHP6 będzie kolejnym krokiem naprzód, takim jak PHP5, i całkowicie usunie błędy. Na koniec dnia zarówno błędy, jak i wyjątki generują ten sam wynik: natychmiastowe zakończenie wykonywania kodu. Z wyjątkiem możesz zdiagnozować, dlaczego i gdzie jest to o wiele łatwiejsze.
Matthew Scharley,
3

Standardowe błędy PHP należy uznać za przestarzałe. PHP zapewnia wbudowaną klasę ErrorException do konwertowania błędów, ostrzeżeń i powiadomień na wyjątki z pełnym odpowiednim śledzeniem stosu. Używasz go w ten sposób:

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

Dzięki temu pytanie staje się dyskusyjne. Wbudowane błędy wywołują teraz wyjątki, więc twój kod również powinien.

Wayne
źródło
1
Jeśli tego użyjesz, musisz sprawdzić rodzaj błędu, aby nie zamienić E_NOTICEs w wyjątki. To by było złe.
Matthew Scharley,
1
Nie, zmiana E_NOTICES w Wyjątki jest dobra! Wszystkie powiadomienia należy traktować jako błędy; to dobra praktyka, niezależnie od tego, czy przekształcasz je w wyjątki, czy nie.
Wayne,
2
Normalnie zgodziłbym się z tobą, ale w momencie, gdy zaczniesz używać kodu strony trzeciej, to szybko ma tendencję do upadku.
Matthew Scharley,
Prawie wszystkie biblioteki stron trzecich to E_STRICT | E_ALL bezpieczne. Jeśli używam kodu, który nie jest, wejdę i naprawię go. Pracowałem w ten sposób od lat bez problemu.
Wayne,
najwyraźniej wcześniej nie korzystałeś z Dual. Rdzeń jest naprawdę dobry jak ten, ale wielu, wielu modułów innych producentów nie są
Matthew Scharley
0

IMO, jest to całkowicie uzasadniony przypadek użycia dla 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);

Korzystając z tej strategii, otrzymujesz $errcontextparametr, jeśli wykonujesz $exception->getTrace()tę funkcję handleException. Jest to bardzo przydatne do niektórych celów debugowania.

Niestety działa to tylko wtedy, gdy używasz trigger_errorbezpośrednio z kontekstu, co oznacza, że ​​nie możesz użyć funkcji / metody opakowania do aliasu trigger_errorfunkcji (więc nie możesz zrobić czegoś takiego, function debug($code, $message) { return trigger_error($message, $code); }jeśli chcesz, aby dane kontekstu były w twoim śladzie).

Szukałem lepszej alternatywy, ale jak dotąd nie znalazłem żadnej.

John Slegers
źródło