$30 off During Our Annual Pro Sale. View Details »

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

    View Slide

  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

    View Slide

  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

    View Slide

  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);
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  8. View Slide

  9. "When we return null, we are
    essentially creating work for ourselves
    and foisting problems upon our callers."
    Robert C. Martin, "Clean Code"

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. 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;
    }

    View Slide

  14. SPECIAL CASE
    SPECIAL CASE
    "A subclass that provides special
    behavior for particular cases."
    Martin Fowler, "Patterns of Enterprise Application Architecture"

    View Slide

  15. class DeadArticle extends Article
    {
    public function getTitle() : string
    {
    return 'Nothing here';
    }
    public function getContent() : string
    {
    return 'Go back to Home Page';
    }
    }
    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);
    }
    }

    View Slide

  16. Get rid of checks for a null value
    echo $article ? $article->getTitle() : 'Non-existing article';

    View Slide

  17. Get rid of checks for a null value
    echo $article->getTitle();

    View Slide

  18. "Returning null from methods is bad,
    but passing null into methods is worse."
    Robert C. Martin, "Clean Code"

    View Slide

  19. 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;
    }
    }

    View Slide

  20. class Order
    {
    //...
    public function getTotal() : float
    {
    $price = $this->product->getPrice();
    if (null !== $this->discount) {
    $price = $this->discount->apply($price);
    }
    return $price;
    }
    }

    View Slide

  21. 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;
    }
    }

    View Slide

  22. final class NoDiscount implements DiscountInterface
    {
    public function apply(float $productPrice) : float
    {
    return $productPrice;
    }
    }
    $order = new Order($product, $customer, new NoDiscount());

    View Slide

  23. View Slide

  24. EXCEPTION VS SPECIAL CASE
    EXCEPTION VS SPECIAL CASE

    View Slide

  25. Input validation
    class User
    {
    public static function fromInput(array $input) : User
    {
    if (!filter_var($input['email'], FILTER_VALIDATE_EMAIL))
    throw new InvalidUserInputException();
    }
    //...
    }
    }

    View Slide

  26. 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();
    }
    }

    View Slide

  27. USING EXCEPTIONS
    USING EXCEPTIONS
    Should be simple as:
    throw new \Exception('Article with the ID: ' . $id . ' does not ex

    View Slide

  28. View Slide

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

    View Slide

  30. STRUCTURING EXCEPTIONS
    STRUCTURING EXCEPTIONS
    src/
    Article/
    Exception/
    Article.php
    ArticleCollection.php
    ArticleRepositoryInterface.php
    Author/
    Exception/
    Author.php
    AuthorRepositoryInterface.php

    View Slide

  31. CREATING EXCEPTION CLASSES
    CREATING EXCEPTION CLASSES
    class ArticleNotFoundException extends \RuntimeException
    {
    }
    src/
    Article/
    Exception/
    ArticleNotFoundException.php
    InvalidArticleException.php

    View Slide

  32. COHESIVE EXCEPTION CLASSES
    COHESIVE EXCEPTION CLASSES

    View Slide

  33. COHESIVE EXCEPTION CLASSES
    COHESIVE EXCEPTION CLASSES
    class ArticleException extends Exception
    {
    }

    View Slide

  34. COHESIVE EXCEPTION CLASSES
    COHESIVE EXCEPTION CLASSES
    class ArticleNotFoundException extends \RuntimeException
    {
    }
    class InvalidArticleException extends \DomainException
    {
    }

    View Slide

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

    View Slide

  36. 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
    {
    }

    View Slide

  37. 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) {
    //...
    }

    View Slide

  38. FORMATTING EXCEPTION MESSAGES
    FORMATTING EXCEPTION MESSAGES
    Encapsulate noisy message formatting logic into the
    Exception classes themselves.

    View Slide

  39. throw new ArticleNotFoundException(sprintf(
    'Article with the ID: %s does not exist',
    $id
    ));

    View Slide

  40. 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()
    ));

    View Slide

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

    View Slide

  42. 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);

    View Slide

  43. PROVIDE CONTEXT
    PROVIDE CONTEXT
    Save/expose the context of the exceptional condition
    that has occurred.

    View Slide

  44. 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;
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  48. ERROR HANDLING
    ERROR HANDLING

    View Slide

  49. CHALLENGES
    CHALLENGES
    versatility
    user experience
    security
    monitoring / reporting

    View Slide

  50. 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();
    }
    }
    }

    View Slide

  51. ERROR CONTROLLER
    ERROR CONTROLLER
    class ErrorController
    {
    public function errorAction()
    {
    $error = $this->getRequest()->getParam('error_handler');
    switch ($error->type) {
    //...
    }
    }
    }

    View Slide

  52. 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;
    }
    }

    View Slide

  53. CHALLENGES
    CHALLENGES
    versatility
    user experience
    security
    monitoring / reporting

    View Slide

  54. CENTRAL ERROR HANDLER
    CENTRAL ERROR HANDLER
    Wraps the entire system to handle any uncaught
    exceptions from a single place.

    View Slide

  55. 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
    });

    View Slide

  56. EXISTING SOLUTIONS
    EXISTING SOLUTIONS

    View Slide

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

    View Slide

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

    View Slide

  59. 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;
    }
    }

    View Slide

  60. 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();

    View Slide

  61. 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;
    }
    }

    View Slide

  62. class ArticleNotFoundException extends \RuntimeException implement
    ExceptionInterface,
    DontLogInterface
    {
    //...
    }

    View Slide

  63. 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;
    }
    }

    View Slide

  64. interface ProvidesHttpStatusCodeInterface
    {
    public function getHttpStatusCode() : int;
    }
    class ArticleNotFoundException extends \RuntimeException implement
    ExceptionInterface,
    DontLogInterface,
    ProvidesHttpStatusCodeInterface
    {
    //...
    public function getHttpStatusCode() : int
    {
    return 404;
    }
    }

    View Slide

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

    View Slide

  66. TEST EXCEPTIONAL BEHAVIOUR
    TEST EXCEPTIONAL BEHAVIOUR
    a.k.a. Negative Testing

    View Slide

  67. 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',
    ]));
    }
    }

    View Slide

  68. ARRANGE-ACT-ASSERT
    ARRANGE-ACT-ASSERT
    1. initialize SUT/prepare inputs
    2. perform action
    3. verify outcomes

    View Slide

  69. 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());
    }
    }

    View Slide

  70. /**
    * @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
    }
    }

    View Slide

  71. FEEDBACK
    FEEDBACK
    Please rate this talk at: joind.in/talk/889aa

    View Slide

  72. THANK YOU!
    THANK YOU!
    Nikola Poša
    [email protected]
     @nikolaposa

    View Slide

  73. View Slide