Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Von Chaos zu Kontrolle: Exception Handling in Symfony

Von Chaos zu Kontrolle: Exception Handling in Symfony

Fehlerbehandlung in PHP ist leicht gemacht, aber schwerer zu meistern. Wie so oft liegt der Teufel im Detail. Wie mappe ich meine Fehler auf sinnvolle HTTP-Statuscodes? Welche Struktur macht für die Suche und Filterung im Monitoring Sinn und wie gebe ich Kontextinfos für das Debugging mit? Häufig wird diesen Fragen anfangs wenig Beachtung geschenkt und wenn sich die ersten Clients an die Struktur gewöhnt haben, ist ein Umbau schwierig. Die Entwicklung von PHP tendiert dazu, immer strikter zu werden, doch wie können wir die Vorteile davon in unserem Code nutzen und souverän mit Fehlern in Symfony-Anwendungen umgehen? Wir werden in die Welt der Fehlerbehandlung eintauchen und lernen nicht nur die grundlegenden Konzepte der Ausnahmebehandlung kennen, sondern auch bewährte Methoden zur Nutzung der leistungsstarken Fehlerbehandlungs-Komponenten von Symfony entdecken. Von der Erstellung benutzerdefinierter Ausnahmen bis hin zur effizienten Protokollierung und Behandlung von Fehlern erhältst du wertvolle Einblicke, um robuste und fehlerfreie Symfony-Anwendungen zu entwickeln. Egal, ob du ein erfahrener Symfony-Entwickler bist oder gerade erst anfängst, dieser Vortrag wird dir helfen, dein Verständnis für Exception Handling zu vertiefen und die Qualität deines Codes zu verbessern.

Anne-Julia Seitz

October 13, 2023
Tweet

More Decks by Anne-Julia Seitz

Other Decks in Programming

Transcript

  1. Anne-Julia Seitz Softwareentwicklerin 15 Jahre PHP Symfony seit v1.2 seit

    2021 bei QOSSMIC Organisatorin der Symfony User Group Berlin
  2. Workshops & Training für Symfony & React User Groups &

    Community Events Unterstützung von Teams in Symfony- Projekten
  3. Was erwartet euch 1. Exceptions in PHP 2. Exception Handling

    in Symfony 3. Antipatterns 4. Best Practices
  4. Hello World! <!DOCTYPE html> <html> <head> <title>Hello, World!</title> </head> <body>

    <h1><?php echo "Hello, World!"; ?></h1> </body> </html>
  5. Exceptions vor PHP 5 /* intentional Error */ $file =

    @file('not-existing-file.csv') or die("File could not be opened: Error:'" . error_get_las 1 2 3
  6. set_error_handler() Output: function handleError(int $code, string $description, strin echo "Error:

    [$code] $description - $file:$line"; return true; } // Switch to custom error handling $previousHandler = set_error_handler("handleError"); // Trigger an error to test the custom error handler echo $undefinedVariable; // Restore previous error handler restore_error_handler(); 1 2 3 4 5 6 7 8 9 10 11 12 13 Error: [2] Undefined variable $undefinedVariable - /in/5YJdO:
  7. Exceptions ab PHP 5 try { throw new \InvalidArgumentException('Variable must

    be n } catch (\InvalidArgumentException $exception) { // handle exception } finally { // clean up resources } 1 2 3 4 5 6 7
  8. SPL Exceptions Throwable ├── Error │ └── ... └── Exception

    ├── LogicException │ ├── BadFunctionCallException │ │ └── BadMethodCallException │ ├── DomainException │ ├── InvalidArgumentException │ ├── LengthException │ └── OutOfRangeException └── RuntimeException ├── OutOfBoundsException ├── OverflowException ├── RangeException ├── UnderflowException └── UnexpectedValueException
  9. Eigene Exceptions namespace App\Exception; class CategoryAlreadyExists extends \RuntimeException {} 1

    2 3 throw new CategoryAlreadyExists('The category you chose alr 1
  10. public/index.php $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER[' $request = Request::createFromGlobals();

    $response = $kernel->handle($request); $response->send(); $kernel->terminate($request, $response); 1 2 3 4 5 $response = $kernel->handle($request); $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER[' 1 $request = Request::createFromGlobals(); 2 3 $response->send(); 4 $kernel->terminate($request, $response); 5
  11. Was passiert im Kernel? $kernel->handle($request) ➜ boot() ➜ foreach ($this->getBundles()

    as $bundle) ➜ $bundle->boot() // \Symfony\Bundle\FrameworkB ➜ ErrorHandler::register() ➜ set_error_handler([$handler, 'handl 1 2 3 4 5 6 $kernel->handle($request) ➜ boot() 1 2 ➜ foreach ($this->getBundles() as $bundle) 3 ➜ $bundle->boot() // \Symfony\Bundle\FrameworkB 4 ➜ ErrorHandler::register() 5 ➜ set_error_handler([$handler, 'handl 6 ➜ foreach ($this->getBundles() as $bundle) $kernel->handle($request) 1 ➜ boot() 2 3 ➜ $bundle->boot() // \Symfony\Bundle\FrameworkB 4 ➜ ErrorHandler::register() 5 ➜ set_error_handler([$handler, 'handl 6 ➜ $bundle->boot() // \Symfony\Bundle\FrameworkB $kernel->handle($request) 1 ➜ boot() 2 ➜ foreach ($this->getBundles() as $bundle) 3 4 ➜ ErrorHandler::register() 5 ➜ set_error_handler([$handler, 'handl 6 ➜ ErrorHandler::register() ➜ set_error_handler([$handler, 'handl $kernel->handle($request) 1 ➜ boot() 2 ➜ foreach ($this->getBundles() as $bundle) 3 ➜ $bundle->boot() // \Symfony\Bundle\FrameworkB 4 5 6
  12. ErrorHandler Component // Symfony\Component\ErrorHandler\ErrorHandler public static function register(self $handler =

    null, bool { } 1 2 3 if (null === self::$reservedMemory) { 4 self::$reservedMemory = str_repeat('x', 32768); 5 } 6 7 set_error_handler([$handler, 'handleError']) 8 9 if (null === self::$reservedMemory) { self::$reservedMemory = str_repeat('x', 32768); } // Symfony\Component\ErrorHandler\ErrorHandler 1 public static function register(self $handler = null, bool 2 { 3 4 5 6 7 set_error_handler([$handler, 'handleError']) 8 } 9
  13. ErrorHandler->handleError() // Symfony\Component\ErrorHandler\ErrorHandler public function handleError(int $type, string $message, str

    { // ... $logMessage = $this->levels[$type].': '.$message; throw new \ErrorException($logMessage, 0, $type, $file, } 1 2 3 4 5 6 7 throw new \ErrorException($logMessage, 0, $type, $file, // Symfony\Component\ErrorHandler\ErrorHandler 1 public function handleError(int $type, string $message, str 2 { 3 // ... 4 $logMessage = $this->levels[$type].': '.$message; 5 6 } 7
  14. HttpKernel->handle() // Symfony\Component\HttpKernel\HttpKernel public function handle(Request $request, int $type =

    self: { try { return $this->handleRaw($request, $type); } catch (\Throwable $e) { return $this->handleThrowable($e, $request, $type) } } private function handleThrowable(\Throwable $e, Request $r { $event = new ExceptionEvent($this, $request, $type, $e $this->dispatcher->dispatch($event, KernelEvents::EXCE return $event->getResponse(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 return $this->handleThrowable($e, $request, $type) $this->dispatcher->dispatch($event, KernelEvents::EXCE // Symfony\Component\HttpKernel\HttpKernel 1 public function handle(Request $request, int $type = self: 2 { 3 try { 4 return $this->handleRaw($request, $type); 5 } catch (\Throwable $e) { 6 7 } 8 } 9 10 private function handleThrowable(\Throwable $e, Request $r 11 { 12 $event = new ExceptionEvent($this, $request, $type, $e 13 14 return $event->getResponse(); 15 } 16
  15. Return Booleans function divide($numerator, $denominator): float|false { if ($denominator ==

    0) { return false; } else { return $numerator / $denominator; } } $result = divide(10, 0); if ($result === false) { echo "Error: Division by zero is not allowed."; } else { echo "Result: " . $result; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
  16. ExceptionHandling dominiert die Codebasis final class ShowArticleController { #[Route("/articles/{articleId}", name="blog_article_show")]

    public function __invoke(int $articleId, ArticleRepository { try { $article = $articleRepository->find($articleId); return new JsonResponse(['article' => $article]); } catch (ArticleNotFoundException $e) { return new JsonResponse(['error' => $e->errorMessage()] } catch (ArticleExpiredException $e) { return new JsonResponse(['error' => $e->getMessage()], } catch (\Exception $e) { return new JsonResponse(['error' => $e->getMessage()], } } }
  17. Return null // CreateArticleCommandHandler public function handle(CreateArticleCommand $command): ?Ar {

    try { $article = new Article($command->title, $command-> $this->articleRepository->save($article); return $article; } catch (ArticleAlreadyExistsException $exception) { return null; } } // CreateArticleController public function __invoke(CreateArticleCommand $command): R { $articleCreated = $this->handler->handle($command); if (null === $articleCreated) { throw new HttpException(500, 'Could not create art } return new JsonResponse($orderCreated); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 return null; if (null === $articleCreated) { throw new HttpException(500, 'Could not create art } // CreateArticleCommandHandler 1 public function handle(CreateArticleCommand $command): ?Ar 2 { 3 try { 4 $article = new Article($command->title, $command-> 5 $this->articleRepository->save($article); 6 return $article; 7 } catch (ArticleAlreadyExistsException $exception) { 8 9 } 10 } 11 // CreateArticleController 12 public function __invoke(CreateArticleCommand $command): R 13 { 14 $articleCreated = $this->handler->handle($command); 15 16 17 18 return new JsonResponse($orderCreated); 19 } 20
  18. Unterdrückte Exceptions try { $this->doSomethingThrowingAnException(); } catch (\Exception $exception) {}

    1 2 3 4 try { 5 // ... 6 } catch (\Exception $exception) { 7 throw new \RuntimeException('Something bad happened.') 8 } 9 10 try { 11 // ... 12 } catch (\Exception $exception) { 13 throw new \RuntimeException('Something bad happened.', 14 } 15 try { // ... } catch (\Exception $exception) { throw new \RuntimeException('Something bad happened.') } try { 1 $this->doSomethingThrowingAnException(); 2 } catch (\Exception $exception) {} 3 4 5 6 7 8 9 10 try { 11 // ... 12 } catch (\Exception $exception) { 13 throw new \RuntimeException('Something bad happened.', 14 } 15 try { // ... } catch (\Exception $exception) { throw new \RuntimeException('Something bad happened.', } try { 1 $this->doSomethingThrowingAnException(); 2 } catch (\Exception $exception) {} 3 4 try { 5 // ... 6 } catch (\Exception $exception) { 7 throw new \RuntimeException('Something bad happened.') 8 } 9 10 11 12 13 14 15
  19. Exceptions als Kontrollstruktur verwenden try { $this->receiveMoneyFromUser(User $user, Money $money);

    } catch (\PaymentException $exception) { if ($exception instanceof SuccessfulPaymentException) $this->sendEmailToUser($user, 'Thank you for your } elseif ($exception instanceof FailedPaymentException $this->sendEmailToAdmin('Payment failed: ' . $exce } else { $this->sendEmailToAdmin('Something failed: ' . $ex } } 1 2 3 4 5 6 7 8 9 10 11 if ($exception instanceof SuccessfulPaymentException) } elseif ($exception instanceof FailedPaymentException try { 1 $this->receiveMoneyFromUser(User $user, Money $money); 2 } catch (\PaymentException $exception) { 3 4 $this->sendEmailToUser($user, 'Thank you for your 5 6 $this->sendEmailToAdmin('Payment failed: ' . $exce 7 } else { 8 $this->sendEmailToAdmin('Something failed: ' . $ex 9 } 10 } 11
  20. AppException to rule them all try { throw new AppException('Sorry

    something went wrong.'); } catch (AppException $exception) { if (str_contains($exception->getMessage(), 'Article not f return new Response($exception->getMessage(), Respons } return new Response($exception->getMessage(), Response::H } 1 2 3 4 5 6 7 8
  21. Verwendung der HttpExceptions namespace Symfony\Component\HttpKernel\Exception; HttpException ├── AccessDeniedHttpException ├── BadRequestHttpException

    ├── ConflictHttpException ├── GoneHttpException ├── LengthRequiredHttpException ├── MethodNotAllowedHttpException ├── NotAcceptableHttpException ├── NotFoundHttpException ├── PreconditionFailedHttpException ├── PreconditionRequiredHttpException ├── ServiceUnavailableHttpException ├── TooManyRequestsHttpException ├── UnauthorizedHttpException └── UnsupportedMediaTypeHttpException
  22. Ad-hoc Error Handling final class CreateArticleController { public function __invoke(Request

    $request): Response { try { // ... } catch (\Exception $exception) { return $this->json(['message' => $exception->getM } } }
  23. SPL Exceptions erweitern namespace App\Exception; class ArticleNotFoundException extends \RuntimeException im

    { } 1 2 3 4 5 Throwable └── Exception ├── LogicException │ ├── Symfony\Component\Console\Exception\LogicExcep │ └── Symfony\Component\ExpressionLanguage\SyntaxErr └── RuntimeException ├── App\Exception\ArticleNotFoundException └── Symfony\Component\Filesystem\Exception\Runtim
  24. 1. Alle Exceptions try { throw new \App\Exception\ArticleNotFoundException(); } catch

    (\Exception $exception) ( // ... ) 1 2 3 4 5 } catch (\Exception $exception) ( try { 1 throw new \App\Exception\ArticleNotFoundException(); 2 3 // ... 4 ) 5
  25. 2. Alle unsere Exceptions try { throw new \App\Exception\ArticleNotFoundException(); }

    catch( \App\Exception\ExceptionInterface $exception ) ( // ... ) 1 2 3 4 5 } catch( \App\Exception\ExceptionInterface $exception ) ( try { 1 throw new \App\Exception\ArticleNotFoundException(); 2 3 // ... 4 ) 5
  26. 3. Spezifische SPL Exception try { throw new \App\Exception\ArticleNotFoundException(); }

    catch( \RuntimeException $exception ) ( // ... ) 1 2 3 4 5 } catch( \RuntimeException $exception ) ( try { 1 throw new \App\Exception\ArticleNotFoundException(); 2 3 // ... 4 ) 5
  27. 4. Erweiterte SPL exception von der App try { throw

    new \App\Exception\ArticleNotFoundException(); } catch( \App\Exception\ArticleNotFoundException $exceptio // ... ) 1 2 3 4 5 } catch( \App\Exception\ArticleNotFoundException $exceptio try { 1 throw new \App\Exception\ArticleNotFoundException(); 2 3 // ... 4 ) 5
  28. Static Factory Methods namespace App\Exception; class ArticleNotFoundException extends \RuntimeException i

    { public static function create(): self { return new self('Article was not found.'); } public static function createWithArticleTitle(string $ { return new self(sprintf('Article "%s" was not foun } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 throw ArticleNotFoundException::createWithArticleTitle('errar
  29. Wir erstellen einen ExceptionListener #[AsEventListener(event: KernelEvents::EXCEPTION, priority final class ExceptionListener

    { public function __invoke(ExceptionEvent $event): void { $exception = $event->getThrowable(); $response = match (true) { $exception instanceof HttpExceptionInterface => new Response($exception->getMessage(), $exception- default => new Response('Internal Server Error', Response::HT }; $event->setResponse($response); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $exception instanceof HttpExceptionInterface => new Response($exception->getMessage(), $exception- default => new Response('Internal Server Error', Response::HT #[AsEventListener(event: KernelEvents::EXCEPTION, priority 1 final class ExceptionListener 2 { 3 public function __invoke(ExceptionEvent $event): void 4 { 5 $exception = $event->getThrowable(); 6 $response = match (true) { 7 8 9 10 11 }; 12 $event->setResponse($response); 13 } 14 } 15
  30. public function __invoke(ExceptionEvent $event): void { $exception = $event->getThrowable(); $response

    = match (true) { $exception instanceof HttpExceptionInterface => new Response($exception->getMessage(), $exception->g $exception instanceof ExceptionInterface => new Response($exception->getMessage(), $this->mapExc default => new Response('Internal Server Error', Respo }; $event->setResponse($response); } 1 2 3 4 5 6 7 8 9 10 11 12 $exception instanceof ExceptionInterface => new Response($exception->getMessage(), $this->mapExc public function __invoke(ExceptionEvent $event): void 1 { 2 $exception = $event->getThrowable(); 3 $response = match (true) { 4 $exception instanceof HttpExceptionInterface => 5 new Response($exception->getMessage(), $exception->g 6 7 8 default => new Response('Internal Server Error', Respo 9 }; 10 $event->setResponse($response); 11 } 12
  31. private function mapExceptionToStatusCode(ExceptionInterfa { return match (true) { $exception instanceof

    ArticleNotFoundException, $exception instanceof ImageNotFoundException => Respon $exception instanceof ArticleExpiredException => Respo // ... default => Response::HTTP_INTERNAL_SERVER_ERROR, }; } 1 2 3 4 5 6 7 8 9 10
  32. final class ShowArticleController { #[Route("/articles/{articleId}", name="blog_article_show" public function __invoke(int $articleId,

    ArticleRepositor { $article = $articleRepository->find($articleId); return $this->render('articles/show-article.html.twig', } } 1 2 3 4 5 6 7 8 9
  33. Request ID in Exception Kontext namespace App\Logger; use Monolog\Attribute\AsMonologProcessor; #[AsMonologProcessor]

    final class UidProcessor extends \Monolog\Processor\UidProc { } 1 2 3 4 5 6 7 8
  34. Mess up your code, and you need good error handling

    but mess up your error handling, you’re screwed!