Error Reporting in ZF2: form messages, custom error pages, logging

14,006 views

Published on

Errors frustrate users. No matter if it's their fault or applications', risks that they'll lose interest in our product is high. In this presentation, given at the Italian ZFDay 2014, I discuss about these issues and provide some hints for improving error reporting and handling.

Published in: Technology
2 Comments
23 Likes
Statistics
Notes
No Downloads
Views
Total views
14,006
On SlideShare
0
From Embeds
0
Number of Embeds
224
Actions
Shares
0
Downloads
186
Comments
2
Likes
23
Embeds 0
No embeds

No notes for slide

Error Reporting in ZF2: form messages, custom error pages, logging

  1. 1. Error Reporting in Zend Framework 2 Zend Framework Day – Turin, Italy – 07/02/2014
  2. 2. STEVE MARASPIN
  3. 3. @maraspin
  4. 4. http://www.mvlabs.it/
  5. 5. http://friuli.grusp.org/ 5
  6. 6. WHY WORRY?
  7. 7. WE SCREW UP
  8. 8. WE ALL SCREW UP
  9. 9. Application Failures
  10. 10. Application Failures User Mistakes
  11. 11. INCREASED SUPPORT COST
  12. 12. ABANDONMENT
  13. 13. THE BOTTOM LINE
  14. 14. User Input = Mistake Source
  15. 15. Validation Handling
  16. 16. User Privacy Concerns • Over 40% of online shoppers are very concerned over the use of personal information • Public opinion polls have revealed a general desire among Internet users to protect their privacy Online Privacy: A Growing Threat. - Business Week, March 20, 2000, 96. Internet Privacy in ECommerce: Framework, Review, and Opportunities for Future Research - Proceedings of the 41st Hawaii International Conference on System Sciences - 2008
  17. 17. Validation Handling
  18. 18. Improved Error Message
  19. 19. +70% CONVERSIONS
  20. 20. 21
  21. 21. Creating A Form in ZF2 <?php namespace ApplicationForm; use ZendFormForm; class ContactForm extends Form { public function __construct() { parent::__construct(); // ... } //... }
  22. 22. Creating A Form in ZF2 <?php namespace ApplicationForm; use ZendFormForm; class ContactForm extends Form { public function __construct() { parent::__construct(); // ... } //... }
  23. 23. Adding Form Fields public function init() { $this->setName('contact'); $this->setAttribute('method', 'post'); […].. $this->add(array('name' => 'email', 'type' => 'text', 'options' => array( 'label' => 'Name', ), ); $this->add(array('name' => 'message', 'type' => 'textarea', 'options' => array( 'label' => 'Message', ), ); //. […].. }
  24. 24. Adding Form Fields public function init() { $this->setName('contact'); $this->setAttribute('method', 'post'); […].. $this->add(array('name' => 'email', 'type' => 'text', 'options' => array( 'label' => 'Name', ), ); $this->add(array('name' => 'message', 'type' => 'textarea', 'options' => array( 'label' => 'Message', ), ); //. […].. }
  25. 25. VALIDATION
  26. 26. Form Validation: InputFilter
  27. 27. Validation: inputFilter <?php namespace ApplicationForm; […] class ContactFilter extends InputFilter { public function __construct() { // filters go here } }
  28. 28. Validation: inputFilter <?php namespace ApplicationForm; […] class ContactFilter extends InputFilter { public function __construct() { // filters go here } }
  29. 29. Required Field Validation $this->add(array( 'name' => 'email', 'required' => true, 'filters' => array( array( 'name' => 'StringTrim'), ), 'validators' => array( array( 'name' => 'EmailAddress', ) ) ));
  30. 30. Required Field Validation $this->add(array( 'name' => 'email', 'required' => true, 'filters' => array( array( 'name' => 'StringTrim'), ), 'validators' => array( array( 'name' => 'EmailAddress', ) ) ));
  31. 31. InputFilter Usage <?php namespace ApplicationController; […] class IndexController extends AbstractActionController { public function contactAction() { $form = new Contact(); $filter = new ContactFilter(); $form->setInputFilter($filter); return new ViewModel(array( 'form' => $form ); } }
  32. 32. InputFilter Usage <?php namespace ApplicationController; […] class IndexController extends AbstractActionController { public function contactAction() { $form = new Contact(); $filter = new ContactFilter(); $form->setInputFilter($filter); return new ViewModel(array( 'form' => $form ); } }
  33. 33. Standard Error Message
  34. 34. Improved Error Message
  35. 35. Error Message Customization $this->add(array( 'name' => 'email', 'required' => true, 'filters' => array( array('name' => 'StringTrim'), ), 'validators' => array( array('name' =>'NotEmpty', 'options' => array( 'messages' => array( NotEmpty::IS_EMPTY => 'We need an '. 'e-mail address to be able to get back to you' ), ), ), array('name' => 'EmailAddress'), ) ));
  36. 36. Error Message Customization $this->add(array( 'name' => 'email', 'required' => true, 'filters' => array( array('name' => 'StringTrim'), ), 'validators' => array( array('name' =>'NotEmpty', 'options' => array( 'messages' => array( NotEmpty::IS_EMPTY => 'We need an '. 'e-mail address to be able to get back to you' ), ), ), array('name' => 'EmailAddress'), ) ));
  37. 37. More than we need…
  38. 38. Check Chain $this->add(array( 'name' => 'email', 'required' => true, 'filters' => array( array('name' => 'StringTrim'), ), 'validators' => array( array('name' =>'NotEmpty', 'options' => array( 'messages' => array( NotEmpty::IS_EMPTY => 'We need an '. 'e-mail address to be able to get back to you' ), ), 'break_chain_on_failure' => true, ), array('name' => 'EmailAddress'), ) ));
  39. 39. Ok, good…
  40. 40. …but, what if?
  41. 41. Words to Avoid http://uxmovement.com/forms/how-to-make-your-form-error-messages-more-reassuring/
  42. 42. A few tips: • • • 45 Provide the user with a solution to the problem Do not use technical jargon, use terminology that your audience understands Avoid uppercase text and exclamation points
  43. 43. Improved message
  44. 44. Condensing N messages into 1 $this->add(array( 'name' => 'email', 'required' => true, 'validators' => array( array('name' =>'NotEmpty', 'options' => array( 'messages' => array( NotEmpty::IS_EMPTY => 'We need an '. 'e-mail address to be able to get back to you' )), 'break_chain_on_failure' => true, ), array('name' => 'EmailAddress', 'options' => array( 'message' => 'E-Mail address does not seem to be valid. Please make sure it contains the @ symbol and a valid domain name.', )));
  45. 45. Condensing N messages into 1 $this->add(array( 'name' => 'email', 'required' => true, 'validators' => array( array('name' =>'NotEmpty', 'options' => array( 'messages' => array( NotEmpty::IS_EMPTY => 'We need an '. 'e-mail address to be able to get back to you' )), 'break_chain_on_failure' => true, ), array('name' => 'EmailAddress', 'options' => array( 'message' => 'E-Mail address does not seem to be valid. Please make sure it contains the @ symbol and a valid domain name.', )));
  46. 46. Messages VS message $this->add(array( 'name' => 'email', 'required' => true, 'validators' => array( array('name' =>'NotEmpty', 'options' => array( 'messages' => array( NotEmpty::IS_EMPTY => 'We need an '. 'e-mail address to be able to get back to you' )), 'break_chain_on_failure' => true, ), array('name' => 'EmailAddress', 'options' => array( 'message' => 'E-Mail address does not seem to be valid. Please make sure it contains the @ symbol and a valid domain name.', )));
  47. 47. Translated Error Messages
  48. 48. Default Message Translation public function onBootstrap(MvcEvent $e) { $translator = $e->getApplication() ->getServiceManager()->get('translator'); $translator->addTranslationFile( 'phpArray', __DIR__ . '/../../vendor/zendframework/zendframework/'. 'resources/languages/it/Zend_Validate.php', 'default', 'it_IT' ); AbstractValidator::setDefaultTranslator($translator); }
  49. 49. Custom Message Translation $this->add(array( 'name' => 'email', 'required' => true, 'validators' => array( array('name' =>'NotEmpty', 'options' => array( 'translator' => $translator, 'message' => $translator->translate( 'Make sure your e-mail address contains the @ symbol and a valid domain name.' )), 'break_chain_on_failure' => true, ), )));
  50. 50. Form Factory $translator = $I_services->get('translator'); $I_form = new Contact(); $I_filter = new ContactFilter($translator); $I_form->setInputFilter($I_filter); return $I_form;
  51. 51. Translated Error Message
  52. 52. http://patterntap.com/pattern/funny-and-helpful-404-error-page-mintcom
  53. 53. 56
  54. 54. Error Display Configuration Skeleton Applicaton Configuration
  55. 55. Hiding Exception Traces 'view_manager' => array( 'display_not_found_reason' => false, 'display_exceptions' => false, 'doctype' => 'HTML5', 'not_found_template' => 'error/404', 'exception_template' => 'error/index', 'template_map' => array( 'layout/layout' => __DIR__ . '/../view/layout/layout.phtml', 'application/index/index'=> __DIR__ . '/../view/application/index/index.phtml', 'error/404' => __DIR__ . '/../view/error/404.phtml', 'error/index' => __DIR__ . '/../view/error/index.phtml', ), 'template_path_stack' => array( __DIR__ . '/../view', ), ),
  56. 56. Hiding Exception Traces 'view_manager' => array( 'display_not_found_reason' => false, 'display_exceptions' => false, 'doctype' => 'HTML5', 'not_found_template' => 'error/404', 'exception_template' => 'error/index', 'template_map' => array( 'layout/layout' => __DIR__ . '/../view/layout/layout.phtml', 'application/index/index'=> __DIR__ . '/../view/application/index/index.phtml', 'error/404' => __DIR__ . '/../view/error/404.phtml', 'error/index' => __DIR__ . '/../view/error/index.phtml', ), 'template_path_stack' => array( __DIR__ . '/../view', ), ),
  57. 57. Custom Error Pages 'view_manager' => array( 'display_not_found_reason' => false, 'display_exceptions' => false, 'doctype' => 'HTML5', 'not_found_template' => 'error/404', 'exception_template' => 'error/index', 'template_map' => array( 'layout/layout' => __DIR__ . '/../view/layout/layout.phtml', 'application/index/index'=> __DIR__ . '/../view/application/index/index.phtml', 'error/404' => __DIR__ . '/../view/error/404.phtml', 'error/index' => __DIR__ . '/../view/error/index.phtml', ), 'template_path_stack' => array( __DIR__ . '/../view', ), ),
  58. 58. How about PHP Errors? class IndexController extends AbstractActionController { public function indexAction() { 1/0; return new ViewModel(); } }
  59. 59. How about PHP Errors? class IndexController extends AbstractActionController { public function indexAction() { 1/0; return new ViewModel(); } }
  60. 60. Early error detection principle
  61. 61. What can we do?
  62. 62. Handling PHP Errors public function onBootstrap(MvcEvent $I_e) { […] set_error_handler(array('ApplicationModule','handlePhpErrors')); } public static function handlePhpErrors($i_type, $s_message, $s_file, $i_line) { if (!($i_type & error_reporting())) { return }; throw new Exception("Error: " . $s_message . " in file " . $s_file . " at line " . $i_line); }
  63. 63. What happens now? class IndexController extends AbstractActionController { public function indexAction() { 1/0; return new ViewModel(); } }
  64. 64. Now… what if? class IndexController extends AbstractActionController { public function indexAction() { $doesNotExist->doSomething(); return new ViewModel(); } }
  65. 65. Fatal error: Call to a member function doSomething() on a non-object in /srv/apps/zfday/module/Application/src/Application/Controller/IndexController.php on line 20
  66. 66. FATAL ERRORS
  67. 67. Fatal Error Handling public function onBootstrap(MvcEvent $I_e) { […] $am_config = $I_application->getConfig(); $am_environmentConf = $am_config['mvlabs_environment']; // Fatal Error Recovery if (array_key_exists('recover_from_fatal', $am_environmentConf) && $am_environmentConf['recover_from_fatal']) { $s_redirectUrl = $am_environmentConf['redirect_url']; } $s_callback = null; if (array_key_exists('fatal_errors_callback', $am_environmentConf)) { $s_callback = $am_environmentConf['fatal_errors_callback']; } register_shutdown_function(array('ApplicationModule', 'handleFatalPhpErrors'), $s_redirectUrl, $s_callback); }
  68. 68. Fatal Error Handling /** * Redirects user to nice page after fatal has occurred */ public static function handleFatalPhpErrors($s_redirectUrl, $s_callback = null) { if (php_sapi_name() != 'cli' && @is_Array($e = @get_last())) { if (null != $s_callback) { // This is the most stuff we can get. // New context outside of framework scope $m_code = isset($e['type']) ? $e['type'] : 0; $s_msg = isset($e['message']) ? $e['message']:''; $s_file = isset($e['file']) ? $e['file'] : ''; $i_line = isset($e['line']) ? $e['line'] : ''; $s_callback($s_msg, $s_file, $i_line); } header("location: ". $s_redirectUrl); } return false; }
  69. 69. Fatal Error Handling 'mvlabs_environment' => array( 'exceptions_from_errors' => true, 'recover_from_fatal' => true, 'fatal_errors_callback' => function($s_msg, $s_file, $s_line) { return false; }, 'redirect_url' => '/error', 'php_settings' => array( 'error_reporting' => E_ALL, 'display_errors' => 'Off', 'display_startup_errors' => 'Off', ), ),
  70. 70. PHP Settings Conf 'mvlabs_environment' => array( 'exceptions_from_errors' => true, 'recover_from_fatal' => true, 'fatal_errors_callback' => function($s_msg, $s_file, $s_line) { return false; }, 'redirect_url' => '/error', 'php_settings' => array( 'error_reporting' => E_ALL, 'display_errors' => 'Off', 'display_startup_errors' => 'Off', ), ),
  71. 71. PHP Settings public function onBootstrap(MvcEvent $I_e) { […] foreach($am_phpSettings as $key => $value) { ini_set($key, $value); } }
  72. 72. NICE PAGE!
  73. 73. CUSTOMER SUPPORT TEAM REACTION http://www.flickr.com/photos/18548283@N00/8030280738
  74. 74. ENVIRONMENT DEPENDANT CONFIGURATION
  75. 75. During Deployment
  76. 76. Local/Global Configuration Files
  77. 77. During Deployment Runtime
  78. 78. Index.php // Application wide configuration $am_conf = $am_originalConf = require 'config/application.config.php'; // Environment specific configuration $s_environmentConfFile = 'config/application.'.$s_env.'.config.php'; if (file_exists($s_environmentConfFile) && is_readable($s_environmentConfFile)) { // Specific environment configuration merge $am_environmentConf = require $s_environmentConfFile; $am_conf = ZendStdlibArrayUtils::merge($am_originalConf, $am_environmentConf ); } // Additional Specific configuration files are also taken into account $am_conf["module_listener_options"]["config_glob_paths"][] = 'config/autoload/{,*.}' . $s_env . '.php'; ZendMvcApplication::init($am_conf)->run();
  79. 79. application.config.php 'modules' => array( 'Application', ),
  80. 80. Application.dev.config.php 'modules' => array( 'Application', 'ZendDeveloperTools', ),
  81. 81. Enabling Environment Confs // Application nominal environment $am_conf = $am_originalConf = require 'config/application.config.php'; // Environment specific configuration $s_environmentConfFile = 'config/application.'.$s_env.'.config.php'; // Do we have a specific configuration file? if (file_exists($s_environmentConfFile) && is_readable($s_environmentConfFile)) { // Specific environment configuration merge $am_environmentConf = require $s_environmentConfFile; $am_conf = ZendStdlibArrayUtils::merge($am_originalConf, $am_environmentConf ); } // Additional Specific configuration files are also taken into account $am_conf["module_listener_options"]["config_glob_paths"][] = 'config/autoload/{,*.}' . $s_env . '.php'; ZendMvcApplication::init($am_conf)->run();
  82. 82. Env Dependant Conf Files
  83. 83. index.php Check // What environment are we in? $s_env = getenv('APPLICATION_ENV'); if (empty($s_env)) { throw new Exception('Environment not set.'. ' Cannot continue. Too risky!'); }
  84. 84. Apache Config File <VirtualHost *:80> DocumentRoot /srv/apps/zfday/public ServerName www.dev.zfday.it SetEnv APPLICATION_ENV "dev" <Directory /srv/apps/zfday/public> AllowOverride FileInfo </Directory> </VirtualHost>
  85. 85. LOGGING http://www.flickr.com/photos/otterlove/8154505388/
  86. 86. Why Log? Troubleshooting • Stats Generation • Compliance • 95
  87. 87. Zend Log $logger = new ZendLogLogger; $writer = new ZendLogWriterStream('/var/log/app.log'); $logger->addWriter($writer); $logger->info('Informational message'); $logger->log(ZendLogLogger::EMERG, 'Emergency message');
  88. 88. Zend Log $logger = new ZendLogLogger; $writer = new ZendLogWriterStream('/var/log/app.log'); $logger->addWriter($writer); $logger->info('Informational message'); $logger->log(ZendLogLogger::EMERG, 'Emergency message'); ZendLogLogger::registerErrorHandler($logger); ZendLogLogger::registerExceptionHandler($logger);
  89. 89. Writers 98
  90. 90. Writers 99
  91. 91. Logrotate /var/log/app.log { missingok rotate 7 daily notifempty copytruncate compress endscript } 100
  92. 92. Writers 101
  93. 93. Writers 102
  94. 94. Writers 103
  95. 95. Zend Log Ecosystem 104
  96. 96. Filter Example $logger = new ZendLogLogger; $writer1 = new ZendLogWriterStream('/var/log/app.log'); $logger->addWriter($writer1); $writer2 = new ZendLogWriterStream('/var/log/err.log'); $logger->addWriter($writer2); $filter = new ZendLogFilterPriority(Logger::CRIT); $writer2->addFilter($filter); $logger->info('Informational message'); $logger->log(ZendLogLogger::CRIT, 'Emergency message');
  97. 97. Monolog use MonologLogger; use MonologHandlerStreamHandler; $log = new Logger('name'); $log->pushHandler(new StreamHandler('/var/log/app.log', Logger::WARNING)); $log->addWarning('Foo'); $log->addError('Bar');
  98. 98. Monolog Components 107
  99. 99. How to log? class IndexController { public function helloAction() { return new ViewModel('msg' =>"Hello!"); } }
  100. 100. Traditional Invokation
  101. 101. Logging Within Controller class GreetingsController { public function helloAction() { $I_logger = new Logger(); $I_logger->log("We just said Hello!"); return new ViewModel('msg' =>"Hello!"); } }
  102. 102. Single Responsability Violation class GreetingsController { public function helloAction() { $I_logger = new Logger(); $I_logger->log("We just said Hello!"); return new ViewModel('msg' =>"Hello!"); } }
  103. 103. Fat Controllers class GreetingsController { public function helloAction() { $I_logger = new Logger(); $I_logger->log("We just said Hello!"); $I_mailer = new Mailer(); $I_mailer->mail($s_msg); $I_queue = new Queue(); $I_queue->add($s_msg); return new ViewModel('msg' =>"Hello!"); } }
  104. 104. CROSS CUTTING CONCERNS
  105. 105. What can we do?
  106. 106. Handling Events class Module { public function onBootstrap(MvcEvent $e) { $eventManager = $e->getApplication() ->getEventManager(); $moduleRouteListener = new ModuleRouteListener(); $moduleRouteListener->attach($eventManager); $logger = $sm->get('logger'); $eventManager->attach('wesaidHello', function(MvcEvent $event) use ($logger) { $logger->log($event->getMessage()); ); } }
  107. 107. Triggering An Event class GreetingsController { public function helloAction() { $this->eventManager ->trigger('wesaidHello', $this, array('greeting' => 'Hello!') ); return new ViewModel('msg' => "Hello!"); } }
  108. 108. Traditional Invokation
  109. 109. Event Manager
  110. 110. OBSERVER http://www.flickr.com/photos/lstcaress/502606063/
  111. 111. Event Manager
  112. 112. Handling Framework Errors class Module { public function onBootstrap(MvcEvent $e) { $eventManager = $e->getApplication() ->getEventManager(); $moduleRouteListener = new ModuleRouteListener(); $moduleRouteListener->attach($eventManager); $logger = $sm->get('logger'); $eventManager->attach(MvcEvent::EVENT_RENDER_ERROR, function(MvcEvent $e) use ($logger) { $logger->info('An exception has Happened ' . $e->getResult()->exception->getMessage()); }, -200); ); } }
  113. 113. Event Manager
  114. 114. Stuff to take home 1. 2. 3. 123 When reporting errors, make sure to be nice with users Different error reporting strategies could be useful for different environments The event manager reduces coupling and provides flexibility
  115. 115. 2 min intro
  116. 116. https://xkcd.com/208/
  117. 117. Starting Things Up input { stdin { } } output { stdout { codec => rubydebug } }
  118. 118. Starting Things Up input { stdin { } } output { stdout { codec => rubydebug } } java -jar logstash-1.3.3-flatjar.jar agent -f sample.conf
  119. 119. Integrated Elasticsearch input { file { path => ["/opt/logstash/example.log"] } } output { stdout { codec => rubydebug } elasticsearch { embedded => true } } java -jar logstash-1.3.3-flatjar.jar agent -f elastic.conf
  120. 120. Integrated Web Interface input { file { path => ["/opt/logstash/example.log"] } } output { stdout { codec => rubydebug } elasticsearch { embedded => true } } java -jar logstash.jar agent -f elastic.conf --web
  121. 121. Kibana
  122. 122. Thank you for your attention Stefano Maraspin @maraspin
  123. 123. @maraspin
  124. 124. Stefano Maraspin @maraspin

×