Smarty + Doctrine = Smoc

Update: Since the below piece was written I’ve ported the little framework to H2O instead, H2O is a cleaner alternative to Smarty.


So I’m back to the Smarty and Doctrine combination, for the past two days I’ve been working on a simple newsletter application using what I now call Smoc, it’s a play on the words Smarty and Doctrine. The zip contains both Smoc and the application.

Initially I had in mind to rip out Smarty and just use PHP but the ripping job would’ve been too much of an effort at the moment. I needed to get the emailer done asap so the ripping have to wait.

The reason for the name and all is that I finally got around to moving the stuff in the classes folder in each project to its own folder in the lib folder so that many projects can access the same codebase. After a few minutes of contemplation Smoc popped up in my head as a good name for this new folder.

lib.png

Install instructions:
1.) Download
2.) Make sure you have Zend, Smarty and Doctrine in the lib folder as in the picture to the right.
3.) Use the SQL file in the emailer folder or populate.php to create the database structure.
4.) Use Common::hash() to generate a hashed password that you can use to create a default user in phpMyAdmin, more info on the hashing stuff can be found below.
5.) Edit emailer/bootstrap.php to reflect your setup. If you are using virtual servers leave the base directory global empty, otherwise put your sub folder name there (see below for more info).
6.) Access the site through for instance http://localhost/emailer/user/listing.
7.) Use emaler/send_mail.php to send newsletters (more info below).

Changelog:
Apart from the move, some new things have been added to create a fully fledged automatic admin system (a bit exaggerated, I still have search and pagination to do). AdminCtrl.php now looks like this:

class AdminCtrl extends Ctrl{ 
    function init($name){ 
        parent::init($name); 
        $hidden = array('id', 'created_at', 'updated_at'); 
        $this->hidden = array_combine($hidden, $hidden); 
        $this->update_list = true; 
        $this->delete_list = true; 
        $this->setS('pop', NULL); 
    }
    
    function onInsert(){ return $this->listAll(); }
	function onUpdate(){ return $this->listAll(); }
	function onDelete(){ return $this->listAll(); }
    
    function listAll(){
    	$this->checkLogin();
    	$this->assign('update', $this->update_list); 
    	$this->assign('delete', $this->delete_list); 
    	$this->assign('columns', array_keys($this->t->getColumns())); 
    	$this->assign('items', $this->findAllArr()); 
    	$this->assignRelations(); 
    	return $this->fetch('../list.tpl'); 
    }
	
	function assignRelations(){ 
	  foreach($this->t->getRelations() as $key => $value){ 
	    $relations[ $value->getLocal() ]['label'] 	= $this->getMdl($value->getClass())->label; 
	    $relations[ $value->getLocal() ]['Mdl'] 	= $value->getClass(); 
	  } 
	  $this->assign('relations', $relations); 
	}
	
	function getLbl($p, &$s){ 
	  return $this->getCtrl($p['mdl'])->find($p['id'])->$p['lbl']; 
	}
	
	function form(){
		$this->checkLogin();
		$this->setS('pop', NULL);
		return parent::form();
	}
	
	function formCommon($f_head, $type, $tpl = "../ui.tpl"){
		$this->assign('columns', array_diff_key($this->t->getColumns(), $this->hidden)); 
		$this->assignRelations(); 
		return parent::formCommon($f_head, $type, $tpl); 
	}
	
	function checkLogin(){
		if($this->getS('logged_in', 'admin') !== true){
			$this->assign('login_error', 'You have to login.');
			echo $this->login();
			exit;
		}
	}
	
	function login(){ 
		return $this->fetch('../login_form.tpl');
	}
	
	function logout(){ 
		$this->setS('logged_in', false, 'admin');
		return $this->fetch('../login_form.tpl');
	}
}

The main thing here is that I’ve moved the checkLogin function from the user controller into here. That makes it possible to protect the whole admin interface from unauthorized access without much fuss.

Another new thing is that it works when using virtual servers, I quickly realized that it didn’t, hence the fixing which takes the following form, first in the new bootstrap.php file:

$GLOBALS['base_dir'] = '';

Here we use a virtual server, hence the empty string, if you were using a subfolder you would put the name of it there, perhaps like this:

$GLOBALS['base_dir'] = 'emailer';

The next change is in helpers.php (Smarty insert functions):

function insert_path($p){
	return empty($GLOBALS['base_dir']) ? "http://{$_SERVER['SERVER_NAME']}/" : "/{$GLOBALS['base_dir']}/";
}

And this new function can be used like this (from index.tpl):

<link rel="stylesheet" href="{insert name="path"}css/styles.css" type="text/css" />

The site directory is now initiated like this in Ctrl.php:

$this->site_dir	 = empty($GLOBALS['base_dir']) ? "/" : "/{$GLOBALS['base_dir']}/";

I’ve also implemented phpass for password hashing, the library can easily be used through two methods in Common.php:

static function hash($str){
	$hasher = new PasswordHash(8, FALSE);
	return $hasher->HashPassword($str);
}

static function checkHash($str, $old_str){
	$hasher = new PasswordHash(8, FALSE);
	return $hasher->CheckPassword($str, $old_str);
}

The $old_str argument would here contain the hash you already have in the database that you want to test the posted password against.

Those are the most important changes in Smoc itself, let’s take a look at an example of usage in the send_mail.php file:

require_once('bootstrap.php');

include_once('Zend/Loader.php');
Zend_Loader::loadClass('Zend_Mail');
Zend_Loader::loadClass('Zend_Mail_Transport_Smtp');

$send_to = Common::loadCtrl('Sendlist')->findBy('sent', 0);

foreach($send_to as $send){
	$params = Common::objToAssoc($send->N->Acc, array('port', 'username', 'password'));
	$params["auth"] = 'login';
	
	$mail = new Zend_Mail('UTF-8');
	
	if($send->N->Acc->server != $server)
		$smtp_server = new Zend_Mail_Transport_Smtp($send->N->Acc->server, $params);
	$server = $send->N->Acc->server;
	
	$mail->setBodyHtml($send->N->body);
	$mail->setFrom($send->N->Acc->email, $send->N->Acc->domain);
	$mail->addTo($send->R->email, $send->R->email);
	$mail->setSubject($send->N->title);
	$mail->send($smtp_server);
	$send->sent = 1;
	$send->save();
}

Since this file is meant to be run through cron jobs we don’t use the normal routing (Common::index()) here, we simply load the controller directly instead and start by fetching all elements in a sendlist with sent 0.

We loop through the sendlist and use Zend_Mail to send to each, note how we have access to all the relations, N = Newsletter, R = Recipient and Acc = email account to send with.

Finally we set each item’s sent field to 1 so we don’t send to that recipient again and save. In this case, without Doctrine, we would’ve had to create massive SQL statements here to join all the tables, a hassle we are now spared.


Related Posts

Tags: , , , ,