Crafting beautiful software

Post on 14-Apr-2017

1,266 views 0 download

transcript

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('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) 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('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 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, '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 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, '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 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, 'aart.staartjes@hotmail.com', 'Aart Staartjes') );} catch (DomainModel\Exception\InvalidEmailAddressException $e) { // Show invalid email error}

try { $id = UserId::createNew(); $this->commandBus>handle( new RegisterUser((string) $id, 'aart.staartjes@hotmail.com', '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