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

Deep dive into the Symfony Debug component

Deep dive into the Symfony Debug component

A7e7c34aaa3ff7eb359b6449fb8bb043?s=128

Thomas Calvet

June 26, 2019
Tweet

Transcript

  1. Deep dive into the Symfony Debug component

  2. 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
  3. Symfony Symfowhat? 3

  4. 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
  5. Why Symfony? ▪ Open source ▪ Stable and predictable ▪

    Best practices ▪ Great documentation ▪ Big welcoming community ▪ Coopetition ▪ A vision 5
  6. Do not confuse The many debug meanings 6

  7. 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
  8. 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
  9. 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
  10. The Debug component 10

  11. “The Debug component provides tools to ease debugging PHP code.

    11 It improves the DX!
  12. > 125,000,000 installs Used by big PHP projects Such as

    Symfony, Laravel, Drupal and API Platform The 4th most popular Symfony package 12
  13. 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
  14. PHP exception handling 101 14

  15. 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
  16. An exception is a normal PHP object. 16 Fatal error:

    Uncaught Exception: foo $exception = new \Exception('foo'); throw $exception; echo "I love PHP.";
  17. You can catch an exception to handle it. 17 I

    love PHP. try { throw new \Exception('foo'); } catch (\Exception $e) { } echo "I love PHP.";
  18. 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.";
  19. 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.";
  20. 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.
  21. 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.
  22. PHP exception handling 102 22

  23. All core exceptions extends the base \Exception class that implements

    the base \Throwable interface. 23 class LogicException extends Exception { } class Exception implements Throwable { }
  24. 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();
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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); } } }
  30. 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; } } }
  31. 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.
  32. 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.
  33. 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');
  34. The advantages Of using exceptions for your domain errors 34

  35. 35 final class Foo { /** * Returns the number

    of processed elements * or false if an error occurred. * * @return int|false */ public function process() { // ... } }
  36. 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 { // ... } }
  37. 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', ]; } }
  38. An exception carries a meaning in its name. 38 final

    class RateLimitExceededException extends \Exception { }
  39. 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; } }
  40. The Debug ExceptionHandler Features 40

  41. “The Debug ExceptionHandler converts a PHP exception to a pretty

    and useful HTML response content. 41
  42. 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');
  43. 43

  44. 44

  45. 45

  46. The Debug ExceptionHandler Internals 46

  47. 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();
  48. 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); } }
  49. 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; } }
  50. 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
  51. The Debug ExceptionHandler Zoom on... 51

  52. 52

  53. 53

  54. 54

  55. 55

  56. PHP error handling 101 56

  57. 57

  58. 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.';
  59. Some errors does not interrupt the code execution. 59 Notice:

    Undefined variable: foo echo $foo;
  60. 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();
  61. That means you can catch them to handle them as

    exceptions. 61 I love PHP. try { foo(); } catch (\Error $e) { echo "I love PHP."; }
  62. 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 { }
  63. 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 { }
  64. PHP error handling 102 64

  65. All recoverable core errors extends the base \Error class that

    implements the base \Throwable interface. 65 class TypeError extends Error { } class Error implements Throwable { }
  66. 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);
  67. 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;
  68. 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;
  69. 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;
  70. 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
  71. 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."; });
  72. The Debug ErrorHandler Features 72

  73. “The Debug ErrorHandler provides a generic error handler that logs

    PHP errors and that rethrow them as exceptions. 73
  74. 74 echo 1 / 0;

  75. 75

  76. 76

  77. 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)"} []
  78. 78

  79. The Debug ErrorHandler Internals 79

  80. 80

  81. 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; } }
  82. 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']/* ... */) ); } }
  83. 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); } }
  84. 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(/** ... */); } }
  85. The Debug ErrorHandler Zoom on... 85

  86. 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();
  87. 87

  88. 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();
  89. 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
  90. PHP class loading 101 90

  91. Instantiating a class that is unknown will trigger a fatal

    error. 91 new Foo(); Fatal error: Uncaught Error: Class 'Foo' not found
  92. The class needs to be known before. 92 final class

    Foo { } new Foo();
  93. The class can be defined in another included file. 93

    require 'Foo.php'; new Foo();
  94. 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
  95. PHP class loading 102 - autoload! 95

  96. 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.
  97. 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
  98. PHP class loading 103 - spl_autoload! 98

  99. 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();
  100. 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();
  101. You can get all the registered autoload functions. 101 spl_autoload_functions();

  102. You can unregister an autoload function. 102 foreach (spl_autoload_functions() as

    $function) { spl_autoload_unregister($function); }
  103. 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
  104. The Debug DebugClassLoader Features 104

  105. “The Debug DebugClassLoader wraps all the registered class loaders to

    do checks and lightweight static analysis on the classes they load. 105
  106. Extending a @final class will trigger a deprecation. 106 namespace

    A; /** * @final */ class Foo { } namespace B; use A\Foo; final class Bar extends Foo { }
  107. 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)"} []
  108. 108

  109. 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 { }
  110. 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 { }
  111. Using a @deprecated trait will trigger a deprecation. 111 namespace

    A; /** * @deprecated */ trait FooTrait { } namespace B; use A\FooTrait; final class Bar { use FooTrait; }
  112. 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 { }
  113. 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 { } }
  114. 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 { } }
  115. 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.
  116. The Debug DebugClassLoader Internals 116

  117. 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();
  118. 118 final class DebugClassLoader { private $wrappedClassLoader; // __construct and

    enable methods public function loadClass(string $class): void { $this->wrappedClassLoader->load($class); $this->checkClass($class); } }
  119. 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); } } }
  120. 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; } }
  121. 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]; } } // ... } }
  122. 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); } } // ... } }
  123. 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 ); } } // ... } }
  124. The Debug DebugClassLoader Zoom on... 124

  125. 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.
  126. 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.
  127. 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
  128. Upcoming in Symfony 5! 128

  129. 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
  130. 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
  131. Thanks! Any questions? You can find me at: fancyweb on

    GitHub and on the Symfony Devs Slack calvet.thomas@gmail.com by mail 131