S7
zurückException Handling in TYPO3
28.05.2019
Thema: TYPO3

Exceptions in TYPO3, wie gehe ich damit um?

Exception Handling in TYPO3

Beim Kunden mit den FlashMessages war es auch notwendig, Exceptions so sauber wie möglich abzufangen und eben nicht dem Webseiten-Anwender einfach nur die Exception um die Ohren zu werfen.

Der Relaunch des Systems gestaltete sich insofern schon schwierig, als dass die ehemals betreuende Agentur irgendwann angefangen hat, um TYPO3 4.5 herum zu programmieren. Das führte zu diversen Fehlern, die eher schwer zu debuggen waren.

Aus den Fehlern wollte ich beim Relaunch lernen und soweit optimieren, wie möglich. Dazu gehört auch, dass Exceptions, die vorhersehbar und vermeidbar sind, entsprechend behandelt werden und dem User statt der typischen Fehlerseite eine korrekte Nachricht ausgegeben wird, was genau da gerade schief gelaufen ist.

Nach einigen Experimenten mit dem Debugger und durchforsten des StackTrace bin ich zu der Erkenntnis gekommen, dass jede Extbase-Action zuerst durch die ControllerInterface-Methode processRequest() läuft.

Dies machte ich mir zu eigen und habe einen BaseConrtoller geschrieben, der eben diese processRequest() überlädt und den parent-call in einen try-catch-Block packt.

/**
 * @param RequestInterface $request
 * @param ResponseInterface $response
 * @throws Exception
 */
public function processRequest(RequestInterface $request, ResponseInterface $response)
{
    try {
        parent::processRequest($request, $response);
    } catch (Exception $e) {
        $this->handleProcessRequestException($e);
    }
}

Somit ist sichergestellt, dass jede Exception bei mir landet und ich sie einzeln behandeln kann.

Mein BaseController ist abstract, somit kann ich die handleProcessRequestException() als abstract method definieren und der eigentliche Controller hat sie zu behandeln, denn der weiß am besten, welche Exceptions für ihn wichtig sind und wie er sie zu behandeln hat.

Was genau macht die Methode jetzt aber im eigentlichen Controller?

Sie handelt Exceptions je nach Typ ab. Das kann zum Beispiel so aussehen:

/**
 * @param Exception $exception
 * @return void
 * @throws UnsupportedRequestTypeException
 * @throws Exception
 */
public function handleProcessRequestException(Exception $exception)
{
    if ($exception instanceof TargetNotFoundException) {
        $messageTitle = $this->translate('job.list.notfound.header');
        $messageBody = $this->translate('job.list.notfound.message');
        $this->setFlashMessage($messageBody, $messageTitle);
        $this->redirect('list');
        exit;
    }
    
    throw new Exception($exception->getMessage(), $exception->getCode());
}

Die TargetNotFoundException wird geworfen, wenn ein Datensatz nicht existiert, also hidden oder deleted ist. Das sollte eine der am häufigsten geworfenen Exceptions sein und ist auch notwendig.

Ein Datensatz, der ein Ablaufdatum hat und dann nicht mehr aufrufbar ist, zum Beispiel ein Termin, der abgelaufen ist. Suchmaschinen haben den absoluten Link aber eventuell noch in der Liste. Standardverhalten von TYPO3 ist jetzt, dass die showAction in eine TargetNotFoundException läuft und einen StackTrace inklusive Fehlermeldung wirft.

Das ist unschön, denn weder will man seinen Code, noch überhaupt eine Fehlermeldung auf der Website veröffentlichen.

Mit oben gezeigtem Code-Snippet kann somit ein sauberer Redirect inklusive FlashMessage gesendet werden, der die Website sauber funktionieren lässt, man bleibt navigierbar und erhält eine korrekte Meldung, was genau hier schief gelaufen ist.

Die Methode $this->translate(); ist eine Helper des BaseController, welche einfach nur

LocalizationUtility::translate($key, 'my_extension');

aufruft. Das erspart einen Code overhead, wenn man im Controller übersetzen muss.

Das funktioniert sehr gut. Bis man wirklich einen redirect braucht.

Ein redirect wird in TYPO3 immer über eine bestimmte Exception abgefangen und dort behandelt. Meine Methode oben wirft zwar ihr unbekannte Exceptions weiter, aber in dem Fall nicht korrekt.

Das Resultat ist, kein einziger redirect innerhalb dieses Controllers wird noch ausgeführt.

Hier habe ich mit meinem Kollegen einiges an Debugging-Arbeit benötigt, aber wir haben die Stelle gefunden und wissen, was notwendig ist, damit die redirects wieder tun.

if ($exception->getCode() == 1476045828) {
    throw new StopActionException($exception->getMessage(), $exception->getCode());
}

Der Controller wirft die StopActionException mit oben genanntem Code. Die gesamte handleProcessRequestException() sieht also wie folgt aus:

/**
 * @param Exception $exception
 * @return void
 * @throws StopActionException
 * @throws UnsupportedRequestTypeException
 * @throws Exception
 */
public function handleProcessRequestException(Exception $exception)
{
    if ($exception instanceof TargetNotFoundException) {
        $messageTitle = $this->translate('notfound.header');
        $messageBody = $this->translate('notfound.message');
        $this->setFlashMessage($messageBody, $messageTitle);
        $this->redirect('list');
        exit;
    }
    if ($exception->getCode() == 1476045828) {
        throw new StopActionException($exception->getMessage(), $exception->getCode());
    }
    throw new Exception($exception->getMessage(), $exception->getCode());
}

Auch interessant hier ist die RequiredArgumentMissingException. Die tritt auf, wenn eine action ohne benötigten Parameter aufgerufen wird.

Eventuell ist der gesamte Workflow noch verbesserungswürdig, aber er bietet meiner Meinung nach eine gute Möglichkeit, mit wichtigen Exceptions korrekt umzugehen und in Verbindung mit den FlashMessages eine saubere Möglichkeit, Fehler auf der Website zu behandeln, ohne den Besucher zu vertreiben.