error reporting in zf2: form messages, custom error pages, logging

Post on 27-Jan-2015

117 Views

Category:

Technology

4 Downloads

Preview:

Click to see full reader

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.

TRANSCRIPT

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

top related