Upload
stefano-maraspin
View
117
Download
4
Tags:
Embed Size (px)
DESCRIPTION
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.
Citation preview
Error Reporting in Zend Framework 2
Zend Framework Day – Turin, Italy – 07/02/2014
STEVE MARASPIN
@maraspin
http://www.mvlabs.it/
5
http://friuli.grusp.org/
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
Online Privacy: A Growing Threat. - Business Week, March 20, 2000, 96. Internet Privacy in E-
Commerce:
Framework, Review, and Opportunities for Future Research - Proceedings of the 41st Hawaii
International Conference on System Sciences - 2008
• 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
Validation Handling
Improved Error Message
+70% CONVERSIONS
21
Creating A Form in ZF2 <?php namespace Application\Form; use Zend\Form\Form; class ContactForm extends Form { public function __construct() { parent::__construct(); // ... } //... }
Creating A Form in ZF2 <?php namespace Application\Form; use Zend\Form\Form; 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 Application\Form;
[…]
class ContactFilter extends InputFilter
{
public function __construct() {
// filters go here
}
}
Validation: inputFilter <?php
namespace Application\Form;
[…]
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 Application\Controller;
[…]
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 Application\Controller;
[…]
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: • 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
45
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('Application\Module','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('Application\Module', '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 = Zend\Stdlib\ArrayUtils::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'; Zend\Mvc\Application::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 = Zend\Stdlib\ArrayUtils::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'; Zend\Mvc\Application::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 Zend\Log\Logger;
$writer = new Zend\Log\Writer\Stream('/var/log/app.log');
$logger->addWriter($writer);
$logger->info('Informational message');
$logger->log(Zend\Log\Logger::EMERG, 'Emergency message');
Zend Log $logger = new Zend\Log\Logger;
$writer = new Zend\Log\Writer\Stream('/var/log/app.log');
$logger->addWriter($writer);
$logger->info('Informational message');
$logger->log(Zend\Log\Logger::EMERG, 'Emergency message');
Zend\Log\Logger::registerErrorHandler($logger); Zend\Log\Logger::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 Zend\Log\Logger;
$writer1 = new Zend\Log\Writer\Stream('/var/log/app.log');
$logger->addWriter($writer1);
$writer2 = new Zend\Log\Writer\Stream('/var/log/err.log');
$logger->addWriter($writer2);
$filter = new Zend\Log\Filter\Priority(Logger::CRIT);
$writer2->addFilter($filter);
$logger->info('Informational message');
$logger->log(Zend\Log\Logger::CRIT, 'Emergency message');
Monolog
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$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. When reporting errors, make sure to be
nice with users 2. Different error reporting strategies could
be useful for different environments 3. The event manager reduces coupling and
provides flexibility
123
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