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

DPC 2018 - Journey Through "Unhappy Path"

DPC 2018 - Journey Through "Unhappy Path"

Developers naturally gravitate towards the "happy path" - default scenario in application execution in which everything works as expected, consequently neglecting exceptional behavior aspect of a software development.

This talk provides a comprehensive insight into widely adopted practices and patterns for dealing with exceptional conditions. Emphasis is on exceptions and applicable techniques that improve quality of the overall architecture, such as:

- structuring and organizing custom exception classes
- formatting exception messages using named constructors technique
- component-level exception
- exception wrapping

To make the story complete, second part of the talk focuses on a PHP-based solution for establishing a robust and extensible error handling system.

Nikola Poša

June 09, 2018
Tweet

More Decks by Nikola Poša

Other Decks in Programming

Transcript

  1. JOURNEY THROUGH JOURNEY THROUGH "UNHAPPY PATH" "UNHAPPY PATH" DEALING WITH

    EXCEPTIONAL CONDITIONS DEALING WITH EXCEPTIONAL CONDITIONS Nikola Poša Dutch PHP Conference 2018
  2. NIKOLA POŠA NIKOLA POŠA Software Architect and Open-Source Contributor Head

    of Data Integration @ Arbor Education Partners PHPSerbia Conference co-organizer  @nikolaposa  nikolaposa  blog.nikolaposa.in.rs
  3. (UN)HAPPY PATH (UN)HAPPY PATH a.k.a. Normal Flow "Happy path is

    a default scenario featuring no exceptional or error conditions, and comprises nothing if everything goes as expected." Wikipedia
  4. final class DbArticleRepository implements ArticleRepositoryInter { private $dbConnection; public function

    __construct(Connection $dbConnection) { $this->dbConnection = $dbConnection; } public function get(string $id) { $articleRecord = $this->dbConnection->fetchAssoc( "SELECT * FROM articles WHERE id = ?", [$id] ); return new Article($articleRecord); } }
  5. What can possibly go wrong? final class DbArticleRepository implements ArticleRepositoryInter

    { private $dbConnection; public function __construct(Connection $dbConnection) { $this->dbConnection = $dbConnection; } public function get(string $id) { $articleRecord = $this->dbConnection->fetchAssoc( "SELECT * FROM articles WHERE id = ?", [$id] ); return new Article($articleRecord); } }
  6. EXCEPTIONAL BEHAVIOUR EXCEPTIONAL BEHAVIOUR final class DbArticleRepository implements ArticleRepositoryInter {

    public function get(string $id) { $articleRecord = $this->dbConnection->fetchAssoc( "SELECT * FROM articles WHERE id = ?", [$id] ); if (false === $articleRecord) { //??? } return new Article($articleRecord); } }
  7. final class DbArticleRepository implements ArticleRepositoryInter { public function get(string $id)

    { $articleRecord = $this->dbConnection->fetchAssoc( "SELECT * FROM articles WHERE id = ?", [$id] ); if (false === $articleRecord) { return null; } return new Article($articleRecord); } }
  8. "When we return null, we are essentially creating work for

    ourselves and foisting problems upon our callers." Robert C. Martin, "Clean Code"
  9. "All it takes is one missing null check to send

    an application spinning out of control." Robert C. Martin, "Clean Code"
  10. "All it takes is one missing null check to send

    an application spinning out of control." Robert C. Martin, "Clean Code" Fatal error: Call to a member function getId() on null
  11. "If you are tempted to return null from a method,

    consider throwing an exception or returning a Special Case object instead." Robert C. Martin, "Clean Code"
  12. public function get(string $id) : Article { $articleRecord = $this->dbConnection->fetchAssoc("SELECT

    * FRO if (false === $articleRecord) { throw new ArticleNotFoundException(); } return new Article($articleRecord); } interface ArticleRepositoryInterface { /** * @param string $id * @return Article * @throws ArticleNotFoundException */ public function get(string $id) : Article; }
  13. SPECIAL CASE SPECIAL CASE "A subclass that provides special behavior

    for particular cases." Martin Fowler, "Patterns of Enterprise Application Architecture"
  14. class DeadArticle extends Article { public function getTitle() : string

    { return 'Nothing here'; } public function getContent() : string { return 'Go back to <a href="/">Home Page</a>'; } } final class DbArticleRepository implements ArticleRepositoryInter { public function get(string $id) : Article { $articleRecord = $this->dbConnection->fetchAssoc("SELECT if (false === $articleRecord) { return new DeadArticle(); } return new Article($articleRecord); } }
  15. Get rid of checks for a null value echo $article

    ? $article->getTitle() : 'Non-existing article';
  16. "Returning null from methods is bad, but passing null into

    methods is worse." Robert C. Martin, "Clean Code"
  17. class Order { public function __construct( Product $product, Customer $customer,

    DiscountInterface $discount = null ) { //... } } final class PremiumDiscount implements DiscountInterface { public function apply(float $productPrice) : float { return $productPrice * 0.5; } }
  18. class Order { //... public function getTotal() : float {

    $price = $this->product->getPrice(); if (null !== $this->discount) { $price = $this->discount->apply($price); } return $price; } }
  19. class Order { public function __construct( Product $product, Customer $customer,

    DiscountInterface $discount ) { //... } public function getTotal() : float { $price = $this->product->getPrice(); $price = $this->discount->apply($price); return $price; } }
  20. final class NoDiscount implements DiscountInterface { public function apply(float $productPrice)

    : float { return $productPrice; } } $order = new Order($product, $customer, new NoDiscount());
  21. Input validation class User { public static function fromInput(array $input)

    : User { if (!filter_var($input['email'], FILTER_VALIDATE_EMAIL)) throw new InvalidUserInputException(); } //... } }
  22. Enforce business rule class ArticleService { public function get(string $articleId)

    : Article { $articles = $this->articleRepo->findByCriteria([ 'id' => $articleId, ]); if ($articles->isEmpty()) { throw new ArticleNotFoundException(); } return $articles->first(); } }
  23. USING EXCEPTIONS USING EXCEPTIONS Should be simple as: throw new

    \Exception('Article with the ID: ' . $id . ' does not ex
  24. CUSTOM EXCEPTION TYPES CUSTOM EXCEPTION TYPES bring semantics to your

    code emphasise exception type instead of exception message allow caller to act differently based on Exception type
  25. CREATING EXCEPTION CLASSES CREATING EXCEPTION CLASSES class ArticleNotFoundException extends \RuntimeException

    { } src/ Article/ Exception/ ArticleNotFoundException.php InvalidArticleException.php
  26. COMPONENT LEVEL EXCEPTION TYPE COMPONENT LEVEL EXCEPTION TYPE Exception type

    that can be caught for any exception that comes from a certain component.
  27. COMPONENT LEVEL EXCEPTION TYPE COMPONENT LEVEL EXCEPTION TYPE Exception type

    that can be caught for any exception that comes from a certain component. namespace App\Article\Exception; interface ExceptionInterface { } class InvalidArticleException extends \DomainException implements ExceptionInterface { } class ArticleNotFoundException extends \RuntimeException implement ExceptionInterface { }
  28. use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ServerException; use GuzzleHttp\Exception\GuzzleException; //marker interface try {

    //code that can emit exceptions } catch (ClientException $ex) { //... } catch (ServerException $ex) { //... } catch (GuzzleException $ex) { //... }
  29. throw new ArticleNotFoundException(sprintf( 'Article with the ID: %s does not

    exist', $id )); throw new InsufficientPermissionsException(sprintf( 'You do not have permission to %s %s with the id: %s', $privilege, get_class($entity), $entity->getId() ));
  30. class ArticleNotFoundException extends \RuntimeException implement { public static function forId(string

    $id) { return new self(sprintf( 'Article with the ID: %s does not exist', $id )); } }
  31. class ArticleNotFoundException extends \RuntimeException implement { public static function forId(string

    $id) { return new self(sprintf( 'Article with the ID: %s does not exist', $id )); } } throw ArticleNotFoundException::forId($id);
  32. PROVIDE CONTEXT PROVIDE CONTEXT Save/expose the context of the exceptional

    condition that has occurred. final class ArticleNotFoundException extends \RuntimeException imp { private $articleId; public static function forId(string $articleId) { $ex = new self(sprintf('Article with the ID: %s does not $ex->articleId = $articleId; return $ex; } public function getArticleId() : string { return $this->articleId; } }
  33. EXCEPTION WRAPPING EXCEPTION WRAPPING Wrap exceptions thrown from a lower

    layer into appropriate exception which is meaningful for the context of the higher layer operation.
  34. EXCEPTION WRAPPING EXCEPTION WRAPPING Wrap exceptions thrown from a lower

    layer into appropriate exception which is meaningful for the context of the higher layer operation. try { $this->httpClient->request('GET', '/products'); } catch (ConnectException $ex) { throw ApiNotAvailableException::forError($ex); } class ApiNotAvailableException extends \RuntimeException implement { public static function forError(ConnectException $error) { return new self('API is not available', 0, $error); //pre } }
  35. TO SUM UP TO SUM UP create custom, cohesive Exception

    types introduce component-level exception type using Marker Interface encapsulate message formatting logic into Exception classes via Named Constructors save/provide context of the exceptional situation apply exception wrapping technique
  36. INLINE ERROR HANDLING INLINE ERROR HANDLING class ArticleController extends BaseController

    { public function viewAction(RequestInterface $request) { try { $article = $this->articleService->get($request->get(' $this->view->article = $article; } catch (ArticleNotFoundException $ex) { $this->view->error = 'Article not found'; } catch (\Exception $ex) { $this->view->error = $ex->getMessage(); } } }
  37. ERROR CONTROLLER ERROR CONTROLLER class ErrorController { public function errorAction()

    { $error = $this->getRequest()->getParam('error_handler'); switch ($error->type) { //... } } }
  38. ERROR MIDDLEWARE ERROR MIDDLEWARE class ErrorHandler implements MiddlewareInterface { public

    function __invoke(RequestInterface $request, ResponseI { try { $response = $next($request, $response); } catch (\Throwable $e) { $response = $this->handleError($e, $request); } return $response; } }
  39. CENTRAL ERROR HANDLER CENTRAL ERROR HANDLER Wraps the entire system

    to handle any uncaught exceptions from a single place.
  40. CUSTOM SOLUTION CUSTOM SOLUTION set_error_handler(function ($errno, $errstr, $errfile, $errline) if

    (! (error_reporting() & $errno)) { return; } throw new ErrorException($errstr, 0, $errno, $errfile, $errli }); set_exception_handler(function ($exception) { //log exception //display exception info });
  41. EXISTING SOLUTIONS EXISTING SOLUTIONS - stack-based error handling, pretty error

    page, handlers for different response formats (JSON, XML) Whoops
  42. EXISTING SOLUTIONS EXISTING SOLUTIONS - stack-based error handling, pretty error

    page, handlers for different response formats (JSON, XML) - different formatting strategies (HTML, JSON, CLI), logging handler, non-blocking errors Whoops BooBoo
  43. Using Whoops final class ErrorHandlerFactory { public function __invoke(ContainerInterface $container)

    { $whoops = new \Whoops\Run(); if (\Whoops\Util\Misc::isAjaxRequest()) { $whoops->pushHandler(new JsonResponseHandler()); } elseif (\Whoops\Util\Misc::isCommandLine()) { $whoops->pushHandler(new CommandLineHandler()); } else { $whoops->pushHandler(new PrettyPageHandler()); } $whoops->pushHandler(new SetHttpStatusCodeHandler()); $whoops->pushHandler(new LogHandler($container->get('Defau return $whoops; } }
  44. src/bootstrap.php public/index.php bin/app //... initialize DI container $container->get(\Whoops\Run::class)->register(); return $container;

    $container = require __DIR__ . '/../src/bootstrap.php'; $container->get('App\Web')->run(); $container = require __DIR__ . '/../src/bootstrap.php'; $container->get('App\Console')->run();
  45. Logging errors selectively final class LogHandler extends Handler { public

    function handle() { $error = $this->getException(); if ($error instanceof DontLogInterface) { return self::DONE; } $this->logger->error($error->getMessage(), [ 'exception' => $error, ]); return self::DONE; } }
  46. Setting HTTP status code final class SetHttpStatusCodeHandler extends Handler {

    public function handle() { $error = $this->getException(); $httpStatusCode = ($error instanceof ProvidesHttpStatusCo ? $error->getHttpStatusCode() : 500; $this->getRun()->sendHttpCode($httpStatusCode); return self::DONE; } }
  47. interface ProvidesHttpStatusCodeInterface { public function getHttpStatusCode() : int; } class

    ArticleNotFoundException extends \RuntimeException implement ExceptionInterface, DontLogInterface, ProvidesHttpStatusCodeInterface { //... public function getHttpStatusCode() : int { return 404; } }
  48. "The OCP (Open-Closed Principle) is one of the driving forces

    behind the architecture of systems. The goal is to make the system easy to extend without incurring a high impact of change." Robert C. Martin, "Clean Architecture"
  49. TESTING EXCEPTIONS WITH PHPUNIT TESTING EXCEPTIONS WITH PHPUNIT class ArticleTest

    extends TestCase { /** * @test */ public function it_cannot_be_commented_if_comments_are_closed { $article = new Article(); $article->closedForComments(); $this->expectException(CommentsAreClosedException::class) $article->comment(Comment::fromArray([ 'name' => 'John', 'email' => '[email protected]', 'comment' => 'test comment', ])); } }
  50. class ArticleTest extends TestCase { /** * @test */ public

    function it_can_be_commented() { $article = new Article(); $article->comment(Comment::fromArray([ 'name' => 'John', 'email' => '[email protected]', 'comment' => 'test comment', ])); $this->assertCount(1, $article->getComments()); } }
  51. /** * @test */ public function it_cannot_be_commented_if_comments_are_closed() { $article =

    Article::fromArray(['id' => '5']); $article->closedForComments(); try { $post->comment(Comment::fromArray(['comment' => 'test comm $this->fail('Exception should have been raised'); } catch (CommentsAreClosedException $ex) { $this->assertSame('Article #5 is closed for comments', $ex } }