Mar 16, 2015
Table of contents:
An important part of Cribbb is the ability to join a Group. Groups are a way to organise content and discussions around certain topics.
We’ve already created the Domain Layer of the Groups Bounded Context in Creating Domain Objects Recap.
In today’s tutorial we are going to be looking at creating three Application Services related to the Groups Bounded Context.
Firstly we will look at retrieving a Group by it’s slug. The slug is unique reference to the Group that can be used in a URL.
Secondly we will look at creating a new Group by using the Domain Service we created in a previous article.
And thirdly, we will look at adding the functionality to join an existing Group.
These three Application Services will lay the foundation for much of the Group functionality of Cribbb.
The first service we will be looking at will be to find a Group by it’s slug.
As with last week’s tutorial, this is definitely something that could be handled within the controller.
However, by wrapping it in an Application Service it means we maintain that clear line of separation.
The first thing to do is to create a new file called FindGroupBySlug.php
under the Application\Groups
namespace:
<?php namespace Cribbb\Application\Groups;
class FindGroupBySlug
{
}
Inject an instance of GroupRepository
through the __construct()
method and set it as a class property:
/**
* @var GroupRepository
*/
private $groups;
/**
* @var GroupRepository $groups
* @return void
*/
public function __construct(GroupRepository $groups)
{
$this->groups = $groups;
}
At this stage we can also create the test file FindGroupBySlugTest.php
:
<?php namespace Cribbb\Tests\Application\Groups;
use Mockery as m;
use Cribbb\Application\Groups\FindGroupBySlug;
class FindGroupBySlugTest extends \PHPUnit_Framework_TestCase
{
/** @var GroupRepository */
private $groups;
/** @var FindGrouBySlug */
private $service;
public function setUp()
{
$this->groups = m::mock("Cribbb\Domain\Model\Groups\GroupRepository");
$this->service = new FindGroupBySlug($this->groups);
}
}
In the setUp()
method, first I will mock an instance of GroupRepository
.
Next I will instantiate a new instance of FindGroupBySlug
and injected the mocked repository.
By instantiating the object in the setUp()
method it means that the class will be available for each test and so it saves us repeating this boilerplate code.
This Application Service is fairly simple and so we only need to implement a single method.
The find()
method should search the database for a Group of a given slug, and then return it if a result is found.
If the slug does not exist, we can throw a new Exception
:
/**
* Find a Group by it's slug
*
* @param string $slug
* @return Group
*/
public function find($slug)
{
$group = $this->groups->groupBySlug($slug);
if ($group) return $group;
throw new ValueNotFoundException("$slug is not a valid Group slug");
}
The ValueNotFoundException
can be caught in the framework layer and then re-thrown as a 404
.
To test this Application Service we can write the following two assertions.
First we can ensure an exception is thrown by telling the mocked repository to not return anything when it is called:
/** @test */
public function should_throw_exception_on_invalid_slug()
{
$this->setExpectedException('Cribbb\Domain\Model\ValueNotFoundException');
$this->groups->shouldReceive('groupBySlug')->once()->andReturn(null);
$this->service->find('cribbb');
}
Secondly we can ensure that a Group
should be returned from the method by telling the mocked repository to return a Group when it is called:
/** @test */
public function should_find_group()
{
$group = m::mock('Cribbb\Domain\Model\Groups\Group');
$this->groups->shouldReceive('groupBySlug')->once()->andReturn($group);
$group = $this->service->find('cribbb');
$this->assertInstanceOf('Cribbb\Domain\Model\Groups\Group', $group);
}
In this method the GroupRepository
should expect that the groupBySlug()
method is called once.
The next Application Service we will look at will be for creating a new Group.
As we’ve seen a couple of times recently, the purpose of this Application Service is to wrap this functionality and provide a public API.
The first thing we need to do is to create a new NewGroup.php
file under the Application\Groups
namespace:
<?php namespace Cribbb\Application\Groups;
class NewGroup
{
}
We can also inject an instance of the UserRepository
as well as the NewGroupService
:
/**
* @var UserRepository
*/
private $users;
/**
* @var NewGroupService
*/
private $service;
/**
* @var UserRepository $users
* @var NewGroupService $service
* @return void
*/
public function __construct(UserRepository $users, NewGroupService $service)
{
$this->users = $users;
$this->service = $service;
}
We can also create the NewGroupTest.php
file:
<?php namespace Cribbb\Application\Groups;
use Mockery as m;
use Cribbb\Application\Groups\NewGroup;
use Cribbb\Domain\Model\Identity\UserRepository;
use Cribbb\Domain\Services\Groups\NewGroupService;
class NewGroupTest extends \PHPUnit_Framework_TestCase
{
/** @var UserRepository */
private $users;
/** @var GroupRepository */
private $groups;
/** @var NewGroup */
private $service;
public function setUp()
{
$this->users = m::mock("Cribbb\Domain\Model\Identity\UserRepository");
$this->groups = m::mock("Cribbb\Domain\Model\Groups\GroupRepository");
$this->service = new NewGroup(
$this->users,
new NewGroupService($this->groups)
);
}
}
Once again in the setUp()
file I will create two mock objects for the two repositories and I will instantiate a new instance of NewGroup
and make it available as a class property.
The public API of this Application is fairly simple. We will need a single create()
method that should accept the id of the user who is creating the new group and the name the group that should be created:
/**
* Create a new Group
*
* @param string $user_id
* @param string $name
* @return Group
*/
public function create($user_id, $name)
{
}
The first thing we need to do is to turn the user id into an instance of User
.
We can do that in a private
method:
/**
* 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");
}
This method is basically exactly the same as the method from the FindGroupBySlug
Application Service from earlier. If the user id is not valid we can throw a ValueNotFoundException
to abort the process.
Once we have an instance of User
we can pass the object as well as the $name
of the new proposed Group to the Domain Service:
/**
* Create a new Group
*
* @param string $user_id
* @param string $name
* @return Group
*/
public function create($user_id, $name)
{
$user = $this->findUserById($user_id);
$group = $this->service->create($user, $name);
/** Dispatch Domain Events */
return $group;
}
If everything goes smoothly we will be returned a new instance of Group
that will be loaded with Domain Events.
At this point those Domain Events can now be dispatched.
And finally we can return the instance of Group
from the Application Service.
To test this Application Service we can write the following two assertions.
First we can check to make sure a ValueNotFoundException
is thrown when the user id is not valid. We can do this by instructing the mock object to return a null
when the userById
method is called:
/** @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', 'Cribbb');
}
Because this is a private
method we can’t call it directly.
In the next test we can assert that everything goes smoothly by asserting that we are returned an instance of Group
:
/** @test */
public function should_create_new_group()
{
$user = m::mock('Cribbb\Domain\Model\Identity\User');
$this->users->shouldReceive('userById')->once()->andReturn($user);
$this->groups->shouldReceive('groupOfName')->once()->andReturn(null);
$this->groups->shouldReceive('add')->once();
$group = $this->service->create('7c5e8127-3f77-496c-9bb4-5cb092969d89', 'Cribbb');
$this->assertInstanceOf('Cribbb\Domain\Model\Groups\Group', $group);
}
Finally we can create the Application Service for joining an existing group. This service should accept the user’s id and the group’s id as raw strings.
So the first thing to do is to create a new JoinGroup.php
file under the Application\Groups
namespace:
<?php namespace Cribbb\Application\Groups;
class JoinGroup
{
}
Inject the UserRepository
and the GroupRepository
through the __construct()
method and add them as class properties:
/**
* @var UserRepository
*/
private $users;
/**
* @var GroupRepository
*/
private $groups;
/**
* @var UserRepository $users
* @var GroupRepository $groups
* @return void
*/
public function __construct(UserRepository $users, GroupRepository $groups)
{
$this->users = $users;
$this->groups = $groups;
}
Once again, we can also create the JoinGroupTest.php
file at this stage too:
<?php namespace Cribbb\Tests\Application\Groups;
use Mockery as m;
use Cribbb\Application\Groups\JoinGroup;
class JoinGroupTest extends \PHPUnit_Framework_TestCase
{
/** @var UserRepository */
private $users;
/** @var GroupRepository */
private $groups;
/** @var JoinGroup */
private $service;
public function setUp()
{
$this->users = m::mock("Cribbb\Domain\Model\Identity\UserRepository");
$this->groups = m::mock("Cribbb\Domain\Model\Groups\GroupRepository");
$this->service = new JoinGroup($this->users, $this->groups);
}
}
In the setUp()
method, first mock both of the repositories. Next create a new instance of JoinGroup
and inject the two mock objects. This will make the service class available for each test.
The public API of this Application Service will be a single join()
method that will accept the $user_id
and the $group_id
:
/**
* Allow a User to Join a Group
*
* @param string $user_id
* @param string $group_id
* @return Group
*/
public function join($user_id, $group_id)
{
}
First we need to turn the raw id strings into instances of User
and Group
.
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 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");
}
Again, similarly to the previous Application Services, here we are searching the repository for the particular instance. If an instance is found we can return it. If nothing is found we can throw a new ValueNotFoundException
.
Once we have instances of User
and Group
we can call the addMember()
method on the Group
Domain Object and pass the User
instance:
/**
* Allow a User to Join a Group
*
* @param string $user_id
* @param string $group_id
* @return Group
*/
public function join($user_id, $group_id)
{
$user = $this->findUserById($user_id);
$group = $this->findGroupById($group_id);
$group->addMember($user);
/** Dispatch Domain Events */
return $group;
}
This action will likely generate new Domain Events that can now be dispatched at this stage.
Finally we can return the Group
from the method.
To test this Application Service we can write assertions that are fairly similar to what we’ve seen so far.
First we can assert that an ValueNotFoundException
is thrown if either the user or the group id is invalid. We can do that by instructing the mock objects to return null
:
/** @test */
public function should_throw_exception_on_invalid_user()
{
$this->setExpectedException('Cribbb\Domain\Model\ValueNotFoundException');
$this->users->shouldReceive('userById')->once()->andReturn(null);
$this->service->join('7c5e8127-3f77-496c-9bb4-5cb092969d89', 'a3d9e532-0ea8-4572-8e83-119fc49e4c6f');
}
/** @test */
public function should_throw_exception_on_invalid_group()
{
$this->setExpectedException('Cribbb\Domain\Model\ValueNotFoundException');
$this->users->shouldReceive('userById')->once()->andReturn(true);
$this->groups->shouldReceive('groupById')->once()->andReturn(null);
$this->service->join('7c5e8127-3f77-496c-9bb4-5cb092969d89', 'a3d9e532-0ea8-4572-8e83-119fc49e4c6f');
}
Next we can ensure that the user is added to the group correctly with the following test:
/** @test */
public function should_allow_user_to_join_the_group()
{
$user = m::mock('Cribbb\Domain\Model\Identity\User');
$group = m::mock('Cribbb\Domain\Model\Groups\Group');
$group->shouldReceive('addMember')->once();
$this->users->shouldReceive('userById')->once()->andReturn($user);
$this->groups->shouldReceive('groupById')->once()->andReturn($group);
$group = $this->service->join('7c5e8127-3f77-496c-9bb4-5cb092969d89', 'a3d9e532-0ea8-4572-8e83-119fc49e4c6f');
$this->assertInstanceOf('Cribbb\Domain\Model\Groups\Group', $group);
}
This test is asserting that the correct repository methods are called and that we are returned an instance of Group
. You could also assert that the returned $group
object has the correct number of members.
Today’s tutorial was a good chunk of the foundation for much of the Groups functionality of Cribbb.
We have of course missed some details along the way, but it’s better to get something working, rather than trying to take on the whole scope in one swing.
As with the last couple of tutorials, you could definitely deal with this kind of logic in the Controller.
It’s important that you weigh the pros and cons of a methodology. Sometimes abstracts can be good, and sometimes they can be bad. But you will only learn what you prefer by experimenting and learning from the experience.
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.