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.

907a556f3e67367906a0863aa6f5d379?s=128

Nikola Poša

November 04, 2017
Tweet

Transcript

  1. 1.

    JOURNEY THROUGH JOURNEY THROUGH "UNHAPPY PATH" "UNHAPPY PATH" DEALING WITH

    EXCEPTIONAL CONDITIONS DEALING WITH EXCEPTIONAL CONDITIONS Nikola Poša PHP Central Europe 2017
  2. 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
  3. 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
  4. 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); }
  5. 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); }
  6. 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); } }
  7. 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); } }
  8. 8.
  9. 9.

    "When we return null, we are essentially creating work for

    ourselves and foisting problems upon our callers." Robert C. Martin, "Clean Code"
  10. 10.

    "All it takes is one missing null check to send

    an application spinning out of control." Robert C. Martin, "Clean Code"
  11. 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
  12. 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"
  13. 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); }
  14. 14.

    SPECIAL CASE SPECIAL CASE "A subclass that provides special behavior

    for particular cases." Martin Fowler, "Patterns of Enterprise Application Architecture"
  15. 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 <a href="/">Home Page</a>'; } }
  16. 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); } }
  17. 17.

    Get rid of checks for a null value echo $article

    ? $article->getTitle() : 'Non-existing article';
  18. 19.

    "Returning null from methods is bad, but passing null into

    methods is worse." Robert C. Martin, "Clean Code"
  19. 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; } }
  20. 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; } }
  21. 23.
  22. 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; } }
  23. 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());
  24. 27.

    DO NOT MESS WITH NULL DO NOT MESS WITH NULL

    do not return null from methods
  25. 28.

    DO NOT MESS WITH NULL DO NOT MESS WITH NULL

    do not return null from methods do not accept null in methods
  26. 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
  27. 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
  28. 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
  29. 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
  30. 33.

    Input validation class User { public static function fromInput(array $input)

    : User { if (!filter_var($input['email'], FILTER_VALIDATE_EMAIL)) { throw new InvalidUserInputException(); } //... } }
  31. 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; } }
  32. 35.

    USING EXCEPTIONS USING EXCEPTIONS Should be simple as: throw new

    \Exception('Article with the ID: ' . $id . ' does not exist');
  33. 36.
  34. 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
  35. 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
  36. 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
  37. 43.

    COHESIVE EXCEPTION CLASSES COHESIVE EXCEPTION CLASSES Exception classes should conform

    SRP. class ArticleNotFoundException extends \RuntimeException { } class InvalidArticleException extends \DomainException { }
  38. 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
  39. 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 { }
  40. 46.
  41. 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) { //... }
  42. 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 ));
  43. 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() ));
  44. 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 )); } }
  45. 51.

    Throwing an exception becomes more readable and expressive, as in:

    throw ArticleNotFoundException::forId($id);
  46. 52.

    PROVIDE CONTEXT PROVIDE CONTEXT Take advantage of using Named Constructors

    to save the context of the exceptional situation that has occurred.
  47. 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; } }
  48. 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.
  49. 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 } }
  50. 58.

    TO SUM UP TO SUM UP create custom, cohesive Exception

    types introduce component-level exception type using Marker Interface
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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(); } } }
  56. 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(); } } }
  57. 66.

    ERROR CONTROLLER ERROR CONTROLLER offered by MVC frameworks all the

    errors get forwarded to it allows for handling errors from a single place
  58. 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; } }
  59. 68.

    All these solutions ignore the fact that application may support

    to be invoked through different ports (web, CLI, API)
  60. 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
  61. 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
  62. 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 });
  63. 75.

    ... or use some of the existing solutions - stack-based

    error handling, pretty error page, handlers for different response formats (JSON, XML) Whoops
  64. 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
  65. 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;
  66. 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();
  67. 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; } }
  68. 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; } }
  69. 82.

    interface ProvidesHttpStatusCodeInterface { public function getHttpStatusCode() : int; } class

    ArticleNotFoundException extends \RuntimeException implements ExceptionInterface, DontLogInterface, ProvidesHttpStatusCodeInterface { //... public function getHttpStatusCode() : int { return 404; } }
  70. 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' => 'john@example.com', 'comment' => 'test comment', ])); }
  71. 87.

    class ArticleTest extends TestCase { /** * @test */ public

    function it_can_be_commented() { $article = new Article(); $article->comment(Comment::fromArray([ 'name' => 'John', 'email' => 'john@example.com', 'comment' => 'test comment', ])); $this->assertCount(1, $article->getComments()); } }
  72. 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 } }