Sending notifications to web users is generally taken for granted when writing a PHP application. The excelent PHP mail function is generally used. But what happens when my site becomes popular. Does the mail function grow with my site? Or how about if I wanted to send more types of messages than a simple email. This little article explains how I am currently investigating how I could possibly scale up messaging a user in a web enviroment. A big warning first though, I am not a PHP expert, programmer even, and I am just learning about OOP and PHP and patterns, so please read this at your own risk.
My Problem
I am playing around with PHP at the moment to maybe build a website that *may* get a tad popular (hope is good!:) . My problem here is that I may have to scale the site very quickly and I want to advoid having to rewrite code as much as possible. I also am a big fan of the separation of services where possible. So a database should IMHO sit on a dedicated box, a web server just be a web server etc.
One of the important aspects of this site, is its notification to the user on the occurance of an event. This will initally be an email, but eventually I would like notifications to be delivered to the user via AIM, Google Talk (Or for that matter any Jabber user), Text message (SMS) and maybe even MSN Messenger. In short, whatever way I can get a notification to a user I want to be able to do it. However if the site is popular, do I want the server I am busy serving clients on to be under the burden of having to send mail at the same time, ideally it should not. Now I know the mail function can be used to send the mail to a dedicated mail server, but again I also want to be able to send other types of messages too.
My Solution
First off, I know that this as a solution will only scale so far. I am also aware that my code is really really bad here, but I have just started to play with all this OOP stuff in PHP5, so please any advice you can give me in improving my code, style and method of working, please do so. Of course a lot of error checking has gone out the window here for the purpose of this blog post, along with security too, this solution is presented as way you *may* be able to do things. Its not to be taken as an authorative way to do this.
Right, enough with the disclaimers, lets get cracking. After a wee while playing around and doing a little bit of searching, I decided the best way to implement all this was by heavy use of the new OOP features in PHP5 and using SOAP. The reason I choose SOAP was quite simply, I could direct my code to fire off a soap message faily easily to a SOAP service which could be on the same box (In which case you maybe get twice the overhead of sending a mail using the mail function) or as is most likley the case on another interally acessable box, dedicated to notifications only. So what I really needed at the end of the day, was a Messaging Abstraction layer. Something that would just handle the sending of a message and leave the app to get on with the rest of its processing.
Enter Interfaces
This is when I discovered Interfaces in PHP, below is a cut down version of my Messaging interface.
interface MessengerInterface {
public function validateSender($sender);
public function validateRecipient($recipient);
public function sendMessage ();
}
As you can see its very basic. All I want to do is validate my senders and recipients, and send my message. Nothing colud be simpler… right…
I now needed a class that would send an email, but it had to implement the above interface, otherwise what would the point be? So I ran up a rather nasty class to mange this.
class EmailMessenger extends Messenger implements MessengerInterface {
private $sender;
private $recipient;
private $subject;
private $message;
private $messageFormat;
/**
* Method constructor. Validates the recipients and sender and sends the message.
*
* @param string $sender
* @param string $recipient
* @param string $subject
* @param stringe $message
*/
public function __construct($sender, $recipient, $subject, $message) {
$this->sender = $sender;
$this->recipient = $recipient;
$this->message = $message;
// We need a subject for emails
if(!isset($subject)) {
throw new MessengerException("You need to specify a subject for this type of transport");
}
$this->subject = $subject;
// Set message format to true if the message contains HTML
$this->messageFormat = $this->isHtmlMessage($this->message);
// Now we validate the sender address.
self::validateSender($this->sender);
// Now the recipient
self::validateRecipient($this->recipient);
// Now we'll try and send the mail;
self::sendMessage();
}
public function validateSender($sender) {
if (!eregi("^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$", $sender)) {
throw new MessengerException("Invalid Email Sender Address Format: $sender");
return false;
}
return true;
}
public function validateRecipient($recipient) {
if (!eregi("^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$", $recipient)) {
throw new MessengerException("Invalid Email Recipient Address Format: $recipient");
return false;
}
return true;
}
public function sendMessage () {
// Supress an error in case we are on a win32 platform. The exception should handle the error.
if(!@mail("$this->recipient", "$this->subject","$this->message","From: $this->sender \r\n")) {
throw new MessengerException("Mail Server/Service Unavailable. Sorry I Unable To Send Mail At Present.");
return false;
}
return true;
}
/**
* Checks a message to see if it contains HTML tags.
*
* @param string $message
* @return bool True if message contains HTML false otherwise.
* @access private
*/
private function isHtmlMessage($sting) {
if(preg_match("/([\< ])([^\>]{1,})*([\>])/i", $string)) {
return true;
}
return false;
}
}
As you can see, all of the work is done when the class is called, I don’t think this is good pratice, but for the purpose of this, I am going to leave well enough alone. I also have made this class a “sub” class of my main Messenger class. The messenger class is where the magic happens, it basically decides what “sub” class should be called depending on the message format.
class Messenger {
/**
* The Senders Address
*
* @var string
*/
private $sender;
/**
* The Recipients Address
*
* @var String
*/
private $recipient;
/**
* The Message Being Seny
*
* @var string
*/
private $message;
/**
* The transport or format of sending the message
*
* @var string
*/
private $transport;
/**
* Contains an instance of our message transport.
*
* @var object
*/
private $messageTransport;
/**
* Contains the message subject if set.
*
* @var string
*/
private $subject;
/**
* Messenger construct, requires that the sender, recipient, message and transport be
* provided otherwise it throws a general MessengerException Error.
*
* @param string $sender The sender of the message.
* @param string $recipient The recipent of the message.
* @param string $message The message that is being sent.
* @param string $subject Optional, only used when sending a mail message.
* @param string $transport Optional, defaults to email
* @throws MessengerException
*/
public function __construct($sender, $recipient, $message, $subject=null, $transport = null) {
if(!$sender) {
throw new MessengerException("You did not specify a sender address.");
}
if(!$recipient) {
throw new MessengerException("You did not specify a recipent address.");
}
if(!$message) {
throw MessengerException("You did not specify a message to send.");
}
if(!$transport) {
throw new MessengerException("You did not specify a transport type.");
}
$this->sender = $sender;
$this->recipient = $recipient;
$this->message = $message;
// If subject is set.
if(isset($subject)) : $this->subject = $subject; endif;
// Convert the trasport argument to lower case. This is case we will pattern match
// on lower case only.
$this->transport = strtolower($transport);
// We'll set our types to be nice.
settype($this->sender, "string");
settype($this->recipient, "string");
settype($this->subject, "string");
settype($this->message, "string");
settype($this->transport, "string");
// Pattern Factory (I think)
switch ($this->transport) {
case "email":
require_once('EmailMessenger.class.php');
try {
$this->messageTransport = new EmailMessenger($this->sender, $this->recipient, $this->subject, $this->message);
}
// Catch the exception and save the message to the message queue if possible.
catch (MessengerException $e) {
require_once('MessageQueue.class.php');
try {
throw new MessengerException("Not Yet Implemented");
//MessageQueue::saveMessage($db, $this->sender, $this->recipient, $this->subject, $this->message, $this->transport);
}
catch (MessengerException $e) {
throw new MessengerException("$e->getMessage()");
}
}
break;
case "jabber":
require_once('JabberMessenger.class.php');
throw new MessengerException("Not Yet Implemented");
//$this->messageTransport = new JabberMessenger($this->sender, $this->recipient, $this->message);
break;
default:
/**
* @todo Can remove this prob, to make class more readable. Also we'll choose email as the default transport.
*/
require_once('EmailMessenger.class.php');
$this->messageTransport = new EmailMessenger($this->sender, $this->recipient, $this->subject, $this->message);
break;
}
}
}
Okay so here you can see, depending on the transport that is being requested, we’ll fire up a new instance of messaging sub classes to send the message.
Washing With SOAP
Soap intergration was quite easy. As I am lazy and could not be arsed finding out the format of a WSDL file, I used the inbuilt wsdl generator included with Zend IDE. This was based around a very simple php file below, called simply MessengerService.
require_once('Messenger.class.php');
/**
* Send a mail message via soap.
*
* @param string $sender
* @param string $recipient
* @param string $subject
* @param string $message
* @return boolean
*/
function sendEmail($sender,$recipient,$subject,$message) {
try {
$Messenger = new Messenger($sender,$recipient,$message,$subject,'EMAIL');
return true;
}
catch (MessengerException $e) {
throw new SoapFault("999", "Fault With Request: $e->getMessage()");
}
}
$SoapServer = new SoapServer('Messenger1.wsdl');
$SoapServer->addFunction('sendEmail');
$SoapServer->handle();
Here I have the EMAIL transport hard coded, as I was just testing this out, but as you can guess, by changing the transport, a different type of message can be sent. Its quite nice this way too as to send a messafe I only have to do the following;
$newSoapClient = new SoapClient('http://notify.dev/Messenger.wsdl');
$newSoapClient->sendEmail("$sender","$recipient","$message", "$subject"); // Real world would include a transport type var.
This messenger service can sit on the same box as your web app, or when things become too busy, moved off to another box to handle notifications.
Finally
There are some questions I have all the same. How do large sites like Yahoo handle mail notifications within their applications? Do the use a similar theory or are the mails sent locally? If you have expeience with such a problem I would love to hear from you. Secondly I think the overhead on a SOAP request migh be higher than a mail function request, but again this is only a theory I am playing with at the moment.
If you did find this long post handy, or indeed if you have suggestions on how I can improve this or my PHP, please drop me a line.