CodeIgniter手册<卷一>

有几年没进“园子”了,这还是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并提供了一个能添加查询参数的可复用的接口。我们可以任意添加或删除作用域,它是简单或者综合全面仅仅取决于我们的喜好。甚至不必限制数据库查询。我们共享了一个类的实例,所以我们能够添加任何东西到实例变量中,然后可以打印,可以通过我们方法中的作用域操作其他和数据相关的进程。还剩下一个问题了。

posted @ 2013-01-24 22:40  Partoo  阅读(317)  评论(0编辑  收藏  举报