cult3

Creating a new Thread Application Service

Mar 23, 2015

Table of contents:

  1. The business rules of starting a new thread
  2. Set up
  3. Implementation
  4. Tests
  5. Conclusion

An important aspect of the functionality of Cribbb is the ability to create new threads. A thread is a topic of discussion within a Group.

There are some important business rules around starting a new thread, and so we need to ensure that we enforce these rules through the objects we write.

In today’s tutorial we will look at creating the new thread Application Service.

The business rules of starting a new thread

The purpose of Cribbb is the ability to aggregate discussions around certain subjects and genres. Therefore, an important aspect of functionality for Cribbb is the ability to create new threads of discussion.

However, there are some important rules around how this should work.

Threads are created within a Group where that Group represents a certain subject or genre.

In order to create a new Thread, the User must be an existing member of the Group.

We can deal with this business rule by encapsulating it as a method on an existing Domain Object. In order to create a new Thread, you should have to call a method on the Group Entity.

This will ensure that the User is already a member of the Group, and that the created Thread will belong to the Group.

We have already encapsulated this business logic as part of the Group Entity in the tutorial Enforcing Business Rules through Aggregate Instantiation.

So hopefully that is all clear. If you are still unsure about how this business rule is enforced, take a look at the tutorial linked above.

Set up

So the first thing we need to do is to create a new Discussion namespace under Cribbb\Application and then create a new file called NewThread.php:

<?php namespace Cribbb\Application\Discussion;

class NewThread
{
}

Inject instances of UserRepository, GroupRepository and ThreadRepository through the __construct() method and set them as class properties:

/**
 * @var UserRepository
 */
private $users;

/**
 * @var GroupRepository
 */
private $groups;

/**
 * @var ThreadRepository
 */
private $threads;

/**
 * @var UserRepository $users
 * @var GroupRepository $groups
 * @var ThreadRepository $threads
 * @return void
 */
public function __construct(UserRepository $users, GroupRepository $groups, ThreadRepository $threads)
{
    $this->users = $users;
    $this->groups = $groups;
    $this->threads = $threads;
}

At this point we can also create the test file. Create a new file NewThreadTest.php:

<?php namespace Cribbb\Tests\Application\Discussion;

use Mockery as m;
use Cribbb\Application\Discussion\NewThread;

class NewGroupTest extends \PHPUnit_Framework_TestCase
{
    /** @var UserRepository */
    private $users;

    /** @var GroupRepository */
    private $groups;

    /** @var ThreadRepository */
    private $threads;

    /** @var NewThread */
    private $service;

    public function setUp()
    {
        $this->users = m::mock("Cribbb\Domain\Model\Identity\UserRepository");
        $this->groups = m::mock("Cribbb\Domain\Model\Groups\GroupRepository");
        $this->threads = m::mock(
            "Cribbb\Domain\Model\Discussion\ThreadRepository"
        );

        $this->service = new NewThread(
            $this->users,
            $this->groups,
            $this->threads
        );
    }
}

In the setUp() method we need to mock each of the repositories and then inject the mocks into a new instance of the NewThread service class.

By instantiating in the setUp() method we can save ourselves from repeating this bootstrapping code before each method.

Implementation

The NewThread class should have a single public create() method that accepts the user id, the group id and the subject of the new thread:

/**
 * Create a new Thread
 *
 * @param string $user_id
 * @param string $group_id
 * @param string $subject
 * @return Thread
 */
public function create($user_id, $group_id, $subject)
{

}

First we need to turn those raw ids into Domain Objects. As we’ve seen a couple of times over recent weeks, we can achieve that through a couple of private methods:

/**
 * Find a User by their id
 *
 * @param string $id
 * @return User
 */
private function findUserById($id)
{
    $user = $this->users->userById(UserId::fromString($id));

    if ($user) return $user;

    throw new ValueNotFoundException("$id is not a valid user id");
}

/**
 * Find a Group by its id
 *
 * @param string $id
 * @return Group
 */
private function findGroupById($id)
{
    $group = $this->groups->groupById(GroupId::fromString($id));

    if ($group) return $group;

    throw new ValueNotFoundException("$id is not a valid group id");
}

Now that we have instances of User and Group we can call the startNewThread() method on the Group Domain Object to create a new Thread:

/**
 * Create a new Thread
 *
 * @param string $user_id
 * @param string $group_id
 * @param string $subject
 * @return Thread
 */
public function create($user_id, $group_id, $subject)
{
    $user = $this->findUserById($user_id);
    $group = $this->findGroupById($group_id);

    $thread = $group->startNewThread($user, $subject);

    $this->threads->add($thread);

    /* Dispatch Domain Events */

    return $thread;
}

Once we have the new Thread object we can pass it to the ThreadRepository to store in the database.

At this point we can also dispatch any Domain Events and then finally we can return the Thread object from the method.

Tests

As we’ve seen a couple of times over the last couple of weeks, the tests for this service class are pretty straight forward.

First we can test to ensure that if an invalid user of group id is passed to the create() method, an ValueNotFoundException should be thrown:

/** @test */
public function should_throw_exception_on_invalid_user_id()
{
    $this->setExpectedException('Cribbb\Domain\Model\ValueNotFoundException');

    $this->users->shouldReceive('userById')->once()->andReturn(null);

    $this->service->create(
        '7c5e8127-3f77-496c-9bb4-5cb092969d89',
        'a3d9e532-0ea8-4572-8e83-119fc49e4c6f',
        'Hello World');
}

/** @test */
public function should_throw_exception_on_invalid_group_id()
{
    $this->setExpectedException('Cribbb\Domain\Model\ValueNotFoundException');

    $this->users->shouldReceive('userById')->once()->andReturn(true);
    $this->groups->shouldReceive('groupById')->once()->andReturn(null);

    $this->service->create(
        '7c5e8127-3f77-496c-9bb4-5cb092969d89',
        'a3d9e532-0ea8-4572-8e83-119fc49e4c6f',
        'hello world');
}

We can test to ensure this is working correctly by instructing the mock repositories to return null in each test.

Next we can ensure that everything is working as it should be by asserting that the methods on the repositories are called correctly and that we are returned a new instance of Thread:

/** @test */
public function should_create_new_thread()
{
    $user = m::mock('Cribbb\Domain\Model\Identity\User');
    $group = m::mock('Cribbb\Domain\Model\Groups\Group');
    $thread = m::mock('Cribbb\Domain\Model\Discussion\Thread');

    $this->users->shouldReceive('userById')->once()->andReturn($user);
    $this->groups->shouldReceive('groupById')->once()->andReturn($group);

    $group->shouldReceive('startNewThread')->once()->andReturn($thread);

    $this->threads->shouldReceive('add')->once();

    $thread = $this->service->create(
        '7c5e8127-3f77-496c-9bb4-5cb092969d89',
        'a3d9e532-0ea8-4572-8e83-119fc49e4c6f',
        'Hello World');

    $this->assertInstanceOf('Cribbb\Domain\Model\Discussion\Thread', $thread);
}

Conclusion

By enforcing the business rules as part of the existing domain object we’ve placed the responsibility in the right place and prevent it from leaking out.

This application service provides a nice and convenient way of accepting raw strings and then dealing with the actually creating the new thread.

The outside world does not need to know what goes on in this application service as it only needs to provide the raw inputs and get then return the right output.

This is a series of posts on building an entire Open Source application called Cribbb. All of the tutorials will be free to web, and all of the code is available on GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.