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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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"

    View full-size slide

  12. 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 full-size slide

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

    View full-size slide

  14. 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 full-size slide

  15. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  19. Passing null is possible if the code allows it

    View full-size slide

  20. 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 full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

  23. 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 full-size slide

  24. DO NOT MESS WITH NULL
    DO NOT MESS WITH NULL

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  27. 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 full-size slide

  28. 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 full-size 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
    ood of repeated checks for a null value
    null increases risk of fatal errors

    View full-size slide

  30. 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 full-size slide

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

    View full-size slide

  32. 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 full-size slide

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

    View full-size slide

  34. 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 full-size slide

  35. 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 full-size slide

  36. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  41. 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 full-size slide

  42. 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 full-size slide

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

    View full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  50. 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 full-size slide

  51. 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 full-size slide

  52. 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 full-size slide

  53. TO SUM UP
    TO SUM UP

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  56. 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 full-size slide

  57. 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 full-size slide

  58. 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 full-size slide

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

    View full-size slide

  60. 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 full-size slide

  61. 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 full-size slide

  62. 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 full-size slide

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

    View full-size slide

  64. 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 full-size slide

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

    View full-size slide

  66. CENTRAL ERROR HANDLER
    CENTRAL ERROR HANDLER

    View full-size slide

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

    View full-size slide

  68. 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 full-size slide

  69. 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 full-size slide

  70. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  73. ... 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 full-size slide

  74. 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 full-size slide

  75. 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 full-size slide

  76. 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 full-size slide

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

    View full-size slide

  78. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  82. 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 full-size slide

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

    View full-size slide

  84. 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 full-size slide

  85. /**
    * @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 full-size slide

  86. Defensive programming using Offensive countermeasures

    View full-size slide

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

    View full-size slide

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

    View full-size slide