Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
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->...
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 funct...
Requirement
A user needs a valid email
private function setEmail(string $email)
{
if ('' === $email) {
throw new InvalidArgumentException('E-mail is required');
...
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 InvalidArgumentExce...
final class EmailAddress
{
public function __construct(string $email)
{
if ('' === $email) {
throw new InvalidArgumentExce...
final class EmailAddress
{
public function __construct(string $email)
{
if ('' === $email) {
throw new InvalidArgumentExce...
final class EmailAddress
{
public function __construct(string $email)
{
if ('' === $email) {
throw new InvalidArgumentExce...
final class EmailAddress
{
public function __construct(string $email)
{
if ('' === $email) {
throw new InvalidArgumentExce...
class User
{
public function __construct(EmailAddress $email, string $name)
{
$this->setEmail($email);
$this->setName($nam...
// Before
$user = $form->getData();
Controller
// Before
$user = $form->getData();
//After
$data = $form->getData();
$user = new User(new EmailAddress($data['email']), $...
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 ...
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 $tenEu...
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 clas...
public function registerAction(Request $request) : Response
{
// [..]
$user = new User(new EmailAddress($data['email']), $...
Let’s move the persistence
out of the controller
class DoctrineUserRepository
{
//[..]
public function save(User $user)
{
$this->em->persist($user);
$this->em->flush();
}
...
public function registerAction(Request $request) : Response
{
// [..]
$user = new User(new EmailAddress($data['email']), $...
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();
$me...
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->renderTempla...
public function registerAction(Request $request)
{
if ($form->isValid()) {
// [..]
$this->userRegisteredNotifier->notify($...
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 Use...
public function registerAction(Request $request) : JsonResponse
{
$data = $this->getRequestData($request);
$user = new Use...
public function registerAction(Request $request) : JsonResponse
{
$data = $this->getRequestData($request);
$user = new Use...
$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)
{
...
class RegisterUserHandler
{
//[..]
public function handle(RegisterUser $registerUser)
{
}
}
Command handler
class RegisterUserHandler
{
//[..]
public function handle(RegisterUser $registerUser)
{
$user = new User(new EmailAddress(...
class RegisterUserHandler
{
//[..]
public function handle(RegisterUser $registerUser)
{
$user = new User(new EmailAddress(...
class RegisterUserHandler
{
//[..]
public function handle(RegisterUser $registerUser)
{
$user = new User(new EmailAddress(...
public function registerAction(Request $request) : Response
{
if ($form->isValid()) {
// [..]
$data = $form->getData();
$t...
public function registerAction(Request $request) : JsonResponse
{
// [..]
$this->registerUserHandler->handle(
new Register...
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...
A great command bus
implementation
github.com/SimpleBus/MessageBus
public function handle($command, callable $next)
{
$this->logger->log($this->level, 'Start, [command => $command]);
$next(...
public function handle($command, callable $next)
{
if ($this->canBeDelayed($command)) {
$this->commandQueue->add($command)...
//Before
$this->registerUserHandler->handle(
new RegisterUser($data['email'], $data['name'])
);
//After
$this->commandBus-...
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 $emailAddr...
class User implements ContainsRecordedMessages
{
//[..]
}
Model
class User implements ContainsRecordedMessages
{
use PrivateMessageRecorderCapabilities;
//[..]
}
Model
class User implements ContainsRecordedMessages
{
use PrivateMessageRecorderCapabilities;
public static function register(E...
class User implements ContainsRecordedMessages
{
use PrivateMessageRecorderCapabilities;
public static function register(E...
class User implements ContainsRecordedMessages
{
use PrivateMessageRecorderCapabilities;
public static function register(E...
class User implements ContainsRecordedMessages
{
use PrivateMessageRecorderCapabilities;
public static function register(E...
class RegisterUserHandler
{
//[..]
public function handle(RegisterUser $registerUser)
{
// save user
foreach ($user->recor...
class NotifyUserWhenUserIsRegistered
{
//[..]
public function handle(UserIsRegistered $userIsRegistered)
{
$this->userRegi...
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 d...
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();
}
...
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)
{
/...
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...
Be clear about your exceptions
Some note
interface UserRepository
{
/**
* @throws UserNotFoundException
* @throws ServiceUnavailableException
*/
public function fi...
class DoctrineUserRepository implements UserRepositoryInterface
{
/**
* @throws DoctrineDBALExceptionConnectionException
*...
Don’t do this
class DoctrineUserRepository implements UserRepositoryInterface
{
/**
* @throws DoctrineDBALExceptionConnect...
class DoctrineUserRepository implements UserRepositoryInterface
{
public function find(UserId $userId) : User
{
try {
if (...
class DoctrineUserRepository implements UserRepositoryInterface
{
public function find(UserId $userId) : User
{
try {
if (...
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...
src/UserBundle/
├── Command
├── Controller
├── Entity
├── Event
├── Form
├── Notifier
├── Repository
└── ValueObject
Let’s...
src/UserBundle/
├── Command
│ ├── RegisterUser.php
│ └── RegisterUserHandler.php
├── Controller
│ ├── RegisterUserApiContr...
src/UserBundle/
├── Command
│ ├── RegisterUser.php
│ └── RegisterUserHandler.php
├── Controller
│ ├── RegisterUserApiContr...
src/UserBundle/
├── Command
│ ├── RegisterUser.php
│ └── RegisterUserHandler.php
├── Controller
│ ├── RegisterUserApiContr...
src/UserBundle/
├── Command
│ ├── RegisterUser.php
│ └── RegisterUserHandler.php
├── Controller
│ ├── RegisterUserApiContr...
src/UserBundle/
├── Command
│ ├── RegisterUser.php
│ └── RegisterUserHandler.php
├── Controller
│ ├── RegisterUserApiContr...
src/UserBundle/
├── Command
│ ├── RegisterUser.php
│ └── RegisterUserHandler.php
├── Controller
│ ├── RegisterUserApiContr...
src/UserBundle/
├── Command
│ ├── RegisterUser.php
│ └── RegisterUserHandler.php
├── Controller
│ ├── RegisterUserApiContr...
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', 'A...
public function can_register_user()
{
$this->registerUserHandler->handle(new RegisterUser('aart.staartjes@hotmail.com', 'A...
public function can_register_user()
{
$this->registerUserHandler->handle(new RegisterUser('aart.staartjes@hotmail.com', 'A...
There was 1 error:
1) UserDomainModelUserRegisterUserTest::can_register_user
UserDomainModelExceptionUserNotFoundException...
public function can_register_user()
{
$this->registerUserHandler->handle(new RegisterUser('aart.staartjes@hotmail.com', 'A...
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 identit...
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(U...
public function can_register_user()
{
$id = Uuid::uuid4();
}
public function can_register_user()
{
$id = Uuid::uuid4();
new RegisterUser((string) $id, 'aart.staartjes@hotmail.com', 'A...
public function can_register_user()
{
$id = Uuid::uuid4();
$this->registerUserHandler->handle(
new RegisterUser((string) $...
public function can_register_user()
{
$id = Uuid::uuid4();
$this->registerUserHandler->handle(
new RegisterUser((string) $...
phpunit --bootstrap=vendor/autoload.php test/
PHPUnit 5.3.2 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 1...
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 InvalidArgumentExce...
Now we know exactly what
we are talking about
public function can_register_user()
{
$id = UserId::createNew();
$this->registerUserHandler->handle(
new RegisterUser((str...
phpunit --bootstrap vendor/autoload.php src/JO/User/
PHPUnit 4.8.11 by Sebastian Bergmann and contributors.
.
Time: 266 ms...
Everything clear?
Let’s test our value objects
public function can_not_create_invalid_user_id()
{
$this->expectException(InvalidArgumentException::class);
UserId::fromSt...
public function can_not_create_invalid_email_address()
{
$this->expectException(InvalidArgumentException::class);
new Emai...
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::fromSt...
$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 InvalidEmailAddressExc...
try {
$id = UserId::createNew();
$this->commandBus>handle(
new RegisterUser((string) $id, 'aart.staartjes@hotmail.com', 'A...
try {
$id = UserId::createNew();
$this->commandBus>handle(
new RegisterUser((string) $id, 'aart.staartjes@hotmail.com', 'A...
public function can_not_create_invalid_user_id()
{
$this->expectException(InvalidUserIdException::class);
UserId::fromStri...
PHPUnit 5.3.2 by Sebastian Bergmann and contributors.
... 3 / 3 (100%)
Time: 122 ms, Memory: 8.00Mb
OK (3 tests, 6 asserti...
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 lear...
Intention revealing code
Testable code
Preventing the big ball of mud
Anemic domain models (anti pattern)
Value objects
Wh...
Intention revealing code
Testable code
Preventing the big ball of mud
Anemic domain models (anti pattern)
Value objects
De...
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 ...
What did we learn?
Writing fast tests (mocked environment)
Commands
Domain Events
Dependency inversion principle
Up front ...
What did we learn?
Writing fast tests (mocked environment)
Commands
Domain Events
Dependency inversion principle
Up front ...
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 t...
We wrote intention revealing code. Separated the
domain, infrastructure and application. Created
abstractions to improve t...
We wrote intention revealing code. Separated the
domain, infrastructure and application. Created
abstractions to improve t...
We wrote intention revealing code. Separated the
domain, infrastructure and application. Created
abstractions to improve t...
We wrote intention revealing code. Separated the
domain, infrastructure and application. Created
abstractions to improve t...
Questions?Questions?
Please rate!
joind.in/17557
Further reading
Used resources
http://www.slideshare.net/matthiasnoback/hexagonal-architecture-messageoriented-software-design
https://www...
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Crafting beautiful software
Upcoming SlideShare
Loading in …5
×

Crafting beautiful software

0 views

Published on

All projects start with a lot of enthusiasm. As many projects grow the technical debt gets bigger and the enthusiasm gets less. Almost any developer can develop a great project, but the key is maintaining an ever evolving application with minimal technical debt without loosing enthusiasm.

During this talk you will be taken on the journey of application design. The starting point is an application that looks fine but contains lots of potential pitfalls. We will address the problems and solve them with beautiful design. We end up with testable, nicely separated software with a clear intention.

Published in: Software
  • Be the first to comment

Crafting beautiful software

  1. 1. Crafting beautiful software
  2. 2. The start
  3. 3. After a while
  4. 4. A year later
  5. 5. Source: fideloper.com/hexagonal- architecture
  6. 6. Let’s prevent an exponential growth in technical debt
  7. 7. Problems with the PHP industry standard
  8. 8. Problems with the PHP industry standard Lack of intention
  9. 9. Problems with the PHP industry standard Lack of intention Heavy coupling
  10. 10. Problems with the PHP industry standard Lack of intention Heavy coupling Anemic domain models
  11. 11. $ whoami Jorn Oomen Freelance PHP Web developer Good weather cyclist linkedin.com/in/jornoomen @jornoomen
  12. 12. Let’s craft beautiful software
  13. 13. Requirement A user needs to be registered
  14. 14. class User { private $id; private $name; private $email; // Getters and setters } Model
  15. 15. 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
  16. 16. Requirement A user has to have a name and an email
  17. 17. 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
  18. 18. Requirement A user needs a valid email
  19. 19. 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
  20. 20. It’s becoming messy already
  21. 21. It’s becoming messy already A case for the value object
  22. 22. final class EmailAddress { } Value object
  23. 23. final class EmailAddress { public function __construct(string $email) { } } Value object
  24. 24. final class EmailAddress { public function __construct(string $email) { if ('' === $email) { throw new InvalidArgumentException('E-mail is required'); } } } Value object
  25. 25. 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
  26. 26. 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
  27. 27. 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
  28. 28. 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
  29. 29. class User { public function __construct(EmailAddress $email, string $name) { $this->setEmail($email); $this->setName($name); } //[..] } Model
  30. 30. // Before $user = $form->getData(); Controller
  31. 31. // Before $user = $form->getData(); //After $data = $form->getData(); $user = new User(new EmailAddress($data['email']), $data['name']) Controller
  32. 32. Recap
  33. 33. Recap Email validation is handled by the value object
  34. 34. Recap Email validation is handled by the value object Name and email are required constructor arguments
  35. 35. 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
  36. 36. if (!empty($user->getEmail())) { if (filter_var($user->getEmail(), FILTER_VALIDATE_EMAIL)) { //Send an email } } Before
  37. 37. //Send an email After
  38. 38. “A small simple object, like money or a date range, whose equality isn't based on identity.” Martin Fowler
  39. 39. Value objects Small logical concepts Contain no identity Are immutable Equality is based on value
  40. 40. ==
  41. 41. $fiveEur = MoneyMoney::EUR(500); $tenEur = $fiveEur->add($fiveEur); echo $fiveEur->getAmount(); // outputs 500 echo $tenEur->getAmount(); // outputs 1000 Immutability example
  42. 42. We have now enforced our business rules
  43. 43. Everything clear?
  44. 44. Some observation We are now directly using doctrine for persistence
  45. 45. 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
  46. 46. public function registerAction(Request $request) : Response { // [..] $user = new User(new EmailAddress($data['email']), $data['name']); $this->em->persist($user); $this->em>flush(); } Controller
  47. 47. Let’s move the persistence out of the controller
  48. 48. class DoctrineUserRepository { //[..] public function save(User $user) { $this->em->persist($user); $this->em->flush(); } } Repository
  49. 49. public function registerAction(Request $request) : Response { // [..] $user = new User(new EmailAddress($data['email']), $data['name']); $this->userRepository->save($user); } Controller
  50. 50. Doctrine is great but we don’t want to marry it A switch of persistence can be done by changing a single class
  51. 51. Requirement A user needs to receive a registration confirmation
  52. 52. public function registerAction(Request $request) { if ($form->isValid()) { // [..] $content = $this->renderTemplate(); $message = $this->createMailMessage($content, $user); $this->mailer->send($message); } } Controller
  53. 53. The controller is getting too FAT
  54. 54. Let’s move the notifying out of the controller
  55. 55. class UserRegisteredNotifier { //[..] public function notify(string $email, string $name) { $content = $this->renderTemplate(); $message = $this->createMailMessage($content, $email, $name); $this->mailer->send($message); } } Notifier
  56. 56. public function registerAction(Request $request) { if ($form->isValid()) { // [..] $this->userRegisteredNotifier->notify($data['email'], $data['name']); } } Controller
  57. 57. So this looks better already
  58. 58. Everything clear?
  59. 59. Requirement User registration has to be available through an API
  60. 60. public function registerAction(Request $request) : JsonResponse { } Controller
  61. 61. public function registerAction(Request $request) : JsonResponse { $data = $this->getRequestData($request); } Controller
  62. 62. public function registerAction(Request $request) : JsonResponse { $data = $this->getRequestData($request); $user = new User(new EmailAddress($data['email'], $data['name']); $this->userRepository->save($user); } Controller
  63. 63. 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
  64. 64. 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
  65. 65. $this->userRepository->save($user); $this->userRegisteredNotifier->notify($data['email'], $data['name']); Controller
  66. 66. Introducing the command pattern
  67. 67. class RegisterUser // A command always has a clear intention { public function __construct(string $email, string $name) { $this->email = $email; $this->name = $name; } // Getters } Command
  68. 68. class RegisterUserHandler { //[..] public function handle(RegisterUser $registerUser) { } } Command handler
  69. 69. class RegisterUserHandler { //[..] public function handle(RegisterUser $registerUser) { $user = new User(new EmailAddress($registerUser->getEmail()), $registerUser->getName()); } } Command handler
  70. 70. class RegisterUserHandler { //[..] public function handle(RegisterUser $registerUser) { $user = new User(new EmailAddress($registerUser->getEmail()), $registerUser->getName()); $this->userRepository->save($user); } } Command handler
  71. 71. 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
  72. 72. public function registerAction(Request $request) : Response { if ($form->isValid()) { // [..] $data = $form->getData(); $this->registerUserHandler->handle( new RegisterUser($data['email'], $data['name']) ); } } Controller
  73. 73. public function registerAction(Request $request) : JsonResponse { // [..] $this->registerUserHandler->handle( new RegisterUser($data['email'], $data['name']) ); return new JsonResponse([]); } Controller
  74. 74. Commands
  75. 75. Commands Only contain a message
  76. 76. Commands Only contain a message Have a clear intention (explicit)
  77. 77. Commands Only contain a message Have a clear intention (explicit) Are immutable
  78. 78. Commands Only contain a message Have a clear intention (explicit) Are immutable Command handlers never return a value
  79. 79. Commands and the command bus
  80. 80. A command bus is a generic command handler Commands and the command bus
  81. 81. Commands and the command bus A command bus is a generic command handler It receives a command and routes it to the handler
  82. 82. 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
  83. 83. A great command bus implementation github.com/SimpleBus/MessageBus
  84. 84. 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
  85. 85. public function handle($command, callable $next) { if ($this->canBeDelayed($command)) { $this->commandQueue->add($command); } else { $next($command); } } Queueing middleware example
  86. 86. //Before $this->registerUserHandler->handle( new RegisterUser($data['email'], $data['name']) ); //After $this->commandBus->handle( new RegisterUser($data['email'], $data['name']) ); Controller
  87. 87. The command bus Provides the ability to add middleware
  88. 88. The command bus Provides the ability to add middleware Now logs every command for us
  89. 89. The command bus Provides the ability to add middleware Now logs every command for us Allows queueing of (slow) commands
  90. 90. Everything clear?
  91. 91. The handler is still dealing with secondary concerns
  92. 92. Introducing domain events
  93. 93. class UserIsRegistered // An event tells us what has happened { public function __construct(int $userId, string $emailAddress, string $name) {} // Getters } Event
  94. 94. class User implements ContainsRecordedMessages { //[..] } Model
  95. 95. class User implements ContainsRecordedMessages { use PrivateMessageRecorderCapabilities; //[..] } Model
  96. 96. class User implements ContainsRecordedMessages { use PrivateMessageRecorderCapabilities; public static function register(EmailAddress $email, string $name) : self { } //[..] } Model
  97. 97. class User implements ContainsRecordedMessages { use PrivateMessageRecorderCapabilities; public static function register(EmailAddress $email, string $name) : self { $user = new self($email, $name); } //[..] } Model
  98. 98. 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
  99. 99. 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
  100. 100. class RegisterUserHandler { //[..] public function handle(RegisterUser $registerUser) { // save user foreach ($user->recordedMessages() as $event) { $this->eventBus->handle($event); } } } Command handler
  101. 101. class NotifyUserWhenUserIsRegistered { //[..] public function handle(UserIsRegistered $userIsRegistered) { $this->userRegisteredNotifier->notify($userIsRegistered->getEmail(), $userIsRegistered->getName()); } } Event listener
  102. 102. Domain events
  103. 103. Domain events Are in past tense
  104. 104. Domain events Are in past tense Are always immutable
  105. 105. Domain events Are in past tense Are always immutable Can have zero or more listeners
  106. 106. So we are pretty happy now Controller creates simple command Mailing doesn’t clutter our code
  107. 107. We now have a rich user model
  108. 108. We now have a rich user model It contains data
  109. 109. We now have a rich user model It contains data It contains validation
  110. 110. We now have a rich user model It contains data It contains validation It contains behaviour
  111. 111. “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)
  112. 112. Everything clear?
  113. 113. We are still coupled to doctrine for our persistence
  114. 114. 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
  115. 115. We shouldn’t depend on any persistence implementation
  116. 116. We shouldn’t depend on any persistence implementation A case for the dependency inversion principle
  117. 117. interface UserRepository { public function save(User $user); public function find(int $userId) : User; } Repository
  118. 118. class DoctrineUserRepository implements UserRepositoryInterface { //[..] } Repository
  119. 119. class RegisterUserHandler { public function __construct(UserRepositoryInterface $userRepository, MessageBus $eventBus) { //[..] } } Command handler
  120. 120. DI: Dependency injection Our situation
  121. 121. DI: Dependency injection IoC: Inversion of control Our situation
  122. 122. DI: Dependency injection IoC: Inversion of control DIP: Dependency inversion principle Our situation
  123. 123. Tests - InMemoryUserRepository Decoupling provides options
  124. 124. Tests - InMemoryUserRepository Development - MysqlUserRepository Decoupling provides options
  125. 125. Tests - InMemoryUserRepository Development - MysqlUserRepository Production - WebserviceUserRepository Decoupling provides options
  126. 126. Be clear about your exceptions Some note
  127. 127. interface UserRepository { /** * @throws UserNotFoundException * @throws ServiceUnavailableException */ public function find(int $userId) : User; //[..] } Repository
  128. 128. 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
  129. 129. 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
  130. 130. 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
  131. 131. 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
  132. 132. The promise of a repository interface is now clear, simple and implementation independent
  133. 133. Everything clear?
  134. 134. src/UserBundle/ ├── Command ├── Controller ├── Entity ├── Event ├── Form ├── Notifier ├── Repository └── ValueObject Let’s look at the structure
  135. 135. 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
  136. 136. 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
  137. 137. 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
  138. 138. 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
  139. 139. 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
  140. 140. 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
  141. 141. 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
  142. 142. 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
  143. 143. We are looking pretty good Application, domain and infrastructural concerns are separated.
  144. 144. Everything clear?
  145. 145. Let’s test this awesome software
  146. 146. public function can_register_user() { }
  147. 147. public function can_register_user() { $this->registerUserHandler->handle(new RegisterUser('aart.staartjes@hotmail.com', 'Aart Staartjes')); }
  148. 148. public function can_register_user() { $this->registerUserHandler->handle(new RegisterUser('aart.staartjes@hotmail.com', 'Aart Staartjes')); $user = $this->inMemoryUserRepository->find(1); }
  149. 149. 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]); }
  150. 150. There was 1 error: 1) UserDomainModelUserRegisterUserTest::can_register_user UserDomainModelExceptionUserNotFoundException: User with id 1 not found FAILURES! Tests: 1, Assertions: 0, Errors: 1.
  151. 151. 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]); }
  152. 152. We rely on the magic of the persistence layer
  153. 153. User provides identity - The email Unique identity options
  154. 154. User provides identity - The email Persistence mechanism generates identity - Auto increment Unique identity options
  155. 155. User provides identity - The email Persistence mechanism generates identity - Auto increment Application generates identity - UUID Unique identity options
  156. 156. Let’s remove the magic By implementing an up front id generation strategy
  157. 157. composer require ramsey/uuid
  158. 158. 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
  159. 159. public function can_register_user() { $id = Uuid::uuid4(); }
  160. 160. public function can_register_user() { $id = Uuid::uuid4(); new RegisterUser((string) $id, 'aart.staartjes@hotmail.com', 'Aart Staartjes') }
  161. 161. public function can_register_user() { $id = Uuid::uuid4(); $this->registerUserHandler->handle( new RegisterUser((string) $id, 'aart.staartjes@hotmail.com', 'Aart Staartjes') ); }
  162. 162. 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 }
  163. 163. 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)
  164. 164. Tested
  165. 165. But a Uuid can still be any id We can be more explicit
  166. 166. 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
  167. 167. Now we know exactly what we are talking about
  168. 168. 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 }
  169. 169. 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)
  170. 170. Everything clear?
  171. 171. Let’s test our value objects
  172. 172. public function can_not_create_invalid_user_id() { $this->expectException(InvalidArgumentException::class); UserId::fromString('invalid format'); }
  173. 173. public function can_not_create_invalid_email_address() { $this->expectException(InvalidArgumentException::class); new EmailAddress('invalid format'); }
  174. 174. PHPUnit 5.3.2 by Sebastian Bergmann and contributors. ... 3 / 3 (100%) Time: 120 ms, Memory: 8.00Mb
  175. 175. Great that’s tested But..
  176. 176. 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'); }
  177. 177. $this->commandBus>handle( new RegisterUser('invalid id', 'invalid email', $name = '’) );
  178. 178. We still have limited control over our exceptions Useful domain exceptions can give us more control
  179. 179. namespace UserDomainModelException; abstract class DomainException extends DomainException {}
  180. 180. namespace UserDomainModelException; abstract class DomainException extends DomainException {} class InvalidEmailAddressException extends DomainException {} class InvalidUserIdException extends DomainException {} class NoEmailAddressProvidedException extends DomainException {}
  181. 181. try { $id = UserId::createNew(); $this->commandBus>handle( new RegisterUser((string) $id, 'aart.staartjes@hotmail.com', 'Aart Staartjes') ); } catch (DomainModelExceptionInvalidEmailAddressException $e) { // Show invalid email error }
  182. 182. 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 }
  183. 183. 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'); }
  184. 184. PHPUnit 5.3.2 by Sebastian Bergmann and contributors. ... 3 / 3 (100%) Time: 122 ms, Memory: 8.00Mb OK (3 tests, 6 assertions)
  185. 185. Everything clear?
  186. 186. So what did we create?
  187. 187. Intention revealing code What did we learn?
  188. 188. Intention revealing code Testable code What did we learn?
  189. 189. Intention revealing code Testable code Preventing the big ball of mud What did we learn?
  190. 190. Intention revealing code Testable code Preventing the big ball of mud Anemic domain models (anti pattern) What did we learn?
  191. 191. Intention revealing code Testable code Preventing the big ball of mud Anemic domain models (anti pattern) Value objects What did we learn?
  192. 192. 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?
  193. 193. What did we learn? Writing fast tests (mocked environment)
  194. 194. What did we learn? Writing fast tests (mocked environment) Commands
  195. 195. What did we learn? Writing fast tests (mocked environment) Commands Domain Events
  196. 196. What did we learn? Writing fast tests (mocked environment) Commands Domain Events Dependency inversion principle
  197. 197. What did we learn? Writing fast tests (mocked environment) Commands Domain Events Dependency inversion principle Up front id generation strategy
  198. 198. What did we learn? Writing fast tests (mocked environment) Commands Domain Events Dependency inversion principle Up front id generation strategy Creating powerful domain exceptions
  199. 199. 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
  200. 200. We wrote intention revealing code. Separated the
  201. 201. We wrote intention revealing code. Separated the domain, infrastructure and application. Created
  202. 202. We wrote intention revealing code. Separated the domain, infrastructure and application. Created abstractions to improve testability and flexibility. We
  203. 203. 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
  204. 204. 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
  205. 205. 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.
  206. 206. 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!
  207. 207. Questions?Questions?
  208. 208. Please rate! joind.in/17557
  209. 209. Further reading
  210. 210. 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

×