HANDLING EXCEPTIONAL CONDITIONSHANDLING EXCEPTIONAL CONDITIONS
WITH GRACE AND STYLEWITH GRACE AND STYLE
Nikola Poša · @nikolaposa
Я радий бути тутЯ радий бути тут
Я радий бути тутЯ радий бути тут
ABOUT MEABOUT ME
Software Architect specializing in PHP-based
applications
Lead Architect at Arbor Education Partners
PHP Serbia Conference co-organizer
 @nikolaposa
 blog.nikolaposa.in.rs
AGENDAAGENDA
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
HAPPY PATHHAPPY 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
“
$user = $userRepository->get('John');
if ($user->isSubscribedTo($notification)) {
$notifier->notify($user, $notification);
}
1
2
3
4
5
$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
$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
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
Vague Interface
interface UserRepository
{
public function get(string $username): ?User;
}
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);
}
DO NOT MESS WITHDO NOT MESS WITH
NULLNULL
When we return null, we are
essentially creating work for
ourselves and foisting problems upon
our callers.
Robert C. Martin, "Clean Code"
“
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"
“
THROW EXCEPTIONTHROW EXCEPTION
interface UserRepository
* @throws UserNotFound
public function get(string $username): User;
1
{2
/**3
* @param string $username4
5
* @return User6
*/7
8
}9
throw new UserNotFound();
final class DbUserRepository implements UserRepository1
{2
public function get(string $username): User3
{4
$userRecord = $this->db->fetchAssoc('SELECT * FROM use5
6
if (false === $userRecord) {7
8
}9
10
return User::fromArray($userRecord);11
}12
}13
interface UserRepository
{
@throws UserNotFound
public function get(string $username): User;
}
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 {
$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);
}
SPECIAL CASESPECIAL CASE
a.k.a. Null Object
A subclass that provides special
behavior for particular cases.
Martin Fowler, "Patterns of Enterprise Application
Architecture"
“
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 UserRepository1
{2
public function get(string $username): User3
{4
$userRecord = $this->db->fetchAssoc('SELECT * FROM use5
6
if (false === $userRecord) {7
8
}9
10
return User::fromArray($userRecord);11
}12
}13
$user = $userRepository->get('John');
if ($user->isSubscribedTo($notification)) {
$notifier->notify($user, $notification);
}
Checking for Special Case
Checking for Special Case
if ($user instanceof UnknownUser) {
//do something
}
Checking for Special Case
if ($user instanceof UnknownUser) {
//do something
}
if ($user === User::unknown()) {
//do something
}
Special Case factory
class User
{
public static function unknown(): User
{
static $unknownUser = null;
if (null === $unknownUser) {
$unknownUser = new UnknownUser();
}
return $unknownUser;
}
}
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;
}
}
Returning null from methods is
bad, but passing null into methods
is worse.
Robert C. Martin, "Clean Code"
“
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;
}
}
?Discount $discount
if (null !== $this->discount) {
}
class Order1
{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(): float13
{14
$price = $this->product->getPrice();15
16
17
$price = $this->discount->apply($price);18
19
20
return $price;21
}22
}23
Discount $discount
class Order1
{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
Discount $discount
class Order1
{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());
EXCEPTION VS SPECIAL CASEEXCEPTION 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
USING EXCEPTIONSUSING EXCEPTIONS
Should be simple as:
throw new Exception('User was not found by username: ' . $usernam
Should be simple as:
throw new Exception('User was not found by username: ' . $usernam
CUSTOM EXCEPTION TYPESCUSTOM EXCEPTION TYPES
bring semantics to your code
emphasise exception type instead of exception
message
allow caller to act di erently based on
Exception type
STRUCTURING EXCEPTIONSSTRUCTURING EXCEPTIONS
src/
Todo/
Exception/
Model/
User/
Exception/
Model/
CREATING EXCEPTION CLASSESCREATING EXCEPTION CLASSES
src/
User/
Exception/
InvalidUsername.php
UsernameAlreadyTaken.php
UserNotFound.php
final class UserNotFound extends Exception
{
}
use AppUserExceptionUserNotFoundException;
try {
throw new UserNotFoundException();
} catch (UserNotFoundException $exception) {
}
use AppUserExceptionUserNotFoundException;
try {
throw new UserNotFoundException();
} catch (UserNotFoundException $exception) {
}
use AppUserExceptionUserNotFound;
try {
throw new UserNotFound();
} catch (UserNotFound $exception) {
}
COMPONENT LEVEL EXCEPTIONCOMPONENT LEVEL EXCEPTION
TYPETYPE
COMPONENT LEVEL EXCEPTIONCOMPONENT LEVEL EXCEPTION
TYPETYPE
namespace AppUserException;
interface ExceptionInterface
{
}
final class InvalidUsername extends Exception implements
ExceptionInterface
{
}
final class UserNotFound extends Exception implements
ExceptionInterface
{
}
use GuzzleHttpExceptionClientException;
use GuzzleHttpExceptionServerException;
use GuzzleHttpExceptionGuzzleException; //marker interface
try {
//code that can emit exceptions
} catch (ClientException $ex) {
//...
} catch (ServerException $ex) {
//...
} catch (GuzzleException $ex) {
//...
}
FORMATTING EXCEPTIONFORMATTING EXCEPTION
MESSAGESMESSAGES
FORMATTING EXCEPTIONFORMATTING EXCEPTION
MESSAGESMESSAGES
throw new UserNotFound(sprintf(
'User was not found by username: %s',
$username
));
FORMATTING EXCEPTIONFORMATTING EXCEPTION
MESSAGESMESSAGES
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()
));
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
));
}
}
Named Constructors communicate the intent
throw UserNotFound::byUsername($username);
Coherent exceptional conditions
throw TodoNotOpen::triedToSetDeadline($deadline, $this->status);
throw TodoNotOpen::triedToMarkAsCompleted($this->status);
PROVIDE CONTEXTPROVIDE CONTEXT
PROVIDE CONTEXTPROVIDE 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;
}
}
EXCEPTION WRAPPINGEXCEPTION WRAPPING
EXCEPTION WRAPPINGEXCEPTION WRAPPING
try {
return $this->toResult(
$this->httpClient->request('GET', '/users')
);
} catch (ConnectException $ex) {
throw ApiNotAvailable::reason($ex);
}
1
2
3
4
5
6
7
EXCEPTION WRAPPINGEXCEPTION 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
EXCEPTION WRAPPINGEXCEPTION 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 Exce1
{2
public static function reason(ConnectException $error): se3
{4
return new self(5
'API is not available',6
0,7
8
);9
}10
}11
RETROSPECTRETROSPECT
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
ERROR HANDLINGERROR HANDLING
WHEN TO CATCH EXCEPTIONS?WHEN TO CATCH EXCEPTIONS?
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
CENTRAL ERROR HANDLERCENTRAL ERROR HANDLER
Wraps the entire system to handle any uncaught
exceptions from a single place
CHALLENGESCHALLENGES
user experience
security
logging
CHALLENGESCHALLENGES
user experience
security
logging
adaptability
EXISTING SOLUTIONSEXISTING SOLUTIONS
EXISTING SOLUTIONSEXISTING SOLUTIONS
- stack-based error handling, pretty
error page, handlers for di erent response
formats (JSON, XML)
Whoops
EXISTING SOLUTIONSEXISTING 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
Using Whoops
final class ErrorHandlerFactory
{
public function __invoke(ContainerInterface $container)
{
$whoops = new WhoopsRun();
if (WhoopsUtilMisc::isAjaxRequest()) {
$whoops->pushHandler(new JsonResponseHandler());
} elseif (WhoopsUtilMisc::isCommandLine()) {
$whoops->pushHandler(new CommandLineHandler());
} else {
$whoops->pushHandler(new PrettyPageHandler());
}
$whoops->pushHandler(new SetHttpStatusCodeHandler());
$whoops->pushHandler(new LogHandler($container->get('Logg
return $whoops;
}
}
src/bootstrap.php
public/index.php
bin/app
//... initialize DI container
$container->get(WhoopsRun::class)->register();
return $container;
$container = require __DIR__ . '/../src/bootstrap.php';
$container->get('AppWeb')->run();
$container = require __DIR__ . '/../src/bootstrap.php';
$container->get('AppConsole')->run();
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;
}
}
final class UserNotFound extends Exception implements
ExceptionInterface,
DontLog
{
//...
}
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;
}
}
interface ProvidesHttpStatusCode
{
public function getHttpStatusCode(): int;
}
final class UserNotFound extends Exception implements
ExceptionInterface,
DontLog,
ProvidesHttpStatusCode
{
//...
public function getHttpStatusCode(): int
{
return 404;
}
}
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"
“
TEST EXCEPTIONALTEST EXCEPTIONAL
BEHAVIOURBEHAVIOUR
a.k.a. Negative Testing
TESTING EXCEPTIONS WITHTESTING EXCEPTIONS WITH
PHPUNITPHPUNIT
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
TESTING EXCEPTIONS WITHTESTING EXCEPTIONS WITH
PHPUNITPHPUNIT
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 TestCase1
{2
/**3
* @test4
*/5
public function it_throws_exception_on_reopening_if_incomp6
{7
$todo = Todo::from('Book flights', TodoStatus::OPEN())8
9
10
11
12
}13
}14
ARRANGE-ACT-ASSERTARRANGE-ACT-ASSERT
1. initialize SUT/prepare inputs
2. perform action
3. verify outcomes
class TodoTest extends TestCase
{
/**
* @test
*/
public function it_gets_completed()
{
$todo = Todo::from('Book flights', TodoStatus::OPEN());
$todo->complete();
$this->assertTrue($todo->isCompleted());
}
}
/**
* @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()
);
}
}
Thank youThank you
Drop me some feedback and make this
presentation better
·
joind.in/talk/8a8d6
@nikolaposa blog.nikolaposa.in.rs

Nikola Poša "Handling exceptional conditions with grace and style"