Slide 1

Slide 1 text

Von Chaos zu Kontrolle Exception Handling in Symfony SymfonyLive Berlin 2023

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Anne-Julia Seitz Softwareentwicklerin 15 Jahre PHP Symfony seit v1.2 seit 2021 bei QOSSMIC Organisatorin der Symfony User Group Berlin

Slide 4

Slide 4 text

Workshops & Training für Symfony & React User Groups & Community Events Unterstützung von Teams in Symfony- Projekten

Slide 5

Slide 5 text

Was erwartet euch 1. Exceptions in PHP 2. Exception Handling in Symfony 3. Antipatterns 4. Best Practices

Slide 6

Slide 6 text

Exceptions in PHP

Slide 7

Slide 7 text

Hello World! Hello, World!

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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:

Slide 10

Slide 10 text

trigger_error() if ($divisor == 0) { trigger_error("Cannot divide by zero.", E_USER_ERROR); } 1 2 3

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

SPL Exceptions Throwable ├── Error │ └── ... └── Exception ├── LogicException │ ├── BadFunctionCallException │ │ └── BadMethodCallException │ ├── DomainException │ ├── InvalidArgumentException │ ├── LengthException │ └── OutOfRangeException └── RuntimeException ├── OutOfBoundsException ├── OverflowException ├── RangeException ├── UnderflowException └── UnexpectedValueException

Slide 13

Slide 13 text

Eigene Exceptions namespace App\Exception; class CategoryAlreadyExists extends \RuntimeException {} 1 2 3 throw new CategoryAlreadyExists('The category you chose alr 1

Slide 14

Slide 14 text

set_error_handler vs. try-catch

Slide 15

Slide 15 text

ExceptionHandling in Symfony

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

@throws

Slide 22

Slide 22 text

Recap Symfony behandelt alle Fehler als Exceptions HttpKernel dispatched ein kernel.exception Event

Slide 23

Slide 23 text

Antipatterns

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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()], } } }

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Broad Catching try { $this->doSomethingThrowingAnException(); } catch (\Exception $exception) { // ... } 1 2 3 4 5

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Verwendung der HttpExceptions namespace Symfony\Component\HttpKernel\Exception; HttpException ├── AccessDeniedHttpException ├── BadRequestHttpException ├── ConflictHttpException ├── GoneHttpException ├── LengthRequiredHttpException ├── MethodNotAllowedHttpException ├── NotAcceptableHttpException ├── NotFoundHttpException ├── PreconditionFailedHttpException ├── PreconditionRequiredHttpException ├── ServiceUnavailableHttpException ├── TooManyRequestsHttpException ├── UnauthorizedHttpException └── UnsupportedMediaTypeHttpException

Slide 32

Slide 32 text

Ad-hoc Error Handling final class CreateArticleController { public function __invoke(Request $request): Response { try { // ... } catch (\Exception $exception) { return $this->json(['message' => $exception->getM } } }

Slide 33

Slide 33 text

Best Practices

Slide 34

Slide 34 text

Eigenes ExceptionInterface namespace App\Exception; interface ExceptionInterface extends \Throwable { } 1 2 3 4 5

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Mögliche Granularität des ExceptionHandlings

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Recap

Slide 48

Slide 48 text

Mess up your code, and you need good error handling but mess up your error handling, you’re screwed!

Slide 49

Slide 49 text

Danke!

Slide 50

Slide 50 text

Fin.