Mar 30, 2015
Table of contents:
Last week we looked at building an Application Service for creating new Thread
objects.
However, a thread is not very interesting without any posts. This week we will look at building an Application Service to create new Post
objects.
Create a new Post
object is quite similar to creating a new Thread
object as there are some important business rules that we need to enforce when creating a new Post
object.
In today’s tutorial we will look at writing an Application Service to create new Post
objects.
Before we get into writing code, first we will remind ourselves of the business rules around creating a new post.
A post must belong to a user and a thread, and it should be instantiated with a body.
In order to encapsulate the instantiation process of creating a new Post
object we added a method on the Thread
Domain Object. This ensures that when the Post
is created it will belong to a valid Thread
.
We implemented this functionality in Using Aggregates as a Gateway to Functionality.
The Thread
should own the Post
because the Post
does not make sense outside of the context of a Thread
. It therefore makes sense for the Thread
object to be responsible for creating new Post
objects.
So the first thing we will do will be to create a new file called NewPost.php
under the Discussion
namespace under Cribbb\Application
:
<?php namespace Cribbb\Application\Discussion;
class NewPost
{
}
We will need instances of UserRepository
, ThreadRepository
and PostRepository
so we can inject them through the __construct()
method and set them as class properties:
/**
* @var UserRepository
*/
private $users;
/**
* @var ThreadRepository
*/
private $threads;
/**
* @var PostRepository
*/
private $posts;
/**
* @var UserRepository $users
* @var ThreadRepository $threads
* @var PostRepository $posts
* @return void
*/
public function __construct(UserRepository $users, ThreadRepository $threads, PostRepository $posts)
{
$this->users = $users;
$this->threads = $threads;
$this->posts = $posts;
}
We can also create the NewPostTest.php
:
<?php namespace Cribbb\Tests\Application\Discussion;
use Mockery as m;
use Cribbb\Application\Discussion\NewPost;
class NewPostTest extends \PHPUnit_Framework_TestCase
{
/** @var UserRepository */
private $users;
/** @var ThreadRepository */
private $threads;
/** @var PostRepository */
private $posts;
/** @var NewPost */
private $service;
public function setUp()
{
$this->users = m::mock("Cribbb\Domain\Model\Identity\UserRepository");
$this->threads = m::mock(
"Cribbb\Domain\Model\Discussion\ThreadRepository"
);
$this->posts = m::mock("Cribbb\Domain\Model\Discussion\PostRepository");
$this->service = new NewPost(
$this->users,
$this->threads,
$this->posts
);
}
}
Before we write the tests, first I will implement the setUp()
method to mock the three repositories we need.
I will also instantiate a new instance of NewPost
and inject the mocked repositories. This can be set as a class property on the test class so that it is available for each test. This saves us the hassle of having to bootstrap the service class before each test.
The public API of this Application Service will be a single create()
method that accepts the $user_id
, $thread_id
and the $body
:
/**
* Create a new Post
*
* @param string $user_id
* @param string $thread_id
* @param string $body
* @return Post
*/
public function create($user_id, $thread_id, $body)
{
}
The first thing we need to do is to turn the raw ids into objects. We can do that with the following two 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 Thread by its id
*
* @param string $id
* @return Thread
*/
private function findThreadById($id)
{
$thread = $this->threads->threadById(ThreadId::fromString($id));
if ($thread) return $thread;
throw new ValueNotFoundException("$id is not a valid thread id");
}
Once we have instances of User
and Thread
we can call the createNewPost()
on the Thread
object and pass the User
and the $body
:
/**
* Create a new Post
*
* @param string $user_id
* @param string $thread_id
* @param string $body
* @return Post
*/
public function create($user_id, $thread_id, $body)
{
$user = $this->findUserById($user_id);
$thread = $this->findThreadById($thread_id);
$post = $thread->createNewPost($user, $body);
$this->posts->add($post);
/* Dispatch Domain Events */
return $post;
}
If everything goes smoothly we should be returned a new instance of Post
.
We can now add the Post
to the PostRepository
using the add()
method.
Finally we can dispatch any Domain Events that we generated and then return the $post
from the method.
To test this Application Service, we can write the following three assertions.
Firstly we can assert that an ValueNotFoundException
is thrown when the user id or the thread id is invalid:
/** @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_thread_id()
{
$this->setExpectedException('Cribbb\Domain\Model\ValueNotFoundException');
$this->users->shouldReceive('userById')->once()->andReturn(true);
$this->threads->shouldReceive('threadById')->once()->andReturn(null);
$this->service->create(
'7c5e8127-3f77-496c-9bb4-5cb092969d89',
'a3d9e532-0ea8-4572-8e83-119fc49e4c6f',
'hello world');
}
We can simulate that the either the user or the thread was not found by instructing the mocked repositories to return null
when the respective method is called.
Next we can assert that everything goes smoothly by informing the mocked objects what methods should be called and then asserting that we are returned an instance of Post
:
/** @test */
public function should_create_new_post()
{
$user = m::mock('Cribbb\Domain\Model\Identity\User');
$thread = m::mock('Cribbb\Domain\Model\Discussion\Thread');
$post = m::mock('Cribbb\Domain\Model\Discussion\Post');
$this->users->shouldReceive('userById')->once()->andReturn($user);
$this->threads->shouldReceive('threadById')->once()->andReturn($thread);
$thread->shouldReceive('createNewPost')->once()->andReturn($post);
$this->posts->shouldReceive('add')->once();
$post = $this->service->create(
'7c5e8127-3f77-496c-9bb4-5cb092969d89',
'a3d9e532-0ea8-4572-8e83-119fc49e4c6f',
'Hello World');
$this->assertInstanceOf('Cribbb\Domain\Model\Discussion\Post', $post);
}
This week’s post was the last of this little section on building out the Application layer of the application. We’ve covered all of the main functionality application and we now have a consistent way of sending in requests to the Domain.
The benefit of this approach is we have kept all of this code out of the Controller. Instead of dealing with looking up objects and calling Domain methods, we can abstract those details behind these Application services.
We can also “drive” the application using any number of different methods, rather than having that responsibility leak into the HTTP layer.
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.