Symfony World blog is not maintained anymore. Check new sys.exit() programming blog.

simple mailing system with symfony - part I

This article is the beginning of a tutorial presenting an easy way to implement database-templated-mailing system for symfony projects.


the basics


The basic version uses 2 tables: mail_queue and mail_template. For each type of mails, one template object is created.


mail_template

name, description - name & description of the template
template_code - (HTML, including variables marked as {variable})
template_data - what variables are defined, just a description, no calculations performed


For each single mail to be sent, one queue object is created.


mail_queue

template_id - which template to use
mail_data - serialized array: variables=>values
mail_recipent - formatted/serialized recipent information
mail_author - as above
mail_subject - string
send_at - time that this email should be sent
sent - boolean, already sent


Table structure for mail_template and mail_queue tables

E-mail is sent when sent=0 (and marked 1 afterwards, not to be sent again; in case of errors - mark as 2) and when actual time has gone beyond send_at. send_at column is especially useful, as you may create emails in a long time advance. For example, you want to send a reminder E-mail: create a new MailQueue object with its datetime value equal to current timestamp + two weeks. This would make the E-mail to be sent in two weeks time from now.


example usage


What kind of mails can we define? It depends on your project functionalities. For example, if your project is a stock managing software that stores information about products and their states, you can send important E-mails when one of the following situations take place:

  • status of a product is changing - other employees shall be informed about that,
  • quantity of a product reached alarming level - supply needed immediately,
  • inform customers when product becomes available again,
  • and so on...
A template object is created for each of the situations defined above. There's a MailTools class with static methods processing mail sending. Just like notifyProductStatusHasChanged method, taking two parameters: Product and status change. This method creates a mail_queue instance, defining specific data for this type of E-mail, using Article object passed as the parameter.


some details on implementation


Symfony Swift Mailer integration is provided with a ready to use mail queueing system. However, I decided to create a special task, defined to send E-mails that are marked with sent=0. The code is pretty obvious:

$mails = Doctrine::getTable('MailQueue')
  ->getMails2bSentQuery()
  ->execute();
 
foreach($mails as $m)
{
  $m->send();
}

Above task is run by a cron job (every few minutes). getMails2bSentQuery method looks for MailQueue objects that have sent=0 and send_at <= now. For each of those objects, the template code (HTML) is filled with variable data and sent afterwards. The mail data is unserialized first and each variable is injected into the HTML code using str_replace function:



protected function generateContent()
{
  $code = $this->getTemplate()->getTemplateCode();
  $data = unserialize($this->getMailData());
  foreach($data as $key => $value)
    $code = str_replace('{'.$key.'}', $value, $code);
  return $code;
}

Each mail template is defined using HTML/CSS code. It works similar to smarty template engine. Just define a list of variables of and all occurences of {VARIABLE} will be replaced with the parameter value you pass.


to be continued

This was just a brief overview. In the near future, precise code examples will be provided in tutorial's part 2.

Doctrine SoftDelete behavior usage

Scene from "The Neverending Story" by Wolfgang Petersen (1984)

SoftDelete

Recently, I created a detailed list of Doctrine behaviors ready to use in symfony projects. Among them, there is the SoftDelete behavior I want to focus on this time. For a chosen model, it adds a deleted_at column, which defines if a record has been marked as deleted (and if so, when).


where to use it?

Suppose your system needs to store all the detailed data history, every modification has to be marked and stay there forever. And even if the application allows users to delete objects, they are not really deleted - it is just an abstraction layer - in fact, objects are marked as deleted, but they stay in the database (and the application treats them as if they were deleted). This is where SoftDelete comes handy.


the code

All you gotta do is just to state that a given model is SoftDelete:

GivenModel:
  actAs:
    SoftDelete: ~
After this, the deleted_at column is added (of course, both to SQL and PHP class). But that's not all the job. Unfortunately, you have to tell the application how to use this column manually. Fortunately, this is really easy. For example, if you have an admin generated module, soft-deleted objects shall never appear in the list. Add/modify the buildQuery action of the admin module:
protected function buildQuery()
  {
    return parent::buildQuery()
      ->andWhere('deleted_at IS NULL');
  }
Additionally, if you want to have a full protection, you should update your admin module edit action to disable executing it for a soft-deleted object. So this was for the backend. For frontend, you shall modify your eventual data retrieving table class methods, like the following:
public function getObjectByIdQuery($id)
  {
    return Doctrine_Query::create()
      ->from('GivenModel gm')
      ->where('gm.id = ?', $id)
      ->andWhere('gm.deleted_at IS NULL');
  }
Of course, soft-deleted objects can NEVER be accessible from any type of frontend applications. There is no way to give a detailed list of modifications you need to provide 100% data protection, because all applications have different structure - you have to go through your functionalities on your own. These code lines above are just examples you will probably use.


enable DQL callbacks

Alternatively, you may enable DQL callbacks for your models in each of your config/ProjectConfiguration.class.php files:

public function configureDoctrine(Doctrine_Manager $manager)
  {
    // Enable callbacks so that softDelete behavior can be used
    $manager->setAttribute(Doctrine_Core::ATTR_USE_DQL_CALLBACKS, true);
  }
this will dynamically add an andWhere DQL clause to all SELECT queries for SoftDelete models (Doctrine_Template_Listener_SoftDelete::preDqlDelete()). Thanks to it, you don't have to manually update all possible SELECT queries in your project (such as buildQuery mentioned above).


be careful!

As the official behavior docs say, SoftDelete overrides the delete() method. When delete() is called, instead of deleting the record from the database, a deleted_at date is set. This means, if you try to delete records without using Doctrine_Record::delete() method, SoftDelete won't work! To provide 100% data protection covering SoftDelete you should replace all code like

Doctrine_Query::create()
  ->delete()
  ->from('GivenModel gm')
  ->where('gm.id = ?', $id)
  ->execute();
with
$objects = Doctrine_Query::create()
  ->from('Manufacturer m')
  ->where('m.id = ?', $id)
  ->execute();
 
foreach($objects as $object)
  $object->delete();
to make the Doctrine_Record::delete() be called whenever an object is supposed to be deleted, which finally means that the desired SoftDelete behavior is fired always when it should be.


objects related to SoftDelete-able objects

But there comes another question - what to do with related records of a soft-deleted object? For example, we have two model classes with the SoftDelete behavior: Manufacturer and Supplier which have a m:n relation table, ManufacturerSupplier which defines who supplies which manufacturers. If some specific manufacturer and supplier objects are marked as soft-deleted, their corresponding ManufacturerSupplier object (if it exists, let's suppose it does) is left and no one knows how to treat it. It is not marked as soft-deleted, since it hasn't got SoftDelete behavior. And it still exists in the database, like a full-fledged record. Probably, it does not raise any application problems, but there is a data consistency question - what does such m:n table record represent, when at least one of its related objects are soft-deleted? Shall it be soft-deleted after the related master object is soft-deleted? Or maybe hard-deleted?