Oct 27, 2014
Table of contents:
Last week we looked at writing the registration component of Cribbb as a Domain Service. The registration process is going to be a very important part of Cribbb and so I decided to model it as a Domain Service.
Registering new users into a new application is fairly straight forward and fits nicely into the Identity
namespace we’re currently working on.
Another important aspect of Identity is the functionality to reset a password. When developing an application it is very easy to forget about implementing the ability for your users to reset their passwords. But if you launch without it, your new users will soon let you know about it with support requests!
In this week’s tutorial we’re going to be looking at implementing the functionality to reset passwords as a Domain Service.
In last week’s tutorial we analysed whether the registration functionality should be a Domain Service or not.
I decided that I would write the registration functionality as a Domain Service for the following 3 reasons:
User
object would end up with too much responsibility if it knew about the database and the ability to hash passwords. When it comes to providing password reminder functionality, I think you could argue that the three points above are equally as valid.
Firstly, the functionality does not fit on any existing Domain Object. The User
object will require a resetPassword()
method, but it should not have the responsibility to query the database or hash a new password. We need to tell the User
object about the new password, not expect it to create it itself.
Secondly, resetting a user’s password will require multiple Domain Objects and Services. We will need a Reminder
Entity, and the ability to generate a ReminderCode
Value Object and assert whether it is valid or not. We need a way of encapsulating this logic or it will leak out into the application.
And thirdly, whilst you could argue that resetting a password should be an Application Service, I tend to prefer to keep this kind of functionality insulated in the Domain, and then allow the Application layer to act as thin layer of co-ordination on top. I’m still figuring out this structure though, so if you don’t feel like it is right for your application, feel free to implement it as an Application Service.
Before I jump into the code, first I’ll briefly explain how the process for allowing a user to reset their password will work.
When a user has forgotten their password, they will be directed to a form where they can submit their registered email address.
At this point we will check to ensure that the email is registered with the application.
If the email is valid we can create a new Reminder
that will expire in 60 minutes.
We can then send the user an email with a link to reset their password.
The user will receive an email with a URL back to Cribbb that contains a unique code.
When the user clicks on the link they will be taken to a page that will allow them to reset the password.
The unique code in the URL will be validated against the current valid Reminder
codes to ensure it is valid.
If the code is valid, the user will be able to reset their password.
I’m sure you’ve probably reset your password hundreds of times using web applications, but if you’ve never built a password reminder service, I think it can useful to know what’s going on under the hood.
So now that we have the theory down, we can start to think about what code we need to write to actually make the functionality work.
The Domain is the most important part of Domain Driven Design and so naturally the bulk of the logic should be encapsulated in Domain Objects. If the bulk of the logic was captured in Services or Infrastructure we would know that something had gone wrong.
Reminder
. A Reminder
is stored in the database and has a lifecycle, so it should be an Entity. The Reminder
object should be responsible for determining if it is valid or not. ReminderCode
. The ReminderCode
can be a Value Object and so we can model it separately. resetPassword()
method to the existing User
class. ReminderService
that will co-ordinate the various Domain Objects and Services and provide an API to the Application layer of the application ReminderRepository
.
Value Objects are always the easiest bit of developing functionality like this so we’ll start by creating the ReminderCode
class:
<?php namespace Cribbb\Domain\Model\Identity;
use RuntimeException;
use Cribbb\Domain\ValueObject;
class ReminderCode implements ValueObject
{
/**
* @var string
*/
private $value;
/**
* Create a new ReminderCode
*
* @param string $value
* @return void
*/
private function __construct($value)
{
$this->value = $value;
}
/**
* Generate a new ReminderCode
*
* @return ReminderCode
*/
public static function generate($length = 40)
{
$bytes = openssl_random_pseudo_bytes($length * 2);
if (!$bytes) {
throw new RuntimeException("Failed to generate token");
}
return new static(
substr(
str_replace(["+", "=", "/"], "", base64_encode($bytes)),
0,
$length
)
);
}
/**
* Create a new instance from a native form
*
* @param mixed $native
* @return ValueObject
*/
public static function fromNative($native)
{
return new ReminderCode($native);
}
/**
* Determine equality with another Value Object
*
* @param ValueObject $object
* @return bool
*/
public function equals(ValueObject $object)
{
return $this == $object;
}
/**
* Return the object as a string
*
* @return string
*/
public function toString()
{
return $this->value;
}
/**
* Return the object as a string
*
* @return string
*/
public function __toString()
{
return $this->value;
}
}
The ReminderCode
requires a single generate()
method that can be called statically. We can also make the __construct()
method private
because the code should be generated internally.
This class should also implement the ValueObject
interface and we can provide a fromNative()
method so we can rehydrate data from the database.
The tests for this class are basically the same as the other Value Object tests:
<?php namespace Cribbb\Tests\Domain\Model\Identity;
use Cribbb\Domain\Model\Identity\ReminderCode;
class ReminderCodeTest extends \PHPUnit_Framework_TestCase
{
/** @test */
public function should_generate_new_code()
{
$code = ReminderCode::generate();
$this->assertInstanceOf(
"Cribbb\Domain\Model\Identity\ReminderCode",
$code
);
}
/** @test */
public function should_throw_exception_if_code_generation_fails()
{
$this->setExpectedException("RuntimeException");
$code = ReminderCode::generate(null);
}
/** @test */
public function should_create_a_code_from_a_string()
{
$code = ReminderCode::fromNative(
"D1zcA5ncaEHzmjvCGjJIt3Kd8sGxTTtE7DkathqB"
);
$this->assertInstanceOf(
"Cribbb\Domain\Model\Identity\ReminderCode",
$code
);
}
/** @test */
public function should_test_equality()
{
$one = ReminderCode::fromNative(
"D1zcA5ncaEHzmjvCGjJIt3Kd8sGxTTtE7DkathqB"
);
$two = ReminderCode::fromNative(
"D1zcA5ncaEHzmjvCGjJIt3Kd8sGxTTtE7DkathqB"
);
$three = ReminderCode::fromNative(
"sdffpweofpojwepfnpowefopwfpowejfpopopfep"
);
$this->assertTrue($one->equals($two));
$this->assertFalse($one->equals($three));
}
/** @test */
public function should_return_as_string()
{
$code = ReminderCode::fromNative(
"D1zcA5ncaEHzmjvCGjJIt3Kd8sGxTTtE7DkathqB"
);
$this->assertEquals(
"D1zcA5ncaEHzmjvCGjJIt3Kd8sGxTTtE7DkathqB",
$code->toString()
);
$this->assertEquals(
"D1zcA5ncaEHzmjvCGjJIt3Kd8sGxTTtE7DkathqB",
(string) $code
);
}
}
These tests are asserting that we can create a new ReminderCode
instance and the equals()
method can correctly identify equality.
The Reminder
class will be an Entity and so it will require a unique Id. In this project I’m using UUIDs as unique identifiers instead of auto-incrementing database ids:
<?php namespace Cribbb\Domain\Model\Identity;
use Rhumsaa\Uuid\Uuid;
use Cribbb\Domain\Identifier;
use Cribbb\Domain\UuidIdentifier;
class ReminderId extends UuidIdentifier implements Identifier
{
/**
* @var Uuid
*/
protected $value;
/**
* Create a new ReminderId
*
* @return void
*/
public function __construct(Uuid $value)
{
$this->value = $value;
}
}
This class is basically exactly the same as the UserId
class from a couple of weeks ago.
The tests are also essentially the same as the UserId
tests as well as the ReminderCode
tests:
<?php namespace Cribbb\Tests\Domain\Model\Identity;
use Rhumsaa\Uuid\Uuid;
use Cribbb\Domain\Model\Identity\ReminderId;
class ReminderIdTest extends \PHPUnit_Framework_TestCase
{
/** @test */
public function should_require_instance_of_uuid()
{
$this->setExpectedException("Exception");
$id = new ReminderId();
}
/** @test */
public function should_create_new_reminder_id()
{
$id = new ReminderId(Uuid::uuid4());
$this->assertInstanceOf("Cribbb\Domain\Model\Identity\ReminderId", $id);
}
/** @test */
public function should_generate_new_reminder_id()
{
$id = ReminderId::generate();
$this->assertInstanceOf("Cribbb\Domain\Model\Identity\ReminderId", $id);
}
/** @test */
public function should_create_reminder_id_from_string()
{
$id = ReminderId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");
$this->assertInstanceOf("Cribbb\Domain\Model\Identity\ReminderId", $id);
}
/** @test */
public function should_test_equality()
{
$one = ReminderId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");
$two = ReminderId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");
$three = ReminderId::generate();
$this->assertTrue($one->equals($two));
$this->assertFalse($one->equals($three));
}
/** @test */
public function should_return_reminder_id_as_string()
{
$id = ReminderId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");
$this->assertEquals(
"d16f9fe7-e947-460e-99f6-2d64d65f46bc",
$id->toString()
);
$this->assertEquals(
"d16f9fe7-e947-460e-99f6-2d64d65f46bc",
(string) $id
);
}
}
The Reminder
Entity is fairly similar to the User
Entity that we created a couple of weeks ago.
However I’ll walk through it step-by-step because it’s quite a big class.
Firstly, the Entity should implement the AggregateRoot
interface and it should use the RecordsEvents
trait (I renamed HasEvents
to RecordsEvents
):
<?php namespace Cribbb\Domain\Model\Identity;
use Carbon\Carbon;
use Cribbb\Domain\RecordsEvents;
use Doctrine\ORM\Mapping as ORM;
use Cribbb\Domain\AggregateRoot;
use Cribbb\Domain\Model\Identity\Events\ReminderWasCreated;
/**
* @ORM\Entity
* @ORM\Table(name="reminders")
*/
class Reminder implements AggregateRoot
{
use RecordsEvents;
}
Note: I’m using Doctrine annotations because this is an Entity. If you are unfamiliar with Doctrine annotations, take a look at this tutorial.
Next we can define the class properties:
/**
* @ORM\Id
* @ORM\Column(type="string")
*/
private $id;
/**
* @ORM\Column(type="string")
*/
private $email;
/**
* @ORM\Column(type="string")
*/
private $code;
/**
* @ORM\Column(type="string")
*/
private $created_at;
We don’t need to associate a Reminder
code to a user because that would be an unnecessary association. Instead we can simply record the email address.
Next we can implement the __construct()
method:
/**
* Create a new Reminder
*
* @param ReminderId $reminderId
* @param Email $email
* @param ReminderCode $code
* @return void
*/
public function __construct(ReminderId $reminderId, Email $email, ReminderCode $code)
{
$this->setId($reminderId);
$this->setEmail($email);
$this->setCode($code);
$this->setCreatedAt(Carbon::now());
$this->record(new ReminderWasCreated);
}
Notice how I’m passing in the ReminderId
, Email
and ReminderCode
and then I’m using the class’ internal set()
methods.
I’m also recording a ReminderWasCreated
Domain Event that we can use to send the email to the user.
Next we can implement the getter and setter methods:
/**
* Get the Reminder id
*
* @return ReminderId
*/
public function id()
{
return ReminderId::fromString($this->id);
}
/**
* Set the Reminder id
*
* @param ReminderId $id
* @return void
*/
private function setId(ReminderId $id)
{
$this->id = $id->toString();
}
/**
* Get the Reminder email
*
* @return Email
*/
public function email()
{
return Email::fromNative($this->email);
}
/**
* Set the Reminder email
*
* @param Email $email
* @return void
*/
private function setEmail(Email $email)
{
$this->email = $email->toString();
}
/**
* Get the Reminder code
*
* @return ReminderCode
*/
public function code()
{
return ReminderCode::fromNative($this->code);
}
/**
* Set the Reminder Code
*
* @param ReminderCode $code
* @return void
*/
private function setCode(ReminderCode $code)
{
$this->code = $code->toString();
}
/**
* Get the Created At timestamp
*
* @return Carbon
*/
public function createdAt()
{
return Carbon::createFromFormat('Y-m-d H:i:s', $this->created_at);
}
/**
* Set the Created At timestamp
*
* @param Carbon $timestamp
* @return void
*/
private function setCreatedAt(Carbon $timestamp)
{
$this->created_at = $timestamp->toDateTimeString();
}
I prefer to not prefix my getters with get
and to have all setters as private
unless they are explicitly required by the Domain. This is because I find $reminder->code()
cleaner to write, and I want to control how data is set on an Entity.
You will also notice that I’m using the Carbon
package for my timestamps. A lot of people will tell you not to rely on third-party libraries as part of your Domain code. For the most part I would agree with that statement, however I don’t want to have to mess with date and time logic, so I’m fine with using a fantastic third-party library like Carbon in this situation.
Finally I can implement the isValid()
method:
/**
* Check to see if the Reminder is valid
*
* @return bool
*/
public function isValid()
{
return $this->createdAt()->addHour()->isFuture();
}
This method ensures that the logic behind whether a reminder is valid or not is encapsulated inside the Reminder
Entity.
The next thing we need to do is to define the ReminderRepository
interface. As we saw a couple of weeks ago, we should define Repository interfaces as part of the Domain of our application, and leave the implementation up to the Infrastructure layer of the application.
The first method we will need will be to return a new instance of ReminderId
:
<?php namespace Cribbb\Domain\Model\Identity;
interface ReminderRepository
{
/**
* Return the next identity
*
* @return ReminderId
*/
public function nextIdentity();
}
As I mentioned a couple of weeks ago, the repository should be conceptionally responsible for managing the unique id’s of the collection of Entities.
Next we need an add()
method:
/**
* Add a new Reminder
*
* @param Reminder $reminder
* @return void
*/
public function add(Reminder $reminder);
As with the UserRepository
, this method should accept a whole instance of Reminder
. The add()
method’s responsibility is simply to add the entity to the collection.
Next we will need a method to search for a reminder by an Email
and a ReminderCode
:
/**
* Find a Reminder by Email and ReminderCode
*
* @param Email $email
* @param ReminderCode $code
* @return Reminder
*/
public function findReminderByEmailAndCode(Email $email, ReminderCode $code);
This will be used when the user clicks on their email link. We need to ensure that the reminder code exists and that it is associated with the correct email address.
And finally we have three methods to delete existing reminders:
/**
* Delete a Reminder by it's ReminderCode
*
* @param RemindeCode $code
* @return void
*/
public function deleteReminderByCode(ReminderCode $code);
/**
* Delete existing Reminders for Email
*
* @param Email $email
* @return void
*/
public function deleteExistingRemindersForEmail(Email $email);
/**
* Delete all expired Reminders
*
* @return void
*/
public function deleteExpiredReminders();
The functionality of these methods should be fairly self explanatory from the method names, but I’ll explain each one as we implement it in the Doctrine Repository and the ReminderService
.
Next we need to create the Doctrine implementation of the Reminder Repository. This will be a new file called ReminderDoctrineORMRepository
under the Repositories
namespace in the Infrastructure
directory:
<?php namespace Cribbb\Infrastructure\Repositories;
use Carbon\Carbon;
use Doctrine\ORM\EntityManager;
use Cribbb\Domain\Model\Identity\Email;
use Cribbb\Domain\Model\Identity\Reminder;
use Cribbb\Domain\Model\Identity\ReminderId;
use Cribbb\Domain\Model\Identity\ReminderCode;
use Cribbb\Domain\Model\Identity\ReminderRepository;
class ReminderDoctrineORMRepository implements ReminderRepository
{
}
As with the Repository implementation from last week, here I’m using a plain PHP object that does not inherit from Doctrine’s EntityRepository
class.
The first thing I will do will be to inject the EntityManager
through the __construct()
method:
/**
* @var EntityManager
*/
private $em;
/**
* Create a new ReminderDoctrineORMRepository
*
* @param EntityManager $em
* @return void
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
The EntityManager
is the real power behind Doctrine, and so we need an instance of it to talk to the database.
Next we can define the nextIdentity()
method:
/**
* Return the next identity
*
* @return ReminderId
*/
public function nextIdentity()
{
return ReminderId::generate();
}
In this method we need to return a new instance of ReminderId
and so we can use the static generate()
method to do just that.
Next we can define the method to find a reminder by an email and reminder code:
/**
* Find a Reminder by Email and ReminderCode
*
* @param Email $email
* @param ReminderCode $code
* @return Reminder
*/
public function findReminderByEmailAndCode(Email $email, ReminderCode $code)
{
$query = $this->em->createQuery('
SELECT r FROM Cribbb\Domain\Model\Identity\Reminder r
WHERE r.code = :code
AND r.email = :email
');
$query->setParameters([
'code' => $code->toString(),
'email' => $email->toString()
]);
return $query->getOneOrNullResult();
}
In this method I’m using Doctrine’s DQL. If you are unfamiliar with DQL you should read my previous tutorial Using Doctrine Query Language.
If you are familiar with SQL, the code above should look fairly familiar.
The one thing to notice is I’m using the getOneOrNullResult()
method. This means if nothing is found by the query, a null
response is returned.
Next we can define the add()
method:
/**
* Add a new Reminder
*
* @param Reminder $reminder
* @return void
*/
public function add(Reminder $reminder)
{
$this->em->persist($reminder);
$this->em->flush();
}
If you read last week’s posts, the code above should look familiar. Here we are passing a whole Reminder
instance into the Repository for it to be persisted.
Next we can define the first of the three delete methods:
/**
* Delete a Reminder by it's ReminderCode
*
* @param RemindeCode $code
* @return void
*/
public function deleteReminderByCode(ReminderCode $code)
{
$query = $this->em->createQuery(
'DELETE Cribbb\Domain\Model\Identity\Reminder r WHERE r.code = :code');
$query->setParameters(['code' => $code->toString()]);
$query->execute();
}
In this method we have created a way to delete a reminder by it’s code. This will be useful for cleaning up after a reminder has successfully been used to reset a password.
Next we can define a method to clean up multiple requests:
/**
* Delete existing Reminders for Email
*
* @param Email $email
* @return void
*/
public function deleteExistingRemindersForEmail(Email $email)
{
$query = $this->em->createQuery(
'DELETE Cribbb\Domain\Model\Identity\Reminder r WHERE r.email = :email');
$query->setParameters(['email' => $email->toString()]);
$query->execute();
}
If the user doesn’t receive their password reset email straight away, they might request another email. This method will clean up an previously requested reminders from the database so we don’t have multiple reminder codes hanging around.
And finally a method for deleting expired reminders:
/**
* Delete all expired Reminders
*
* @return void
*/
public function deleteExpiredReminders()
{
$query = $this->em->createQuery(
'DELETE Cribbb\Domain\Model\Identity\Reminder r WHERE r.created_at < :timestamp');
$query->setParameters(['timestamp' => Carbon::now()]);
$query->execute();
}
Once a reminder has expired there is no point in keeping it in the database. This method will simply delete any reminders that have expired. This could be run as an action in the Reminder service or perhaps an automated cron job.
If you read last week’s post you will remember that we introduced the concept of Doctrine fixtures. Fixtures allow you to seed the database with data so you can run your database tests with existing data. This saves you from having to write code in each test to set up the database.
Create a new class under the Fixtures
namespace and copy the following class:
<?php namespace Cribbb\Tests\Infrastructure\Repositories\Fixtures;
use Carbon\Carbon;
use Cribbb\Domain\Model\Identity\Email;
use Cribbb\Domain\Model\Identity\Reminder;
use Cribbb\Domain\Model\Identity\ReminderId;
use Cribbb\Domain\Model\Identity\ReminderCode;
use Doctrine\Common\Persistence\ObjectManager;
use Cribbb\Domain\Model\Identity\HashedPassword;
use Doctrine\Common\DataFixtures\FixtureInterface;
class ReminderFixtures implements FixtureInterface
{
/**
* Load the User fixtures
*
* @param ObjectManager $manager
* @return void
*/
public function load(ObjectManager $manager)
{
// Create valid Reminder
$id = ReminderId::generate();
$email = new Email("first@domain.com");
$code = ReminderCode::fromNative("code+99");
$reminder = new Reminder($id, $email, $code);
$manager->persist($reminder);
// Create expired Reminder
Carbon::setTestNow(Carbon::create(2014, 10, 11, 10, 23, 34));
$id = ReminderId::generate();
$email = new Email("first@domain.com");
$code = ReminderCode::fromNative("code+1");
$reminder = new Reminder($id, $email, $code);
Carbon::setTestNow();
$manager->persist($reminder);
$manager->flush();
}
}
In this fixture class I’m creating two existing reminders, one that is valid and one that has expired. This will allow us to test each of the Repository methods by querying for actual data.
When it comes to writing fixtures, I prefer to write the absolute minimum. Fixtures can end up getting out of control if you aren’t strict with them, and you don’t want to end up with false positives in your tests because you don’t fully understand what is going on in your database set up.
Next we can write the Repository tests. Here is the initial test file set up:
<?php namespace Cribbb\Tests\Infrastructure\Repositories;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Artisan;
use Cribbb\Domain\Model\Identity\Email;
use Doctrine\Common\DataFixtures\Loader;
use Cribbb\Domain\Model\Identity\Reminder;
use Cribbb\Domain\Model\Identity\ReminderId;
use Cribbb\Domain\Model\Identity\ReminderCode;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Cribbb\Infrastructure\Repositories\ReminderDoctrineORMRepository;
use Cribbb\Tests\Infrastructure\Repositories\Fixtures\ReminderFixtures;
class ReminderDoctrineORMRepositoryTest extends \TestCase
{
/** @var ReminderDoctrineORMRepository */
private $repository;
/** @var EntityManager */
private $em;
/** @var ORMExecutor */
private $executor;
/** @var Loader */
private $loader;
public function setUp()
{
parent::setUp();
Artisan::call("doctrine:schema:create");
$this->em = App::make("Doctrine\ORM\EntityManagerInterface");
$this->repository = new ReminderDoctrineORMRepository($this->em);
$this->executor = new ORMExecutor($this->em, new ORMPurger());
$this->loader = new Loader();
$this->loader->addFixture(new ReminderFixtures());
}
}
As with the Repository tests from last week, here I’m using a setUp()
method to get everything in place for the tests. This includes migrating the database and loading the fixtures.
The first test should simply assert that an instance of ReminderId
is returned from the nextIdentity()
method:
/** @test */
public function should_return_next_identity()
{
$this->assertInstanceOf(
'Cribbb\Domain\Model\Identity\ReminderId', $this->repository->nextIdentity());
}
The next test should find a reminder by it’s email and reminder code:
/** @test */
public function should_find_reminder_by_email_and_code()
{
$this->executor->execute($this->loader->getFixtures());
$code = ReminderCode::fromNative('code+99');
$email = new Email('first@domain.com');
$reminder = $this->repository->findReminderByEmailAndCode($email, $code);
$this->assertInstanceOf('Cribbb\Domain\Model\Identity\Reminder', $reminder);
$this->assertEquals($code, $reminder->code());
$this->assertEquals($email, $reminder->email());
}
First I run the fixtures so I know the database will be populated with the reminder I’m looking for.
Next I set up the ReminderCode
and Email
instances and then pass them to the Repository.
Finally I can assert that the returned $reminder
is the one I was looking for.
The next test should assert that I can successfully add a reminder to the database:
/** @test */
public function should_add_new_reminder()
{
$id = ReminderId::generate();
$code = ReminderCode::fromNative('new');
$email = new Email('new@domain.com');
$reminder = new Reminder($id, $email, $code);
$this->repository->add($reminder);
$this->em->clear();
$reminder = $this->repository->findReminderByEmailAndCode($email, $code);
$this->assertInstanceOf('Cribbb\Domain\Model\Identity\Reminder', $reminder);
$this->assertEquals($code, $reminder->code());
$this->assertEquals($email, $reminder->email());
}
In this test I create a new Reminder
instance and then pass it to the Repository.
Next I clear the EntityManager
and then search for the Reminder
by it’s email and code.
Finally I can assert that the reminder was found and the correct instance was returned.
In the next test I want to ensure that I can delete a reminder by it’s code:
/** @test */
public function should_delete_reminder_by_code()
{
$this->executor->execute($this->loader->getFixtures());
$code = ReminderCode::fromNative('code+99');
$email = new Email('new@domain.com');
$this->repository->deleteReminderByCode($code);
$reminder = $this->repository->findReminderByEmailAndCode($email, $code);
$this->assertEquals(null, $reminder);
}
First we run the fixtures to make sure the database is seeded with reminders.
Next we can create the ReminderCode
and Email
Value Objects we need to pass to the deleteReminderByCode()
.
Next we call the method and then attempt to find the reminder we just deleted.
Finally we can assert that the returned value from the Repository is null
.
The next test is fairly similar to the previous test, but this time we are deleting by just the email, rather than the code:
/** @test */
public function should_delete_existing_reminders_by_email()
{
$this->executor->execute($this->loader->getFixtures());
$code = ReminderCode::fromNative('code+99');
$email = new Email('new@domain.com');
$this->repository->deleteExistingRemindersForEmail($email);
$reminder = $this->repository->findReminderByEmailAndCode($email, $code);
$this->assertEquals(null, $reminder);
}
And finally we can test that the Repository will delete the expired reminders whenever the deleteExpiredReminders()
is called:
/** @test */
public function should_delete_expired_reminders()
{
$this->executor->execute($this->loader->getFixtures());
$code = ReminderCode::fromNative('code+1');
$email = new Email('new@domain.com');
$this->repository->deleteExpiredReminders();
$reminder = $this->repository->findReminderByEmailAndCode($email, $code);
$this->assertEquals(null, $reminder);
}
This should delete the expired fixture that we created earlier.
Now that we have the Entities, Value Objects and Repositories in places, we can look at using these individual components to build the ReminderService
.
Here is the basic class definition:
<?php namespace Cribbb\Domain\Services\Identity;
use Cribbb\Domain\Model\Identity\Email;
use Cribbb\Domain\Model\Identity\Reminder;
use Cribbb\Domain\Model\Identity\Password;
use Cribbb\Domain\Model\Identity\ReminderCode;
use Cribbb\Domain\Model\InvalidValueException;
use Cribbb\Domain\Model\ValueNotFoundException;
use Cribbb\Domain\Model\Identity\UserRepository;
use Cribbb\Domain\Model\Identity\ReminderRepository;
class ReminderService
{
}
We can also create the test file. As with the RegisterUserService
from last week, there is no need to hit the database during these tests. However, we will need to create some in-memory fixtures so we don’t have to repeat the same set up code for each test.
Here is what the test class set up looks like:
<?php namespace Cribbb\Tests\Domain\Services;
use Mockery as m;
use Carbon\Carbon;
use Cribbb\Domain\Model\Identity\User;
use Cribbb\Domain\Model\Identity\Email;
use Cribbb\Domain\Model\Identity\UserId;
use Cribbb\Domain\Model\Identity\Username;
use Cribbb\Domain\Model\Identity\Reminder;
use Cribbb\Domain\Model\Identity\ReminderId;
use Cribbb\Domain\Model\Identity\ReminderCode;
use Cribbb\Domain\Model\Identity\HashedPassword;
use Cribbb\Domain\Services\Identity\ReminderService;
class ReminderServiceTest extends \PHPUnit_Framework_TestCase
{
/** @var ReminderRepository */
private $reminders;
/** @var UserRepository */
private $users;
/** @var ReminderService */
private $service;
/** @var User */
private $user;
/** @var array */
private $fixture;
public function setUp()
{
$id = UserId::generate();
$email = new Email("name@domain.com");
$username = new Username("username");
$password = new HashedPassword("qwerty123");
$this->user = User::register($id, $email, $username, $password);
$this->fixture = [
"id" => ReminderId::generate(),
"code" => ReminderCode::generate(),
"email" => new Email("name@domain.com"),
];
$this->users = m::mock("Cribbb\Domain\Model\Identity\UserRepository");
$this->reminders = m::mock(
"Cribbb\Domain\Model\Identity\ReminderRepository"
);
$this->hasher = m::mock(
"Cribbb\Domain\Services\Identity\HashingService"
);
$this->service = new ReminderService(
$this->reminders,
$this->users,
$this->hasher
);
}
}
The first thing to do is define the __construct()
method and inject the class dependencies:
/**
* @var ReminderRepository
*/
private $reminders;
/**
* @var UserRepository
*/
private $users;
/**
* @var HashingService
*/
private $hasher;
/**
* Create a new ReminderService
*
* @param ReminderRepository $reminders
* @param UserRepository $users
* @return void
*/
public function __construct(ReminderRepository $reminders, UserRepository $users, HashingService $hasher)
{
$this->reminders = $reminders;
$this->users = $users;
$this->hasher = $hasher;
}
We’re going to need the ReminderRepository
we just wrote as well as the UserRepository
so we can search for existing users and the HashingService
so we can hash the user’s new password.
Once again notice how I’m relying on the interface for each of these services and not the concrete implementation.
The first method we need to write is for requesting a reminder email:
/**
* Request a password reminder Token
*
* @param string $email
* @return Reminder
*/
public function request($email)
{
$email = new Email($email);
$this->findUserByEmail($email);
$this->reminders->deleteExistingRemindersForEmail($email);
$id = $this->reminders->nextIdentity();
$reminder = new Reminder($id, $email, ReminderCode::generate());
$this->reminders->add($reminder);
return $reminder;
}
When the request comes in, the $email
will be a string. The first thing to do will be to turn the $email
string into a Email
value object. This will ensure that the email passes our internal requirements for a valid email address.
Next we need to check to ensure that the email address is an actual registered email address in our application. This is something we will need to do in multiple methods of this service class and so we can abstract it to a private
method:
/**
* Attempt to find a user by their email address
*
* @param Email $email
* @return Cribbb\Domain\Model\Identity\User
*/
private function findUserByEmail(Email $email)
{
$user = $this->users->userOfEmail($email);
if ($user) return $user;
throw new ValueNotFoundException("$email is not a registered email address");
}
If the email is not found in the database we can just abort the process here by throwing an exception.
Back in the request()
method we can delete any existing reminders for this user by calling the deleteExistingRemindersForEmail()
method on the ReminderRepository
and passing the $email
:
$this->reminders->deleteExistingRemindersForEmail($email);
Finally we can create a new Reminder
instance, add it to the Repository and then return the object from the method:
$id = $this->reminders->nextIdentity();
$reminder = new Reminder($id, $email, ReminderCode::generate());
$this->reminders->add($reminder);
return $reminder;
The first test for this method is to ensure that an exception is thrown if no user is found:
/** @test */
public function should_throw_exception_when_user_does_not_exist()
{
$this->setExpectedException('Cribbb\Domain\Model\ValueNotFoundException');
$this->users->shouldReceive('userOfEmail')->andReturn(null);
$this->service->request('nope@domain.com');
}
In this method I’m telling the UserRepository
mock to return null
to simulate that no user was found.
Next we can test to ensure a new Reminder
is returned from the request()
method when everything goes smoothly:
/** @test */
public function should_request_and_return_new_reminder()
{
$this->users->shouldReceive('userOfEmail')->andReturn($this->user);
$this->reminders->shouldReceive('deleteExistingRemindersForEmail');
$this->reminders->shouldReceive('nextIdentity')->andReturn(ReminderId::generate());
$this->reminders->shouldReceive('add');
$reminder = $this->service->request('name@domain.com');
$this->assertInstanceOf('Cribbb\Domain\Model\Identity\Reminder', $reminder);
}
Next we can write the code for the check()
method. This method will be called when the user clicks on the link in the reminder email. This method should ensure that the reminder code exists, is valid and belongs to the correct email address:
/**
* Check to see if the email and token combination are valid
*
* @param string $email
* @param string $code
* @return bool
*/
public function check($email, $code)
{
$code = ReminderCode::fromNative($code);
$email = new Email($email);
$reminder = $this->reminders->findReminderByEmailAndCode($email, $code);
if ($reminder && $reminder->isValid()) return true;
return false;
}
First we turn the raw values in Value Objects.
Next we pass the valid objects to the Repository and we save the return value into the $reminder
variable.
Next we check to see if the $reminder
is not null
and if it is valid.
If both conditions pass, we can return true
, otherwise we return false
.
Notice how we’ve kept the logic around what is deemed “valid” internal to the Reminder
object.
The first test for the check()
method should return true
when a reminder is found and it is valid:
/** @test */
public function should_find_reminder_and_return_true_when_valid()
{
$reminder = new Reminder(
$this->fixture['id'],
$this->fixture['email'],
$this->fixture['code']
);
$this->reminders->shouldReceive('findReminderByEmailAndCode')->andReturn($reminder);
$this->assertTrue($this->service->check('name@domain.com', 'abc123'));
}
The next test should return false
when the reminder code is invalid:
/** @test */
public function should_find_reminder_and_return_false_when_invalid()
{
Carbon::setTestNow(Carbon::create(2014, 10, 11, 10, 23, 34));
$reminder = new Reminder(
$this->fixture['id'],
$this->fixture['email'],
$this->fixture['code']
);
Carbon::setTestNow();
$this->reminders->shouldReceive('findReminderByEmailAndCode')->andReturn($reminder);
$this->assertFalse($this->service->check('name@domain.com', 'abc123'));
}
In this test I’m using the setTestNow()
on Carbon
to artificially set the current timestamp. This is a really nice feature of Carbon that alone makes it worth using.
Finally we can write the method that will accept the request from the user to update their password.
/**
* Reset a user's password
*
* @param string $email
* @param string $password
* @param string $code
* @return User;
*/
public function reset($email, $password, $code)
{
if ($this->check($email, $code)) {
$user = $this->findUserByEmail(Email::fromNative($email));
$password = $this->hasher->hash(new Password($password));
$user->resetPassword($password);
$this->users->update($user);
$this->reminders->deleteReminderByCode(ReminderCode::fromNative($code));
return $user;
}
throw new InvalidValueException("$code is not a valid reminder code");
}
First we pass the $email
and $code
to the check()
method again to ensure nothing has been tampered with. If the check()
method returns false
we can abort by throwing an exception.
We can test for this behaviour with the following test:
/** @test */
public function should_throw_exception_during_reset_attempt_when_email_or_code_are_invalid()
{
$this->setExpectedException('Cribbb\Domain\Model\InvalidValueException');
$this->reminders->shouldReceive('findReminderByEmailAndCode')->andReturn(null);
$this->service->reset('name@domain.com', 'qwerty123', 'abc123');
}
In this test I’m telling the mocked ReminderRepository
to return null
when the findReminderByEmailAndCode()
method is called.
If the check()
method returns true
we can reset the user’s password:
$user = $this->findUserByEmail(new Email($email));
$password = $this->hasher->hash(new Password($password));
$user->resetPassword($password);
$this->users->update($user);
$this->reminders->deleteReminderByCode(ReminderCode::fromNative($code));
return $user;
First we attempt to find the user using the findUserByEmail()
from earlier. If the user is not found, an exception will be thrown.
Next we can pass the password into the HashingService
to receive an instance of HashedPassword
in return.
Next we can pass the HashedPassword
to the resetPassword()
method on the User
object. This method looks like this:
/**
* Reset the User's password
*
* @param HashedPassword $password
* @return void
*/
public function resetPassword(HashedPassword $password)
{
$this->password = $password;
$this->record(new PasswordWasReset($this));
}
Notice how we also record a PasswordWasReset
Domain Event. This will be used to send a confirmation email to the user that they successfully changed their password.
Next we can pass the user to the UserRepository
to persist the user to the database:
$this->users->update($user);
Now that the reminder has been successfully used, we can delete it from the database using the deleteReminderByCode()
method:
$this->reminders->deleteReminderByCode(ReminderCode::fromNative($code));
And finally, we can return the $user
from the method. The $user
object is loaded with the PasswordWasReset
Domain Event ready to be fired:
return $user;
To test this method we can assert that each of the dependencies is called correctly and the $user
is returned:
/** @test */
public function should_reset_password_and_return_user()
{
$reminder = new Reminder(
$this->fixture['id'],
$this->fixture['email'],
$this->fixture['code']
);
$this->reminders->shouldReceive('findReminderByEmailAndCode')->andReturn($reminder);
$this->users->shouldReceive('userOfEmail')->andReturn($this->user);
$this->hasher->shouldReceive('hash')->andReturn(new HashedPassword('qwerty123'));
$this->users->shouldReceive('update');
$this->reminders->shouldReceive('deleteReminderByCode');
$user = $this->service->reset('name@domain.com', 'qwerty123', 'abc123');
$this->assertInstanceOf('Cribbb\Domain\Model\Identity\User', $user);
}
The functionality to reset passwords is an important part of all consumer web applications. It’s easy to overlook as it’s probably not something you think about pre-launch, but it is something that your users will end up requesting.
In this tutorial I’ve decided to model this functionality as a Domain Service. As with last week, I feel that this functionality is important, but I think creating this as an Application Service would also be a fine choice.
I think an important takeaway from this tutorial was how much of the functionality we built into Entities and Value Objects. If you find yourself in a situation where your services have more responsibility than your Domain Objects, it’s probably a sign that you’re doing something wrong. Domain Objects should hold the responsibility for business logic.
Following on from last week, hopefully this was another good example of using a Repository interface in the Domain layer, and a Repository implementation in the Infrastructure layer. It’s also really important that you test your Repository implementations by allowing them to hit the database.
And finally, I hope this was a clear example of the division of responsibility in a web application. Imagine if we had written this functionality as a single service class? That class would be huge and very difficult to test or modify! Instead we’ve split the responsibility down into individual objects and services. This makes our code easier to understand, test and evolve and it also allows us to create complex functionality by composing these individual objects in different ways.
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.