STBlog学习笔记
本帖最后由 qi_ruo 于 2011-3-12 02:39 编辑1 数据库设计 -- 万事开头难
STBlog是一个单用户多权限的博客系统,对于这种类型的博客,数据库设计一般需要这几张表:
[*]用户表(users) -- 保存用户的信息,包括用户名,密码,电子邮件,用户组等信息;[*]设置表(settings) -- 保存博客的一些设置,如博客的名称,描述等其他一些个性化设置;[*]文章表(posts) -- 最主要的一张表,用于保存文章,包括文章标题,内容,创作时间等;[*]评论表(comments) -- 评论使得博客更易于交流,用于保存评论,包括评论者的姓名,电子邮件,内容,评论时间等;
以上是几个最主要的元素,有时我们需要将文章分类,这样更易于管理和查阅,如果一篇文章只能放在一个分类下,那么只需要创建categories表就可以了,然后在文章表里添加一个字段:post_id;如果一篇文章可以放在多个目录下,我们则需要两张表:
[*]分类表(categories) -- 用于保存分类,包括分类名,描述等;[*]分类映射表(categoryRelationships) -- 用于保存文章和分类的映射关系;
Tag(标签)是很流行的一个元素,它跟分类有点类似,但又有自己的特点,因为一篇文章可能涉及到很多方面,有很多的关键词,如果为每个关键词创建一个分类,将使得目录结构异常的庞大,Tag则很好的解决了这个问题,各个Tag是一种平行的关系,而且可以根据Tag进行相关性的分析,从而产生一种相关性的分类,为了创建我们需要两张表:
[*]Tag表(tags) -- 用于保存标签,包括标签名,描述等;[*]Tag映射表(TagRelationships) -- 用于保存文章和标签的映射关系;
此外,在文章中加入附件如(图片,音乐或其他文档)成为一个常用的功能,为了保存文章的附件信息,一般而言,一个附件只能属于一篇文章,所以我们只需要创建一张附件表:
[*]附件表(attachments) -- 用于保存附件信息,包括附件的文件名,所属文章的ID等;
最后,有时我们需要在博客中创建一些单独的页面如个人介绍,联系方法等,所以我们需要创建一个页面表用于保存页面信息:
[*]页面表(Pages) -- 用于保存页面信息,包括页面标题,页面内容等;
经过初步的规划,我们设计了十张表,但我们发现在这十张表里,有些表是类似的,我们可以将它们放在同一张表里,比如分类表和Tag表,他们的表结构是一样的,且与文章表的关系都是多对一的关系,所以我们把分类表和Tag表合并成一个表:
[*]项目表(metas) -- 用于保存文章的分类和标签,包括项目名,描述以及类别(category/tag)等;[*]项目映射表(relationships) -- 用于保存项目和文章之间的映射关系,包括文章ID以及项目ID;
此外,我们来比较文章表,评论表,附件表以及页面表这几张表的相同和差异,我们以文章表作为参考:
[*]文章与评论 -- 相似:都有内容、创建时间等;差异:文章可以单独存在,评论必须归属于某篇文章,而且文章的用户信息保存在用户表中,而评论必须保存评论者的信息,如姓名、邮件等;[*]文章与附件 -- 相似:都有作者、标题、创建时间等;差异:都可以单独存在,但附件一般情况下都属于某篇文章;[*]文章与页面 -- 相似:都有作者、标题、内容、创建时间等;差异:页面需要一个order字段保存在主页显示的位置;
在STBlog中,将文章表、附件表以及页面表合并为一张内容表posts,而将评论表独立出来,可能是基于这些方面的考虑:
[*]评论需要记录评论所属的文章ID; (这一点跟附件有点类似,在内容表中将order这个表示页面排序的字段用于保存附件的所属文章ID)[*]评论需要记录评论者姓名、邮件、网址、IP地址、客户端;(关于评论者的信息也可以放在users表中,对于没有注册的用户可以添加一个组guest)[*]为了统计方便,需要加入ownerId这个字段来记录评论所属文章的作者ID;(这个是可选的,加入这个字段只是为了方便统计属于某个用户的评论)[*]如果支持多级评论的话还需要记录上级评论的ID;(这个功能目前还没有实现)
到现在为止,我们只需要设计users, settings, posts, comments, metas, relationships 这六张表了。
2 用户及权限验证 -- 应用程序的门神
用户验证是Web应用程序的一个常见的功能,CodeIgniter框架没有自带Auth库,但在CodeIgniter Wiki上我们可以找到一些比较好用的Auth库,如Ion Auth, FreakAuth等,STBlog实现了一个简单的用户及权限验证(application/libraries/Auth.php)。
为了灵活的使用前台和后台,STBlog设计了两个父控制器:用于前台的ST_Controller以及用于后台的ST_Auth_Controller。
我们来看一下我们登录的时候,系统处理的过程:
[*]Login控制器的index()方法首先调用users_mdl模型的validate_user($username, $password)函数,先通过$username来获取user对象并调用Common类的静态函数hash_Validate($source, $target)判断密码是否正确,如果正确则返回该user对象;[*]调用auth类的process_login($user)函数,首先把用户对象$user赋值给auth的成员变量_user,并更新用户的最后登录时间、最后活跃时间以及密钥,再把用户对象序列化保存在$this->session->userdata('user')中,最后把auth的成员变量_hasLogin设置为TRUE;[*] 如果存在ref变量则跳转到ref页面,否则跳转至admin/dashboard页面;
再来看一下我们查看后台页面时,系统处理的过程,我们以admin/dashboard页面为例:
1) Dashboard的构造函数首先调用父类ST_Auth_Controller类的构造函数,加载验证库,并调用auth类的函数hasLogin()检查用户是否已经登录:
[*]如果auth类的_hasLogin已经设置为TRUE则返回TRUE;(此时_hasLogin还没有被设置)[*]如果auth类的_user成员的uid存在,则调用users_mdl模型的get_user_by_id($uid)方法获取用户对象;(_user对象在auth类的构造函数中从session变量user获取)[*]如果该对象的token与auth类的_user成员的token相等,则更新用户的最后活跃时间,并设置auth的_hasLogin为TRUE;(这里我们考虑下什么时候为什么要检查token,用户每次登录后都会产生新的token,所以如果你先在一个浏览器登录后台,再用另一个浏览器重新登录,数据库中的token将会更新,所以在第一个浏览器中用户因为token不等而退出,这样做是为了更加安全吧 如果用户没有登录则跳转登陆页面(redirect('admin/login?ref='.urlencode($this->uri->uri_string()));),这里使用了一个技巧,把用户想登录的后台页面的地址保存在ref参数中,这样当用户登录后,就可以直接跳转到先前想要浏览的页面;
2) 执行Dashboard自己的构造函数,给一些通用的视图变量赋值;
3) 调用默认执行函数index(),首先调用auth类的exceed('contributor')函数检查该用户是否有'contributor'的权限,在STBlog中共有三个用户组,分别是administrator=>0(管理员), editor=>1(编辑)以及contributor=>2(贡献者),数字越小,权限越大;如果权限不够则会显示错误,否则继续执行下面的脚本;
3 主题 -- 让博客更有个性化
为了让博客变得更有个性化,很多博客系统都添加了这个功能,STBlog当然也不例外。主题一般是一组视图文件的集合,多个主题放在同一个目录下,当显示前台页面时,系统根据当前设置的主题动态的加载相关目录下的视图文件,为了更方便的调用视图,STBlog扩展了CI_Loader类(application/libraries/MY_Loader.php),给Loader添加了一个公有成员$theme以及函数switch_theme_on(),这个函数的功能就是把Loader的_ci_view_path根据当前的主题($theme)设置为相应的目录,此时在前台控制器里执行$this->load->view('page');时,系统会自动到当前主题的目录下加载视图文件。
但我们发现在STBlog的前台控制器中并没有使用$this->load->view();而是$this->load_theme_view(); load_theme_view()是在前台父控制器ST_Controller里定义的一个函数,我们来看看他的功能,首先检查相应的视图文件是否存在,然后调用$this->load->view()加载视图,下面是检查是否设置了缓存,如果设置了缓存,则缓存当前页面。
load_theme_view()的第三个参数$cached默认为TRUE,当设置为FALSE时即使设置了缓存功能,也不会缓存当前页面,主要用在一些不需要的场合,如Comments控制器的index()页面,由于一些视图参数需要动态的设置,如果该页设置了缓存,会产生一些错误的提示。 本帖最后由 qi_ruo 于 2011-3-12 02:43 编辑
4 缓存 -- 让页面飞起来
缓存包括页面缓存和数据库缓存两个方面,页面缓存上面已经介绍过,这里指的是数据库缓存类application/libraries/Stcache.php,先引用Saturn的一段注释:
一个简单的key-value文件缓存实现,非常适合缓存系统经常调用的(从数据库中提取的)一些数据,比如用户参数信息.为什么用户参数信息的存储不使用CI默认提供的DB缓存?因为CI提供的DB缓存的生成是以controller/method的形式生成,对于那些被多个控制器同时调用的公共数据,比如配置信息,采用DB缓存会生成同样内容的缓存文件。所以,我绝不是因为蛋疼而写了这个类。
整个缓存类有三个函数_file($key)用于设置缓存文件名,set($key, $data)用于把$data序列化并保存到相应文件,get($key)用于找到缓存文件,并取出文件中序列化串代表的对象。
我们来看一个使用的例子,在前台的视图文件中我们会看到setting_item()函数,比如head.php中的setting_item('blog_title');大概意思我们能猜出来,就是取出博客标题的意思,这个函数是在application/libraries/Common.php定义的。
setting_item():首先声明了一个静态数组,然后检查在这个数组中有没有需要查询的元素,如果没有的话则调用get_settings()函数引用赋值,再检查是否有该元素,如果有返回该元素的值,否则返回FALSE;(静态变量的好处是可以保留已查询的元素,比如在一个脚本里已经通过setting_item('blog_title')取出blog_title的值,那么第二次取blog_title的值时可以直接从保存在内存的静态变量取,而不需要数据库查询)
get_settings():首先声明了一个静态变量,如果这个变量还没有被设置,则加载stache类库,并调用get('settings')函数赋值,如果没有该缓存文件,则执行SQL查询生成关联数组,并通过set('settings', $settings)生成缓存文件,最后赋值给静态变量并返回;
所以查询一个设置值时,按照对“静态变量->缓存文件->数据库”的顺序进行查询,提高了性能。
stcache不仅能缓存设置值,前台页面的其他一些元素也可以缓存,比如页面导航栏st_plugins/navigation/Navigation.php,在输出导航栏时,首先检查是否存在'Widget::Navigation'缓存文件,只有在该文件不存在的时候,才进行SQL查询;在其他插件中我们都能看到stcache缓存类的使用。
缓存的好处是毋庸置疑的,但它的优点也是它的缺点,页面不能即时更新,比如你在后台修改了某个页面的标题,但不管怎么刷新,前台的导航栏显示的依旧是以前的标题,这时你可以手动删除缓存目录下的缓存文件,好在不用这么麻烦,在STBlog的后台的静态缓存页面有一个清除缓存的链接,点击一下就可以清除缓存了,我们来看一下这个文件admin/settings.php的cacheClear()函数,它依次调用utility类的两个函数clear_file_cache()和clear_db_cache()分别删除页面缓存和数据库缓存目录下的所有文件。
5 插件 -- 扩展博客功能的途径
关于插件的机制的理解及实现请参阅Saturn写的一篇日志 PHP中插件机制的一种实现方案
STBlog中的插件类存在于application/libraries/Plugin.php,在插件类的构造函数中实现调用utility类的get_active_plugins()函数获取已激活的插件数组,然后遍历这个数组,依次加载插件文件并初始化。此外还有三个函数,register()用于注册插件方法,check_hook_exist($hook)用于检查插件方法是否存在,trigger($hook)用于触发插件函数。
我们来看一个插件的实例:st_plugins/navigation/Navigation.php
1) 在Plugin类的构造函数中通过utility类的get_active_plugins()函数获取所有已激活的插件,并创建他们的实例;
2) 在每个插件类的构造函数中把插件函数注册到Plugin类的$_listeners成员变量中;如在Navigation插件类的构造函数中通过$plugin->register('Widget::Navigation', $this, 'render');把$_listeners['Widget::Navigation']['Navigation->render']设置为array(object(Navigation), 'render');
3) 当程序遇到$this->plugin->trigger()函数时,触发插件函数;如在themes/default/header.php视图文件中触发Navigation插件函数:$this->plugin->trigger('Widget::Navigation', '<li><a href="{permalink}">{title}</a></li>'); trigger()函数确定该触发函数是否已注册,并且在该插件对象中存在此函数时,使用call_user_func_array()调用此触发函数,在该例中将执行Navigation类的render()方法,该函数的功能是获取所有的页面,并把参数$format中的{permalink}和{title}置换成页面的缩略名和标题打印出来;
4) 激活与禁用插件
在admin/plugins.php的Plugins控制器的active方法中首先通过plugins_mdl的get($name)方法获取获取该插件的信息:如作者、版本等;再调用plugins_mdl的active($plugin)方法激活该插件:首先检查是否该插件已激活,如果没有的话,把该插件添加到已激活的插件数组中,并把它序列化保存在settings表的active_plugins项目中;禁用插件与此类似。
6 备份插件 -- 今天你备份了吗?
定期的给博客备份可以使我们发现服务器故障或数据丢失时不至于感到沮丧,一个最直接的方法就是用PHPMyAdmin的导出功能导出SQL文件,不过这有点麻烦,CodeIgniter的dbutil类可以帮助我们备份数据库,但仅在MySQL数据库时有效。
为了添加备份功能,我们可以添加一个控制器或者在某个已存在的控制器中加入一个方法,比如可以在Settings控制器中加入一个backup方法:public function backup(){
$this->load->dbutil();
$this->load->helper('download');
$backup =& $this->dbutil->backup();
$file_name = 'stblog-' . date('Y-m-d') . '.sql.gz';
force_download($file_name, $backup);
}这样我们通过访问admin/settings/backup时就可以下载备份文件了。但如果使用插件来实现就更合适了,因为这样不仅不需要改变原控制器的结构,而且可以在需要备份的时候才激活插件,这样更加灵活。
STBlog自带的插件大多是在视图文件中用于显示内容的插件,那么怎样才能在控制器中没有backup函数但通过访问admin/settings/backup实现备份功能呢,我们可以从jeongee编写的STBlog新浪微博插件中得到一些启发,我们可以在settings控制器的构造函数中触发一个插件函数,用于检查URI的第三个参数是不是backup,如果不是继续执行下面的代码,否则运行插件函数里的代码。class Backup
{
private $_CI;
public function __construct(&$plugin)
{
$plugin->register('Backup::Backup', $this, 'backup');
$plugin->register('Backup::Display', $this, 'display');
$this->_CI = &get_instance();
}
// 用于备份的插件函数
public function backup()
{
if ($this->_CI->uri->segment(3) != 'backup') return;
$this->_CI->load->dbutil();
$this->_CI->load->helper('download');
$backup =& $this->_CI->dbutil->backup();
$file_name = 'stblog-' . date('Y-m-d') . '.sql.gz';
force_download($file_name, $backup);
}
// 用于显示备份链接的插件函数
public function display($format)
{
echo str_replace('{backup}', anchor(site_url('admin/settings/backup'), '备份数据库'), $format);
}
}在Settings控制器的构造函数中触发'Backup :: Backup'插件$this->auth->exeed('administrator');
$this->plugin->trigger('Backup::Backup');在视图文件views/admin/dashboard的侧栏触发'Backup :: Display'插件<li><?php echo anchor(site_url('admin/settings/general'),'修改系统设置');?></li>
<?php $this->plugin->trigger('Backup::Display', '<li>{backup}</li>');?>这样就可以显示一个备份的链接;在后台的插件管理中激活该备份插件就可以备份博客的SQL文件了。
附件是我编写的一个简单的备份插件,可以选择将备份文件本地下载或发送到指定邮箱,欢迎大家使用。
本帖最后由 qi_ruo 于 2011-3-12 02:32 编辑
7 微博插件 -- 让更多的朋友看到你的日志
国内的微博近来都发布了自己的开放平台,并采用OAuth协议为第三方提供接入服务,jeongee开发的STBlog新浪微博插件可以使我们发布文章时自动发布一条消息到自己的新浪微博,使得我们的好友能够及时的获悉博客的更新,非常的方便,我们大致的了解下这个插件。
在新浪微博的开放平台的网站 http://open.t.sina.com.cn/ 我们可以了解OAuth的原理以及开发一个应用的大致过程。
我们来看一下Sina_weibo这个类(st_plugis/sina_weibo/Sina_weibo.php):
首先是$akey和$skey,也称为API KEY以及Secret Key,这个是这个应用在新浪微博开放平台注册应用时分配的两个Key;
在构造函数中注册了三个插件函数,分别是login(), callback(), 以及update()[*]在Posts控制器的构造函数中触发callback()函数;[*]当文章发布后触发update()函数;[*]在撰写文章的视图中触发login()函数
1) 当我们访问admin/posts/write时,构造函数首先触发callback()函数,首先检查第三个参数是不是'sina_weibo_callback',因为不是,所以脚本继续执行;
2) 当显示视图时触发login()函数,首先调用_check_auth()函数检查是否存在session变量'last_key',因为不存在,所以创建了一个WeiboOAuth()认证对象,参数分别是API Key和Secret Key,首先调用认证对象的getRequestToken()函数,将返回一个包含oauth_token和oauth_token_secret的数组,并将它们保存在session数组中;将oauth_token和回调页面的地址传递给getAuthorizeURL()函数将获取一个类似 http://api.t.sina.com.cn/oauth/authorize?oauth_token=$token 的网址,这个网址将用户重定向到授权页;最后将这个网址输出在页面上。
3) 如果我们点击这个链接,网页将重定向至新浪微博的授权页,如果用户同意授权,页面将重定向至admin/posts/sina_weibo_callback/callback这个页面,也就是刚才指定的回调页面;
4) Posts的构造函数中将触发callback()函数,因为第三个参数正是'sina_weibo_callback',callback()将继续执行,因为第四个参数是'callback',将创建一个认证对象,参数分别是API Key, Secret Key, oauth_token以及oauth_token_secret,这称为用Request Token换取Access Token的过程,我们将得到一个Access Token以及Access Secret并将它保存在session变量'last_key'中;最后重定向到admin/posts页面;
5) Posts的构造函数再次触发callback()函数,因为第三个参数不再是'callback'所以脚本继续执行,当打印视图时将再次触发login()函数,首先调用_check_auth()函数检查是否存在session变量'last_key',也就是Access Token和Access Secret,因为刚才已经得到了这个Token,所以将创建一个WeiboClient客户对象,参数分别是API Key, Secret Key, Access Token以及Access Secret,调用这个客户对象的verify_credentials()函数将获取该Access Token的信息以及对应用户的信息,我们将之保存在这个插件类的account成员变量中;
6) 下面我们写好了文章,并选择同步发布到微博,点击更新文章,页面提交到admin/posts/write,首先触发callback()函数,因为第三个参数是不是'sina_weibo_callback'所以继续执行,当文章成功保存后,触发update()函数;update()检查用户是否选择同步发布到微博,如果是则调用_check_auth()验证并调用客户对象的update()函数发布一条微博;update()结束后页面将跳转到文章管理页面。
7) 如果我们回到撰写文章页面,点击退出新浪微博,将重定向至admin/posts/sina_weibo_callback/logout并触发callback()函数,因为URI的第四个参数是logout,将会把session的last_key也就是Access Token和Access Secret清空并重定向至admin/posts;因为此时Access Token不再存在,所以在打印视图触发login()函数时将显示登录微博的链接;
以上就是这个微博插件的工作流程,国内的很多网站像腾讯微博、网易微博、搜狐微博、人人网等近来都发布了开放平台并使用OAuth进行认证,文档和API等在各自的网站上都有说明。
附件是仿照jeongee编写的腾讯微博插件,欢迎大家使用。使用方法请参照 http://codeigniter.org.cn/forums/thread-7015-1-1.html
其中三个插件分别改为:$this->plugin->trigger('tencent_weibo_callback');
$this->plugin->trigger('tencent_weibo_update');
$this->plugin->trigger('tencent_weibo_login');
写了这么多,强烈支持啊,看来大家的学习激情还是不错的啊,嘿嘿,无法加分,HEX速度来加上!
:handshake 好文章,强烈支持 看了这个 简直是祝我一臂之力. 不错 学习了 受益匪浅~~~非常感谢~~~~ 我参考 inktype 做过一个项目,也是很精彩的项目,有空可以学习 inktype
哈~~不错。学习下。