cult3

Creating the Groups Application Services

Mar 16, 2015

Table of contents:

  1. Finding a Group by it’s slug
  2. Creating a new Group
  3. Joining an existing Group
  4. Conclusion

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.

Finding a Group by it’s slug

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.

Set up

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.

Implementation

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.

Tests

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.

Creating a new Group

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.

Set Up

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.

Implementation

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.

Tests

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

Joining an existing 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.

Set up

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.

Implementation

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.

Tests

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.

Conclusion

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.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.