The blog saga continues, we still don’t have any fancy Wordpress style filtering of the content. You know, creating these nice looking quotes and filtering potentially nasty html and stuff. Sure enough, TinyMCE has some function for allowing only certain tags and discarding others but I haven’t had time to look at that yet, I will though… Fancy filtering is not in the requirements for this project either however. But changing and deleting blog posts are necessary actions though, so is some kind of search for articles. Let’s begin by looking at what’s been added to the desktop menu:
INSERT INTO `db_menuitems` (`id`, `db_menu_id`, `item_class`, `item_link`, `item_image`, `image_alt`, `link_text`, `position`, `item_imageover`, `nosess_show`, `sess_show`) VALUES (26, 4, '', 'index/d/c/blog/f/administrate', '', '', 'Administrate Blog', 6, '', 0, 1);
INSERT INTO `db_menuitems` (`id`, `db_menu_id`, `item_class`, `item_link`, `item_image`, `image_alt`, `link_text`, `position`, `item_imageover`, `nosess_show`, `sess_show`) VALUES (27, 4, '', 'index/d/c/blog/f/getSearchForm', '', '', 'Search for articles', 7, '', 0, 1);
So we’ve got two entry points to the new stuff. Administrate() and getSearchForm(). Let’s begin with the administration part:
function administrate(){
$articles = $this->getArticles($this->usrid)->toArray();
foreach($articles as &$article)
$article['categories'] = $this->getCatsByArticle($article['id'])->toArray();
$this->assign_by_ref('articles', $articles);
return $this->fetch('blog_administrate.tpl');
}
function getArticles($id = null, $count = null, $offset = null, $where = "user_id = ?", $order = "post_date DESC"){
$where = !empty($where) ? $this->obj->getAdapter()->quoteInto($where, $id) : null;
return $this->obj->fetchAll($where, $order, $count, $offset);
}
function getCatsByArticle($article_id = false){
if($article_id)
return $this->obj->fetchRow("id = $article_id")->findManyToManyRowset('blogcategories', 'blogcatcon');
return array();
}
The getArticles function is basically just an alias with some probable default values set. In this case it will retrieve all articles of the logged in user. Next we append all connected categories to each article, the number of round trips to the database could become huge here which is not optimal of course. However, focus is on speed at the moment, we have to get finished sometime and we don’t want to get bogged down by creating some massive optimized SQL statement that will fetch the right stuff from the beginning. Besides, SQL is not my strong side, I can hack most stuff but I’m not lightning fast, going with this possibly temporary PHP solution will do for now and is much faster. Furthermore, when the service is released it won’t get a bazillion users straight away either, performance can always be fine tuned later if need be.
You might also notice that we have a new alias, actually two:
function assign($key, $value = false){
if(is_array($key))
$this->smarty->assign($key);
else
$this->smarty->assign($key, $value);
}
function assign_by_ref($key, $value = false){
if(is_array($key))
$this->smarty->assign_by_ref($key);
else
$this->smarty->assign_by_ref($key, $value);
}
K, let’s look at the template blog_administrate.tpl:
{config_load file="$config_file"}
<table>
<tr>
<td>{#article_headline#}</td>
<td>{#categories#}</td>
<td>{#post_date#}</td>
<td>{#actions#}</td>
</tr>
{foreach from=$articles item=article}
<tr>
<td width="300px">
{$article.headline}
</td>
<td>
{foreach from=$article.categories item=category}
{$category.headline},
{/foreach}
</td>
<td>
{$article.post_date|date_format:$date_format}
</td>
<td>
<a href="{$baseUrl}/index/d/c/blog/f/updateArticle/id/{$article.id}">{#edit#}</a>
<a href="{$baseUrl}/index/d/c/blog/f/viewArticle/id/{$article.id}">{#view#}</a>
<a href="{$baseUrl}/index/d/c/blog/f/deleteArticle/id/{$article.id}">{#delete#}</a>
</td>
</tr>
{/foreach}
</table>
So, we’ve got three new functions here, updateArticle, viewArticle and deleteArticle:
function updateArticle($params){
if(!empty($params['id'])){
$entry = $this->obj->fetchRow("id = {$params['id']}");
$this->assign('article', $entry->toArray());
$this->assignCategories($params['id']);
$this->assign('update', 'yes');
return $this->fetch('blog_write.tpl');
}else
return "We really need an id if we are to update something";
}
function viewArticle($params){
$article = $this->obj->fetchRow("id = {$params['id']}")->toArray();
$article['categories'] = $this->getCatsByArticle($article['id'])->toArray();
$this->assign('article', $article);
return $this->fetch('blog_view_article.tpl');
}
function deleteArticle($params){
$article = $this->obj->fetchRow("id = {$params['id']}");
$this->deleteConsByArticle($article->id);
$article->delete();
return $this->administrate();
}
Apparently updateArticle will launch the blog_write.tpl with a flag called ‘update’ set to ‘yes’. Of course when saving there the good old saveArticle function will be called. In that function we now do one extra thing, in addition to the user id we also keep track of the user name in each article. Why we do this will become apparent soon enough.
Let’s move on to assignCategories() and deleteConsByArticle():
function assignCategories($article_id = false){
$categories = $this->cat_obj->fetchAll("user_id = {$this->usrid}")->toArrayWithKey('id');
$cur_cats = $this->getCatsByArticle($article_id);
foreach($cur_cats as $cur_cat)
$categories[ $cur_cat->id ]['checked'] = 'checked = "checked"';
$this->assign('categories', $categories);
}
function deleteConsByArticle($article_id){
$this->con_obj->delete("article_id = $article_id");
}
The delete connection function is just an alias. Why don’t I use the $_referenceMap with a cascade delete like it says in the ZF manual, or InnoDB with this set in the schema?
1.) I tried my ass off with the reference map but I couldn’t get it to work, I must have missed something, beats me what I missed though because this isn’t supposed to be complicated at all. Let me know if you’ve made it work and how you did it.
2.) InnoDB has wreaked havoc with my data on several occasions by crashing miserably, I don’t know why. It might have been my fault or something but it doesn’t really matter because I’ve never had any problems with MyISAM, that’s why I’m prepared to go through the extra trouble of explicitly deleting dependencies in this way.
You might notice something strange in assignCategories, it seems that the rowset has got a new function called toArrayWithKey. It has and it is my doing, instead of making yet another static function I opted to put the functionality in an extension of the rowset class this time because it’s somewhat prettier:
class ExtRowset extends Zend_Db_Table_Rowset_Abstract{
function toArrayWithKey($primary = false){
if(!$primary) return false;
$rarr = array();
$rowArr = $this->toArray();
foreach($rowArr as $row){
$key = $row[ $primary ];
$rarr[ $key ] = $row;
}
return $rarr;
}
function toKeyValueArray($key_field, $value_field){
$rarr = array();
$rowArr = $this->toArray();
foreach($rowArr as $row)
$rarr[ $row[$key_field] ] = $row[ $value_field ];
return $rarr;
}
}
ToArrayWithKey will convert the rowset to a 2d array and assign each sub array a key that matches the id of each entry. The whole point of this is to be able to easily keep track of checked / not checked categories in the template. Take a look at assignCategories again and you will understand what I mean. This functionality is only needed if we allow updating of articles, which we currently do. One more thing, we have to use the new ExtRowset class in ExtModel like this at the top: protected $_rowsetClass = ‘ExtRowset’; Don’t forget to include the new rowset script in the bootstrap file either.
Let’s retrace our steps to the beginning and the new menu item that links to getSearchForm():
function getSearchForm(){
return $this->fetch('blog_search.tpl');
}
And the template:
{config_load file="$config_file"}
<form id="search_category_form" action="{$baseUrl}/index/d/c/blog/f/searchCategory" method="post">
{#search_categories#}:
<br/>
<input type="text" style="width:250px;" id="search_field" name="search_field">
<br/>
<input type="submit" value="{#submit#}">
</form>
<br/>
<div>
<a href="{$baseUrl}/index/d/c/blog/f/getNewArticles">
{#view_new_posts#}
</a>
</div>
Let’s do searchCategory first:
function searchCategory($params){
$search_params = array(
"count" => $this->session->conf->cat_count,
"where" => array("headline LIKE '%{$params['search_field']}%'", "id IN ( SELECT category_id FROM com_blog_connect )"),
"obj" => $this->cat_obj,
"offset" => $params['offset']
);
$categories = $this->sumCategories( $this->getSearchResults($search_params) );
if(empty($params['offset'])){
$search_params['count'] = null;
$temp = $this->getSearchResults($search_params);
$this->session->category->tot_count = count($temp);
}
$this->assignPaginationArray($this->session->category->tot_count, $this->session->conf->cat_count, $params['offset']);
$this->assign('categories', $categories);
return $this->fetch('blog_cat_search_result.tpl');
}
Yeah, I know, it’s a biggie but it has it’s reasons, in probably 90% of all cases it would be smaller and easier. So we begin by creating an array that we will use with the necessary information like “count” which is how many results we want to display per page, “where” which is yet another array with what we want to search for, “obj” is the object we want to use in the search, leaving this empty will default to $this->obj, last but not least the “offset” which will control where we are in the pagination.
First of all, let’s start with $this->session->conf->cat_count. Since the last part I’ve mustered the strength to actually start putting stuff in a configuration table which we will later be able to change in the future admin interface:
CREATE TABLE `com_blog_config` (
`id` int(5) NOT NULL auto_increment,
`conf_var` varchar(100) NOT NULL,
`conf_val` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `com_blog_config` (`id`, `conf_var`, `conf_val`) VALUES (1, 'cat_count', '2');
INSERT INTO `com_blog_config` (`id`, `conf_var`, `conf_val`) VALUES (2, 'date_format', '%b %e, %Y');
INSERT INTO `com_blog_config` (`id`, `conf_var`, `conf_val`) VALUES (3, 'articles_per_page', '10');
INSERT INTO `com_blog_config` (`id`, `conf_var`, `conf_val`) VALUES (4, 'new_articles_count', '50');
Cat_count is 2 at the moment, it will probably be 10 or 15 in the end but I’ve set it to 2 now because that makes it easier for me to test pagination logic with low amounts of data.
This information is fetched in init() which we should revisit:
function init(){
parent::init();
$this->name = "blog";
$this->obj = $this->loadModel($this->name);
$this->cat_obj = $this->loadModel('blogcategories');
$this->con_obj = $this->loadModel('blogcatcon');
$this->usrid = $this->gs->usr_info['id'];
parent::finishInit();
$this->chkSessRdr();
parent::setConfig('blogconfig');
$this->assign('date_format', $this->session->conf->date_format);
}
In ExtController:
function setConfig($table){
if(!isset($this->session->conf))
$this->session->conf = (object)$this->loadModel($table)->fetchAllKeyValue();
}
In ExtModel:
function fetchAllKeyValue(){
return $this->fetchAll()->toKeyValueArray($this->_key_field, $this->_value_field);
}
In blogconfig.php:
class Blogconfig extends ExtModel{
protected $_name = 'com_blog_config';
protected $_primary = 'id';
protected $_key_field = 'conf_var';
protected $_value_field = 'conf_val';
}
Here we use the other function we have extended rowset with to fetch the config info as an array that has the field conf_var as it’s keys and conf_val as values. Notice the explicit type conversion from array to object, it’s easier to write ->var instead of [’var’], at least in my book.
The next function is getSearchResults:
function getSearchResults($params){
extract($params);
$obj = empty($obj) ? $this->obj : $obj;
return $obj->fetchAll($where, $order_by, $count, $offset)->toArray();
}
It’s basically just an alias which I’ve created to avoid a fetchAll function with an ugly list of long strings as parameters. We need the params array in more places and this is also a good way of encapsulating the dangerous but powerful extract function. I know, it’s a borderline example of trying to minimize code size at the expense of readability but we go with it for now. Let’s look at sumCategories:
function sumCategories($categories){
$final_cats = array();
foreach($categories as $cat)
$final_cats[ $cat['headline'] ] += $this->con_obj->fetchAll("category_id = {$cat['id']}")->count();
arsort($final_cats);
return $final_cats;
}
What’s happening here is that there might exist categories with the same headline created and attached to different users. When doing the search by category we want the number of articles in each category irrespective of which user has written something in that category. In this case we basically treat the categories as tags by their headlines even though there is a connection to a unique user for each category. Yes it’s complicated and convoluted, but if we had had a tag system then the complexity would have shown it’s ugly face when retrieving “categories” in the write/update template instead. Having a twofold solution like we maybe should have, would’ve added extra code and complexity in it’s own way. It’s always easy to create complexity but sometimes it’s impossible to avoid it. Note also that we are doing a looping retrieve from the database again, not exactly optimized.