cult3

Creating the Follow a User Application Service

Mar 09, 2015

Table of contents:

  1. What is this Application Service going to do?
  2. Setting up the structure
  3. Finding a User by their Id
  4. Following another user
  5. Conclusion

A very important part of modern consumer web applications is the ability to “follow” another user.

The functionality to build a social graph within an application allows the user to curate their own experience with your product.

A user is also more likely to keep coming back if they can focus their experience with your product around their particular interests.

We’ve already created the ability to follow another user in the article Creating the Twitter Follower Model using Doctrine.

In today’s article we will be looking at exposing this functionality as an Application Service to make it part of the public API of the application.

What is this Application Service going to do?

Before we can build out a solution, first we need to understand the problem we face.

When a user wants to request to follow another user, they will be sending a request to our application.

This will contain the current user’s user_id and the intended friend’s user_id.

The problem we face is we need a way of capturing those ids, ensuring they are valid and then retrieving the Domain Objects to create the new relationship.

This could be handled in the Controller, but because we’re going all in on this whole Application Services thing, I think it makes more sense to maintain the consistency and wrap it in an Application Service instead.

Setting up the structure

So the first thing we need to do is to set up the structure. As with last week, I’m going to create a new file under Application\Identity called FollowUser.php:

<?php namespace Cribbb\Application\Identity;

class FollowUser
{
}

This class should be injected with an instance of the UserRepository:

/**
 * @var UserRepository
 */
private $users;

/**
 * @var UserRepository $users
 * @return void
 */
public function __construct(UserRepository $users)
{
    $this->users = $users;
}

At this point we can also create the FollowUserTest.php file:

<?php namespace Cribbb\Application\Identity;

use Mockery as m;
use Cribbb\Application\Identity\FollowUser;

class FollowUserTest extends \PHPUnit_Framework_TestCase
{
    /** @var UserRepository */
    private $users;

    /** @var FollowUser */
    private $service;

    public function setUp()
    {
        $this->users = m::mock("Cribbb\Domain\Model\Identity\UserRepository");

        $this->service = new FollowUser($this->users);
    }
}

As with the previous Application Service test files, I will be using the setUp() method to mock the UserRepository and to create a new instance of FollowUser that will be available for each test.

Finding a User by their Id

Next up we need to write a method that will find the user by their id. If the user is found we can return the object. If not, we can throw an Exception:

/**
 * 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");
}

We don’t need to expose this method outside of the class and so we’ll declare it as private.

First we use the UserRepository to find the user by their id. If the user is found, we can return the $user object.

If the user is not found we can throw an Exception. Something has gone wrong if the user is attempting to follow another user that does not exist.

Due to the fact that this is a private method we can’t test it directly.

Following another user

With the private method for finding a user by their id in place, we can now create the method to follow another user. This method should accept the user id of the user and the user they wish to follow as strings:

/**
 * Follow another User
 *
 * @param string $current_user_id
 * @param string $user_to_follow_id
 * @return User
 */
public function follow($current_user_id, $user_to_follow_id)
{
    $user = $this->findUserById($current_user_id);
    $friend = $this->findUserById($user_to_follow_id);

    $user->follow($friend);

    /** Dispatch Domain Events */

    return $user;
}

First we use the findUserById() to find both of the users. If either of the users is not found an Exception will be thrown and we can abort.

You can decide whether you want to catch the exception or let it bubble up.

Next we can call the follow() method on the $user object.

When a user follows another user, the $user object will be loaded with Domain Events. This will be useful for sending an email notification to the user to let them know they have just been followed.

Finally we can return the user from the method.

To ensure this method is working correctly, we can write the following two tests.

First we can assert that an Exception is thrown when one of the user ids 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->follow('7c5e8127-3f77-496c-9bb4-5cb092969d89', 'a3d9e532-0ea8-4572-8e83-119fc49e4c6f');
}

We can simulate an invalid id by instructing the mocked UserRepository to return a null.

Next we can assert that everything goes smoothly with the following test:

/** @test */
public function should_follow_other_user()
{
    $user = m::mock('Cribbb\Domain\Model\Identity\User');
    $user->shouldReceive('follow')->once();

    $friend = m::mock('Cribbb\Domain\Model\Identity\User');

    $this->users->shouldReceive('userById')->times(2)->andReturn($user, $friend);

    $user = $this->service->follow('7c5e8127-3f77-496c-9bb4-5cb092969d89', 'a3d9e532-0ea8-4572-8e83-119fc49e4c6f');

    $this->assertInstanceOf('Cribbb\Domain\Model\Identity\User', $user);
}

In this test I’m instructing the mocked UserRepository to return the two mocked User objects and for the $user object to expect the follow() method to be invoked.

Conclusion

Functionality like this is fairly easy to implement as there are many different ways in which you could implement it.

I think the benefit of this approach is that we’ve maintained the line between the framework and the application.

We use this layer to encapsulate the process of turning a raw id into a Domain Object.

This means the Controller does not need to know anything about the Domain Objects, all the Controller needs to be concerned about is sending in the request and then sending the response back out.

Another layer of indirection is certainly not always a good thing, but I think its important that you explore the boundaries between too much and not enough.

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.