有几年没进“园子”了,这还是13年鼓捣CI时翻地两篇文章,当时还为把"Fat Model,Thin Controller"翻成“轻Controller,重Model”很是得意了一阵儿。这两篇应该不完整,一直放在草稿箱里。作者忘记名字了,只记得知道Laravel也是通过他,可惜连原文也找不到了。
第一部分:Models
重Model,轻Controller
标准Model约定
观察者模式
作用域
验证
MY_Model
第二部分:Views
Presenters
局部视图
分段缓存
第三部分:Controllers
自动加载视图
布局
自动加载模型
过滤器
第四部分:REST
HTTP:被遗忘的协议
CodeIgniter友好化半自动路由
卷一介绍
Ruby on Rails与其编程语言Ruby风靡web开发领域。在短短的八年间,他们成功积累了庞大的群体,并在网络上为包括Twitter,Shopify和Basecamp在内的网站提供着强大动力。优雅的语法,迅捷的开发速度和颇受争议的“约定胜于配置”的模式使其受到广泛赞誉。
其成功秘诀是什么呢?为什么我们这些CodeIgniter的开发者不能从中分到一杯羹呢?
在这本简约不简单的书中,我将去阐述,不仅我们要吃杯羹,我们还要遍尝美味佳肴。我们就来看看,如何以CodeIgniter友好的方式去实现那些让Rails开发者愉悦的那些基本理念。我们将探索“约定胜于配置”的理念并通过一些简单的步骤用在CodeIgniter中,用其轻松加愉快地编写可靠的应用。通过学习诸如“不自我重复”(DRY)的设计原则,我们会对自己的代码进行整理,并使用REST风格的控制器为整个应用提供统一的URL模式。
尽管有些师心自用,但我们将要学到的技术是非常灵活的。我真诚地希望你能够发现本书一些让CodeIgniter编程有趣起来的理念,说实话,真的很有意思。
PART1 Models
让我们先来看看MVC中的M,了解一下最好的model是什么样的,以及model层应该有多么强大,是的,它本应该很强。我们将要仔细研究全面提升models性能的方法,也会看一些Rails风格的模式从而在编写models时更加有效率。我们也会讨论一些在CodeIgniter世界中常见的一些错误,去看看该如何修正他们并找到更好的解决方法。
MVC设计模式教给我们一套创建结构更加健壮应用的规则。这其中最重要的就是models存储所有有关数据处理的代码,这些代码控制着数据或数据状态的变化。CodeIgniter的MVC实现是非常松散的,一定程度上它允许开发者绕过model层直接在controller层与数据库交互。这促生了一些不好的做法,例如,将验证逻辑放在controller中。事实上,MVC中的M应当存放关乎这样的代码:存放数据到数据库中,验证数据,发送邮件,连接API或者计算统计数据。
在Rails的世界中,整个构思都围绕着两个理念:重model轻controller和约定胜于配置。
重Model轻Controller
这个想法告诉我们,我们需要在models中根据逻辑来编写我们的应用。下面是第一个结论:如果你无法确定代码放置位置就应当放在model中。
models应当是重量级的。他们应该是英国周日午餐里的多汁鸡,用特制的应用程序做馅,可口,肉感,肥厚。控制器应该是卤汁,轻量级的,清淡,简练。别误解我的意思,它也十分重要,卤汁将肉和配料融合在一起,并用盘子提供了一个通信交流的平台。但,他不是这道菜的重点。
继续沿用这个比方,view就是配料。蔬菜,调味品和码在上面的肉。所有的这些盛在盘中,让人看上去垂涎欲滴。
就此打住,再说下去就饿了,我希望这让原理变得清晰。驱动应用的应该是model,不是controller,model才是驱使一切的。我们依靠controller将其融为一体,依靠view提供给用户直观友好的数据表现,但真正起决定作用的是model。好比我们之所以坐在桌前,是因为想吃鸡。
同样要记住的是,数据不一定是来自数据库中的。它有可能来自API,或者服务器端的一个文件,或者用户的session。它甚至来自另外一个不同的数据库。所谓解耦就是整个应用使用同一界面与任意数据源的数据交互。解耦性对应用的可持续增长至关重要。
标准Model约定
可以这么说,Ruby on Rails最重要的设计决策就是“约定胜于配置”,在整个项目中建立起一套约定,这比依靠大量的配置和更改来说强多了。这样一来,开发者只需做少量决策,定义那些特殊的地方就行了。
实际操作中,当建立model时,我们做了一堆关于数据库和其对应着表的假设。这让我们可以写更少的代码,然全局保持一直能让我们抽象出一些模块化的东西。
这是我的约定:
- 每个基于数据库的model映射一个单独的数据库表
- model的命名为单数,数据库表的命名为复数
- 每个数据库表包含一个id列
- 每个数据库表包含created_at和updated_at列
这些都是做得合理的假设,通过他们,我们可以提高model层的代码质量并予以简化。
让我们一一看来,通过例子来看看该如何实现。
每个基于数据库的model映射一个单独的数据库表
任何规则都有例外(马上就会看到)。不过,大部分情况下,每个基于数据库的model都会主要与一个数据库表交互。
我们会经常通过不同的渠道用一些方法与数据库表进行交互,比如标准的增删改查(CRUD)方法。正因为此,我们准备通过下面这个类来定义数据库表。
public function get($where) { return $this->db->where($where) ->get('users') ->row(); } public function get_all($where) { return $this->db->where($where) ->get('users') ->result(); } public function insert($user) { return $this->db->insert('users', $user); } public function update($where, $user) { return $this->db->where($where) ->update('users', $user); } public function delete($where) { return $this->db->where($where) ->delete('users'); }
简单说一下,我们其实没有必要通过类来定义,我们可以用一个实例变量来一次性定义表。
class User_model extends CI_Model { protected $_table = 'users'; // ... $this->db->get($this->_table); $this->db->insert($this->_table, $user); $this->db->update($this->_table, $user); $this->db->delete($this->_table);
我用$this->_table调用,所以不会和CodeIgniter的table类库冲突。
As well as specifying the table across most–if not all–of our models, chances are we're going to have this aforementioned standard roster of methods in them too。我们可以扩展CodeIgniter的CI_Model建立一个MY_Model类来代替那些重复内容。这保证我们遵循了另一个Rails的重要原则--不自我重复(DRY).
我们所有的models都可以扩展这个MY_Model,通过扩展MY_Model我们能任意获取这些基本的CRUD功能。在application/core目录中创建一个新文件,叫做MY_Model.php。
class MY_Model extends CI_Model { public function get ($where ) { return $this->db->where($where ) ->get ($this->_table ) ->row (); } public function get_all($where ) { return $this->db->where($where ) ->get ($this->_table ) ->result (); } public function insert ($data) { return $this->db->insert ($this->_table , $data); } public function update ($where , $data) { return $this->db->where($where ) ->update ($this->_table , $data); } public function delete ($where ) { return $this->db->where($where ) ->delete ($this->_table ); } }
CodeIgniter将会为我们加载这些。我们能够从MY_Model扩展我们自己的model,记住要定义一个表:
class User_model extends MY_Model { protected $_table = 'users'; } class Post_model extends MY_Model { protected $_table = 'posts'; } class Category_model extends MY_Model { protected $_table = 'categories'; }
这下我们就能完全一致、优雅时尚、不用来回复制代码地来访问这三个表了。
model的命名为单数,数据库表的命名为复数
你注意到我们刚才写的models的一个模式了吗?
每个model有一个单数名称(缀上了_model),然后每个表都是这些单数名称的复数。这将会非常非常地方便!显而易见地,我们可以将单数的model名称复数化从而自动猜测出表名。
写点代码来猜解吧。可以将它放在MY_Model类的构造器中,这能确保model一旦加载它们就能运行。
public function __construct () {
确保也调用了CI_Model的控制器,我们需要访问CodeIgniter核心。
parent ::__construct ();
现在可以加载CodeIgniter的inflector辅助类了,它包含了一堆有用的函数用以处理英文字符串。
$this->load->helper ('inflector' );
最后,我们可以用get_class()取得类名,记得要去掉_model并将其复数化。对于非常规的命名会仅仅去猜解一下表名。
if ( ! $this->_table ) { $this->_table = strtolower(plural (str_replace ('_model' , '', get_class($this)))); } }
plural()是个非常先进的函数,能将大部分的单词复数化。如果纠结于一个复数化单词,可以很容易地在model中直接定义然后去覆盖。
这下我们就不用在model里对表显式定义了。
class User_model extends MY_Model { } class Post_model extends MY_Model { } class Category_model extends MY_Model { }
享用吧!
每个数据库表包含一个id列
我们做了另外一个稳妥地假设是,只使用id列查询数据库表的单行。有了这个假设,我们可以将上段中的CRUD方法重写一下。把get()方法小改一下:
public function get () { $args = func_get_args (); if (count($args) > 1 || is_array ($args[0])) { $this->db->where($args); } else { $this->db->where('id', $args[0]); } return $this->db->get ($this->_table )->row (); }
这样既可以用WHERE的多重条件选择,也可以只通过ID列进行选择。
复制这些方法并命名为get_all(),将最后一行的row()改为result():
return $this->db->get ($this->_table )->result ();
还可以调整insert()方法,让它返回新的ID:
public function insert ($data) { $success = $this->db->insert ($this->_table , $data); if ($success ) { return $this->db->insert_id(); } else { return FALSE; } }
$this->db->insert_id()能得到新插入的ID并作为返回值返回。
最后,用相似手法调整update()和delete()方法:
public function update () { $args = func_get_args (); if (is_array ($args[0])) { $this->db->where($args); } else { $this->db->where('id', $args[0]); } return $this->db->update ($this->_table , $args[1]); } public function delete () { $args = func_get_args (); if (count($args) > 1 || is_array ($args[0])) { $this->db->where($args); } else { $this->db->where('id', $args[0]); } return $this->db->delete ($this->_table ); }
像这样,ID现在默认成为表行的唯一标识符。来看点controller与我们的model交互的代码:
public function test() { $id = $this->user->insert (array( 'username' => 'jamierumbelow' )); $user = $this->user->get ($id ); $this->user->update ($user->id, array( 'username' => 'jamierumbelow' )); $this->user->delete ($user->id); }
共同的约定使得语法简短简洁。使用基本的一些技术,遵循一些基本的原则,能明显改善代码质量。
每个数据库表包含created_at和updated_at列
这是里面有意思的一个。虽然你可能并不总是需要知道数据库表的一行什么时间创建了,什么时间更新了,但这依然很有用。如果特定的资源创新或更改时出现内部错误,它会对我们非常有帮助。此外,当导出这些数据时、维护缓存时、从远程获取数据时,这些时间和日期会帮我们了解的更清晰。
可能大多数时候情况并非如此,当你需要的时候,你会非常乐意将它们放进来。让我们在insert()和update()方法中添加几行:
public function insert ($data) { $data['created_at'] = $data['updated_at'] = date('Y-m-d H:i:s' ); $success = $this->db->insert ($this->_table , $data); if ($success ) { return $this->db->insert_id(); } else { return FALSE; } }
date('Y-m-d H:i:s')你肯定了解,是典型的MySQL的DATETIME格式,根据实际情况调整你的数据库服务器。
public function update () { $args = func_get_args (); $args[1]['updated_at'] = date('Y-m-d H:i:s' ); if (is_array ($args[0])) { $this->db->where($args); } else { $this->db->where('id', $args[0]); } return $this->db->update ($this->_table , $args[1]); }
现在,每当你通过model在数据库里插入或更新东西,created_at和updated_at就会适当更新。
观察者
很多时候,你需要在models中将数据改来改去。通常是像将当前用户ID分配到表中的一行,添加时间戳,加密密码等。用MVC实现起来的一个办法就是重载基本方法或者在model中添加自定义的方法。但可能会正常运行,但并不漂亮,最终不符合DRY原则。
最好的办法是使用被称为观察者模式的技术。观察者是待在你model中的回调方法。他们在一些特定的点(或状态改变)被调用。你可能已经对观察者模式很熟悉了,它被广泛用在整个编程世界中应付各种情况。
我们需要注意一些点(状态变化、瞬间),它们包括发生前和发生后:
- 某一行被创建了
- 某一行被更新了
- 某一行被检索了
- 某一行被删除了
- 验证
我们重点看看第一条。在你想通知状态改变了的方法中,有很多瞬间。不过,设计模式总是惊人的相似,所以,加上这些模式非常容易。
类似验证规则,我们在model顶层定义一个观察者:
class User_model extends MY_Model { public $before_create = array( 'hash_password' ); }
接下来修改MY_Model的insert()方法以使hash_password()方法在插入数据库前被调用。
foreach ($this->before_create as $method) { $data = call_user_func_array(array($this, $method), array($data)); } $success = $this->db->insert ($this->_table , $data);
然后添加before_create数组至MY_Model,这样如果用户没有定义任何方法也不会出错。
public $before_create = array();
接着用同样方法创建after_create():
public $after_create = array(); // ... $success = $this->db->insert ($this->_table , $data); if ($success ) { foreach ($this->after_create as $method) { call_user_func_array(array($this, $method), array($data)); }
但是,挑剔的读者会注意到,我们现在重复的代码违反了DRY原则。让我们将观察者机制抽象到一个observe()函数中。
public function observe($event , $data) { if (isset($this->$event ) && is_array ($this->$event )) { foreach ($this->$event as $method) { $data = call_user_func_array(array($this,$method), array($data)); } } return $data; }
接着就可以用这个函数了,它提供给我们一个很好的观察者,比复制代码好多了。
$data = $this->observe('before_create', $data); $success = $this->db->insert ($this->_table , $data); if ($success ) { $this->observe('after_create', $data);
现在来实现hash_password()函数:
public function hash_password ($user) { $user['password'] = sha1($user['password']); return $user; }
一个用于加密用户密码的简单机制。一定要记住,每个定义的观察者回调函数都需要返回已经通过的$data变量。
适当地使用这个抽象了的观察者模式,我们能够在model中任意添加观察者,甚至将它用在我们的自定义方法中。观察者也是另外一种简化和提高代码性能的方法。
作用域
model作用域能简化(并美化)查询处理,能让数据库查找起来更加的优雅和简便。它开启了一个全新的“有读写能力”的编程新世界。model作用域的本质是将已命名的方法链接到一起,你可以改变查询参数,保留可读性的同时在model中保持逻辑,从而保证代码不自我重复。类似地,CodeIgniter的ActiveRecord允许你将方法链接到一起来创建查询,可以使用model作用域逐步添加到查询中。
编写作用域的窍门是返回$this。通过返回$this,你可以返回当前model类的实例。PHP中可以链接这些方法到其他地方。例如,一个正常的model函数:
public function get_all_confirmed() { return $this->db->where('confirmed' , 1) ->order_by ('date' ) ->get ($this->_table ) ->result (); }
这看起来并不乱套,但如果我们要通过country获取多个confirmed行时会发生什么呢?
public function get_all_confirmed_by_country ($country ) { return $this->db->where('confirmed' , 1) ->where('country', $country ) ->order_by ('date' ) ->get ($this->_table ) ->result (); }
有点乱了。我们还想通过first name获取多个confirmed行,因此定义了另一个get_all_confirmed_by_blah()方法,我们把这些都合并一下:
public function get_all_confirmed_by($key, $value ) { return $this->db->where('confirmed' , 1) ->where($key, $value ) ->order_by ('date' ) ->get ($this->_table ) ->result (); }
那么如果我们只想获取一条记录该怎么办?把方法复制一遍?还是添加一些逻辑然后再加上第三个参数用来返回一个或多个?你会发现,咻的一下整个查询过程就变得复杂了。让我们重新思考一下。如果,我们有一个添加了confirmed查询的干净方法,用一个方法获取所有的行,一个方法根据条件获取所有的行,一个方法根据条件只获取到一行,情形会怎样呢?
public function confirmed() { $this->db->where('confirmed' , TRUE); return $this; } public function by($key, $value ) { $this->db->where($key, $value ); return $this; } public function get_all() { return $this->db->get ($this->_table ) ->result (); } public function get () { return $this->db->get ($this->_table ) ->row (); }
这好了很多。如果结合到之前的MY_Model,我们甚至还能让model更简洁。
public function confirmed() { $this->db->where('confirmed' , TRUE); return $this; }
如何与model交互呢?
$users = $this->user->confirmed()->get_all(); $users_uk = $this->user->confirmed()->by('country', 'United Kingdom' )->get_all();
这几近一个完美的系统了。仅用少量代码,我们大幅清理了model并提供了一个能添加查询参数的可复用的接口。我们可以任意添加或删除作用域,它是简单或者综合全面仅仅取决于我们的喜好。甚至不必限制数据库查询。我们共享了一个类的实例,所以我们能够添加任何东西到实例变量中,然后可以打印,可以通过我们方法中的作用域操作其他和数据相关的进程。还剩下一个问题了。