Slide 1

Slide 1 text

Deep dive into the Symfony Debug component

Slide 2

Slide 2 text

Good evening! I am Thomas Calvet I am a Symfony enthusiast I work at ekino You can find me on GitHub and on the Symfony Devs Slack as fancyweb 2

Slide 3

Slide 3 text

Symfony Symfowhat? 3

Slide 4

Slide 4 text

More than 50 components ▪ Stand-alone ▪ Decoupled ▪ Performant ▪ Modern design ▪ In permanent evolution What is Symfony? And a framework That is built on top of all those components to make them work together in an efficient and simple way to help you create great websites faster! 4

Slide 5

Slide 5 text

Why Symfony? ▪ Open source ▪ Stable and predictable ▪ Best practices ▪ Great documentation ▪ Big welcoming community ▪ Coopetition ▪ A vision 5

Slide 6

Slide 6 text

Do not confuse The many debug meanings 6

Slide 7

Slide 7 text

The Debug component The stand-alone component. This is what we are going to talk about today. Do not confuse (1/3) The Debug bundle The Symfony core bundle that integrates the stand-alone component into the framework. 7

Slide 8

Slide 8 text

The debug-pack The composer meta package (symfony/debug-pack) to require “debug” related packages in one line. Do not confuse (2/3) The debug flag The flag that becomes the %kernel.debug% DI parameter (from the APP_DEBUG environment variable). 8

Slide 9

Slide 9 text

The “debug” commands The console commands to help you get information about your container, router, form types, listeners, etc. Do not confuse (3/3) 9

Slide 10

Slide 10 text

The Debug component 10

Slide 11

Slide 11 text

“The Debug component provides tools to ease debugging PHP code. 11 It improves the DX!

Slide 12

Slide 12 text

> 125,000,000 installs Used by big PHP projects Such as Symfony, Laravel, Drupal and API Platform The 4th most popular Symfony package 12

Slide 13

Slide 13 text

Three features in three classes ExceptionHandler Converts a PHP exception to a pretty and useful HTML response content. ErrorHandler Provides a generic error handler that logs PHP errors and that rethrow them as exceptions. DebugClassLoader Wraps all the registered class loaders to do checks and lightweight static analysis on the classes they load. 13

Slide 14

Slide 14 text

PHP exception handling 101 14

Slide 15

Slide 15 text

You can throw an exception to interrupt the execution of the code. 15 throw new \Exception(); echo "I love PHP."; Fatal error: Uncaught Exception: foo

Slide 16

Slide 16 text

An exception is a normal PHP object. 16 Fatal error: Uncaught Exception: foo $exception = new \Exception('foo'); throw $exception; echo "I love PHP.";

Slide 17

Slide 17 text

You can catch an exception to handle it. 17 I love PHP. try { throw new \Exception('foo'); } catch (\Exception $e) { } echo "I love PHP.";

Slide 18

Slide 18 text

An exception bubbles up indefinitely until it is caught or not. 18 I love PHP. function foo(): void { throw new \Exception('foo'); } try { foo(); } catch (\Exception $e) { } echo "I love PHP.";

Slide 19

Slide 19 text

You can widen or shrink the types of the exceptions you catch. 19 Fatal error: Uncaught Exception: foo try { throw new \Exception('foo'); } catch (\LogicException $e) { } echo "I love PHP.";

Slide 20

Slide 20 text

You can catch several types of exceptions to handle them differently. 20 try { throw new \DomainException('foo'); } catch (\LogicException $e) { } catch (\DomainException $e) { } echo "I love PHP."; I love PHP.

Slide 21

Slide 21 text

Since PHP 7.1, you can catch several types in one catch block. 21 try { throw new \DomainException('foo'); } catch (\LogicException | \DomainException $e) { } echo "I love PHP."; I love PHP.

Slide 22

Slide 22 text

PHP exception handling 102 22

Slide 23

Slide 23 text

All core exceptions extends the base \Exception class that implements the base \Throwable interface. 23 class LogicException extends Exception { } class Exception implements Throwable { }

Slide 24

Slide 24 text

You can only throw objects that implements the base \Throwable interface. 24 Fatal error: Uncaught Error: Cannot throw objects that do not implement Throwable final class Foo { } throw new Foo();

Slide 25

Slide 25 text

And it’s a special interface that cannot be implemented by custom code. All your custom exceptions must extends the base \Exception class. 25 final class Foo implements \Throwable { } Fatal error: Class Foo cannot implement interface Throwable, extend Exception or Error instead

Slide 26

Slide 26 text

Code in the finally block will be executed after the try and catch blocks. 26 try { throw new \Exception('foo'); } catch (\Exception $e) { echo 'catch '; } finally { echo 'finally'; } catch finally

Slide 27

Slide 27 text

Code in the finally block will always be executed after the try and catch blocks, whether there is something to catch or not. 27 try { } catch (\Exception $e) { } finally { echo 'finally'; } finally

Slide 28

Slide 28 text

Code in the finally block will always be executed even if there is no catch block or a return in the try block. 28 try { return; } finally { echo 'finally'; } finally

Slide 29

Slide 29 text

You can catch an exception to wrap it. 29 namespace App\Service; use App\Exception\ProcessException; use Vendor\Ccc\Bar; final class Foo { private $bar; // __construct method public function process(): void { try { $this->bar->init(); } catch (\RuntimeException $e) { throw new ProcessException('Could not process.', 0, $e); } } }

Slide 30

Slide 30 text

You can rethrow an exception. 30 namespace App\Service; final class Foo { // $bar property and __construct and clean methods public function process(): void { try { $this->bar->init(); } catch (\RuntimeException $e) { $this->clean(); throw $e; } } }

Slide 31

Slide 31 text

You can define a custom global exception handler. 31 set_exception_handler(function (\Throwable $e): void { echo 'I love PHP.'; }); throw new \Exception('message'); I love PHP.

Slide 32

Slide 32 text

You can restore the previous global exception handler. 32 set_exception_handler(function (\Throwable $e): void { echo 'I love PHP.'; }); set_exception_handler(function (\Throwable $e): void { die(); }); restore_exception_handler(); throw new \Exception('message'); I love PHP.

Slide 33

Slide 33 text

You can unset the global exception handler by setting it to null. 33 Fatal error: Uncaught Exception: message set_exception_handler(function (\Throwable $e): void { echo 'I love PHP.'; }); set_exception_handler(null); throw new \Exception('message');

Slide 34

Slide 34 text

The advantages Of using exceptions for your domain errors 34

Slide 35

Slide 35 text

35 final class Foo { /** * Returns the number of processed elements * or false if an error occurred. * * @return int|false */ public function process() { // ... } }

Slide 36

Slide 36 text

Using exceptions avoid to use mixed return types. 36 namespace App\Service; use App\Exception\ProcessFailedException; final class Foo { /** * Returns the number of processed elements. * * @throws ProcessFailedException */ public function process(): int { // ... } }

Slide 37

Slide 37 text

Using exceptions avoid to use custom hardcoded array structures. 37 final class Foo { public function process(): array { return [ 'success' => true, 'data' => '...' ]; } public function processssss(): array { return [ 'error_code' => 'invalid_key', ]; } }

Slide 38

Slide 38 text

An exception carries a meaning in its name. 38 final class RateLimitExceededException extends \Exception { }

Slide 39

Slide 39 text

An exception can encapsulate data since it’s a class. 39 namespace App\Exception; use App\Model\Account; final class RateLimitExceededException extends \Exception { private $account; public function __construct(Account $account) { $this->account = $account; } public function getAccount(): Acccount { return $this->account; } }

Slide 40

Slide 40 text

The Debug ExceptionHandler Features 40

Slide 41

Slide 41 text

“The Debug ExceptionHandler converts a PHP exception to a pretty and useful HTML response content. 41

Slide 42

Slide 42 text

42 function a(string $arg): void { b(time()); } function b(int $time): void { c(); } function c(): void { try { d(); } catch (\Exception $e) { throw new \RuntimeException('foo', 12, $e); } } function d(): void { throw new \DomainException('previous'); } a('arg');

Slide 43

Slide 43 text

43

Slide 44

Slide 44 text

44

Slide 45

Slide 45 text

45

Slide 46

Slide 46 text

The Debug ExceptionHandler Internals 46

Slide 47

Slide 47 text

47 final class ExceptionHandler { public static function register(): void { set_exception_handler([new self(), 'handle']); } public function handle(\Exception $exception): void { $this->sendPhpResponse($exception); } } To enable it : ExceptionHandler::register();

Slide 48

Slide 48 text

48 final class ExceptionHandler { // public static function register() { } // public function handle(\Exception $exception) { } public function sendPhpResponse(\Exception $exception): void { header('HTTP/1.0 %s'.$exception->getStatusCode()); header('Content-Type: text/html'); echo $this->getContent($exception); } }

Slide 49

Slide 49 text

49 final class ExceptionHandler { // public static function register() { }; // public function handle(\Exception $exception) { }; // public function sendPhpResponse(\Exception $exception) { }; public function getContent(\Exception $exception): string { // In this method, the $exception variable is processed // to build the HTML content. // ... return $content; } }

Slide 50

Slide 50 text

To summarize the ExceptionHandler ▪ A custom global exception handler is set. ▪ The incoming exception is processed to build a pretty HTML content. ▪ This HTML content is output. 50

Slide 51

Slide 51 text

The Debug ExceptionHandler Zoom on... 51

Slide 52

Slide 52 text

52

Slide 53

Slide 53 text

53

Slide 54

Slide 54 text

54

Slide 55

Slide 55 text

55

Slide 56

Slide 56 text

PHP error handling 101 56

Slide 57

Slide 57 text

57

Slide 58

Slide 58 text

An error can be triggered by PHP if your code is invalid. 58 Warning: Invalid argument supplied for foreach() I love PHP. foreach (null as $_) { } echo 'I love PHP.';

Slide 59

Slide 59 text

Some errors does not interrupt the code execution. 59 Notice: Undefined variable: foo echo $foo;

Slide 60

Slide 60 text

Some errors interrupts the code execution but are recoverable. They are thrown as exception since PHP 7. 60 Fatal error: Uncaught Error: Call to undefined function foo() foo();

Slide 61

Slide 61 text

That means you can catch them to handle them as exceptions. 61 I love PHP. try { foo(); } catch (\Error $e) { echo "I love PHP."; }

Slide 62

Slide 62 text

62 I love PHP. Fatal error: Foo cannot implement ArrayIterator - it is not an interface Some errors interrupt the code execution and are fatal (they are not recoverable). echo “I love PHP.”; interface Foo extends \ArrayIterator { }

Slide 63

Slide 63 text

Some errors prevent the code from being compiled. The code is never executed. 63 Parse error: syntax error, unexpected 'implements' (T_IMPLEMENTS), expecting '{' echo "I love PHP."; interface Foo implements \SeekableIterator { }

Slide 64

Slide 64 text

PHP error handling 102 64

Slide 65

Slide 65 text

All recoverable core errors extends the base \Error class that implements the base \Throwable interface. 65 class TypeError extends Error { } class Error implements Throwable { }

Slide 66

Slide 66 text

You can manually trigger an error with a custom message and a E_USER_* level. 66 Fatal error: foo trigger_error('foo', E_USER_ERROR);

Slide 67

Slide 67 text

You can define a custom global error handler. 67 I love PHP. set_error_handler(function ( int $errno, string $errstr, ?string $errfile, ?int $errline ): bool { echo "I love PHP."; return true; }); echo $foo;

Slide 68

Slide 68 text

You can restore the previous global error handler. 68 I love PHP. set_error_handler(function (): bool { echo "I love PHP."; return true; }); set_error_handler(function (): bool { die(); }); restore_error_handler(); echo $foo;

Slide 69

Slide 69 text

If the callback returns false, the normal PHP error handler is still called afterwards. 69 I love PHP. Notice: Undefined variable: foo set_error_handler(function ( int $errno, string $errstr, ?string $errfile, ?int $errline ): bool { echo "I love PHP."; return false; }); echo $foo;

Slide 70

Slide 70 text

You can unset the global error handler by setting it to null. 70 set_error_handler(function ( int $errno, string $errstr, ?string $errfile, ?int $errline ): bool { echo "I love PHP."; return true; }); set_error_handler(null); echo $foo; Notice: Undefined variable: foo

Slide 71

Slide 71 text

You can register a function that will always be executed on shutdown. 71 I love PHP. register_shutdown_function(function (): void { echo "I love PHP."; });

Slide 72

Slide 72 text

The Debug ErrorHandler Features 72

Slide 73

Slide 73 text

“The Debug ErrorHandler provides a generic error handler that logs PHP errors and that rethrow them as exceptions. 73

Slide 74

Slide 74 text

74 echo 1 / 0;

Slide 75

Slide 75 text

75

Slide 76

Slide 76 text

76

Slide 77

Slide 77 text

77 [2019-06-25 20:27:27] logger.CRITICAL: Uncaught Warning: Division by zero {"exception":"[object] (ErrorException(code: 0): Warning: Division by zero at /Users/thomas.calvet/Fancyweb/talk_debug/index.php:14)"} []

Slide 78

Slide 78 text

78

Slide 79

Slide 79 text

The Debug ErrorHandler Internals 79

Slide 80

Slide 80 text

80

Slide 81

Slide 81 text

81 To enable it : ErrorHandler::register(); use Psr\Log\LoggerInterface; final class ErrorHandler { private $logger; public static function register(): void { register_shutdown_function( __CLASS__.'::handleFatalError' ); set_error_handler([new self(), 'handleError']); } public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; } }

Slide 82

Slide 82 text

82 use Symfony\Component\Debug\Exception\FatalErrorException; final class ErrorHandler { private $exceptionHandler; // enable and setLogger methods public static function handleFatalError(array $error): void { // ... Get the current exception handler. $this->exceptionHandler = $exceptionHandler; $this->handleException( new FatalErrorException($error['type']/* ... */) ); } }

Slide 83

Slide 83 text

83 final class ErrorHandler { private $exceptionHandler; private $loggedErrors; private $logger; // enable, setLogger and handleFatalError methods public function handleException(\Throwable $exception) { if ($this->loggedErrors & $exception->getSeverity()) { $this->logger->log(/** ... */); } $exceptionHandler = $this->exceptionHandler; return $exceptionHandler($exception); } }

Slide 84

Slide 84 text

84 final class ErrorHandler { private $logger; private $thrownErrors; // enable, setLogger, handleFatalError and // handleException methods public function handleError( int $type string $message/** ... */ ): void { if ($this->thrownErrors & $type) { throw new \ErrorException($message/** ... */); } $this->logger->log(/** ... */); } }

Slide 85

Slide 85 text

The Debug ErrorHandler Zoom on... 85

Slide 86

Slide 86 text

Triggering an exception in the magic method __toString() is impossible because not returning a string in this method triggers a fatal error. 86 Fatal error: Uncaught Error: Method Foo::__toString() must return a string value final class Foo { public function __toString() { @trigger_error('bar', E_USER_ERROR); } } echo new Foo();

Slide 87

Slide 87 text

87

Slide 88

Slide 88 text

With the ErrorHandler, the manually triggered error message is visible. 88 Fatal error: Method Foo::__toString() must not throw an exception, caught ErrorException: User Error: bar final class Foo { public function __toString() { @trigger_error('bar', E_USER_ERROR); } } echo new Foo();

Slide 89

Slide 89 text

To summarize the ErrorHandler ▪ A custom global error handler is set and a shutdown function is registered. ▪ The global exception handler is proxyfied to be able to log. ▪ The errors are rethrown as exceptions. 89

Slide 90

Slide 90 text

PHP class loading 101 90

Slide 91

Slide 91 text

Instantiating a class that is unknown will trigger a fatal error. 91 new Foo(); Fatal error: Uncaught Error: Class 'Foo' not found

Slide 92

Slide 92 text

The class needs to be known before. 92 final class Foo { } new Foo();

Slide 93

Slide 93 text

The class can be defined in another included file. 93 require 'Foo.php'; new Foo();

Slide 94

Slide 94 text

Not dynamic Everytime you create a new class, you need to require it manually. Isn’t it enough? No! Performance overhead Required files are parsed and compiled into OP codes at runtime. A request in a web application logically never use all the available classes, so this is wasted memory and time. 94

Slide 95

Slide 95 text

PHP class loading 102 - autoload! 95

Slide 96

Slide 96 text

Warning! Do not use! It’s a legacy usage and it has been deprecated since PHP 7.2! 96 function __autoload(string $class): void { if ('Foo' === $class) { require 'Foo.php'; } } new Foo(); The global function __autoload() is called automatically when the code references a class or an interface that has not been loaded yet.

Slide 97

Slide 97 text

Because it’s an unique function! The __autoload() function is global. That means it can be declared only once : first come, first served. Consequently, interoperability between vendors cannot exist easily. Why it’s still not enough? 97

Slide 98

Slide 98 text

PHP class loading 103 - spl_autoload! 98

Slide 99

Slide 99 text

You can register an autoload function aka a class loader. 99 spl_autoload_register(function (string $class): void { $file = 'classes/'.$class.'.php'; if (file_exists($file)) { require $file; } }); new Foo();

Slide 100

Slide 100 text

You can register an infinite number of autoload functions, thus creating a class loader chain. 100 spl_autoload_register(function (string $class): void { echo "I don't know this class."; }); spl_autoload_register(function (string $class): void { final class Foo { } }); new Foo();

Slide 101

Slide 101 text

You can get all the registered autoload functions. 101 spl_autoload_functions();

Slide 102

Slide 102 text

You can unregister an autoload function. 102 foreach (spl_autoload_functions() as $function) { spl_autoload_unregister($function); }

Slide 103

Slide 103 text

PSR-0, PSR-4 and Composer PSR-0 (2009) Deprecated! A common rule to implement in its class loader to be interoperable with other libraries. PSR-4 (2013) Substituted PSR-0. Dropped compatibility for PEAR-styles classnames and allowed to not have the whole namespace as a directory structure. Composer (2012) Composer is a tool for dependency management in PHP. It generates an autoload.php file that uses the spl_autoload_* functions. 103

Slide 104

Slide 104 text

The Debug DebugClassLoader Features 104

Slide 105

Slide 105 text

“The Debug DebugClassLoader wraps all the registered class loaders to do checks and lightweight static analysis on the classes they load. 105

Slide 106

Slide 106 text

Extending a @final class will trigger a deprecation. 106 namespace A; /** * @final */ class Foo { } namespace B; use A\Foo; final class Bar extends Foo { }

Slide 107

Slide 107 text

107 [2019-06-25 20:40:35] logger.INFO: User Deprecated: The "A\A" class is considered final. It may change without further notice as of its next major version. You should not extend it from "B\B". {"exception":"[object] (ErrorException(code: 0): User Deprecated: The \"A\\A\" class is considered final. It may change without further notice as of its next major version. You should not extend it from \"B\\B\". at /Users/thomas.calvet/Fancyweb/talk_debug/vendor/symfony/debug/DebugClassLo ader.php:201)"} []

Slide 108

Slide 108 text

108

Slide 109

Slide 109 text

Classes considered as being in the same “vendor” don’t trigger deprecations between themselves. 109 namespace A; /** * @final */ class Foo { } final class Bar extends Foo { }

Slide 110

Slide 110 text

Extending or implementing a @deprecated or @internal class or interface will trigger a deprecation. 110 namespace A; /** * @deprecated since version 2.2 */ interface FooInterface { } namespace B; use A\FooInterface; final class Bar implements FooInterface { }

Slide 111

Slide 111 text

Using a @deprecated trait will trigger a deprecation. 111 namespace A; /** * @deprecated */ trait FooTrait { } namespace B; use A\FooTrait; final class Bar { use FooTrait; }

Slide 112

Slide 112 text

Not implementing an interface virtual method will trigger a deprecation. 112 namespace A; /** * @method int count(string $key) */ interface FooInterface { } namespace B; use A\FooInterface; final class Bar implements FooInterface { }

Slide 113

Slide 113 text

Extending an @internal method will trigger a deprecation. 113 namespace A; class Foo { /** * @internal */ public function get(): string { } } namespace B; use A\Foo; final class Bar extends Foo { public function get(): string { } }

Slide 114

Slide 114 text

Not implementing a virtual argument will trigger a deprecation. 114 namespace A; class Foo { /** * @param string $arg */ public function get(/* string $arg */): string { } } namespace B; use A\Foo; final class Bar extends Foo { public function get(): string { } }

Slide 115

Slide 115 text

The DebugClassLoader also throws exceptions with way better messages when there are common autoload problems. 115 // Foo.php final class Bar { } The autoloader expected class "A\A" to be defined in file "/Users/thomas.calvet/Fancyweb/talk_debug/vendor/composer/../. ./A.php". The file was found but the class was not in it, the class name or namespace probably has a typo.

Slide 116

Slide 116 text

The Debug DebugClassLoader Internals 116

Slide 117

Slide 117 text

117 final class DebugClassLoader { private $wrappedClassLoader; public function __construct(callable $wrappedClassLoader) { $this->wrappedClassLoader = $wrappedClassLoader; } public static function enable(): void { foreach (spl_autoload_functions() as $function) { spl_autoload_unregister($function); spl_autoload_register([ new self($function), 'loadClass', ]); } } } To enable it : DebugClassLoader::enable();

Slide 118

Slide 118 text

118 final class DebugClassLoader { private $wrappedClassLoader; // __construct and enable methods public function loadClass(string $class): void { $this->wrappedClassLoader->load($class); $this->checkClass($class); } }

Slide 119

Slide 119 text

119 final class DebugClassLoader { // __construct, enable and loadClass methods public function checkClass(string $class): void { $refl = new \ReflectionClass($class); // ... Check case mismatch and definition problems. $messages = $this->checkAnnotations($refl, $class); foreach ($messages as $message) { @trigger_error($message, E_USER_DEPRECATED); } } }

Slide 120

Slide 120 text

120 final class DebugClassLoader { // __construct, enable, loadClass and checkClass methods public function checkAnnotations( \ReflectionClass $refl, string $class ): array { $deprecations = []; // ... Process and check everything return $deprecations; } }

Slide 121

Slide 121 text

121 final class DebugClassLoader { private static $final = []; // __construct, enable, loadClass and checkClass methods public function checkAnnotations($refl, $class): array { // ... if (false !== $doc = $refl->getDocComment()) { if (preg_match('/@final(.*)/', $doc, $matches) { self::$final[$class] = $matches[1]; } } // ... } }

Slide 122

Slide 122 text

122 final class DebugClassLoader { private static $checkedClasses = []; // __construct, enable, loadClass and checkClass methods public function checkAnnotations($refl, $class): array { // ... if ($parent = get_parent_class($class)) { if (!isset(self::$checkedClasses[$parent])) { $this->checkClass($parent); } } // ... } }

Slide 123

Slide 123 text

123 final class DebugClassLoader { // __construct, enable, loadClass and checkClass methods public function checkAnnotations($refl, $class): array { // ... if ($parent = get_parent_class($class)) { // ... if (isset(self::$final[$parent])) { $deprecations[] = sprintf(' The "%s" class is considered final. You should not extend it from "%s".', $parent, $class ); } } // ... } }

Slide 124

Slide 124 text

The Debug DebugClassLoader Zoom on... 124

Slide 125

Slide 125 text

125 // #\n \* @method\s+(static\s+)?+(?:[\w\|&\[\]\\\]+\s+)?(\w+(?:\s*\([^\ )]*\))?)+(.+?([[:punct:]]\s*)?)?(?=\r?\n \*(?: @|/$|\r?\n))# // #\n\s+\* @param +((?(?!callable *\().*?|callable *\(.*\).*?))(?<= )\$([a-zA-Z0-9_\x7f-\xff]++)# Real DebugClassLoader regexes.

Slide 126

Slide 126 text

126 $doc = $refl->getDocComment(); if ( false !== strpos($doc, '@method') && preg_match_all('/@method(.*)/') ) { } The DebugClassLoader must be fast, because it does its analysis at runtime. This why there are micro optimisations.

Slide 127

Slide 127 text

To summarize the DebugClassLoader ▪ It wraps all the defined class loaders. ▪ It loads the classes by calling the wrapped class loaders. ▪ It does checks and lightweight static analysis to trigger useful deprecations. 127

Slide 128

Slide 128 text

Upcoming in Symfony 5! 128

Slide 129

Slide 129 text

Namespaces mapping https:/ /github.com/symfony/sy mfony/pull/30899 Control the “same vendor” check to force the exposition or mute deprecations. New DebugClassLoader features Strict return types https:/ /github.com/symfony/symf ony/pull/30323 Throw deprecations when classes methods can already use strict return types to prepare for future major versions of their parents / implemented interfaces. 129

Slide 130

Slide 130 text

New ErrorHandler component ▪ https:/ /github.com/symfony/symfony/pull/310 65 by Yonel Ceruto (@yceruto) ▪ Extract the error handler mechanism (from the ExceptionHandler and the ErrorHandler) to a new component. ▪ The goal : be able to render the errors in many formats (HTML, JSON, XML, etc.) ▪ You can also add your own error renderers! 130

Slide 131

Slide 131 text

Thanks! Any questions? You can find me at: fancyweb on GitHub and on the Symfony Devs Slack calvet.thomas@gmail.com by mail 131