by Woody Gilk
@shadowhand
Lone Star PHP 2016
A domain model is a system of abstractions that describes selected aspects of a sphere of knowledge, influence, or activity.
* According to your presenter.
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.
A user must provide the following information:
As a manager,
I want to list all employees based on criteria,
so that I can find specific employees.
A manager has the following options:
And every search must be performed against
the manager's currently selected account.
<?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);
A class should only have one purpose.
Kind of like a function.
<?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;
}
<?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);
}
}
Classes should be shared by abstraction.
Write interfaces, not extensions.
<?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);
}
}
An interface should be as small as possible.
Interfaces can be implemented together.
<?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;
}
}
<?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));
}
}
All code should be testable.
It is the only way achieve isolation.
Converted legacy code to domain code.
Abstracted the persistence layer.
Centralized validation.
Wrote tests.