Mar 23, 2015
Table of contents:
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 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.
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.
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.
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);
}
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.