This document discusses approaches for handling exceptional conditions and errors in code in a graceful manner. It provides best practices for managing exceptions, such as creating custom exception types with meaningful names and contexts, using named constructors to encapsulate message formatting, and establishing a central error handler. Some existing solutions for error handling like Whoops and BooBoo are also presented, which implement stack-based error handling, different response formats, and logging of errors.
3. Я радий бути тутЯ радий бути тут
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
4. 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
5. 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
“
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);
}
19. SPECIAL CASESPECIAL CASE
a.k.a. Null Object
A subclass that provides special
behavior for particular cases.
Martin Fowler, "Patterns of Enterprise Application
Architecture"
“
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 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
24. Checking for Special Case
if ($user instanceof UnknownUser) {
//do something
}
if ($user === User::unknown()) {
//do something
}
25. Special Case factory
class User
{
public static function unknown(): User
{
static $unknownUser = null;
if (null === $unknownUser) {
$unknownUser = new UnknownUser();
}
return $unknownUser;
}
}
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;
}
}
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;
}
}
29. ?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
30. 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
31. 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());
32. 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
34. Should be simple as:
throw new Exception('User was not found by username: ' . $usernam
35. Should be simple as:
throw new Exception('User was not found by username: ' . $usernam
36. 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
46. 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()
));
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
));
}
}
51. 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;
}
}
54. 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
55. 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
56. 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
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
60. CENTRAL ERROR HANDLERCENTRAL ERROR HANDLER
Wraps the entire system to handle any uncaught
exceptions from a single place
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;
}
}
71. interface ProvidesHttpStatusCode
{
public function getHttpStatusCode(): int;
}
final class UserNotFound extends Exception implements
ExceptionInterface,
DontLog,
ProvidesHttpStatusCode
{
//...
public function getHttpStatusCode(): int
{
return 404;
}
}
77. class TodoTest extends TestCase
{
/**
* @test
*/
public function it_gets_completed()
{
$todo = Todo::from('Book flights', TodoStatus::OPEN());
$todo->complete();
$this->assertTrue($todo->isCompleted());
}
}
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()
);
}
}
79. Thank youThank you
Drop me some feedback and make this
presentation better
·
joind.in/talk/8a8d6
@nikolaposa blog.nikolaposa.in.rs