Alexander Steshenko | Technical Blog

BasicCRM registration confirmation

When a company is registered in BasicCRM system, user recieves an email like this:

When you follow this link, the company becomes activated i.e. you confirm that you entered an email address owned by you:

Design

The process of model design is the same as in case with Registering a company I’ve added the user stories I needed:

Then I wrote the code translating the requirements to PHP, did the unit tests for the new requirements. A tiny MVC bit to make it “live” and that was it. I won’t be returning to these points again as it’s all covered in the previous articles pretty thoroughly.

Transaction demarcation

What is worth mentioning here is this piece of code in CompanyService::registerCompany function:

$entityManager->transactional(function($entityManager) use ($company, $mailer) {
    // 7. saves the new company in the data storage
    $entityManager->persist($company);
    $entityManager->flush();
    // 8. sends out a confirmation email to confirm the email address
    $mailer->registrationConfirmation($company);
});

Explanation needed for the usage of transactional, since it wasn’t in the requirements. The thing is that all service functions are of integral, solid matter for the consumer of the model. In this case MVC layer calls Service Layer’s functions and expects them to either work or raise an error (via exceptions mechanism). We don’t expect a function to work “partially”, change some state then rise an error without changing something else.

In many cases when we only deal with the state through Doctrine 2 Entity Manager, we can avoid any involvement into transactions management because it’s done by Doctrine itself. The ORM has a built-in UnitOfWork and when you call “$entityManager->flush()” it runs all the queries needed inside of a transaction. However, when your side effects include something else than talking to the data storage through Doctrine, it may need some help from you.

There are different ways of addressing it. It may be a good case to apply Aspect Oriented Programming. I decided to let the infrastructure specifics affect how the function is done. The code above means that persisting data in database and sending confirmation email is an atomic operation. You can’t do one of these things and fail doing the other. It’s either both or neither.

I used the transactional() method that accepts a Closure as you can see. There are also other ways for working with transactions in Doctrine 2, but I like this one.

Refactoring and Conveniences

Some refactoring was performed in the system, nothing critical: some things were renamed and duplicated code was moved to helper methods, particularly in the unit tests. In case you are interested in the code actual for older articles in this blog, I tagged the changes with the tag “introduction” in my GitHub repo.

I also added /application/configs/config.local.ini, which, if found, is merged with /application/configs/config.ini. This file is not included in the VCS - you may create it yourself and override some parameters specific to your environment e.g. database name and credentials. There is also a new constant that must be defined by all entry points:

/**
 * MVC application entry point
 */

define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));
define('APPLICATION_ENV', 'web');

APPLICATION\ENV means the section of the configuration file that must be used by the application components. It’s more flexible this way and we don’t have to manually inject configuration object in the test suite like I did before.

Mailer

Here is how the last instruction in the registerCompany looks like now:

// 8. sends out a confirmation email to confirm the email address
$mailer->registrationConfirmation($company);

In a way this $mailer->registrationConfirmation($company) is similar to $entityManager->persist($company), looking from within the registerCompany function.

The idea is simple: utilize Zend_View for rendering templates and use Zend_Mail_Transport_* to send messages (which are Zend_Mail objects). I saw people building complex things for this purpose, while I personally find this simple little approach quite deserving. I added some comments to the code for you and here is how it looks:

namespace Infrastructure;

use Zend_Mail as Mail;

class Mailer
{
    /**
     * @var \Zend_Mail_Transport_Abstract
     */
    protected $sender;

    /**
     * @var \Zend_View
     */
    protected $builder;

    /**
     * @var string
     */
    protected $fromEmail;

    /**
     * @var string
     */
    protected $fromName;

    public function __construct(\Zend_Mail_Transport_Abstract $sender, \Zend_View_Interface $builder, 
        $defaultFromEmail, $defaultFromName)
    {
        $this->sender = $sender;
        $this->builder = $builder;
        $this->fromEmail = $defaultFromEmail;
        $this->fromName = $defaultFromName;
    }

    /**
     * Sends registration confirmation email to the administrator of the
     * given company
     *
     * @param \Domain\Company $company
     * @return void
     */
    public function registrationConfirmation(\Domain\Company $company)
    {
        $salt = \ServiceLocator::getDomainConfig()->get('confirmationCodeSalt');
        $admin = $company->getAdmin();
        $this->mail(
            'BasicCRM registration confirmation',
            'company/registration-confirmation',
            $admin->getEmail(),
            $admin->getName(),
            array('companyId' => $company->getId(), 
                  'confirmationCode' => $company->getConfirmationCode($salt))
        );
    }

    /**
     * Renders given template, sets mail message parameters such as
     * to, from, subject
     * and sends using the builder.
     *
     * @throws \RuntimeException
     * @param $subject
     * @param $template
     * @param $toEmail
     * @param $toName
     * @param array $parameters
     * @return void
     */
    protected function mail($subject, $template, $toEmail, $toName, $parameters = array())
    {
        $mail = new Mail();
        // Passing the parameters to the Zend_View instance
        $this->builder->assign($parameters);
        // Here we try to render both .txt and .html files for the template
        // You may wish to refactor the following try & catch blocks to utilizing is_readable() instead
        $atLeastOnePartRendered = false;
        try {
            $mail->setBodyHtml($this->builder->render($template . '.html'));
            $atLeastOnePartRendered = true;
        } catch (\Zend_View_Exception $exception) {}
        try {
            $mail->setBodyText($this->builder->render($template . '.txt'));
            $atLeastOnePartRendered = true;
        } catch (\Zend_View_Exception $exception) {}
        // at least one email version must exist
        if (!$atLeastOnePartRendered) {
            throw new \RuntimeException('No templates found for ' . $template);
        }
        // setting email parameters
        $mail->setSubject($subject);
        $mail->addTo($toEmail, $toName);
        $mail->setFrom($this->fromEmail, $this->fromName);
        // sending
        $this->sender->send($mail);
    }
}

And here is the “configuration” of the Mailer object, ServiceLayer::getMailer():

/**
 * @return \Infrastructure\Mailer
 */
public static function getMailer()
{
    if (self::$mailer === null) {
        // getting the config for the mailer
        $mailConfig = self::getConfig()->get('mail');
        // particular transport class and transport options
        $transportClass = $mailConfig->get('transportClass');
        $options = $mailConfig->get('options');
        // this little stupidity here thanks to Zend_Mail_Transport design defects
        if ($transportClass == '\Zend_Mail_Transport_Smtp') {
            $sender = new \Zend_Mail_Transport_Smtp($options->get('host'), $options->toArray());
        } else {
            $sender = new $transportClass($options->toArray());
        }
        // Defining where templates for the mailer are located
        $builder = new \Zend_View(array('scriptPath' => APPLICATION_PATH . '/templates'));
        self::$mailer = new \Infrastructure\Mailer($sender, $builder,
        // also pass default from email and name to the mailer constructor
            $mailConfig->get('fromEmail'), $mailConfig->get('fromName'));
    }

    return self::$mailer;
}

Zend_View instance which is passed to the Mailer knows where to look for the templates. It’s the /application/templates directory in my case:

The registrationConfirmation method accepts a Company. It gets the data needed to render the template (here it’s the company’s identifier and the security salt) and to define the recipient from the company (the admin, one who registers it) and calls the mail function. I have a plain text email version for this operation, here is how the template looks (templates/company/registration-confirmation.txt)

Welcome to Basic CRM!

Please, confirm your registration by following the link below:

http://basiccrm.lcf.name/company/confirm/id/<?= $this->companyId ?>/code/<?= $this->confirmationCode ?>

To add a html version I’d just need to put a file named registration-confirmation.html to the same directory and it would be automatically included via Zend_Mail::setBodyHtml().

Tests

I extended Domain\RegisterCompanyTest, making sure that $mailer->registrationConfirmation is being called. The same way as we made sure a company is persisted in the data storage in previous articles i.e. using convenient PhpUnit’s mocks.

What is a bit interesting here is the “Functional” test, making sure that emails are actually being sent and contain the information we expect them to contain. I’ve added this piece to CompanyServiceTest::testRegisterCompany:

// also checking whether the confirmation email has been sent and contains expected link
$salt = \ServiceLocator::getDomainConfig()->get('confirmationCodeSalt');
$link = '/company/confirm/id/1/code/' . sha1(1 . $salt . 'New Company');
$this->assertContains($link, $this->getMailMessageText());

The trick is that using a special mail transport for tests suite we don’t get emails sent but saved to filesystem instead. The transport is Zend_Mail_Transport_File, a little contribution to Zend Framework done by Oleg Lobach and myself. With the new APPLICATION_ENV constant all we have to do in order to make the tests suite use the fake transport is to change the configuration file [tests] section. Like this:

[tests : common]
doctrine.db.dbname = "basiccrm_tests"
mail.transportClass = "Zend_Mail_Transport_File"
mail.options.path = APPLICATION_PATH "/../tests/_files"

Comments and feedback are much appreciated. See contact details here