Crafting beautiful software
-
Upload
jorn-oomen -
Category
Software
-
view
1.266 -
download
0
Transcript of Crafting beautiful software
Crafting beautiful software
The start
After a while
A year later
Source: fideloper.com/hexagonal-architecture
Let’s prevent an exponential growth in technical debt
Problems with the PHP industry standard
Problems with the PHP industry standard
Lack of intention
Problems with the PHP industry standard
Lack of intentionHeavy coupling
Problems with the PHP industry standard
Lack of intentionHeavy coupling
Anemic domain models
$ whoamiJorn OomenFreelance PHP Web developerGood weather cyclistlinkedin.com/in/jornoomen@jornoomen
Let’s craft beautiful software
RequirementA user needs to be registered
class User{ private $id; private $name; private $email;
// Getters and setters}
Model
public function registerAction(Request $request) : Response{ $form = $this->formFactory->create(UserType::class); $form->handleRequest($request); if ($form->isValid()) { /** @var User $user */ $user = $form->getData(); $this->em->persist($user); $this->em->flush(); }
return new Response(/**/);}
Controller
RequirementA user has to have a name and an email
public function __construct(string $name, string $email){ $this->setName($name); $this->setEmail($email);}
private function setName(string $name){ if ('' === $name) { throw new \InvalidArgumentException('Name is required'); } $this->name = $name;}
private function setEmail(string $email){ if ('' === $email) { throw new \InvalidArgumentException('E-mail is required'); } $this->email = $email;}
Model
RequirementA user needs a valid email
private function setEmail(string $email){ if ('' === $email) { throw new \InvalidArgumentException('E-mail is required'); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException('E-mail is invalid'); } $this->email = $email;}
Model
It’s becoming messy already
It’s becoming messy alreadyA case for the value object
final class EmailAddress{
}
Value object
final class EmailAddress{ public function __construct(string $email) {
}
}
Value object
final class EmailAddress{ public function __construct(string $email) { if ('' === $email) { throw new \InvalidArgumentException('E-mail is required'); }
}
}
Value object
final class EmailAddress{ public function __construct(string $email) { if ('' === $email) { throw new \InvalidArgumentException('E-mail is required'); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException('E-mail is invalid'); }
}
}
Value object
final class EmailAddress{ public function __construct(string $email) { if ('' === $email) { throw new \InvalidArgumentException('E-mail is required'); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException('E-mail is invalid'); } $this->email = $email; }
}
Value object
final class EmailAddress{ public function __construct(string $email) { if ('' === $email) { throw new \InvalidArgumentException('E-mail is required'); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException('E-mail is invalid'); } $this->email = $email; }
public function toString() : string;
}
Value object
final class EmailAddress{ public function __construct(string $email) { if ('' === $email) { throw new \InvalidArgumentException('E-mail is required'); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException('E-mail is invalid'); } $this->email = $email; }
public function toString() : string;
public function equals(EmailAddress $email) : bool;}
Value object
class User{ public function __construct(EmailAddress $email, string $name) { $this->setEmail($email); $this->setName($name); } //[..]}
Model
// Before$user = $form->getData();
Controller
// Before$user = $form->getData();
//After$data = $form->getData();$user = new User(new EmailAddress($data['email']), $data['name'])
Controller
Recap
RecapEmail validation is handled by the value object
RecapEmail validation is handled by the value object
Name and email are required constructor arguments
RecapEmail validation is handled by the value object
Name and email are required constructor argumentsThe User model is always in a valid state
if (!empty($user->getEmail())) { if (filter_var($user->getEmail(), FILTER_VALIDATE_EMAIL)) { //Send an email }}
Before
//Send an email
After
“A small simple object, like money or a date range, whose equality isn't based on identity.”
Martin Fowler
Value objectsSmall logical concepts
Contain no identityAre immutable
Equality is based on value
==
$fiveEur = \Money\Money::EUR(500);$tenEur = $fiveEur->add($fiveEur);echo $fiveEur->getAmount(); // outputs 500echo $tenEur->getAmount(); // outputs 1000
Immutability example
We have now enforced our business rules
Everything clear?
Some observationWe are now directly using doctrine for persistence
Some observationWe are now directly using doctrine for persistence
A change of persistence would mean changing every class where we save or retrieve the user
public function registerAction(Request $request) : Response{ // [..] $user = new User(new EmailAddress($data['email']), $data['name']); $this->em->persist($user); $this->em>flush();}
Controller
Let’s move the persistence out of the controller
class DoctrineUserRepository{ //[..] public function save(User $user) { $this->em->persist($user); $this->em->flush(); }}
Repository
public function registerAction(Request $request) : Response{ // [..] $user = new User(new EmailAddress($data['email']), $data['name']); $this->userRepository->save($user);}
Controller
Doctrine is great but we don’t want to marry it
A switch of persistence can be done by changing a single class
RequirementA user needs to receive a registration confirmation
public function registerAction(Request $request){ if ($form->isValid()) { // [..] $content = $this->renderTemplate(); $message = $this->createMailMessage($content, $user); $this->mailer->send($message); } }
Controller
The controller is getting too FAT
Let’s move the notifying out of the controller
class UserRegisteredNotifier{ //[..] public function notify(string $email, string $name) { $content = $this->renderTemplate(); $message = $this->createMailMessage($content, $email, $name); $this->mailer->send($message); }}
Notifier
public function registerAction(Request $request){ if ($form->isValid()) { // [..] $this->userRegisteredNotifier->notify($data['email'], $data['name']); }}
Controller
So this looks better already
Everything clear?
RequirementUser registration has to be available through an API
public function registerAction(Request $request) : JsonResponse{
}
Controller
public function registerAction(Request $request) : JsonResponse{ $data = $this->getRequestData($request);
}
Controller
public function registerAction(Request $request) : JsonResponse{ $data = $this->getRequestData($request);
$user = new User(new EmailAddress($data['email'], $data['name']); $this->userRepository->save($user);
}
Controller
public function registerAction(Request $request) : JsonResponse{ $data = $this->getRequestData($request);
$user = new User(new EmailAddress($data['email'], $data['name']); $this->userRepository->save($user);
$this->userRegisteredNotifier->notify($data['email'], $data['name']);
}
Controller
public function registerAction(Request $request) : JsonResponse{ $data = $this->getRequestData($request);
$user = new User(new EmailAddress($data['email'], $data['name']); $this->userRepository->save($user);
$this->userRegisteredNotifier->notify($data['email'], $data['name']);
return new JsonResponse(['id' => $user->getId()]);}
Controller
$this->userRepository->save($user);
$this->userRegisteredNotifier->notify($data['email'], $data['name']);
Controller
Introducing the command pattern
class RegisterUser // A command always has a clear intention{ public function __construct(string $email, string $name) { $this->email = $email; $this->name = $name; } // Getters}
Command
class RegisterUserHandler{ //[..] public function handle(RegisterUser $registerUser) {
}}
Command handler
class RegisterUserHandler{ //[..] public function handle(RegisterUser $registerUser) { $user = new User(new EmailAddress($registerUser->getEmail()), $registerUser->getName());
}}
Command handler
class RegisterUserHandler{ //[..] public function handle(RegisterUser $registerUser) { $user = new User(new EmailAddress($registerUser->getEmail()), $registerUser->getName()); $this->userRepository->save($user);
}}
Command handler
class RegisterUserHandler{ //[..] public function handle(RegisterUser $registerUser) { $user = new User(new EmailAddress($registerUser->getEmail()), $registerUser->getName()); $this->userRepository->save($user); $this->userRegisteredNotifier->notify($registerUser->getEmail()), $registerUser->getName()); }}
Command handler
public function registerAction(Request $request) : Response{ if ($form->isValid()) { // [..] $data = $form->getData(); $this->registerUserHandler->handle( new RegisterUser($data['email'], $data['name']) ); }}
Controller
public function registerAction(Request $request) : JsonResponse{ // [..] $this->registerUserHandler->handle( new RegisterUser($data['email'], $data['name']) );
return new JsonResponse([]);}
Controller
Commands
CommandsOnly contain a message
CommandsOnly contain a message
Have a clear intention (explicit)
CommandsOnly contain a message
Have a clear intention (explicit)Are immutable
CommandsOnly contain a message
Have a clear intention (explicit)Are immutable
Command handlers never return a value
Commands and the command bus
A command bus is a generic command handler
Commands and the command bus
Commands and the command bus
A command bus is a generic command handlerIt receives a command and routes it to the handler
Commands and the command bus
A command bus is a generic command handlerIt receives a command and routes it to the handler
It provides the ability to add middleware
A great command bus implementation
github.com/SimpleBus/MessageBus
public function handle($command, callable $next){ $this->logger->log($this->level, 'Start, [command => $command]);
$next($command);
$this->logger->log($this->level, 'Finished', [command' => $command]);}
Logging middleware example
public function handle($command, callable $next){ if ($this->canBeDelayed($command)) { $this->commandQueue->add($command); } else { $next($command); }}
Queueing middleware example
//Before$this->registerUserHandler->handle( new RegisterUser($data['email'], $data['name']));
//After$this->commandBus->handle( new RegisterUser($data['email'], $data['name']));
Controller
The command busProvides the ability to add middleware
The command busProvides the ability to add middleware
Now logs every command for us
The command busProvides the ability to add middleware
Now logs every command for usAllows queueing of (slow) commands
Everything clear?
The handler is still dealing with secondary concerns
Introducing domain events
class UserIsRegistered // An event tells us what has happened{ public function __construct(int $userId, string $emailAddress, string $name) {} // Getters}
Event
class User implements ContainsRecordedMessages{
//[..]}
Model
class User implements ContainsRecordedMessages{ use PrivateMessageRecorderCapabilities;
//[..]}
Model
class User implements ContainsRecordedMessages{ use PrivateMessageRecorderCapabilities;
public static function register(EmailAddress $email, string $name) : self {
}
//[..]}
Model
class User implements ContainsRecordedMessages{ use PrivateMessageRecorderCapabilities;
public static function register(EmailAddress $email, string $name) : self { $user = new self($email, $name);
}
//[..]}
Model
class User implements ContainsRecordedMessages{ use PrivateMessageRecorderCapabilities;
public static function register(EmailAddress $email, string $name) : self { $user = new self($email, $name); $user->record(new UserIsRegistered($user->id, (string) $email, $name));
}
//[..]}
Model
class User implements ContainsRecordedMessages{ use PrivateMessageRecorderCapabilities;
public static function register(EmailAddress $email, string $name) : self { $user = new self($email, $name); $user->record(new UserIsRegistered($user->id, (string) $email, $name));
return $user; }
//[..]}
Model
class RegisterUserHandler{ //[..] public function handle(RegisterUser $registerUser) { // save user foreach ($user->recordedMessages() as $event) { $this->eventBus->handle($event); } }}
Command handler
class NotifyUserWhenUserIsRegistered{ //[..] public function handle(UserIsRegistered $userIsRegistered) { $this->userRegisteredNotifier->notify($userIsRegistered->getEmail(), $userIsRegistered->getName()); }}
Event listener
Domain events
Domain eventsAre in past tense
Domain eventsAre in past tense
Are always immutable
Domain eventsAre in past tense
Are always immutableCan have zero or more listeners
So we are pretty happy nowController creates simple command
Mailing doesn’t clutter our code
We now have a rich user model
We now have a rich user modelIt contains data
We now have a rich user modelIt contains data
It contains validation
We now have a rich user modelIt contains data
It contains validationIt contains behaviour
“Objects hide their data behind abstractions and expose functions that operate on that data. Data structure expose
their data and have no meaningful functions.” Robert C. Martin (uncle Bob)
Everything clear?
We are still coupled to doctrine for our persistence
class DoctrineUserRepository{ //[..] public function save(User $user) { $this->em->persist($user); $this->em->flush(); }
public function find(int $userId) : User { return $this->em->find(User::class, $userId); }}
Repository
We shouldn’t depend on any persistence implementation
We shouldn’t depend on any persistence implementation
A case for the dependency inversion principle
interface UserRepository{ public function save(User $user);
public function find(int $userId) : User;}
Repository
class DoctrineUserRepository implements UserRepositoryInterface{ //[..]}
Repository
class RegisterUserHandler{ public function __construct(UserRepositoryInterface $userRepository, MessageBus $eventBus) { //[..] }}
Command handler
DI: Dependency injection
Our situation
DI: Dependency injectionIoC: Inversion of control
Our situation
DI: Dependency injectionIoC: Inversion of controlDIP: Dependency inversion principle
Our situation
Tests - InMemoryUserRepository
Decoupling provides options
Tests - InMemoryUserRepositoryDevelopment - MysqlUserRepository
Decoupling provides options
Tests - InMemoryUserRepositoryDevelopment - MysqlUserRepositoryProduction - WebserviceUserRepository
Decoupling provides options
Be clear about your exceptions
Some note
interface UserRepository{ /** * @throws UserNotFoundException * @throws ServiceUnavailableException */ public function find(int $userId) : User;
//[..]}
Repository
class DoctrineUserRepository implements UserRepositoryInterface{ /** * @throws \Doctrine\DBAL\Exception\ConnectionException */ public function find(int $userId) : User;}class InMemoryUserRepository implements UserRepository{ /** * @throws \RedisException */ public function find(int $userId) : User;}
Repository
Don’t do thisclass DoctrineUserRepository implements UserRepositoryInterface{ /** * @throws \Doctrine\DBAL\Exception\ConnectionException */ public function find(int $userId) : User;}class InMemoryUserRepository implements UserRepository{ /** * @throws \RedisException */ public function find(int $userId) : User;}
Repository
class DoctrineUserRepository implements UserRepositoryInterface{ public function find(UserId $userId) : User { try { if ($user = $this->findById($userId)) { return $user; } } catch (ConnectionException $e) { throw ServiceUnavailableException::withOriginalException($e); }
throw UserNotFoundException::withId($userId); }}
Normalize your exceptionsRepository
class DoctrineUserRepository implements UserRepositoryInterface{ public function find(UserId $userId) : User { try { if ($user = $this->findById($userId)) { return $user; } } catch (ConnectionException $e) { throw ServiceUnavailableException::withOriginalException($e); }
throw UserNotFoundException::withId($userId); }}
Normalize your exceptions
The implementor now only has to worry about the exceptions defined in the interface
Repository
The promise of a repository interface is now clear, simple and implementation independent
Everything clear?
src/UserBundle/ ├── Command ├── Controller ├── Entity ├── Event ├── Form ├── Notifier ├── Repository └── ValueObject
Let’s look at the structure
src/UserBundle/ ├── Command ├── Controller ├── Entity ├── Event ├── Form ├── Notifier ├── Repository └── ValueObject
Let’s look at the structure
The domain, infrastructure and application are all mixed in the bundle
src/UserBundle/ ├── Command │ ├── RegisterUser.php │ └── RegisterUserHandler.php ├── Controller │ ├── RegisterUserApiController.php │ └── RegisterUserController.php ├── Entity │ ├── User.php │ └── UserRepository.php ├── Event │ └── UserIsRegistered.php ├── Form │ └── UserType.php ├── Notifier │ ├── NotifyUserWhenUserIsRegistered.php │ └── UserRegisteredNotifier.php ├── Repository │ └── DoctrineUserRepository.php ├── Resources/config/doctrine │ └── User.orm.yml └── ValueObject └── EmailAddress.php
src/UserBundle/ ├── Command │ ├── RegisterUser.php │ └── RegisterUserHandler.php ├── Controller │ ├── RegisterUserApiController.php │ └── RegisterUserController.php ├── Entity │ ├── User.php │ └── UserRepository.php ├── Event │ └── UserIsRegistered.php ├── Form │ └── UserType.php ├── Notifier │ ├── NotifyUserWhenUserIsRegistered.php │ └── UserRegisteredNotifier.php ├── Repository │ └── DoctrineUserRepository.php ├── Resources/config/doctrine │ └── User.orm.yml └── ValueObject └── EmailAddress.php
Domain
src/UserBundle/ ├── Command │ ├── RegisterUser.php │ └── RegisterUserHandler.php ├── Controller │ ├── RegisterUserApiController.php │ └── RegisterUserController.php ├── Entity │ ├── User.php │ └── UserRepository.php ├── Event │ └── UserIsRegistered.php ├── Form │ └── UserType.php ├── Notifier │ ├── NotifyUserWhenUserIsRegistered.php │ └── UserRegisteredNotifier.php ├── Repository │ └── DoctrineUserRepository.php ├── Resources/config/doctrine │ └── User.orm.yml └── ValueObject └── EmailAddress.php
DomainInfrastructure
src/UserBundle/ ├── Command │ ├── RegisterUser.php │ └── RegisterUserHandler.php ├── Controller │ ├── RegisterUserApiController.php │ └── RegisterUserController.php ├── Entity │ ├── User.php │ └── UserRepository.php ├── Event │ └── UserIsRegistered.php ├── Form │ └── UserType.php ├── Notifier │ ├── NotifyUserWhenUserIsRegistered.php │ └── UserRegisteredNotifier.php ├── Repository │ └── DoctrineUserRepository.php ├── Resources/config/doctrine │ └── User.orm.yml └── ValueObject └── EmailAddress.php
DomainInfrastructureApplication
src/UserBundle/├── Command│ ├── RegisterUser.php│ └── RegisterUserHandler.php├── Controller│ ├── RegisterUserApiController.php│ └── RegisterUserController.php├── Entity│ ├── User.php│ └── UserRepository.php├── Event│ └── UserIsRegistered.php├── Form│ └── UserType.php├── Notifier│ ├── NotifyUserWhenUserIsRegistered.php│ └── UserRegisteredNotifier.php├── Repository│ └── DoctrineUserRepository.php├── Resources/config/doctrine│ └── User.orm.yml└── ValueObject └── EmailAddress.php
src/User/└── DomainModel └── User ├── EmailAddress.php ├── User.php ├── UserIsRegistered.php └── UserRepository.php
src/UserBundle/├── Command│ ├── RegisterUser.php│ └── RegisterUserHandler.php├── Controller│ ├── RegisterUserApiController.php│ └── RegisterUserController.php├── Entity│ ├── User.php│ └── UserRepository.php├── Event│ └── UserIsRegistered.php├── Form│ └── UserType.php├── Notifier│ ├── NotifyUserWhenUserIsRegistered.php│ └── UserRegisteredNotifier.php├── Repository│ └── DoctrineUserRepository.php├── Resources/config/doctrine│ └── User.orm.yml└── ValueObject └── EmailAddress.php
src/User/├── DomainModel│ └── User│ ├── EmailAddress.php│ ├── User.php│ ├── UserIsRegistered.php│ └── UserRepository.php└── Infrastructure ├── Messaging/UserRegisteredNotifier.php └── Persistence ├── User │ └── DoctrineUserRepository.php └── config/doctrine └── User.User.orm.yml
src/UserBundle/├── Command│ ├── RegisterUser.php│ └── RegisterUserHandler.php├── Controller│ ├── RegisterUserApiController.php│ └── RegisterUserController.php├── Entity│ ├── User.php│ └── UserRepository.php├── Event│ └── UserIsRegistered.php├── Form│ └── UserType.php├── Notifier│ ├── NotifyUserWhenUserIsRegistered.php│ └── UserRegisteredNotifier.php├── Repository│ └── DoctrineUserRepository.php├── Resources/config/doctrine│ └── User.orm.yml└── ValueObject └── EmailAddress.php
src/User/├── DomainModel│ └── User│ ├── EmailAddress.php│ ├── User.php│ ├── UserIsRegistered.php│ └── UserRepository.php├── Infrastructure│ ├── Messaging/UserRegisteredNotifier.php│ └── Persistence│ ├── User│ │ └── DoctrineUserRepository.php│ └── config/doctrine│ └── User.User.orm.yml└── Application ├── Command │ ├── RegisterUser.php │ └── RegisterUserHandler.php ├── Controller │ ├── RegisterUserApiController.php │ └── RegisterUserController.php ├── Form/UserType.php └── Messaging/NotifyUserWhenUserIsRegistered.php
We are looking pretty goodApplication, domain and infrastructural concerns are
separated.
Everything clear?
Let’s test this awesome software
public function can_register_user(){
}
public function can_register_user(){ $this->registerUserHandler->handle(new RegisterUser('[email protected]', 'Aart Staartjes'));
}
public function can_register_user(){ $this->registerUserHandler->handle(new RegisterUser('[email protected]', 'Aart Staartjes')); $user = $this->inMemoryUserRepository->find(1);
}
public function can_register_user(){ $this->registerUserHandler->handle(new RegisterUser('[email protected]', 'Aart Staartjes')); $user = $this->inMemoryUserRepository->find(1); $this->assertInstanceOf(User::class, $user); $this->assertEquals(new EmailAddress('[email protected]'), $user->getEmail()); $this->assertSame('Aart Staartjes', $user->getName()); $this->assertInstanceOf(UserIsRegistered::class, $this->eventBusMock->getRaisedEvents()[0]);}
There was 1 error:
1) User\DomainModel\User\RegisterUserTest::can_register_userUser\DomainModel\Exception\UserNotFoundException: User with id 1 not found
FAILURES!Tests: 1, Assertions: 0, Errors: 1.
public function can_register_user(){ $this->registerUserHandler->handle(new RegisterUser('[email protected]', 'Aart Staartjes')); $user = $this->inMemoryUserRepository->find(1); $this->assertInstanceOf(User::class, $user); $this->assertEquals(new EmailAddress('[email protected]'), $user->getEmail()); $this->assertSame('Aart Staartjes', $user->getName()); $this->assertInstanceOf(UserIsRegistered::class, $this->eventBusMock->getRaisedEvents()[0]);}
We rely on the magic of the persistence layer
User provides identity - The email
Unique identity options
User provides identity - The emailPersistence mechanism generates identity - Auto increment
Unique identity options
User provides identity - The emailPersistence mechanism generates identity - Auto incrementApplication generates identity - UUID
Unique identity options
Let’s remove the magicBy implementing an up front id generation strategy
composer require ramsey/uuid
class User implements ContainsRecordedMessages{ use PrivateMessageRecorderCapabilities;
public static function register(UuidInterface $id, EmailAddress $email, string $name) : self { $user = new self($id, $email, $name); $user->record(new UserIsRegistered((string) $id, (string) $email, $name));
return $user; } //[..]}
Model
public function can_register_user(){ $id = Uuid::uuid4();
}
public function can_register_user(){ $id = Uuid::uuid4();
new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes')
}
public function can_register_user(){ $id = Uuid::uuid4(); $this->registerUserHandler->handle( new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes') );
}
public function can_register_user(){ $id = Uuid::uuid4(); $this->registerUserHandler->handle( new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes') ); $user = $this->inMemoryUserRepository->find($id); // Assertions}
phpunit --bootstrap=vendor/autoload.php test/PHPUnit 5.3.2 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 125 ms, Memory: 8.00Mb
OK (1 test, 4 assertions)
Tested
But a Uuid can still be any idWe can be more explicit
final class UserId // Simply wrapper of Uuid{ public static function createNew() : self
/** * @throws \InvalidArgumentException */ public static function fromString(string $id) : self
public function toString() : string;}
Value object
Now we know exactly what we are talking about
public function can_register_user(){ $id = UserId::createNew(); $this->registerUserHandler->handle( new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes') ); $user = $this->inMemoryUserRepository->find($id); // Assertions}
phpunit --bootstrap vendor/autoload.php src/JO/User/PHPUnit 4.8.11 by Sebastian Bergmann and contributors.
.
Time: 266 ms, Memory: 5.25Mb
OK (1 test, 4 assertions)
Everything clear?
Let’s test our value objects
public function can_not_create_invalid_user_id(){ $this->expectException(\InvalidArgumentException::class); UserId::fromString('invalid format');}
public function can_not_create_invalid_email_address(){ $this->expectException(\InvalidArgumentException::class); new EmailAddress('invalid format');}
PHPUnit 5.3.2 by Sebastian Bergmann and contributors.
... 3 / 3 (100%)
Time: 120 ms, Memory: 8.00Mb
Great that’s testedBut..
public function can_not_create_invalid_user_id(){ $this->expectException(\InvalidArgumentException::class); UserId::fromString('invalid format');}
public function can_not_create_invalid_email_address(){ $this->expectException(\InvalidArgumentException::class); new EmailAddress('invalid format');}
$this->commandBus>handle( new RegisterUser('invalid id', 'invalid email', $name = '’) );
We still have limited control over our exceptions
Useful domain exceptions can give us more control
namespace User\DomainModel\Exception;
abstract class DomainException extends \DomainException {}
namespace User\DomainModel\Exception;
abstract class DomainException extends \DomainException {}
class InvalidEmailAddressException extends DomainException {}
class InvalidUserIdException extends DomainException {}
class NoEmailAddressProvidedException extends DomainException {}
try { $id = UserId::createNew(); $this->commandBus>handle( new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes') );} catch (DomainModel\Exception\InvalidEmailAddressException $e) { // Show invalid email error}
try { $id = UserId::createNew(); $this->commandBus>handle( new RegisterUser((string) $id, '[email protected]', 'Aart Staartjes') );} catch (DomainModel\Exception\InvalidEmailAddressException $e) { // Show invalid email error} catch (DomainModel\Exception\DomainException $e) { // Some domain exception occurred}
public function can_not_create_invalid_user_id(){ $this->expectException(InvalidUserIdException::class); UserId::fromString('invalid format');}
public function can_not_create_invalid_email_address(){ $this->expectException(InvalidEmailAddressProvidedException::class); new EmailAddress('invalid format');}
PHPUnit 5.3.2 by Sebastian Bergmann and contributors.
... 3 / 3 (100%)
Time: 122 ms, Memory: 8.00Mb
OK (3 tests, 6 assertions)
Everything clear?
So what did we create?
Intention revealing code
What did we learn?
Intention revealing codeTestable code
What did we learn?
Intention revealing codeTestable code
Preventing the big ball of mud
What did we learn?
Intention revealing codeTestable code
Preventing the big ball of mudAnemic domain models (anti pattern)
What did we learn?
Intention revealing codeTestable code
Preventing the big ball of mudAnemic domain models (anti pattern)
Value objects
What did we learn?
Intention revealing codeTestable code
Preventing the big ball of mudAnemic domain models (anti pattern)
Value objectsDecoupling from the framework
What did we learn?
What did we learn?Writing fast tests (mocked environment)
What did we learn?Writing fast tests (mocked environment)
Commands
What did we learn?Writing fast tests (mocked environment)
CommandsDomain Events
What did we learn?Writing fast tests (mocked environment)
CommandsDomain Events
Dependency inversion principle
What did we learn?Writing fast tests (mocked environment)
CommandsDomain Events
Dependency inversion principleUp front id generation strategy
What did we learn?Writing fast tests (mocked environment)
CommandsDomain Events
Dependency inversion principleUp front id generation strategy
Creating powerful domain exceptions
What did we learn?Writing fast tests (mocked environment)
CommandsDomain Events
Dependency inversion principleUp front id generation strategy
Creating powerful domain exceptionsLiskov substitution principle
We wrote intention revealing code. Separated the
We wrote intention revealing code. Separated the domain, infrastructure and application. Created
We wrote intention revealing code. Separated the domain, infrastructure and application. Created
abstractions to improve testability and flexibility. We
We wrote intention revealing code. Separated the domain, infrastructure and application. Created
abstractions to improve testability and flexibility. We used commands to communicate with a unified
voice. Created domain events to allow for extension
We wrote intention revealing code. Separated the domain, infrastructure and application. Created
abstractions to improve testability and flexibility. We used commands to communicate with a unified
voice. Created domain events to allow for extension without cluttering the existing code. We end up with
We wrote intention revealing code. Separated the domain, infrastructure and application. Created
abstractions to improve testability and flexibility. We used commands to communicate with a unified
voice. Created domain events to allow for extension without cluttering the existing code. We end up with
clear, maintainable and beautiful software.
We wrote intention revealing code. Separated the domain, infrastructure and application. Created
abstractions to improve testability and flexibility. We used commands to communicate with a unified
voice. Created domain events to allow for extension without cluttering the existing code. We end up with
clear, maintainable and beautiful software.That keeps us excited!
Questions?Questions?
Please rate!
joind.in/17557
Further reading
Used resourceshttp://www.slideshare.net/matthiasnoback/hexagonal-architecture-messageoriented-software-designhttps://www.youtube.com/watch?v=Eg6m6mU0fH0https://www.youtube.com/watch?v=mQsQ6QZ4dGghttps://kacper.gunia.me/blog/ddd-building-blocks-in-php-value-objecthttp://williamdurand.fr/2013/12/16/enforcing-data-encapsulation-with-symfony-forms/http://simplebus.github.io/MessageBus/http://php-and-symfony.matthiasnoback.nl/2014/06/don-t-use-annotations-in-your-controllers/http://alistair.cockburn.us/Hexagonal+architecturehttp://www.slideshare.net/cakper/2014-0407-php-spec-the-only-design-tool-you-need-4developers/117-enablesRefactoring