The Hidden Architecture
of the Domain
by Woody Gilk
@shadowhand
Lone Star PHP 2016
Who is this fella?
Tech Lead
Contributor
Husband & Father
And other things...
- Tea Drinker
- Bicyclist
- Cook
- @shadowhand
What is the domain?
A domain model is a system of abstractions that describes selected aspects of a sphere of knowledge, influence, or activity.
It is what your business does.
It is not:
- your persistence layer
- your service partners
- your marketing initiatives
It definitely is:
- your voice
- your unique solution
- your business rules
This is mission critical.
What process makes robust code?
Solid is:
- Single responsibility principle
- Open/closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
The most important diagram
in software development. *
* According to your presenter.
What are use cases?
user Stores become use cases
As a <type of user>,
I want to <perform this action>,
so that <this outcome happens>.
As a user who does not have an account,
I want to create an account,
so that I can view my schedule.
Acceptence Criteria
A user must provide the following information:
- First and last name
- A valid email address
- A valid phone number
- A strong password
As a manager,
I want to list all employees based on criteria,
so that I can find specific employees.
Acceptence Criteria
A manager has the following options:
- Show or hide pending employees
- Show or hide deleted employees
- A search phrase
- Sort the results by first or last name
And every search must be performed against
the manager's currently selected account.
User stories can be derived
from existing code or tests.
Let's get real.
Do you have legacy code?
<?php
// UserController::search
$options = [
'show_pending' => $this->query->toBoolean('show_pending', true),
'sorting' => $this->account->setting('schedule.sort_employees', 0),
'show_deleted' => $this->query->toBoolean('show_deleted', false),
'search' => $this->query->get('search', ''),
];
$users = Model_User::all($this->account, $options);
Single responsibility principle
A class should only have one purpose.
Kind of like a function.
SOLID
<?php
// UserController::search
// good: this is controller level code
$options = [
'show_pending' => $this->query->toBoolean('show_pending', true),
'sorting' => $this->account->setting('schedule.sort_employees', 0),
'show_deleted' => $this->query->toBoolean('show_deleted', false),
'search' => $this->query->get('search', ''),
];
// bad: this is domain specific code
$users = Model_User::all($this->account, $options);
<?php
// UserController::create
// good: marshalling data is a controller concern
$fields = $this->body->limit([
'first_name',
'last_name',
'email',
'password',
'phone_number',
]);
// bad: manipulating entities should only be done in the domain layer
$user = new Model_User;
foreach ($fields as $key => $val) {
if ($key == 'password') $val = Model_User::hash_password($val);
$user->{$key} = $val;
}
How do we separate concerns?
<?php
// UserController::search
// good: this is controller level code
$options = [
'show_pending' => $this->query->toBoolean('show_pending', true),
'sorting' => $this->account->setting('schedule.sort_employees', 0),
'show_deleted' => $this->query->toBoolean('show_deleted', false),
'search' => $this->query->get('search', ''),
];
// bad: this is domain specific code
$users = Model_User::all($this->account, $options);
How can we abstract this code?
<?php
// UserController::search
// good: this is controller level code
$options = [
'show_pending' => $this->query->toBoolean('show_pending', true),
'sorting' => $this->account->setting('schedule.sort_employees', 0),
'show_deleted' => $this->query->toBoolean('show_deleted', false),
'search' => $this->query->get('search', ''),
'account_id' => $this->account->id,
];
// better: controller is passing control to the domain
$users = $this->execute(SearchUsersCommand::class, $options);
<?php
final class SearchUsersCommand extends Command
{
private $users;
public function __construct(UsersRepository $users)
{
$this->users = $users;
}
public function execute(array $options)
{
// Searches can only be performed one account at a time
$this->assertContains($options, 'account_id');
return $this->users->search($options);
}
}
Open/closed principle
Classes should be shared by abstraction.
Write interfaces, not extensions.
SOLID
<?php
final class CreateUserCommand extends Command
{
private $users;
public function __construct(UsersRepository $users)
{
$this->users = $users;
}
public function execute(array $options)
{
// A user must provide all of these fields during registration
$this->assertContains($options, [
'first_name',
'last_name',
'email',
'phone_number',
'password',
]);
// ... and users must be created for a specific account
$this->assertContains($options, 'account_id');
// The email address and phone number must be valid
$this->assertValidEmail($options['email']);
$this->assertValidPhoneNumber($options['phone_number']);
// The password must meet minimum security requirements
$this->assertValidPassword($options['password']);
// ... and must never be stored in plaintext!
$options['password'] = password_hash($options['password'], PASSWORD_DEFAULT);
return $this->users->create($options);
}
}
What about that Repository?
Interface segregation principle
An interface should be as small as possible.
Interfaces can be implemented together.
SOLID
<?php
class ModelUsersRepository implements UsersRepository
{
public function search(array $options)
{
$account = Model_Account::factory($options['account_id']);
return Model_User::all($account, $options);
}
public function create(array $options)
{
$user = new Model_User;
$user->values($options);
$user->save();
return $user;
}
}
Nothing changed!
Before: no tests.
After: Happy Tests!
<?php
class SearchUsersCommandTest extends PHPUnit_Framework_TestCase
{
public function testExecute()
{
$options = [
'show_pending' => ...
];
$result = ['some users'];
$users = $this->getMock(UsersRepository::class);
$users->expects($this->once())
->method('search')
->with($options)
->willReturn($result);
$command = new SearchUsersCommand($users);
$this->assertSame($result, $command->execute($options));
}
}
Goodbye, framework.
Hello, Sanity.
Dependency inversion principle
All code should be testable.
It is the only way achieve isolation.
SOLID
Why bother?
- Being able to use right tool for the job
- Upgrading legacy code without breaking everything
- Reusing code in different contexts
- Anticipating changes in scaling
What did we do?
Converted legacy code to domain code.
Abstracted the persistence layer.
Centralized validation.
Wrote tests.
It works.
Go forth.
Make stuff.
BE AWESOME.
The Hidden Architecture of the Domain
By Woody Gilk
The Hidden Architecture of the Domain
- 2,082