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.

https://en.wikipedia.org/wiki/Domain_model

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

  • 1,350
Loading comments...

More from Woody Gilk