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 intention
Heavy coupling
Problems with the PHP
industry standard
Lack of intention
Heavy coupling
Anemic domain models
$ whoami
Jorn Oomen
Freelance PHP Web developer
Good weather cyclist
linkedin.com/in/jornoomen
@jornoomen
Let’s craft beautiful software
Requirement
A 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
Requirement
A 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
Requirement
A 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 already
A 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
Recap
Email validation is handled by the value object
Recap
Email validation is handled by the value object
Name and email are required constructor arguments
Recap
Email validation is handled by the value object
Name and email are required constructor arguments
The 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 objects
Small logical concepts
Contain no identity
Are immutable
Equality is based on value
==
$fiveEur = MoneyMoney::EUR(500);
$tenEur = $fiveEur->add($fiveEur);
echo $fiveEur->getAmount(); // outputs 500
echo $tenEur->getAmount(); // outputs 1000
Immutability example
We have now enforced our
business rules
Everything clear?
Some observation
We are now directly using doctrine for persistence
Some observation
We 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
Requirement
A 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?
Requirement
User 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
Commands
Only contain a message
Commands
Only contain a message
Have a clear intention (explicit)
Commands
Only contain a message
Have a clear intention (explicit)
Are immutable
Commands
Only 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 handler
It receives a command and routes it to the handler
Commands and the
command bus
A command bus is a generic command handler
It 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 bus
Provides the ability to add middleware
The command bus
Provides the ability to add middleware
Now logs every command for us
The command bus
Provides the ability to add middleware
Now logs every command for us
Allows 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 events
Are in past tense
Domain events
Are in past tense
Are always immutable
Domain events
Are in past tense
Are always immutable
Can have zero or more listeners
So we are pretty happy now
Controller creates simple command
Mailing doesn’t clutter our code
We now have a rich user model
We now have a rich user model
It contains data
We now have a rich user model
It contains data
It contains validation
We now have a rich user model
It contains data
It contains validation
It 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 injection
IoC: Inversion of control
Our situation
DI: Dependency injection
IoC: Inversion of control
DIP: Dependency inversion principle
Our situation
Tests - InMemoryUserRepository
Decoupling provides options
Tests - InMemoryUserRepository
Development - MysqlUserRepository
Decoupling provides options
Tests - InMemoryUserRepository
Development - MysqlUserRepository
Production - 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 DoctrineDBALExceptionConnectionException
*/
public function find(int $userId) : User;
}
class InMemoryUserRepository implements UserRepository
{
/**
* @throws RedisException
*/
public function find(int $userId) : User;
}
Repository
Don’t do this
class DoctrineUserRepository implements UserRepositoryInterface
{
/**
* @throws DoctrineDBALExceptionConnectionException
*/
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 exceptions
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 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
Domain
Infrastructure
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
Infrastructure
Application
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 good
Application, 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('aart.staartjes@hotmail.com', 'Aart Staartjes'));
}
public function can_register_user()
{
$this->registerUserHandler->handle(new RegisterUser('aart.staartjes@hotmail.com', 'Aart Staartjes'));
$user = $this->inMemoryUserRepository->find(1);
}
public function can_register_user()
{
$this->registerUserHandler->handle(new RegisterUser('aart.staartjes@hotmail.com', 'Aart Staartjes'));
$user = $this->inMemoryUserRepository->find(1);
$this->assertInstanceOf(User::class, $user);
$this->assertEquals(new EmailAddress('aart.staartjes@hotmail.com'), $user->getEmail());
$this->assertSame('Aart Staartjes', $user->getName());
$this->assertInstanceOf(UserIsRegistered::class, $this->eventBusMock->getRaisedEvents()[0]);
}
There was 1 error:
1) UserDomainModelUserRegisterUserTest::can_register_user
UserDomainModelExceptionUserNotFoundException: User with id 1 not found
FAILURES!
Tests: 1, Assertions: 0, Errors: 1.
public function can_register_user()
{
$this->registerUserHandler->handle(new RegisterUser('aart.staartjes@hotmail.com', 'Aart Staartjes'));
$user = $this->inMemoryUserRepository->find(1);
$this->assertInstanceOf(User::class, $user);
$this->assertEquals(new EmailAddress('aart.staartjes@hotmail.com'), $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 email
Persistence mechanism generates identity - Auto increment
Unique identity options
User provides identity - The email
Persistence mechanism generates identity - Auto increment
Application generates identity - UUID
Unique identity options
Let’s remove the magic
By 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, 'aart.staartjes@hotmail.com', 'Aart Staartjes')
}
public function can_register_user()
{
$id = Uuid::uuid4();
$this->registerUserHandler->handle(
new RegisterUser((string) $id, 'aart.staartjes@hotmail.com', 'Aart Staartjes')
);
}
public function can_register_user()
{
$id = Uuid::uuid4();
$this->registerUserHandler->handle(
new RegisterUser((string) $id, 'aart.staartjes@hotmail.com', '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 id
We 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, 'aart.staartjes@hotmail.com', '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 tested
But..
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 UserDomainModelException;
abstract class DomainException extends DomainException {}
namespace UserDomainModelException;
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, 'aart.staartjes@hotmail.com', 'Aart Staartjes')
);
} catch (DomainModelExceptionInvalidEmailAddressException $e) {
// Show invalid email error
}
try {
$id = UserId::createNew();
$this->commandBus>handle(
new RegisterUser((string) $id, 'aart.staartjes@hotmail.com', 'Aart Staartjes')
);
} catch (DomainModelExceptionInvalidEmailAddressException $e) {
// Show invalid email error
} catch (DomainModelExceptionDomainException $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 code
Testable code
What did we learn?
Intention revealing code
Testable code
Preventing the big ball of mud
What did we learn?
Intention revealing code
Testable code
Preventing the big ball of mud
Anemic domain models (anti pattern)
What did we learn?
Intention revealing code
Testable code
Preventing the big ball of mud
Anemic domain models (anti pattern)
Value objects
What did we learn?
Intention revealing code
Testable code
Preventing the big ball of mud
Anemic domain models (anti pattern)
Value objects
Decoupling 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)
Commands
Domain Events
What did we learn?
Writing fast tests (mocked environment)
Commands
Domain Events
Dependency inversion principle
What did we learn?
Writing fast tests (mocked environment)
Commands
Domain Events
Dependency inversion principle
Up front id generation strategy
What did we learn?
Writing fast tests (mocked environment)
Commands
Domain Events
Dependency inversion principle
Up front id generation strategy
Creating powerful domain exceptions
What did we learn?
Writing fast tests (mocked environment)
Commands
Domain Events
Dependency inversion principle
Up front id generation strategy
Creating powerful domain exceptions
Liskov 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 resources
http://www.slideshare.net/matthiasnoback/hexagonal-architecture-messageoriented-software-design
https://www.youtube.com/watch?v=Eg6m6mU0fH0
https://www.youtube.com/watch?v=mQsQ6QZ4dGg
https://kacper.gunia.me/blog/ddd-building-blocks-in-php-value-object
http://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+architecture
http://www.slideshare.net/cakper/2014-0407-php-spec-the-only-design-tool-you-need-4developers/117-
enablesRefactoring

Crafting beautiful software

Editor's Notes

  • #2 We are going to walk through the process of building an application. We will start with some poor decisions and improve bit by bit and explaining the choices I make Show techniques to decrease technical dept, improve code readability and maintainability So that we end up with beautiful software
  • #3 This is how every projects starts. A lot of excitement. A clean sheet This time I’m going to do everything the right way
  • #4 At the start the amount of progress is huge New features are added rapidly
  • #5 Then it slows down
  • #6 And after a year you are barely moving forward
  • #7 You slow down due to the increase of technical dept Technical debt will increase over time but your goal is to have a linear increase You want to prevent the exponential growth in technical debt You fix one thing and break two
  • #8 Let’s prevent an exponential growth in technical depth
  • #9 Lack of intention - You don’t immediately see what a piece of code is about
  • #10 Lack of intention - You don’t immediately see what a piece of code is about
  • #11 Heavy coupling - to delivery mechanisms like http and persistence mechanisms like MySQL en MongoDB
  • #12 Anemic domain models - Important models for the business that contain no behaviour and no validation
  • #15 We start with a requirement
  • #16 We create our user model with getters and setters
  • #17 We make a controller we check if the form is submitted and valid. We then persist the data. That one requirement handled.
  • #19 We require the name and email in the constructor. We then validate that they are not empty.
  • #20 The customer comes in with the next requirement
  • #21 We expand our email validation so that it also checks for the email format.
  • #22 It’s becoming messy already To solve this we can use a value object
  • #23 It’s becoming messy already To solve this we can use a value object
  • #24 We can create a value object like this
  • #25 The constructor simply takes the argument as a string
  • #26 We validate the the email is not empty
  • #27 And that the email is in a valid format
  • #28 And only then we assign the email to the user
  • #29 Add a toString got retrieve the emailaddress
  • #30 And we can add a check to check if two email addresses are the same.
  • #31 The user can simply accept an email without worrying about validation
  • #32 In the register user action we used to do $form->getData() to get a user
  • #33 We now have to pass the required arguments to the user __construction
  • #37 Our User is now always in a valid state It looks like a small step but we made huge progress. This means we can simplify other parts of the application
  • #38 If we can not rely on the User to be in a valid state we place validation all over our application
  • #39 Because we force our model to always be in a valid state We can simplify this code We can now just send our e-mail
  • #42 Equality is based on value The one 10 euro bill is the same as the other It can still be different bills
  • #43 If we were to create something for a bank, and care about each individual bill, then it would not be a value object.
  • #45 The user always has a name and a valid e-mail
  • #46 Everything clear? Do you also know how to cope with form validation?
  • #49 This is our current implementation, directly using the doctrine entity manager
  • #52 We can simplify the controller and just call the save method
  • #55 Render template Create message Send an email
  • #56 The controller is rendering templates, creating messages and sending e-mails We can easily extract the notifying
  • #58 We separate this into it’s own class We can now easily test it
  • #59 We can now simply call the notifier. The rendering, creating and sending of the mail message will be handled in the notifier.
  • #60 Persistence is handled by the repository And sending a confirmation message by the notifier
  • #63 We create a new api controller With a register action That returns a JSON response
  • #64 We get the request data
  • #65 Create and save the user
  • #66 Notify the user
  • #67 and return a JsonResponse
  • #69 We are duplicating the registration of a user This will become problematic when adding more ways to register a user or when adding new requirements to the registration
  • #71 One simple command to register a new user
  • #72 One simple handler Creates the user Saves the user And notifies the user
  • #73 One simple handler Creates the user Saves the user And notifies the user
  • #74 One simple handler Creates the user Saves the user And notifies the user
  • #75 One simple handler Creates the user Saves the user And notifies the user
  • #76 The web controller is now simply calling RegisterUserHandler::handle
  • #78 We now always register a user in the same way The command is always ignorant of the delivery mechanism. If we want to add an auto generated password we only have to add it in the handler
  • #79 There is always one to one relation
  • #85 A command bus is a generic command handler It receives a command and routes it to the handler It provides the ability to add middleware
  • #86 A command bus is a generic command handler It receives a command and routes it to the handler It provides the ability to add middleware
  • #87 A command bus is a generic command handler It receives a command and routes it to the handler It provides the ability to add middleware
  • #88 A command bus is a generic command handler It receives a command and routes it to the handler It provides the ability to add middleware
  • #89 A command bus is a generic command handler It receives a command and routes it to the handler It provides the ability to add middleware
  • #91 You can easily add logging to log every command in your application
  • #99 We are still sending a confirmation message from within the handler
  • #101 A simple object with the changed data
  • #102 The ContainsRecordedMessages interface tells that we expose events.
  • #103 The PrivateMessageRecorderCapabilities is a trait that allows us to record events and exposes them
  • #104 The User::register method is an explicit way of creating a user. It also takes care of recording the event for us.
  • #108 We can now let the eventbus publish all recorded messages
  • #110 Handler publishes the event, the notifier and the logger listen
  • #111 Some characteristics
  • #120 We started with a data structure but we now have an object with meaningful behaviour
  • #123 In our domain we have our UserRepository that uses doctrine to persist the user.
  • #124 Our RegisterUserHandler shouldn’t deal with low level persistence
  • #125 Our RegisterUserHandler shouldn’t deal with low level persistence
  • #128 We can now rely on the interface instead of the concrete doctrine implementation
  • #129 We are injecting the repository in the RegisterUserHandler The RegisterUserHandler lets the repository handle the persistence We don’t rely on low level details
  • #130 We are injecting the repository in the RegisterUserHandler The RegisterUserHandler lets the repository handle the persistence We don’t rely on low level details
  • #131 We are injecting the repository in the RegisterUserHandler The RegisterUserHandler lets the repository handle the persistence We don’t rely on low level details
  • #132 Small step with huge benefits
  • #133 Small step with huge benefits
  • #134 Small step with huge benefits
  • #138 The one relying on your interface only knows about the exceptions defined in that interface. It can not cope with every possible implementation
  • #145 So this is the entire structure
  • #146 The red classes are part of our domain. They contain the things that have the most value for our business. They contain the business rules
  • #147 The blue parts are infrastructural concerns.
  • #148 The green parts are part of the application
  • #149 The highlighted classes are part of our domain. The framework we happen to use is not important here
  • #150 The highlighted classes are part of our infrastructure. The framework we happen to use is not important here
  • #151 The highlighted classes are part of our application. The framework we happen to use is not important here
  • #152 We are also less coupled to the framework. A switch of framework or using a different framework for an api or microservice is now easier. We are using a structure independent of the framework
  • #155 I agree with this, because testable code is not always good code. But untestable code is almost always bad code.
  • #162 We now require the persistence layer to handle id generation And we don’t know the id until the user is registered When using a command we will never know the id
  • #187 There is still a problem We are using generic exceptions that are used by almost any library
  • #192 We can catch the very specific exception
  • #193 We can catch the general domain exception
  • #194 We can now catch the more explicit exceptions
  • #198 We used the hexagonal architecture consisting of the inside and the outside
  • #199 The application and the domain The hexagonal architecture is also called ports and adapters The number of ports on a hexagon is six. Six is an arbitrary number and not important here.
  • #201 The domain doesn’t know anything about the delivery or persistence mechanisms It contains our most import things, our business rules
  • #223 Please give me some feedback. Tell me what you liked and things I can improve
  • #224 Clean code, DDD in PHP, Implement domain driven design Carlos, the author of DDD in PHP is also present the Dutch PHP conference