7.12. Wyjątki MVC

7.12.1. Wprowadzenie

Komponenty MVC w Zend Framework używają kontrolera frontowego, co oznacza, że wszystkie żądania do danej strony przechodzą przez pojedynczy punkt. W rezultacie wszystkie wyjątki ostatecznie zbierane są w kontrolerze frontowym, pozwalając programiście na obsłużenie ich w jednym miejscu.

Jakkolwiek, wiadomości o wyjątkach oraz informacje o backtrace często zawierają wrażliwe informacje o systemie, jak np. zapytania SQL, informacje o lokacjach plików i wiele innych. Aby pomóc ci chronić swój serwis, domyślnie Zend_Controller_Front łapie wszystkie wyjątki i rejestruje je w obiekcie odpowiedzi; z kolei, obiekt odpowiedzi domyślnie nie wyświetla wiadomości o wyjątkach.

7.12.2. W jaki sposób możesz obsługiwać wyjątki?

Obecnie w komponentach MVC wbudowanych jest kilka mechanizmów pozwalających na obsługę wyjątków

  • Domyślnie rejestrowana i aktywna jest wtyczka obsługi błędów. Ta wtyczka została stworzona aby obsługiwać:

    • Błędy spowodowane brakującym kontrolerem lub akcją

    • Błędy występujące wewnątrz akcji kontrolerów

    Wtyczka działa w oparciu o metodę postDispatch(), i sprawdza czy obiekt uruchamiający, kontroler akcji, lub inny obiekt wyrzucił wyjątek. Jeśli tak, przekazuje ona żądanie do kontrolera obsługi błędu.

    Ta wtyczka obsłuży większość sytuacji, w których został wyrzucony wyjątek, a także poprawnie obsłuży brakujące kontrolery oraz akcje.

  • Zend_Controller_Front::throwExceptions()

    Przekazująć logiczną wartość true do tej metody, możesz nakazać kontrolerowi frontowemu aby zamiast składować wyjątki w obiekcie odpowiedzi, wyrzucił je, żebyś mógł obsłużyć je samodzielnie. Na przykład:

    <?php
    $front->throwExceptions(true);
    try {
        $front->dispatch();
    } catch (Exception $e) {
        // sam obsłuż wyjątki
    }
    

    Ta metoda jest najprawdopodobniej najłatwiejszym sposobem dodania własnej obsługi wyjątków do twojej aplikacji używającej kontrolera frontowego.

  • Zend_Controller_Response_Abstract::renderExceptions()

    Przekazując logiczną wartość true do tej metody, możesz nakazać obiektowi odpowiedzi aby renderował on wyjątki gdy sam będzie renderowany. W takim scenariuszu, każdy wyjątek wyrzucony w twojej aplikacji będzie wyświetlony. To jest jedynie rekomendowane dla nieprodukcyjnych środowisk.

  • Zend_Controller_Front::returnResponse() oraz Zend_Controller_Response_Abstract::isException()

    Przekazanie wartości logicznej true do metody Zend_Controller_Front::returnResponse(), spowoduje, że obiekt Zend_Controller_Front::dispatch() nie będzie renderował odpowiedzi, a zamiast tego ją zwróci. Gdy już masz odpowiedź, możesz sprawdzić czy są w niej wyjątki używając metody isException(), a następnie odebrać wyjątki używając metody getException(). Na przykład:

    <?php
    $front->returnResponse(true);
    $response = $front->dispatch();
    if ($response->isException()) {
        $exceptions = $response->getException();
        // obsługa wyjątków ...
    } else {
        $response->sendHeaders();
        $response->outputBody();
    }
    

    Główną zaletą, dzięki której ta metoda umożliwia więcej niż Zend_Controller_Front::throwExceptions(), jest to, że możesz warunkowo wyświetlać odpowiedź po obsłudze wyjątków.

7.12.3. Wyjątki MVC które możesz napotkać

Różne komponenty MVC -- obiekt żądania, router, obiekt uruchamiający, kontrolery akcji, oraz obiekt odpowiedzi -- każdy może z różnych przyczyn wyrzucać wyjątki. Niektóre wyjątki mogą być warunkowo nadpisane, a inne są używane aby wskazać programiście potrzebę poprawienia aplikacji.

Kilka przykładów:

  • Zend_Controller_Dispatcher::dispatch() domyślnie wyrzuci wyjątek jeśli zażądano nieprawidłowego kontrolera. Są dwa zalecane sposoby na obsłużenie tego:

    • Ustawienie parametru useDefaultControllerAlways.

      W twoim kontrolerze frontowym, lub w obiekcie uruchamiającym, dodaj poniższą dyrektywę:

      <?php
      $front->setParam('useDefaultControllerAlways', true);
      
      // lub
      $dispatcher->setParam('useDefaultControllerAlways', true);
      

      Gdy ta flaga jest ustawiona, obiekt uruchamiający, użyje domyślnego kontrolera oraz akcji zamiast wyrzucania wyjątku. Minusem użycia tej metody jest to, że jakikolwiek błąd literowy w adresie dostępowym do twojej strony spowoduje wyświetlenie strony głównej, co może źle wpłynąć na optymalizację serwisu dla wyszukiwarek internetowych.

    • Wyjątek wyrzucany przez metodę dispatch() jest wyjątkiem Zend_Controller_Dispatcher_Exception zawierającym tekst 'Invalid controller specified'. Użyj jednej z metod opisanych w poprzedniej sekcji aby złapać wyjątek, a następnie przekierować do strony błędu lub do strony głownej.

  • Metoda Zend_Controller_Action::__call() wyrzuci wyjątek Zend_Controller_Action_Exception jeśli nie może uruchomić nieistniejącej metody akcji. Najczęściej będziesz chciał użyć jakiejś domyślnej akcji w kontrolerze w tego typu sprawach. Przykładowe metody za pomocą których możesz to osiśgnąć:

    • Rozszerzenie klasy Zend_Controller_Action i nadpisanie metody __call(). Na przykład:

      <?php
      class My_Controller_Action extends Zend_Controller_Action
      {
          public function __call($method, $args)
          {
              if ('Action' == substr($method, -6)) {
                  $controller = $this->getRequest()->getControllerName();
                  $url = '/' . $controller . '/index';
                  return $this->_redirect($url);
              }
      
              throw new Exception('Invalid method');
          }
      }
      

      Powyższa metoda przechwytuje wszystkie wywołane niezdefiniowane akcje i przekierowuje żądanie do domyślnej akcji w kontrolerze.

    • Rozszerzenie klasy Zend_Controller_Dispatcher o nadpisanie metody getAction(), ktora sprawdza czy akcja istnieje. Na przykład:

      <?php
      class My_Controller_Dispatcher extends Zend_Controller_Dispatcher
      {
          public function getAction($request)
          {
              $action = $request->getActionName();
              if (empty($action)) {
                  $action = $this->getDefaultAction();
                  $request->setActionName($action);
                  $action = $this->formatActionName($action);
              } else {
                  $controller = $this->getController();
                  $action     = $this->formatActionName($action);
                  if (!method_exists($controller, $action)) {
                      $action = $this->getDefaultAction();
                      $request->setActionName($action);
                      $action = $this->formatActionName($action);
                  }
              }
      
              return $action;
          }
      }
      

      Powyższy kod sprawdza czy zażądana akcja istnieje w klasie kontrolera; jeśli nie, resetuje akcję do akcji domyślnej.

      Ta metoda jest wygodna ponieważ możesz w niewidoczny sposób zmienić akcję przed ostatecznym uruchomieniem. Jednak to także oznacza, że jakikolwiek błąd literowy w adresie URL może wciąż uruchomić żądanie poprawnie, co nie jest zbyt dobre dla optymalizacji dla wyszukiwarek internetowych.

    • Użycie metody Zend_Controller_Action::preDispatch() lub Zend_Controller_Plugin_Abstract::preDispatch() do zidentyfikowania nieprawidłowych akcji.

      Rozszerzając klasę Zend_Controller_Action i modyfikując metodę preDispatch(), możesz zmodyfikować wszystkie twoje kontrolery w taki sposób, aby przenosiły one żądanie do innej akcji lub przekierowywały zamiast uruchamiać akcję. Kod wyglądałby podobnie kod nadpisujący metodę __call(), który został przedstawiony wyżej.

      Alternatywnie, możesz sprawdzać te informacje we wtyczce globalnej. Zaletą tego rozwiązania jest to, że kontroler akcji staje się niezależny; jeśli twoja aplikacja składa się z różnorodnych kontrolerów akcji i nie wszystkie dziedziczą z tej samej klasy, ta metoda może dodać konsekwencji w obsłudze różnych klas.

      Przykład:

      <?php
      class My_Controller_PreDispatchPlugin extends Zend_Controller_Plugin_Abstract
      {
          public function preDispatch(Zend_Controller_Request_Abstract $request)
          {
              $dispatcher = Zend_Controller_Front::getInstance()->getDispatcher();
              $controller = $dispatcher->getController($request);
              if (!$controller) {
                  $controller = $dispatcher->getDefaultControllerName($request);
              }
              $action     = $dispatcher->getAction($request);
      
              if (!method_exists($controller, $action)) {
                  $defaultAction = $dispatcher->getDefaultAction();
                  $controllerName = $request->getControllerName();
                  $response = Zend_Controller_Front::getInstance()->getResponse();
                  $response->setRedirect('/' . $controllerName . '/' . $defaultAction);
                  $response->sendHeaders();
                  exit;
              }
          }
      }
      

      W tym przykładzie sprawdzamy czy zażądana akcja jest dostępna w kontrolerze. Jeśli nie, przekierujemy żądanie do domyślnej akcji w kontrolerze, i kończymy wykonywanie skryptu.