cult3

Creating the Foundation of a User Settings System

Feb 09, 2015

Table of contents:

  1. A quick recap on what we’re building
  2. Important things to consider
  3. The Setting ID
  4. The Setting Entity
  5. Conclusion

Over the last couple of week’s we’ve been looking at building out a Notification System for Cribbb.

First we looked at Setting up a Notification system in PHP.

Last week we looked at Storing User Settings in a Relational Database

In my opinion, one of the most important aspects of building out a Notification System is giving your users granular control over what types of notification they receive. You don’t want to annoy your users because it will turn them away from your application and your emails will get marked as spam!

In today’s tutorial we’re going to look at building out a simple User Settings system for a Relational Database. This will allow the users of Cribbb to control which notifications they should receive.

User settings and preferences is quite a common requirement in web applications, so hopefully this tutorial will be useful for you in any number of future projects that you work on.

A quick recap on what we’re building

So before we get into the code, first we’ll do a quick recap of what we are building here and what business rules we are hoping to satisfy.

During the execution of Cribbb, certain key events should trigger a notification for existing users. For example, this might be if the user is followed by another user, or perhaps someone replied to their post.

These events will be triggered by the Domain Events of the application. This means we can add or remove listeners without disturbing the actual code that triggered the event. For more on Domain Events, see Implementing Domain Events.

When something of interest occurs, we will create a new Notification Entity that will represent the notification to the user. This object will be added to a pipeline of notifications that will be presented to the user.

By default, system notifications will be displayed as part of the User Interface and will be sent to the user via email.

The user should also be able to opt-in to certain types of notifications, such as replies to a particular thread of interest.

In either case, the user should be able to granularly control the notification she receives.

Important things to consider

Last week we looked at Storing User Settings in a Relational Database. I decided to use the Property Bag approach where you store each user setting as a key / value pair. I think this is the best solution when storing user settings in a relational database.

There are many potential stumbling blocks when building a user settings system, and so I think it’s good to be pragmatic and put a set of system rules in place from the beginning.

In Cribbb we have two types of notifications.

Firstly, we have system notifications such as “you have been followed by user X” or “user Y has mentioned you in a post”. By default all users should receive these notifications because they are useful for engagement and for getting the most out of the product. As the application grows, the number of default system notifications is likely to increase. This means that early users will need to be grandfathered in to new system settings.

Secondly, we have notifications such as following a thread to get notifications on new updates, or when a new users joins a group. These notifications should be opt-in because the user is likely going to only want to receive notifications from a small selection of sources.

I think when you face a situation like this, it’s important to be pragmatic and follow a convention to make life as easy as possible. I think there is really no need in creating a complex system when you can simply follow a few simple rules.

Our data store is a key / value store and so we can implement the above rules in the following way.

Users will be opted-in to system notifications by default. To opt-out, the user can tick a box that will add a new row to the settings table. When deciding whether to send a system notification, we can check to see if the user has opted-out. If the user has not opted-out, we will send the notification.

If a user wants to opt-in to an optional notification such as thread updates, we will create a new row in the settings table. When a new post is added to a thread we will query the settings table to find all of the users who have opted-in to those notifications to send the email. If the user wishes to stop receiving updates we can simply remove the row from the settings table.

By assuming these defaults we make dealing with notifications a whole lot easier and we remove the burden from having to manage all of the possible settings for each user.

So to summarise, system notifications will be enabled by default and will be opt-out. Thread and Post notifications will be disabled by default and will be opt-in.

This also means because we are following a convention we don’t need to have the value bit of the key / value pair as we will only need to save the key.

The Setting ID

Once again we need to create a new Identifier class and a new Entity class. At this initial stage these two classes are really similar to what we’ve created previously so I won’t go into too much depth. If you want to read more about these types of Entities, take a look at the previous tutorials.

First up we have the SettingId class:

<?php namespace Cribbb\Domain\Model\Identity;

use Rhumsaa\Uuid\Uuid;
use Cribbb\Domain\Identifier;
use Cribbb\Domain\UuidIdentifier;

class SettingId extends UuidIdentifier implements Identifier
{
    /**
     * @var Uuid
     */
    protected $value;

    /**
     * Create a new SettingId
     *
     * @return void
     */
    public function __construct(Uuid $value)
    {
        $this->value = $value;
    }
}

As with previous Identifier classes we can simply extend the UuidIdentifier abstract class.

The tests for this class look like this:

<?php namespace Cribbb\Tests\Domain\Model\Identity;

use Rhumsaa\Uuid\Uuid;
use Cribbb\Domain\Model\Identity\SettingId;

class SettingIdTest extends \PHPUnit_Framework_TestCase
{
    /** @test */
    public function should_require_instance_of_uuid()
    {
        $this->setExpectedException("Exception");

        $id = new SettingId();
    }

    /** @test */
    public function should_create_new_id()
    {
        $id = new SettingId(Uuid::uuid4());

        $this->assertInstanceOf("Cribbb\Domain\Model\Identity\SettingId", $id);
    }

    /** @test */
    public function should_generate_new_id()
    {
        $id = SettingId::generate();

        $this->assertInstanceOf("Cribbb\Domain\Model\Identity\SettingId", $id);
    }

    /** @test */
    public function should_create_id_from_string()
    {
        $id = SettingId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");

        $this->assertInstanceOf("Cribbb\Domain\Model\Identity\SettingId", $id);
    }

    /** @test */
    public function should_test_equality()
    {
        $one = SettingId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");
        $two = SettingId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");
        $three = SettingId::generate();

        $this->assertTrue($one->equals($two));
        $this->assertFalse($one->equals($three));
    }

    /** @test */
    public function should_return_id_as_string()
    {
        $id = SettingId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");

        $this->assertEquals(
            "d16f9fe7-e947-460e-99f6-2d64d65f46bc",
            $id->toString()
        );
        $this->assertEquals(
            "d16f9fe7-e947-460e-99f6-2d64d65f46bc",
            (string) $id
        );
    }
}

If you’ve been following along with this series you will be very familiar with these tests.

The Setting Entity

Next up we have the Setting Entity. This is just your basic Doctrine Entity class:

<?php namespace Cribbb\Domain\Model\Identity;

use Assert\Assertion;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @ORM\Entity
 * @ORM\Table(name="settings")
 */
class Setting
{
    /**
     * @ORM\Id
     * @ORM\Column(type="string")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Cribbb\Domain\Model\Identity\User", inversedBy="settings")
     */
    private $user;

    /**
     * @ORM\Column(type="string")
     */
    private $key;

    /**
     * Create a new Setting
     *
     * @param NotificationId $id
     * @param User $user
     * @param string $key
     * @return void
     */
    public function __construct(SettingId $id, User $user, $key)
    {
        Assertion::string($key);

        $this->setId($id);
        $this->setUser($user);
        $this->setKey($key);
    }

    /**
     * Get the id
     *
     * @return SettingId
     */
    public function id()
    {
        return SettingId::fromString($this->id);
    }

    /**
     * Set the id
     *
     * @param SettingId $id
     * @return void
     */
    private function setId(SettingId $id)
    {
        $this->id = $id->toString();
    }

    /**
     * Get the User
     *
     * @return User
     */
    public function user()
    {
        return $this->user;
    }

    /**
     * Set the User
     *
     * @param User $user
     * @return void
     */
    private function setUser(User $user)
    {
        $this->user = $user;
    }

    /**
     * Get the key
     *
     * @return string
     */
    public function key()
    {
        return $this->key;
    }

    /**
     * Set the key
     *
     * @param string $key
     * @return void
     */
    private function setKey($key)
    {
        $this->key = $key;
    }
}

As I mentioned above, because I’m using a convention for my settings in Cribbb I only need to record the key of the key / value pair.

Once again if you have been following along with this series, this class should look very familiar to you by now.

And once again, here are the tests for this class:

<?php namespace Cribbb\Tests\Domain\Model\Identity;

use Cribbb\Domain\Model\Identity\User;
use Cribbb\Domain\Model\Identity\Email;
use Cribbb\Domain\Model\Identity\UserId;
use Cribbb\Domain\Model\Identity\Setting;
use Cribbb\Domain\Model\Identity\Username;
use Cribbb\Domain\Model\Identity\SettingId;
use Cribbb\Domain\Model\Identity\HashedPassword;

class SettingTest extends \PHPUnit_Framework_TestCase
{
    /** @var User */
    private $user;

    public function setUp()
    {
        $this->user = User::register(
            UserId::generate(),
            new Email("name@domain.com"),
            new Username("username"),
            new HashedPassword("password")
        );
    }

    /** @test */
    public function should_require_id()
    {
        $this->setExpectedException("Exception");

        $setting = new Setting(null, $this->user, "new_follower");
    }

    /** @test */
    public function should_require_user()
    {
        $this->setExpectedException("Exception");

        $setting = new Setting(SettingId::generate(), null, "new_follower");
    }

    /** @test */
    public function should_require_setting_key()
    {
        $this->setExpectedException("Exception");

        $setting = new Setting(SettingId::generate(), $this->user, null);
    }

    /** @test */
    public function should_create_setting()
    {
        $setting = new Setting(
            SettingId::generate(),
            $this->user,
            "new_follower"
        );

        $this->assertInstanceOf(
            "Cribbb\Domain\Model\Identity\Setting",
            $setting
        );
        $this->assertInstanceOf(
            "Cribbb\Domain\Model\Identity\SettingId",
            $setting->id()
        );
        $this->assertInstanceOf(
            "Cribbb\Domain\Model\Identity\User",
            $setting->user()
        );
        $this->assertEquals("new_follower", $setting->key());
    }
}

Conclusion

Today’s tutorial was focused on setting the foundation of the User Settings system.

As I mentioned at the top of the post, I’ve decided to use the Property Bag approach for storing the user’s settings. I think in the vast majority of cases, this will be the best solution.

I’ve also decided to dramatically simplify my code by implementing my rules around certain conventions. I think if you can be pragmatic and put these rules in place, you will save yourself a lot of headache.

However with that being said, you will often need to record much more data on the user’s settings in order to satisfy the business rules. This might include using multiple tables or perhaps a different form of data store.

The notification aspect of an application is likely going to change a lot as the application evolves. It’s therefore really important that you don’t couple your notification code to your application. This can be solved really easily by using the Domain Event infrastructure we’ve already got in place.

We’ve covered a lot of the Domain related code over the last couple of weeks, but in order to put the Domain objects to use, we need to build out the other layers of the application.

From now on we’ll be moving up the application stack to start to implement the higher layers of the application.

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.