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

PHP fwdays '19 - Handling Exceptional Conditions with Grace and Style

PHP fwdays '19 - Handling Exceptional Conditions with Grace and Style

Programmers naturally give more attention to a “happy path” - default scenario in application execution in which everything works as expected, therefore neglecting the opposite way things can go. Topics such as dealing with exceptional conditions, use of exceptions, error handling seem to have been insufficiently studied, and it is very difficult to find useful explanations and tutorials online.

This talk is an in-depth study about practices for dealing with exceptional conditions that promote code that is clean, consistent and convenient to work with. Special attention is given to applicable best practices for managing exceptions in a proper way, including formatting exception messages using named constructors, component-level exception type, exception wrapping.

To make the story complete, second part of the talk introduces a solution for establishing central error handling system that makes this critical aspect of the software stable, unambiguous and easy to maintain.

At the very end, some attention is given to testing exceptions and ways for keeping test code consistent and readable.

Nikola Poša

June 01, 2019
Tweet

More Decks by Nikola Poša

Other Decks in Programming

Transcript

  1. HANDLING EXCEPTIONAL CONDITIONS
    HANDLING EXCEPTIONAL CONDITIONS
    WITH GRACE AND STYLE
    WITH GRACE AND STYLE
    Nikola Poša · @nikolaposa

    View full-size slide

  2. Я радий бути тут
    Я радий бути тут

    View full-size slide

  3. Я радий бути тут
    Я радий бути тут
    ABOUT ME
    ABOUT ME
    Software Architect specializing in PHP-based
    applications
    Lead Architect at Arbor Education Partners
    PHP Serbia Conference co-organizer
     @nikolaposa
     blog.nikolaposa.in.rs

    View full-size slide

  4. AGENDA
    AGENDA
    Approaches for dealing with exceptional
    conditions
    Set of applicable best practices for managing
    exceptions in a proper way
    Solution for establishing central error handling
    system
    Few tips for testing exceptions

    View full-size slide

  5. HAPPY PATH
    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 full-size slide

  6. $user = $userRepository->get('John');
    if ($user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }
    1
    2
    3
    4
    5

    View full-size slide

  7. $user = $userRepository->get('John');
    if ($user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }
    1
    2
    3
    4
    5
    if ($user->isSubscribedTo($notification)) {
    Fatal error: Call to a member function isSubscribedTo() on null
    $user = $userRepository->get('John');
    1
    2
    3
    $notifier->notify($user, $notification);
    4
    }
    5
    6
    7
    8

    View full-size slide

  8. $user = $userRepository->get('John');
    if ($user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }
    1
    2
    3
    4
    5
    $user = $userRepository->get('John');
    if (null !== $user && $user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }
    1
    2
    3
    4
    5

    View full-size slide

  9. Null checks ood
    if (null !== $user) {
    if (null !== $todo) {
    if (null !== $notification) {
    $user = $userRepository->get('John');
    1
    2
    3
    $todo = $todoRepository->get('Book flights');
    4
    5
    6
    $notification = TodoReminder::from($todo, $user);
    7
    8
    9
    if ($user->isSubscribedTo($notification)) {
    10
    $notifier->notify($user, $notification);
    11
    }
    12
    }
    13
    }
    14
    }
    15

    View full-size slide

  10. Vague Interface
    interface UserRepository
    {
    public function get(string $username): ?User;
    }

    View full-size slide

  11. Vague Interface
    interface UserRepository
    {
    public function get(string $username): ?User;
    }
    interface UserRepository
    {
    /**
    * @param UserId $id
    * @return User|bool User instance or boolean false if User w
    */
    public function get(string $username);
    }

    View full-size slide

  12. DO NOT MESS WITH
    DO NOT MESS WITH
    NULL
    NULL

    View full-size slide

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

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

  15. THROW EXCEPTION
    THROW EXCEPTION
    interface UserRepository
    * @throws UserNotFound
    public function get(string $username): User;
    1
    {
    2
    /**
    3
    * @param string $username
    4
    5
    * @return User
    6
    */
    7
    8
    }
    9
    throw new UserNotFound();
    final class DbUserRepository implements UserRepository
    1
    {
    2
    public function get(string $username): User
    3
    {
    4
    $userRecord = $this->db->fetchAssoc('SELECT * FROM use
    5
    6
    if (false === $userRecord) {
    7
    8
    }
    9
    10
    return User::fromArray($userRecord);
    11
    }
    12
    }
    13

    View full-size slide

  16. interface UserRepository
    {
    @throws UserNotFound
    public function get(string $username): User;
    }

    View full-size slide

  17. try {
    $user = $userRepository->get($username);
    if ($user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }
    } catch (UserNotFound $ex) {
    $this->logger->notice('User was not found', ['username' => $u
    }

    View full-size slide

  18. try {
    $user = $userRepository->get($username);
    if ($user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }
    } catch (UserNotFound $ex) {
    $this->logger->notice('User was not found', ['username' => $u
    }
    try {
    $this->notifyUserIfSubscribed($username, $notification);
    } catch (\Throwable $ex) {
    $this->log($ex);
    }

    View full-size slide

  19. SPECIAL CASE
    SPECIAL CASE
    a.k.a. Null Object
    A subclass that provides special
    behavior for particular cases.
    Martin Fowler, "Patterns of Enterprise Application
    Architecture"

    View full-size slide

  20. class UnknownUser extends User
    {
    public function username(): string
    {
    return 'unknown';
    }
    public function isSubscribedTo(Notification $notification): b
    {
    return false;
    }
    }
    return new UnknownUser();
    final class DbUserRepository implements UserRepository
    1
    {
    2
    public function get(string $username): User
    3
    {
    4
    $userRecord = $this->db->fetchAssoc('SELECT * FROM use
    5
    6
    if (false === $userRecord) {
    7
    8
    }
    9
    10
    return User::fromArray($userRecord);
    11
    }
    12
    }
    13

    View full-size slide

  21. $user = $userRepository->get('John');
    if ($user->isSubscribedTo($notification)) {
    $notifier->notify($user, $notification);
    }

    View full-size slide

  22. Checking for Special Case

    View full-size slide

  23. Checking for Special Case
    if ($user instanceof UnknownUser) {
    //do something
    }

    View full-size slide

  24. Checking for Special Case
    if ($user instanceof UnknownUser) {
    //do something
    }
    if ($user === User::unknown()) {
    //do something
    }

    View full-size slide

  25. Special Case factory
    class User
    {
    public static function unknown(): User
    {
    static $unknownUser = null;
    if (null === $unknownUser) {
    $unknownUser = new UnknownUser();
    }
    return $unknownUser;
    }
    }

    View full-size slide

  26. Special Case object as private nested class
    class User
    {
    public static function unknown(): User
    {
    static $unknownUser = null;
    if (null === $unknownUser) {
    $unknownUser = new class extends User {
    public function username(): string
    {
    return 'unknown';
    }
    public function isSubscribedTo(Notification $noti
    {
    return false;
    }
    };
    }
    return $unknownUser;
    }
    }

    View full-size slide

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

    View full-size slide

  28. class Order
    {
    public function __construct(
    Product $product,
    Customer $customer,
    ?Discount $discount
    ) {
    $this->product = $product;
    $this->customer = $customer;
    $this->discount = $discount;
    }
    }
    final class PremiumDiscount implements Discount
    {
    public function apply(float $productPrice): float
    {
    return $productPrice * 0.5;
    }
    }

    View full-size slide

  29. ?Discount $discount
    if (null !== $this->discount) {
    }
    class Order
    1
    {
    2
    public function __construct(
    3
    Product $product,
    4
    Customer $customer,
    5
    6
    ) {
    7
    $this->product = $product;
    8
    $this->customer = $customer;
    9
    $this->discount = $discount;
    10
    }
    11
    12
    public function total(): float
    13
    {
    14
    $price = $this->product->getPrice();
    15
    16
    17
    $price = $this->discount->apply($price);
    18
    19
    20
    return $price;
    21
    }
    22
    }
    23

    View full-size slide

  30. Discount $discount
    class Order
    1
    {
    2
    public function __construct(
    3
    Product $product,
    4
    Customer $customer,
    5
    6
    ) {
    7
    $this->product = $product;
    8
    $this->customer = $customer;
    9
    $this->discount = $discount;
    10
    }
    11
    }
    12

    View full-size slide

  31. Discount $discount
    class Order
    1
    {
    2
    public function __construct(
    3
    Product $product,
    4
    Customer $customer,
    5
    6
    ) {
    7
    $this->product = $product;
    8
    $this->customer = $customer;
    9
    $this->discount = $discount;
    10
    }
    11
    }
    12
    final class NoDiscount implements Discount
    {
    public function apply(float $productPrice): float
    {
    return $productPrice;
    }
    }
    $order = new Order($product, $customer, new NoDiscount());

    View full-size slide

  32. EXCEPTION VS SPECIAL CASE
    EXCEPTION VS SPECIAL CASE
    Special Case as default strategy instead of
    optional parameters
    Exceptions break normal ow to split business
    logic from error handling
    Special Case handles exceptional behaviour
    Exception emphasizes violated business rule

    View full-size slide

  33. USING EXCEPTIONS
    USING EXCEPTIONS

    View full-size slide

  34. Should be simple as:
    throw new \Exception('User was not found by username: ' . $usernam

    View full-size slide

  35. Should be simple as:
    throw new \Exception('User was not found by username: ' . $usernam

    View full-size slide

  36. CUSTOM EXCEPTION TYPES
    CUSTOM EXCEPTION TYPES
    bring semantics to your code
    emphasise exception type instead of exception
    message
    allow caller to act di erently based on
    Exception type

    View full-size slide

  37. STRUCTURING EXCEPTIONS
    STRUCTURING EXCEPTIONS
    src/
    Todo/
    Exception/
    Model/
    User/
    Exception/
    Model/

    View full-size slide

  38. CREATING EXCEPTION CLASSES
    CREATING EXCEPTION CLASSES
    src/
    User/
    Exception/
    InvalidUsername.php
    UsernameAlreadyTaken.php
    UserNotFound.php
    final class UserNotFound extends \Exception
    {
    }

    View full-size slide

  39. use App\User\Exception\UserNotFoundException;
    try {
    throw new UserNotFoundException();
    } catch (UserNotFoundException $exception) {
    }

    View full-size slide

  40. use App\User\Exception\UserNotFoundException;
    try {
    throw new UserNotFoundException();
    } catch (UserNotFoundException $exception) {
    }
    use App\User\Exception\UserNotFound;
    try {
    throw new UserNotFound();
    } catch (UserNotFound $exception) {
    }

    View full-size slide

  41. COMPONENT LEVEL EXCEPTION
    COMPONENT LEVEL EXCEPTION
    TYPE
    TYPE

    View full-size slide

  42. COMPONENT LEVEL EXCEPTION
    COMPONENT LEVEL EXCEPTION
    TYPE
    TYPE
    namespace App\User\Exception;
    interface ExceptionInterface
    {
    }
    final class InvalidUsername extends \Exception implements
    ExceptionInterface
    {
    }
    final class UserNotFound extends \Exception implements
    ExceptionInterface
    {
    }

    View full-size slide

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

  44. FORMATTING EXCEPTION
    FORMATTING EXCEPTION
    MESSAGES
    MESSAGES

    View full-size slide

  45. FORMATTING EXCEPTION
    FORMATTING EXCEPTION
    MESSAGES
    MESSAGES
    throw new UserNotFound(sprintf(
    'User was not found by username: %s',
    $username
    ));

    View full-size slide

  46. FORMATTING EXCEPTION
    FORMATTING EXCEPTION
    MESSAGES
    MESSAGES
    throw new UserNotFound(sprintf(
    'User was not found by username: %s',
    $username
    ));
    throw new InsufficientPermissions(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 into Exception classes
    final class UserNotFound extends \Exception implements ExceptionI
    {
    public static function byUsername(string $username): self
    {
    return new self(sprintf(
    'User was not found by username: %s',
    $username
    ));
    }
    }

    View full-size slide

  48. Named Constructors communicate the intent
    throw UserNotFound::byUsername($username);

    View full-size slide

  49. Coherent exceptional conditions
    throw TodoNotOpen::triedToSetDeadline($deadline, $this->status);
    throw TodoNotOpen::triedToMarkAsCompleted($this->status);

    View full-size slide

  50. PROVIDE CONTEXT
    PROVIDE CONTEXT

    View full-size slide

  51. PROVIDE CONTEXT
    PROVIDE CONTEXT
    final class UserNotFound extends \Exception implements ExceptionI
    {
    private string $username;
    public static function byUsername(string $username): self
    {
    $ex = new self(sprintf('User was not found by username: %
    $ex->username = $username;
    return $ex;
    }
    public function username(): string
    {
    return $this->username;
    }
    }

    View full-size slide

  52. EXCEPTION WRAPPING
    EXCEPTION WRAPPING

    View full-size slide

  53. EXCEPTION WRAPPING
    EXCEPTION WRAPPING
    try {
    return $this->toResult(
    $this->httpClient->request('GET', '/users')
    );
    } catch (ConnectException $ex) {
    throw ApiNotAvailable::reason($ex);
    }
    1
    2
    3
    4
    5
    6
    7

    View full-size slide

  54. EXCEPTION WRAPPING
    EXCEPTION WRAPPING
    try {
    return $this->toResult(
    $this->httpClient->request('GET', '/users')
    );
    } catch (ConnectException $ex) {
    throw ApiNotAvailable::reason($ex);
    }
    1
    2
    3
    4
    5
    6
    7
    final class ApiNotAvailable extends \Exception implements Exce
    {
    public static function reason(ConnectException $error): se
    {
    return new self(
    'API is not available',
    0,
    $error //preserve previous error
    );
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    View full-size slide

  55. EXCEPTION WRAPPING
    EXCEPTION WRAPPING
    try {
    return $this->toResult(
    $this->httpClient->request('GET', '/users')
    );
    } catch (ConnectException $ex) {
    throw ApiNotAvailable::reason($ex);
    }
    1
    2
    3
    4
    5
    6
    7
    final class ApiNotAvailable extends \Exception implements Exce
    {
    public static function reason(ConnectException $error): se
    {
    return new self(
    'API is not available',
    0,
    $error //preserve previous error
    );
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $error //preserve previous error
    final class ApiNotAvailable extends \Exception implements Exce
    1
    {
    2
    public static function reason(ConnectException $error): se
    3
    {
    4
    return new self(
    5
    'API is not available',
    6
    0,
    7
    8
    );
    9
    }
    10
    }
    11

    View full-size slide

  56. RETROSPECT
    RETROSPECT
    1. create custom, cohesive Exception types
    2. introduce component-level exception type
    3. use Named Constructors to encapsulate
    message formatting and express the intent
    4. capture & provide the context of the
    exceptional condition
    5. apply exception wrapping to rethrow more
    informative exception

    View full-size slide

  57. ERROR HANDLING
    ERROR HANDLING

    View full-size slide

  58. WHEN TO CATCH EXCEPTIONS?
    WHEN TO CATCH EXCEPTIONS?

    View full-size slide

  59. WHEN TO CATCH EXCEPTIONS?
    WHEN TO CATCH EXCEPTIONS?
    Do NOT catch exceptions
    unless you can handle the problem so that the
    application continues to work

    View full-size slide

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

    View full-size slide

  61. CHALLENGES
    CHALLENGES
    user experience
    security
    logging

    View full-size slide

  62. CHALLENGES
    CHALLENGES
    user experience
    security
    logging
    adaptability

    View full-size slide

  63. EXISTING SOLUTIONS
    EXISTING SOLUTIONS

    View full-size slide

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

    View full-size slide

  65. EXISTING SOLUTIONS
    EXISTING SOLUTIONS
    - stack-based error handling, pretty
    error page, handlers for di erent response
    formats (JSON, XML)
    - di erent formatting strategies
    (HTML, JSON, CLI), logging handler, non-
    blocking errors
    Whoops
    BooBoo

    View full-size slide

  66. 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('Logg
    return $whoops;
    }
    }

    View full-size slide

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

  68. Logging errors
    final class LogHandler extends Handler
    {
    public function handle()
    {
    $error = $this->getException();
    if ($error instanceof DontLog) {
    return self::DONE;
    }
    $this->logger->error($error->getMessage(), [
    'exception' => $error,
    ]);
    return self::DONE;
    }
    }

    View full-size slide

  69. final class UserNotFound extends \Exception implements
    ExceptionInterface,
    DontLog
    {
    //...
    }

    View full-size slide

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

    View full-size slide

  71. interface ProvidesHttpStatusCode
    {
    public function getHttpStatusCode(): int;
    }
    final class UserNotFound extends \Exception implements
    ExceptionInterface,
    DontLog,
    ProvidesHttpStatusCode
    {
    //...
    public function getHttpStatusCode(): int
    {
    return 404;
    }
    }

    View full-size slide

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

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

    View full-size slide

  74. TESTING EXCEPTIONS WITH
    TESTING EXCEPTIONS WITH
    PHPUNIT
    PHPUNIT
    class TodoTest extends TestCase
    {
    /**
    * @test
    */
    public function it_throws_exception_on_reopening_if_incomp
    {
    $todo = Todo::from('Book flights', TodoStatus::OPEN())
    $this->expectException(CannotReopenTodo::class);
    $todo->reopen();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    View full-size slide

  75. TESTING EXCEPTIONS WITH
    TESTING EXCEPTIONS WITH
    PHPUNIT
    PHPUNIT
    class TodoTest extends TestCase
    {
    /**
    * @test
    */
    public function it_throws_exception_on_reopening_if_incomp
    {
    $todo = Todo::from('Book flights', TodoStatus::OPEN())
    $this->expectException(CannotReopenTodo::class);
    $todo->reopen();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    $this->expectException(CannotReopenTodo::class);
    $todo->reopen();
    class TodoTest extends TestCase
    1
    {
    2
    /**
    3
    * @test
    4
    */
    5
    public function it_throws_exception_on_reopening_if_incomp
    6
    {
    7
    $todo = Todo::from('Book flights', TodoStatus::OPEN())
    8
    9
    10
    11
    12
    }
    13
    }
    14

    View full-size slide

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

    View full-size slide

  77. class TodoTest extends TestCase
    {
    /**
    * @test
    */
    public function it_gets_completed()
    {
    $todo = Todo::from('Book flights', TodoStatus::OPEN());
    $todo->complete();
    $this->assertTrue($todo->isCompleted());
    }
    }

    View full-size slide

  78. /**
    * @test
    */
    public function it_throws_exception_on_reopening_if_incomplete()
    {
    $todo = Todo::from('Book flights', TodoStatus::OPEN());
    try {
    $todo->reopen();
    $this->fail('Exception should have been raised');
    } catch (CannotReopenTodo $ex) {
    $this->assertSame(
    'Tried to reopen todo, but it is not completed.',
    $ex->getMessage()
    );
    }
    }

    View full-size slide

  79. Thank you
    Thank you
    Drop me some feedback and make this
    presentation better
    ·
    joind.in/talk/8a8d6
    @nikolaposa blog.nikolaposa.in.rs

    View full-size slide