#NieDlaKlepaczyKodu:Symfony: Różne sposoby na strony błędów w kontrolerach
#niedlaklepaczykodusymfony <<< Subskrybuj ten tag po więcej albo czarnolistuj, jeśli nie chcesz widzieć wpisów z tej serii. Więcej info na dole wpisu.
Symfonowe kontrolery działają w ramach abstrakcji HTTP: przyjmują „żądanie” (reprezentowane przez obiekt Request) i zwracają „odpowiedź” (obiekt Response).
Załóżmy, że chcemy odpowiedzieć klientowi (przeglądarce/użytkownikowi), że strona nie istnieje, czyli zwrócić odpowiedź ze statusem 404. Najbardziej oczywistym sposobem będzie nadanie tego kodu obiektowi odpowiedzi:
#[1] use Symfony\Component\HttpFoundation\Response; // dalej będę pomijał `````` return new Response('Not found', 404); ```Ale kto by pamiętał te wszystkie kody numeryczne odpowiedzi? Lepiej użyć stałej:``` #[2] return new Response('Not found', Response::HTTP_NOT_FOUND); ```Jeśli będziemy rzucali taki wyjątek często, pojawia się duplikacja domyślnej wiadomości. Na szczęście mamy ją w zmiennej:``` #[3] return new Response( Response::$statusTexts[Response::HTTP_NOT_FOUND], Response::HTTP_NOT_FOUND ); ```Zrobiło się sporo kodu, który znów możemy uznać za duplikację. Dodatkowo takie tworzenie stron błędów ma pewien minus: wyglądają kijowo w przeglądarkach.Dokumentacja Symfony [podpowiada](https://symfony.com/doc/current/book/controller.html#managing-errors-and-404-pages) więc, że możemy rzucić wyjątkiem [HttpExceptionInterface](https://github.com/symfony/http-kernel/blob/v3.0.6/Exception/HttpExceptionInterface.php), a Symfony wtedy zaprezentuje domyślną stronę błędów (którą możemy możemy zmodyfikować do swoich potrzeb):``` #[4] throw $this->createNotFoundException(); ```Owa metoda [kontrolera bazowego](https://github.com/symfony/framework-bundle/blob/v3.0.6/Controller/Controller.php#L238) nie robi nic więcej jak utworzenie obiektu wyjątku – możemy to zrobić sami:``` #[5] use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; // dalej będę pomijał `````` throw new NotFoundHttpException(); ```Co jednak, gdy chcemy, żeby programista miał więcej info dlaczego wystąpił wyjątek? Możemy przekazać wiadomość, która zostanie potem zapisana do logów i/lub wyświetlona na stronie błędów w środowisku developerskim:``` #[6] throw new NotFoundHttpException('Article not found'); ```Komunikat może być bardziej rozbudowany, tu przydaje się funkcja formatowania napisu:``` #[7] throw new NotFoundHttpException(sprintf( 'Article with id "%d" was not found', $articleId )); ```Co jeśli w kontrolerze musimy tak zrobić w kilku miejscach? Wyciągamy metodę pomocniczą:``` #[8] public function showArticleAction($articleId) { $article = $this->articleRepository->findById($articleId);
if (null === $article) { throw $this->createArticleNotFoundHttpException($articleId); } } `````` private function createArticleNotFoundHttpException($articleId) { return new NotFoundHttpException(sprintf( 'Article with id "%d" was not found', $articleId )); } ```W ten sposób unikamy duplikacji tworzenia odpowiedniego wyjątku i jego komunikatu. Co jeśli chcemy tak zrobić w wielu kontrolerach? W OOP dziedziczenie idzie na ratunek:``` #[9] abstract class ArticleController { protected function createArticleNotFoundHttpException($articleId) { return new NotFoundHttpException(sprintf( 'Article with id "%d" was not found', $articleId )); } } `````` // i analogicznie np CreateArticleController czy SearchArticleController class ShowArticleController extends ArticleController { public function showArticleAction($articleId) { throw $this->createArticleNotFoundHttpException($articleId); } } ```Inny sposób, który da nam więcej elastyczności, to skorzystać z dziedziczenia przy wyjątkach:``` #[10] class ArticleNotFoundHttpException extends NotFoundHttpException { public function __construct($articleId) { parent::__construct(sprintf( 'Article with id "%d" was not found', $articleId )); } } `````` class ArticleController { public function showArticleAction($articleId) { throw new ArticleNotFoundHttpException($articleId); } } ```Dlaczego mówiłem, że da nam to więcej elastyczności? Bo teraz możemy rozpoznać rodzaj wyjątku i zrobić z nim coś więcej niż domyślnie robi Symfony z wyjątkami HTTP.Możemy przykładowo zaprezentować dedykowaną stronę błędów dla nieznalezionych artykułów (w odróżnieniu od „ogólnej“ strony błędów 404). [Poczytaj dokumentację](https://symfony.com/doc/current/cookbook/controller/error_pages.html).Jak już „robimy coś więcej“ z tym wyjątkiem, to zapewne przyda się też więcej informacji. Możemy rozbudować jego klasę:``` #[11] class ArticleNotFoundHttpException extends NotFoundHttpException { private $articleId; `````` public function __construct($articleId) { $this->articleId = $articleId;
parent::__construct(sprintf( 'Article with id "%d" was not found', $articleId )); } `````` public function getArticleId() { return $this->articleId; } }
Być może zauważyłeś już pewien problem: NotFoundHttpException ma też swoje pola, które warto uzupełnić. Do tego być może dorobiłeś już własną obsługę wyjątków o braku artykułu (np strona błędów z dedykowaną grafiką czy linkami) => a jednak niekoniecznie chcesz mieć zawsze taki sam komunikat. Mógłbyś chcieć z kontrolera przekazać własny. Pełna więc wersja klasy z wyjątkiem:
#[12] class ArticleNotFoundHttpException extends NotFoundHttpException { private $articleId; `````` public function __construct( $articleId, $message = null, Exception $previous = null, $code = 0 ) { $this->articleId = $articleId;
if (null === $message) { $message = sprintf( 'Article with id "%d" was not found', $articleId ); }
parent::__construct($message, $previous, $code); } `````` public function getArticleId() { return $this->articleId; } } ```No i już możemy go rzucić bezpośrednio:``` #[13] $article = $this->articleRepository->findOneById($articleId); `````` if (null === $article) { throw new ArticleNotFoundHttpException($articleId); } ```Albo przechwycić wyjątek domenowy i dać własny komunikat:``` #[14] try { $article = $this->articleRepository->findOneById($articleId); } catch (ArticleNotFoundException $ex) { throw new ArticleNotFoundHttpException( $articleId, sprintf( 'Article with id "%d" was not found by ArticleRepository', $articleId ), $ex ); }
-----------------
To pierwszy wpis z serii #niedlaklepaczykodusymfony – będzie o tym jak pisać i nie pisać webaplikacji z wykorzystaniem Symfony.
Jeśli chcesz dostawać powiadomienia o nowych wpisach, zapraszam do subskrypcji taga.
@MacDada: Podoba mnie się to, bo jest mało o symfony a dużo o ogólnych zasadach tworzenia rzeczy. Im bardziej będziesz szedł w tym kierunku tym bardziej będę subskrybował :)
@MacDada: ja bym chyba polecaił w traity i z metody createArticleNotFoundHttpException zrobił createNotFoundHttpException gdzie przekazywałbym dwa parametry(co jako string i id), tj zatrzymał się na punkcie 9 i trochę go "ulepszył" aby móc skorzystać z tego w każdym kontrolerze
ewentualnie aby w samym traitcie pobierało prefix kontrolera(o ile w ogóle jest coś takiego możliwe), ale to raczej głupie rozwiązanie bo nie zawsze jakiś Controller będzie rzucał not found bo nie
ja bym chyba polecaił w traity i z metody createArticleNotFoundHttpException zrobił createNotFoundHttpException gdzie przekazywałbym dwa parametry(co jako string i id), tj zatrzymał się na punkcie 9 i trochę go "ulepszył" aby móc skorzystać z tego w każdym kontrolerze
@Jurigag: Yep, jest też taka opcja. Chociaż ja raczej mam awersję do traitów, co też w sumie mógłbym opisać w dedykowanym artykule ;)
#NieDlaKlepaczyKodu:Symfony: Różne sposoby na strony błędów w kontrolerach#niedlaklepaczykodusymfony <<< Subskrybuj ten tag po więcej albo czarnolistuj, jeśli nie chcesz widzieć wpisów z tej serii. Więcej info na dole wpisu.
Symfonowe kontrolery działają w ramach abstrakcji
HTTP: przyjmują „żądanie” (reprezentowane przez obiektRequest) i zwracają „odpowiedź” (obiektResponse).Załóżmy, że chcemy odpowiedzieć klientowi (przeglądarce/użytkownikowi), że strona nie istnieje, czyli zwrócić odpowiedź ze statusem
404.Najbardziej oczywistym sposobem będzie nadanie tego kodu obiektowi odpowiedzi:
#[1]
#use Symfony\Component\HttpFoundation\Response; // dalej będę pomijał
``````
return new Response('Not found', 404);
```Ale kto by pamiętał te wszystkie kody numeryczne odpowiedzi? Lepiej użyć stałej:```
[2]
#return new Response('Not found', Response::HTTP_NOT_FOUND);
```Jeśli będziemy rzucali taki wyjątek często, pojawia się duplikacja domyślnej wiadomości. Na szczęście mamy ją w zmiennej:```
[3]
#return new Response(
Response::$statusTexts[Response::HTTP_NOT_FOUND],
Response::HTTP_NOT_FOUND
);
```Zrobiło się sporo kodu, który znów możemy uznać za duplikację. Dodatkowo takie tworzenie stron błędów ma pewien minus: wyglądają kijowo w przeglądarkach.Dokumentacja Symfony [podpowiada](https://symfony.com/doc/current/book/controller.html#managing-errors-and-404-pages) więc, że możemy rzucić wyjątkiem [HttpExceptionInterface](https://github.com/symfony/http-kernel/blob/v3.0.6/Exception/HttpExceptionInterface.php), a Symfony wtedy zaprezentuje domyślną stronę błędów (którą możemy możemy zmodyfikować do swoich potrzeb):```
[4]
#throw $this->createNotFoundException();
```Owa metoda [kontrolera bazowego](https://github.com/symfony/framework-bundle/blob/v3.0.6/Controller/Controller.php#L238) nie robi nic więcej jak utworzenie obiektu wyjątku – możemy to zrobić sami:```
[5]
#use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; // dalej będę pomijał
``````
throw new NotFoundHttpException();
```Co jednak, gdy chcemy, żeby programista miał więcej info dlaczego wystąpił wyjątek? Możemy przekazać wiadomość, która zostanie potem zapisana do logów i/lub wyświetlona na stronie błędów w środowisku developerskim:```
[6]
#throw new NotFoundHttpException('Article not found');
```Komunikat może być bardziej rozbudowany, tu przydaje się funkcja formatowania napisu:```
[7]
#throw new NotFoundHttpException(sprintf(
'Article with id "%d" was not found',
$articleId
));
```Co jeśli w kontrolerze musimy tak zrobić w kilku miejscach? Wyciągamy metodę pomocniczą:```
[8]
#public function showArticleAction($articleId)
{
$article = $this->articleRepository->findById($articleId);
if (null === $article) {
throw $this->createArticleNotFoundHttpException($articleId);
}
}
``````
private function createArticleNotFoundHttpException($articleId)
{
return new NotFoundHttpException(sprintf(
'Article with id "%d" was not found',
$articleId
));
}
```W ten sposób unikamy duplikacji tworzenia odpowiedniego wyjątku i jego komunikatu.
Co jeśli chcemy tak zrobić w wielu kontrolerach? W OOP dziedziczenie idzie na ratunek:```
[9]
#abstract class ArticleController
{
protected function createArticleNotFoundHttpException($articleId)
{
return new NotFoundHttpException(sprintf(
'Article with id "%d" was not found',
$articleId
));
}
}
``````
// i analogicznie np CreateArticleController czy SearchArticleController
class ShowArticleController extends ArticleController
{
public function showArticleAction($articleId)
{
throw $this->createArticleNotFoundHttpException($articleId);
}
}
```Inny sposób, który da nam więcej elastyczności, to skorzystać z dziedziczenia przy wyjątkach:```
[10]
#class ArticleNotFoundHttpException extends NotFoundHttpException
{
public function __construct($articleId)
{
parent::__construct(sprintf(
'Article with id "%d" was not found',
$articleId
));
}
}
``````
class ArticleController
{
public function showArticleAction($articleId)
{
throw new ArticleNotFoundHttpException($articleId);
}
}
```Dlaczego mówiłem, że da nam to więcej elastyczności? Bo teraz możemy rozpoznać rodzaj wyjątku i zrobić z nim coś więcej niż domyślnie robi Symfony z wyjątkami HTTP.Możemy przykładowo zaprezentować dedykowaną stronę błędów dla nieznalezionych artykułów (w odróżnieniu od „ogólnej“ strony błędów 404). [Poczytaj dokumentację](https://symfony.com/doc/current/cookbook/controller/error_pages.html).Jak już „robimy coś więcej“ z tym wyjątkiem, to zapewne przyda się też więcej informacji. Możemy rozbudować jego klasę:```
[11]
class ArticleNotFoundHttpException extends NotFoundHttpException
{
private $articleId;
``````
public function __construct($articleId)
{
$this->articleId = $articleId;
parent::__construct(sprintf(
'Article with id "%d" was not found',
$articleId
));
}
``````
public function getArticleId()
{
return $this->articleId;
}
}
Być może zauważyłeś już pewien problem:
NotFoundHttpExceptionma też swoje pola, które warto uzupełnić. Do tego być może dorobiłeś już własną obsługę wyjątków o braku artykułu (np strona błędów z dedykowaną grafiką czy linkami) => a jednak niekoniecznie chcesz mieć zawsze taki sam komunikat. Mógłbyś chcieć z kontrolera przekazać własny. Pełna więc wersja klasy z wyjątkiem:
#[12]
#class ArticleNotFoundHttpException extends NotFoundHttpException
{
private $articleId;
``````
public function __construct(
$articleId,
$message = null,
Exception $previous = null,
$code = 0
) {
$this->articleId = $articleId;
if (null === $message) {
$message = sprintf(
'Article with id "%d" was not found',
$articleId
);
}
parent::__construct($message, $previous, $code);
}
``````
public function getArticleId()
{
return $this->articleId;
}
}
```No i już możemy go rzucić bezpośrednio:```
[13]
#$article = $this->articleRepository->findOneById($articleId);
``````
if (null === $article) {
throw new ArticleNotFoundHttpException($articleId);
}
```Albo przechwycić wyjątek domenowy i dać własny komunikat:```
[14]
try {
$article = $this->articleRepository->findOneById($articleId);
} catch (ArticleNotFoundException $ex) {
throw new ArticleNotFoundHttpException(
$articleId,
sprintf(
'Article with id "%d" was not found by ArticleRepository',
$articleId
),
$ex
);
}
-----------------
To pierwszy wpis z serii #niedlaklepaczykodusymfony – będzie o tym jak pisać i nie pisać webaplikacji z wykorzystaniem Symfony.
Jeśli chcesz dostawać powiadomienia o nowych wpisach, zapraszam do subskrypcji taga.
Jeśli nie chcesz widzieć takich wpisów, wrzuć ten tag na #czarnolisto – zwłaszcza, że będę „spamował“ wieloma innymi tagami, jak #programowanie #php #symfony #symfony2 #symfony3 ;)
Zapraszam do komentowania, code review, pytań, sugestii, propozycji nowych wpisów!
@normanos: Niestety #maciej zepsuł https://wykop-code.appspot.com/
Ale gdzieś trzeba zacząć. Jak się okaże, że mam co pisać i są tacy co chcą czytać, to będę myślał nad lepszą warstwą persystencji i prezentacji ;-)
@MacDada: no właśnie. Optuje za drugą opcją ;) a czytać ma kto bo teksty na WM robią po kilkanaście tys. Uu Minimum.
createArticleNotFoundHttpExceptionzrobił createNotFoundHttpException gdzie przekazywałbym dwa parametry(co jako string i id), tj zatrzymał się na punkcie 9 i trochę go "ulepszył" aby móc skorzystać z tego w każdym kontrolerzeewentualnie aby w samym traitcie pobierało prefix kontrolera(o ile w ogóle jest coś takiego możliwe), ale to raczej głupie rozwiązanie bo nie zawsze jakiś Controller będzie rzucał not found bo nie
@Jurigag: Yep, jest też taka opcja. Chociaż ja raczej mam awersję do traitów, co też w sumie mógłbym opisać w dedykowanym artykule ;)