• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
Error Reporting in ZF2: form messages, custom error pages, logging
 

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

on

  • 3,873 views

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 ...

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.

Statistics

Views

Total Views
3,873
Views on SlideShare
3,795
Embed Views
78

Actions

Likes
5
Downloads
37
Comments
0

4 Embeds 78

http://www.scoop.it 61
http://www.slideee.com 9
https://twitter.com 7
http://www.linkedin.com 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

CC Attribution-NonCommercial-ShareAlike LicenseCC Attribution-NonCommercial-ShareAlike LicenseCC Attribution-NonCommercial-ShareAlike License

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel

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

  • Error Reporting in Zend Framework 2 Zend Framework Day – Turin, Italy – 07/02/2014
  • STEVE MARASPIN
  • @maraspin
  • http://www.mvlabs.it/
  • http://friuli.grusp.org/ 5
  • WHY WORRY?
  • WE SCREW UP
  • WE ALL SCREW UP
  • Application Failures
  • Application Failures User Mistakes
  • INCREASED SUPPORT COST
  • ABANDONMENT
  • THE BOTTOM LINE
  • User Input = Mistake Source
  • Validation Handling
  • 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
  • Validation Handling
  • Improved Error Message
  • +70% CONVERSIONS
  • 21
  • Creating A Form in ZF2 <?php namespace ApplicationForm; use ZendFormForm; class ContactForm extends Form { public function __construct() { parent::__construct(); // ... } //... }
  • Creating A Form in ZF2 <?php namespace ApplicationForm; use ZendFormForm; class ContactForm extends Form { public function __construct() { parent::__construct(); // ... } //... }
  • 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', ), ); //. […].. }
  • 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', ), ); //. […].. }
  • VALIDATION
  • Form Validation: InputFilter
  • Validation: inputFilter <?php namespace ApplicationForm; […] class ContactFilter extends InputFilter { public function __construct() { // filters go here } }
  • Validation: inputFilter <?php namespace ApplicationForm; […] class ContactFilter extends InputFilter { public function __construct() { // filters go here } }
  • Required Field Validation $this->add(array( 'name' => 'email', 'required' => true, 'filters' => array( array( 'name' => 'StringTrim'), ), 'validators' => array( array( 'name' => 'EmailAddress', ) ) ));
  • Required Field Validation $this->add(array( 'name' => 'email', 'required' => true, 'filters' => array( array( 'name' => 'StringTrim'), ), 'validators' => array( array( 'name' => 'EmailAddress', ) ) ));
  • 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 ); } }
  • 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 ); } }
  • Standard Error Message
  • Improved Error Message
  • 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'), ) ));
  • 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'), ) ));
  • More than we need…
  • 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'), ) ));
  • Ok, good…
  • …but, what if?
  • Words to Avoid http://uxmovement.com/forms/how-to-make-your-form-error-messages-more-reassuring/
  • 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
  • Improved message
  • 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.', )));
  • 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.', )));
  • 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.', )));
  • Translated Error Messages
  • 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); }
  • 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, ), )));
  • Form Factory $translator = $I_services->get('translator'); $I_form = new Contact(); $I_filter = new ContactFilter($translator); $I_form->setInputFilter($I_filter); return $I_form;
  • Translated Error Message
  • http://patterntap.com/pattern/funny-and-helpful-404-error-page-mintcom
  • 56
  • Error Display Configuration Skeleton Applicaton Configuration
  • 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', ), ),
  • 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', ), ),
  • 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', ), ),
  • How about PHP Errors? class IndexController extends AbstractActionController { public function indexAction() { 1/0; return new ViewModel(); } }
  • How about PHP Errors? class IndexController extends AbstractActionController { public function indexAction() { 1/0; return new ViewModel(); } }
  • Early error detection principle
  • What can we do?
  • 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); }
  • What happens now? class IndexController extends AbstractActionController { public function indexAction() { 1/0; return new ViewModel(); } }
  • Now… what if? class IndexController extends AbstractActionController { public function indexAction() { $doesNotExist->doSomething(); return new ViewModel(); } }
  • 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
  • FATAL ERRORS
  • 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); }
  • 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; }
  • 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', ), ),
  • 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', ), ),
  • PHP Settings public function onBootstrap(MvcEvent $I_e) { […] foreach($am_phpSettings as $key => $value) { ini_set($key, $value); } }
  • NICE PAGE!
  • CUSTOMER SUPPORT TEAM REACTION http://www.flickr.com/photos/18548283@N00/8030280738
  • ENVIRONMENT DEPENDANT CONFIGURATION
  • During Deployment
  • Local/Global Configuration Files
  • During Deployment Runtime
  • 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();
  • application.config.php 'modules' => array( 'Application', ),
  • Application.dev.config.php 'modules' => array( 'Application', 'ZendDeveloperTools', ),
  • 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();
  • Env Dependant Conf Files
  • 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!'); }
  • 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>
  • LOGGING http://www.flickr.com/photos/otterlove/8154505388/
  • Why Log? Troubleshooting • Stats Generation • Compliance • 95
  • 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');
  • 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);
  • Writers 98
  • Writers 99
  • Logrotate /var/log/app.log { missingok rotate 7 daily notifempty copytruncate compress endscript } 100
  • Writers 101
  • Writers 102
  • Writers 103
  • Zend Log Ecosystem 104
  • 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');
  • 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');
  • Monolog Components 107
  • How to log? class IndexController { public function helloAction() { return new ViewModel('msg' =>"Hello!"); } }
  • Traditional Invokation
  • Logging Within Controller class GreetingsController { public function helloAction() { $I_logger = new Logger(); $I_logger->log("We just said Hello!"); return new ViewModel('msg' =>"Hello!"); } }
  • Single Responsability Violation class GreetingsController { public function helloAction() { $I_logger = new Logger(); $I_logger->log("We just said Hello!"); return new ViewModel('msg' =>"Hello!"); } }
  • 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!"); } }
  • CROSS CUTTING CONCERNS
  • What can we do?
  • 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()); ); } }
  • Triggering An Event class GreetingsController { public function helloAction() { $this->eventManager ->trigger('wesaidHello', $this, array('greeting' => 'Hello!') ); return new ViewModel('msg' => "Hello!"); } }
  • Traditional Invokation
  • Event Manager
  • OBSERVER http://www.flickr.com/photos/lstcaress/502606063/
  • Event Manager
  • 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); ); } }
  • Event Manager
  • 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
  • 2 min intro
  • https://xkcd.com/208/
  • Starting Things Up input { stdin { } } output { stdout { codec => rubydebug } }
  • Starting Things Up input { stdin { } } output { stdout { codec => rubydebug } } java -jar logstash-1.3.3-flatjar.jar agent -f sample.conf
  • 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
  • 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
  • Kibana
  • Thank you for your attention Stefano Maraspin @maraspin
  • @maraspin
  • Stefano Maraspin @maraspin