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

PHP CE 2017 - Journey Through "Unhappy Path"

PHP CE 2017 - Journey Through "Unhappy Path"

Developers naturally gravitate towards the "happy path" - default scenario in application execution in which everything works as expected, consequently neglecting exceptions and error handling aspect of a software development. This talk provides a comprehensive insight into widely adopted practices and patterns for dealing with exceptional and error conditions. You will learn about writing and organizing custom exception classes, formatting exception messages, component-level exceptions technique, and much more. In a word - Exceptions cheat-sheet. Once thrown, the exceptions should be caught, so you'll also be introduced with different error handling strategies and concepts.

Nikola Poša

November 04, 2017
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
    PHP Central Europe 2017

    View Slide

  2. NIKOLA POŠA
    NIKOLA POŠA
    Web developer and open-source contributor
    Head of Data Integration @ Arbor Labs
    PHPSerbia Conference co-organizer
     @nikolaposa
     nikolaposa
     blog.nikolaposa.in.rs

    View Slide

  3. DON'T WORRY, LET'S START WITH HAPPY
    DON'T WORRY, LET'S START WITH HAPPY
    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 ArticleRepositoryInterface
    {
    private $dbConnection;
    public function __construct(Connection $dbConnection)
    {
    $this->dbConnection = $dbConnection;
    }
    public function findById(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 ArticleRepositoryInterface
    {
    private $dbConnection;
    public function __construct(Connection $dbConnection)
    {
    $this->dbConnection = $dbConnection;
    }
    public function findById(string $id)
    {
    $articleRecord = $this->dbConnection->fetchAssoc(
    "SELECT * FROM articles WHERE id = ?",
    [$id]
    );
    return new Article($articleRecord);
    }

    View Slide

  6. EXCEPTIONAL BEHAVIOUR
    EXCEPTIONAL BEHAVIOUR
    breaks normal ow after some exceptional condition is
    met
    changes the normal ow of program execution
    final class DbArticleRepository implements ArticleRepositoryInterface
    {
    public function findById(string $id)
    {
    $articleRecord = $this->dbConnection->fetchAssoc(
    "SELECT * FROM articles WHERE id = ?",
    [$id]
    );
    if (false === $articleRecord) {
    //???
    }
    return new Article($articleRecord);
    }
    }

    View Slide

  7. The easiest solution is not always the best
    final class DbArticleRepository implements ArticleRepositoryInterface
    {
    public function findById(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. Throw meaningful Exceptions as part of your API contract
    interface ArticleRepositoryInterface
    {
    /**
    * @param string $id
    * @return Article
    * @throws ArticleNotFoundException
    */
    public function findById(string $id) : Article;
    }
    public function findById(string $id) : Article
    {
    $articleRecord = $this->dbConnection->fetchAssoc("SELECT * FROM artic
    if (false === $articleRecord) {
    throw new ArticleNotFoundException();
    }
    return new Article($articleRecord);
    }

    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. Create a Special Case object that exhibits the default
    behavior
    class DeadArticle extends Article
    {
    public function getTitle() : string
    {
    return 'Nothing here';
    }
    public function getContent() : string
    {
    return 'Go back to Home Page';
    }
    }

    View Slide

  16. Return a Special Case that has the same interface as what the
    caller expects
    final class DbArticleRepository implements ArticleRepositoryInterface
    {
    public function findById(string $id) : Article
    {
    $articleRecord = $this->dbConnection->fetchAssoc(
    "SELECT * FROM articles WHERE id = ?",
    [$id]
    );
    if (false === $articleRecord) {
    return new DeadArticle();
    }
    return new Article($articleRecord);
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  20. Passing null is possible if the code allows it

    View Slide

  21. Passing null is possible if the code allows it
    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

  22. Nullable parameters force us to check for a null value
    class Order
    {
    //...
    public function getTotal() : float
    {
    $price = $this->product->getPrice();
    if (null !== $this->discount) {
    $price = $this->discount->apply($price);
    }
    return $price;
    }
    }

    View Slide

  23. View Slide

  24. Make all the parameters required; get rid of null checks
    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

  25. Pass a Special Case object that has the default behavior
    final class NoDiscount implements DiscountInterface
    {
    public function apply(float $productPrice) : float
    {
    return $productPrice;
    }
    }
    $order = new Order($product, $customer, new NoDiscount());

    View Slide

  26. DO NOT MESS WITH NULL
    DO NOT MESS WITH NULL

    View Slide

  27. DO NOT MESS WITH NULL
    DO NOT MESS WITH NULL
    do not return null from methods

    View Slide

  28. DO NOT MESS WITH NULL
    DO NOT MESS WITH NULL
    do not return null from methods
    do not accept null in methods

    View Slide

  29. DO NOT MESS WITH NULL
    DO NOT MESS WITH NULL
    do not return null from methods
    do not accept null in methods
    null introduces ambiguity

    View Slide

  30. DO NOT MESS WITH NULL
    DO NOT MESS WITH NULL
    do not return null from methods
    do not accept null in methods
    null introduces ambiguity
    ood of repeated checks for a null value

    View Slide

  31. DO NOT MESS WITH NULL
    DO NOT MESS WITH NULL
    do not return null from methods
    do not accept null in methods
    null introduces ambiguity
    ood of repeated checks for a null value
    null increases risk of fatal errors

    View Slide

  32. SPECIAL CASE VS EXCEPTION
    SPECIAL CASE VS EXCEPTION
    Special Case can remedy most of exceptional conditions
    throwing an exception typically aims to emphasize the
    violated business logic rule
    Exceptions stop program execution

    View Slide

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

    View Slide

  34. Data existence validation
    class ArticleService
    {
    public function get(string $articleId) : Article
    {
    $article = $this->articleRepo->findById($articleId);
    if (! $article->exists()) {
    throw new ArticleNotFoundException();
    }
    return $article;
    }
    }

    View Slide

  35. USING EXCEPTIONS
    USING EXCEPTIONS
    Should be simple as:
    throw new \Exception('Article with the ID: ' . $id . ' does not exist');

    View Slide

  36. View Slide

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

  38. STRUCTURING EXCEPTIONS
    STRUCTURING EXCEPTIONS
    keep exceptions under Exception namespace
    multiple Exception namespaces for every
    module/component
    src/
    Article/
    Exception/
    Article.php
    ArticleCollection.php
    ArticleRepositoryInterface.php
    Author/
    Exception/
    Author.php
    AuthorRepositoryInterface.php

    View Slide

  39. CREATING EXCEPTION CLASSES
    CREATING EXCEPTION CLASSES
    use descriptive names
    use Exception as a suf x by "convention"
    inherit from SPL exception classes
    choosing a parent exception class is about semantic
    meaning you want to achieve
    class ArticleNotFoundException extends \RuntimeException
    {
    }
    src/
    Article/
    Exception/
    ArticleNotFoundException.php
    InvalidArticleException.php

    View Slide

  40. COHESIVE EXCEPTION CLASSES
    COHESIVE EXCEPTION CLASSES
    Exception classes should conform SRP.

    View Slide

  41. COHESIVE EXCEPTION CLASSES
    COHESIVE EXCEPTION CLASSES
    Exception classes should conform SRP.
    class ArticleException extends Exception
    {
    }

    View Slide

  42. COHESIVE EXCEPTION CLASSES
    COHESIVE EXCEPTION CLASSES
    Exception classes should conform SRP.
    class ArticleException extends Exception
    {
    }

    View Slide

  43. COHESIVE EXCEPTION CLASSES
    COHESIVE EXCEPTION CLASSES
    Exception classes should conform SRP.
    class ArticleNotFoundException extends \RuntimeException
    {
    }
    class InvalidArticleException extends \DomainException
    {
    }

    View Slide

  44. COMPONENT LEVEL EXCEPTION TYPE
    COMPONENT LEVEL EXCEPTION TYPE
    exception type that can be caught for any exception that
    comes from a certain component
    accomplished by using Marker Interface pattern
    best practice for library code

    View Slide

  45. COMPONENT LEVEL EXCEPTION TYPE
    COMPONENT LEVEL EXCEPTION TYPE
    exception type that can be caught for any exception that
    comes from a certain component
    accomplished by using Marker Interface pattern
    best practice for library code
    namespace App\Article\Exception;
    interface ExceptionInterface
    {
    }
    class ArticleNotFoundException extends \RuntimeException implements
    ExceptionInterface
    {
    }

    View Slide

  46. Caller gets any number of opportunities to catch an exception
    that comes from a given component:

    View Slide

  47. Caller gets any number of opportunities to catch an exception
    that comes from a given component:
    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

  48. FORMATTING EXCEPTION MESSAGES
    FORMATTING EXCEPTION MESSAGES
    Formatting logic in a place where exceptions are thrown
    results in distracting and noisy code.
    throw new ArticleNotFoundException(sprintf(
    'Article with the ID: %s does not exist',
    $id
    ));

    View Slide

  49. FORMATTING EXCEPTION MESSAGES
    FORMATTING EXCEPTION MESSAGES
    Formatting logic in a place where exceptions are thrown
    results in distracting and noisy code.
    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

  50. Encapsulate formatting logic into Exception classes by using
    Named Constructors technique.
    class ArticleNotFoundException extends \RuntimeException implements Excep
    {
    public static function forId(string $id)
    {
    return new self(sprintf(
    'Article with the ID: %s does not exist',
    $id
    ));
    }
    }

    View Slide

  51. Throwing an exception becomes more readable and
    expressive, as in:
    throw ArticleNotFoundException::forId($id);

    View Slide

  52. PROVIDE CONTEXT
    PROVIDE CONTEXT
    Take advantage of using Named Constructors to save the
    context of the exceptional situation that has occurred.

    View Slide

  53. PROVIDE CONTEXT
    PROVIDE CONTEXT
    Take advantage of using Named Constructors to save the
    context of the exceptional situation that has occurred.
    final class ArticleNotFoundException extends \RuntimeException implements
    {
    private $articleId;
    public static function forId(string $articleId)
    {
    $ex = new self(sprintf('Article with the ID: %s does not exist',
    $ex->articleId = $articleId;
    return $ex;
    }
    public function getArticleId() : string
    {
    return $this->articleId;
    }
    }

    View Slide

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

    View Slide

  55. EXCEPTION WRAPPING
    EXCEPTION WRAPPING
    Used for wrapping 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 implements Excep
    {
    public static function forError(ConnectException $error)
    {
    return new self('API is not available', 0, $error); //preserve pr
    }
    }

    View Slide

  56. TO SUM UP
    TO SUM UP

    View Slide

  57. TO SUM UP
    TO SUM UP
    create custom, cohesive Exception types

    View Slide

  58. TO SUM UP
    TO SUM UP
    create custom, cohesive Exception types
    introduce component-level exception type using Marker
    Interface

    View Slide

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

    View Slide

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

    View Slide

  61. 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 when writing
    libraries

    View Slide

  62. ERROR HANDLING
    ERROR HANDLING
    Process of responding to and recovering from error
    conditions.

    View Slide

  63. CHALLENGES
    CHALLENGES
    covers different ways of using application
    provide just enough error details to be helpful for users
    suppress the error details that are not intended for
    users
    collect as much information as possible that will bene t
    maintainers

    View Slide

  64. INLINE ERROR HANDLING
    INLINE ERROR HANDLING
    class ArticleController extends BaseController
    {
    public function viewAction(RequestInterface $request)
    {
    try {
    $article = $this->articleService->get($request->get('id'));
    $this->view->article = $article;
    } catch (ArticleNotFoundException $ex) {
    $this->view->error = 'Article not found';
    } catch (\Exception $ex) {
    $this->view->error = $ex->getMessage();
    }
    }
    }

    View Slide

  65. INLINE ERROR HANDLING
    INLINE ERROR HANDLING
    Usually leads to maintenance dif culties and code
    duplication.
    class ArticleController extends BaseController
    {
    public function viewAction(RequestInterface $request)
    {
    try {
    $article = $this->articleService->get($request->get('id'));
    $this->view->article = $article;
    } catch (ArticleNotFoundException $ex) {
    $this->view->error = 'Article not found';
    } catch (\Exception $ex) {
    $this->view->error = $ex->getMessage();
    }
    }
    }

    View Slide

  66. ERROR CONTROLLER
    ERROR CONTROLLER
    offered by MVC frameworks
    all the errors get forwarded to it
    allows for handling errors from a single place

    View Slide

  67. ERROR MIDDLEWARE
    ERROR MIDDLEWARE
    offered by middleware frameworks
    set as the outermost middleware
    allows for handling errors from a single place
    class ErrorHandler implements MiddlewareInterface
    {
    public function __invoke(RequestInterface $request, ResponseInterface
    {
    try {
    $response = $next($request, $response);
    } catch (\Throwable $e) {
    $response = $this->handleError($e, $request);
    }
    return $response;
    }
    }

    View Slide

  68. All these solutions ignore the fact that application may
    support to be invoked through different ports (web, CLI, API)

    View Slide

  69. CENTRAL ERROR HANDLER
    CENTRAL ERROR HANDLER

    View Slide

  70. CENTRAL ERROR HANDLER
    CENTRAL ERROR HANDLER
    wraps the entire system to handle any uncaught
    exceptions

    View Slide

  71. CENTRAL ERROR HANDLER
    CENTRAL ERROR HANDLER
    wraps the entire system to handle any uncaught
    exceptions
    unique and uniform solution for different ways of using
    application

    View Slide

  72. CENTRAL ERROR HANDLER
    CENTRAL ERROR HANDLER
    wraps the entire system to handle any uncaught
    exceptions
    unique and uniform solution for different ways of using
    application
    framework-agnostic, reusable between projects

    View Slide

  73. Build your own, by overriding PHP's default error handler...
    set_error_handler(function ($errno, $errstr, $errfile, $errline) {
    if (! (error_reporting() & $errno)) {
    return;
    }
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
    });
    set_exception_handler(function ($exception) {
    //log exception
    //display exception info
    });

    View Slide

  74. ... or use some of the existing solutions

    View Slide

  75. ... or use some of the existing solutions
    - stack-based error handling, pretty error page,
    handlers for different response formats (JSON, XML)
    Whoops

    View Slide

  76. ... or use some of the 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

  77. 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('DefaultLogge
    return $whoops;

    View Slide

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

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

  80. class ArticleNotFoundException extends \RuntimeException implements
    ExceptionInterface,
    DontLogInterface
    {
    //...
    }

    View Slide

  81. Setting HTTP status code
    final class SetHttpStatusCodeHandler extends Handler
    {
    public function handle()
    {
    $error = $this->getException();
    $httpStatusCode = ($error instanceof ProvidesHttpStatusCodeInterf
    ? $error->getHttpStatusCode()
    : 500;
    $this->getRun()->sendHttpCode($httpStatusCode);
    return self::DONE;
    }
    }

    View Slide

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

    View Slide

  83. New kid on the block
    github.com/wecodein/error-handling

    View Slide

  84. TEST EXCEPTIONAL BEHAVIOUR
    TEST EXCEPTIONAL BEHAVIOUR
    Negative Testing
    ensure application is capable of handling improper user
    behaviour

    View Slide

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

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

    View Slide

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

  88. /**
    * @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 comment']))
    $this->fail('Exception should have been raised');
    } catch (CommentsAreClosedException $ex) {
    $this->assertSame('Article #5 is closed for comments', $ex->getMe
    }
    }

    View Slide

  89. Defensive programming using Offensive countermeasures

    View Slide

  90. FEEDBACK
    FEEDBACK
    Please rate this talk at: joind.in/talk/45452

    View Slide

  91. THANK YOU!
    THANK YOU!
    QUESTIONS?
    QUESTIONS?
    Nikola Poša
    [email protected]
     @nikolaposa

    View Slide