cult3

Creating a New Post Application Service

Mar 30, 2015

Table of contents:

  1. The business rules of creating a new Post
  2. Set up
  3. Implementation
  4. Test
  5. Conclusion

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.

The business rules of creating a new Post

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.

Set up

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.

Implementation

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.

Test

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);
}

Conclusion

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.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.