PHP MVC、模板引擎及权限设计

本文转自"PHP爱好者":http://www.phpfans.org/?cat=1
一般用到“引擎”两字都会感觉比较高级,"模板引擎"这四个字听起来很高深的样子,类似游戏3D引擎、Zend引擎等,其实都是唬人的,骗外行人的。所以在我初学PHP的那会,也因为这四个字导致了我觉得很难而没有去看他到底是什么样一个东西,直到很长时间以后使用Smarty才真正了解模板引擎的原理和作用。Smarty(http://smarty.php.net),PHP官方模板引擎,看名字给人感觉应该很快,其实很慢,即使他有预编译(另一个看起来很高级的名词,同样也是唬人的,下面我会讲到这个)。[注:我刚才点开Smarty发现他说他已经不是一个PHP子项目了,汗,看来确实唬人,哈玩笑^_^]。其实在PHP里,模板引擎扮演着View(其实通俗说就是页面,看英文有时候会给人很高级的错觉)的角色,这是一个很重要的角色,因为用户的交互啊,界面效果啊等等都在这里,这是最终用户看到的你的系统的样子。

  开头就说模板引擎,只是跟大家说明一下这个东西其实没有什么难理解的,明白其原理以后你会发现他是纸老虎,所以你要有信心你会很轻松看完此文。

  为了更好的说明模板引擎所扮演的角色,我不得不也谈谈MVC。这个话题恐怕互联网上谈及的很多,我也只能根据我的理解来描述,可能有不恰当的地方,欢迎讨论。通常的MVC是指Model、View和Controller。也就是模型、视图和控制器。我理解MVC也是在学了PHP不短时间后了,当时请教老廖(http://qeephp.com),才恍然大悟。

  先来说说Controller,也就是控制器,控制器是个什么东西呢?在PHP里他是扮演一个接收用户请求,把用户请求定位到指定数据模型的角色。解释起来感觉不是很好解释,来看一个简单的留言本的例子:

 
01. //用户请求可能是 http://www.example.com/guest.php?module=list
02. $module = $_GET['module'];
03.   
04. switch ($module) {
05.     case 'list':
06.         require_once 'list.php';
07.         break;
08.     case 'add':
09.         require_once 'add.php';
10.         break;
11.     case 'del':
12.         require_once 'del.php';
13.         break;
14.     default:
15.         require_once 'list.php';
16.         break;
17. }

  是不是看起来很简单好像没什么东西呀,只是根据用户的请求参数包含不同的文件而已。没错,确实很容易,这个switch语句其实就一个最简单的控制器的实现。他控制什么?他控制你根据不同的用户请求参数调用不同的数据模型处理用户请求。那么这里的list可能是一个留言列表,add是添加留言,del是删除留言。Controller的传统实现可以这么简单,当然现在的很多技巧包括根据不同的用户请求包含不同的业务逻辑处理类,比如list自动定位到/model/List.class.php这样的一些技巧性操作等。

  再来说说Model,其实我们一般花比较长时间设计和编写的也是这块内容,也就是具体的业务逻辑实现。比如一个留言列表要处理些什么,都是在这里实现。还是直接看一个Model例子比较直观:

 
01. //Guest_List.class.php
02. class Guest_List {
03.     public $page = 1;
04.     public function __construct() {
05.         $this->db = DB::init($GLOBALS['dsn']);
06.         $this->page = (int) $_GET['page'];
07.     }
08.   
09.     public function getList() {
10.         $begin = $this->page * 10;
11.         $sql = "SELECT * FROM guest ORDER BY addTime DESC LIMIT $begin, 10";
12.         return $this->db->getAll($sql);
13.     }
14. }

  这里的Guest_List就是一个简单的Model实现,构造函数取得页数page参数,getList方法查询留言列表并返回结果集。那么在list.php里可能是这样调用的:

 
1. //list.php
2. require_once 'Guest_List.class.php';
3. $model = new Guest_List();
4. $lists = $model->getList();

  嗯,其实很多MVC框架都是这么实现的,只不过可能加了一些自动调用的机制,会根据用户请求自动调用类,自动执行方法,呵呵。Model大功告成。这里需要明确一点就是,Model只是返回视图上所可能需要用到的数据,他不负责任何和显示有关的事情,那么显示相关的就交给View来做了。我们是不是不知不觉已经把表现和业务逻辑分离了?没错,分离就是这么简单。

  好了,来看看View怎么利用Model返回的数据来显示页面吧。最简单的例子,我们只需要在list.php里增加一行即可。

1. //list.php
2. require_once 'Guest_List.class.php';
3. $model = new Guest_List();
4. $lists = $model->getList();
5. //上面是Model,那么下面就是View
6. require_once 'list.html';

  来看看View都做些什么吧,我们用list.html来表示留言列表所展现给用户的界面文件,用html来命名看起来会更直观一些,他好像是个html文件,负责输出html代码给浏览器。来看看list.html可能长什么样子:

  01. <!--list.html-->
02. <table>
03.   <?php foreach ($lists as $value) { ?>
04.   <tr>
05.     <td><?php echo htmlspecialchars($value['guest_user_name']);?></td>
06.     <td><?php echo date('Y-m-d H:i', $value['guest_date_time']);?></td>
07.     <td><?php echo htmlspecialchars($value['guest_content']);?></td>
08.   </tr>
09.   <?php } ?>
10. </table>

  不难看出来这个文件所做的只不过是遍历留言数组$lists,然后输出每一行的留言,对留言的内容处理做了htmlspecialchars和date转换(与显示相关的处理),除了和显示相关的操作,他没有再做任何业务逻辑了(也不应该有)。

  我发现写到这里真的没有什么好写的了,MVC就是这些(或者再做一些扩展),至于怎么做到表现和业务分离,那么就是在你的Model里只返回数据,也就是你View所需要用到的数据,而你的View拿到这些数据后负责去显示他就可以了,不应该在你的Model里做显示和视觉相关的操作,也不应该在你的View里做一些业务逻辑相关的操作,把这两者分清楚,就自然而然的表现与业务分离了。

  接下来说说负责View的模板引擎吧,其实你在上面应该已经看到了一个最简陋的模板引擎,那就是View部分的 require_once 语句。厄,实在是太简单了,模板引擎其实是调度并解析模板的东西,其中调度模板由 require_once 搞定了,那么解析呢?这里由 PHP 引擎本身来搞定了。哈,没错,我一直都认为 PHP 是个最好的模板引擎。

  不过还是不得不说说传统的模板引擎的实现原理,一般来说会有这么几个步骤:
  1、注册变量,也就是把从Model返回的数据注册到模板引擎中,告诉模板引擎这个变量可以使用,其实所谓的注册也只不过是不得不这么做,因为一般引擎内部函数是没办法直接访问Model返回的变量的(变量作用域的问题),所以不得不加一个注册操作,把这些变量转换从模板引擎类的属性等。
  2、模板解析,就是读取模板文件,按照模板语法将标签解析成 PHP 语法,或者执行一些替换操作,用变量内容替换掉模板标签,其实效果都差不多。
  3、如果不是将变量内容替换掉模板标签,那么基本上第三步就是将注册的变量和解析完的模板融合在一起输出,类似于上面的list.html,是个解析完的文件,然后输出。

  一般模板引擎还会提供不少用于显示内容处理的插件,比如日期转换、字符串处理、生成表格、生成select等,这些给页面制作提供了一些方便。Smarty还包含了一些页面缓存机制,也很不错。

  很多模板引擎都顶着语法简单的嚎头,美其名曰降低美工的学习门槛。其实我不得不问,有多少模板是由美工来做的呢?而且对比两种语法,不觉得 PHP 的简单循环和输出有什么难以理解的,对比下面两种语法:

 
1. <!-- <?php foreach ($lists as $value) { ?> -->
2. <?=value['userName']?>
3. <!-- <?php } ?> -->

  和

1. <!-- loop lists value -->
2. {value['userName']}
3. <!-- loop -->

  我左看右看都觉得他们差不多,呵呵,与其再学习一套语法,还不如直接用你已经非常熟悉的PHP呢,为什么要虐待自己呢?而且从可维护性的角度来讲,维护PHP语法和维护模板语法,哪种更容易呢?PHP是标准,只要会PHP都知道他怎么写,表示什么,但是模板引擎千奇百怪的,各种语法都有,不是一个统一的标准,我想谁维护一个从来没有用过的模板,都需要花不少时间去学习引擎语法。更何况即使模板可以那样写,最终还是需要一堆正则替换成PHP语法。我敢肯定,前面写的哪种模板引擎语法最终会被转换成它上面那种PHP。其实模板引擎的解析也就是将模板语法转换成PHP语法的过程。抛开效率来说,多此一举。就象《C专家编程》作者说的,即使你能用宏把C写成看起来好像另外一种语言,但是你不要这么做,同样的这句告诫是否适合于模板引擎呢,它看起来很像另外一种语言。当然我这篇文章不是来批判模板引擎的,哈。它既然存在,也有其存在的道理,某些场合还是不得不用的,比如如果你把模板提供给用户去制作和使用,那么你不得不采用标签以限制用户使用PHP语法,来增强系统安全性。

  再来说效率问题,由于模板引擎要解析模板语法,会用到很多正则匹配和替换,那么在实际运行中是比较消耗系统资源的,而且当模板标签非常复杂或者嵌套多层的时候,效率是比较低的,因为有了一种处理方法,就是预编译。所谓的预编译,就是把带有模板语法的模板,通过处理,转换成 PHP 语法的文件,只要模板文件没有被修改,那么直接包含编译后的文件即可,这样就不需要再次替换和匹配,可以大大提高效率,不过由于模板引擎的复杂性,导致编译后的结果文件仍然比我们一般写出来的PHP文件复杂得多。所以其实效率还是远低于直接编写PHP模板的。有兴趣的可以打开一个Smarty编译过的文件,看看其嵌套,其实要比直接循环来得复杂。

  本文写到这里也差不多了,具体模板引擎如何编译如何处理,各个模板引擎的方式都不一样,有兴趣的可以去下载几个比较经典的引擎看看,比如Smarty。随后我附上我自己用的PHP模板引擎。

  PS:其实我觉得MVC应该叫做CMV是不是更符合逻辑呢?没有考证过这个词的由来^_^。

我的真正PHP模板引擎:

001. <?php
002. /**
003.  * 模板引擎
004.  *
005.  * Copyright(c) 2005-2008 by 陈毅鑫(深空). All rights reserved
006.  *
007.  * To contact the author write to {@link mailto:shenkong@php.net}
008.  *
009.  * @author 陈毅鑫(深空)
010.  * @version $Id: Template.class.php 1687 2008-07-07 01:16:07Z skchen $
011.  * @package Template
012.  */
013.   
014. defined('FW') || exit(header('HTTP/1.0 400 Bad Request'));
015.   
016. class Template {
017.     protected static $obj;
018.   
019.     public $vars;
020.     public $includeFiles;
021.     public $includeFile;
022.     public $templates;
023.     public $template;
024.     public $contents;
025.     protected $_content;
026.     protected $_contents;
027.     protected $_path;
028.   
029.     protected function __construct() {
030.         $this->vars = array();
031.         require_once ROOT_PATH . "lib/template.func.php";
032.     }
033.   
034.     /**
035.      * 初始化模板引擎
036.      *
037.      * @return object 模板引擎对象
038.      */
039.     public static function &init() {
040.         if (is_null(self::$obj)) {
041.             self::$obj = new Template();
042.         }
043.         return self::$obj;
044.     }
045.   
046.     /**
047.      * 注册模板变量
048.      *
049.      * 注册模板变量后在模板里就可以直接使用该变量,注册与被注册变量名不一定要一样
050.      * 如:$template->assign('var', $value);
051.      * 意思是将当前变量$value注册成模板变量var,在模板里就可以直接调用$val
052.      *
053.      * @param string $var 注册到模板里的变量名的字符串形式,不包含$
054.      * @param mixed $value 需要注册的变量
055.      */
056.     public function assign($var, $value) {
057.         if (is_array($var)) {
058.             foreach ($var as $key => $val) {
059.                 $this->vars[$key] = $val;
060.             }
061.         } else {
062.             $this->vars[$var] = $value;
063.         }
064.     }
065.   
066.     /**
067.      * 解析模板文件
068.      *
069.      * 解析模板,并将变量植入模板,解析完后返回字符串结果
070.      *
071.      * @param unknown_type $templates
072.      * @return unknown
073.      */
074.     public function fetch($templates) {
075.         if (is_array($templates)) {
076.             $this->templates = $templates;
077.         } else {
078.             $this->templates = func_get_args();
079.         }
080.         extract($this->vars);
081.   
082.         $this->_contents = '';
083.         foreach ($this->templates as $this->template) {
084.             ob_end_clean();
085.             ob_start();
086.             $this->_path = $this->getPath($this->template);
087.             require $this->_path;
088.             $this->_content = ob_get_contents();
089.             ob_end_clean();
090.             ob_start();
091.             $this->_contents .= $this->_content;
092.             $this->contents[$this->template] = $this->_content;
093.         }
094.         return $this->_contents;
095.     }
096.   
097.     public function getPath($path) {
098.         $path = explode(".", $path);
099.         $num = count($path);
100.         if ($num == 1) {
101.             return ROOT_PATH . "template" . DIRECTORY_SEPARATOR . $path[0] . ".html";
102.         } elseif ($num > 1) {
103.             $templatePath = '';
104.             $templatePath = $path[$num - 1];
105.             array_pop($path);
106.             $templatePath = ROOT_PATH . implode(DIRECTORY_SEPARATOR, $path) . DIRECTORY_SEPARATOR . 'template' . DIRECTORY_SEPARATOR . $templatePath . ".html";
107.             return $templatePath;
108.         } else {
109.             return false;
110.         }
111.     }
112.   
113.     public function display($templates = array()) {
114.         if (!is_array($templates)) {
115.             $templates = func_get_args();
116.         }
117.         if (empty($templates)) {
118.             foreach ($this->templates as $this->template) {
119.                 echo $this->contents[$this->template];
120.             }
121.         } else {
122.             echo $this->fetch($templates);
123.         }
124.     }
125. }
126.   
127. //end of script
 
 
01. <?php
02. /**
03.  * 模板扩充函数
04.  *
05.  * Copyright(c) 2005 by 陈毅鑫(深空). All rights reserved
06.  *
07.  * To contact the author write to {@link mailto:shenkong@php.net}
08.  *
09.  * @author 陈毅鑫(深空)
10.  * @version $Id: template.func.php 1687 2008-07-07 01:16:07Z skchen $
11.  * @package Template
12.  */
13.   
14. defined('FW') || exit(header('HTTP/1.0 400 Bad Request'));
15.   
16. /**
17.  * 包含模板
18.  *
19.  * 当你需要在主模板文件里(有些模板引擎称之为layout布局模板,其实不是所有模板都是布局)
20.  * 再包含其他公共模板的时候,使用该函数进行包含,则所有已注册的变量均可在被包含文件里使
21.  * 用,貌似支持多层嵌套,没有测试过,参数可以使用数组,也可以使用多个参数,如:
22.  * <?=includeFile('user.header', 'user.main', 'user.footer')?> 或者
23.  * <?=includeFile(array('user.header', 'user.main', 'user.footer'))?>
24.  *
25.  * @param string|array $filename 模板名(module.templateName形式)
26.  */
27. function includeFile($templates) {
28.     $template = Template::init();
29.     if (is_array($templates)) {
30.         $template->includeFiles = $templates;
31.     } else {
32.         $template->includeFiles = func_get_args();
33.     }
34.     extract($template->vars);
35.     foreach ($template->includeFiles as $template->includeFile) {
36.         require $template->getPath($template->includeFile);
37.     }
38. }
39.   
40. //end of script
 

------------------------------------------------------------------------------------------------------------------------------------------

 

  PHPChina的专家版在谈权限设计,苦于没有权限回帖,特发此博文谈谈简单的权限设计。讨论在这里
  最简单的权限验证,应该是登录态的验证,如果登录,则可以怎样,没有登录,则不能怎样:

 
1. if ($isLogin === true) {
2.     //do something
3. } else {
4.     //do nothing
5. }

  一般使用会话或者Cookie来保存登录态,具体实现不在此文讨论范围。一般权限都和人挂勾,首先识别你是谁,然后看你有能力做什么,然后再确认你的能力在这个地方是否可以使,一个权限验证算是基本上完成。我们围绕这几点来看权限如何去设计。
  首先要能识别操作者是何许人,我们需要一张保存操作者信息的表,也就是通常所说的用户表。简单的用户表如下:

 
1. CREATE TABLE user (
2.     userId int(10) unsigned NOT NULL,
3.     username varchar(255) NOT NULL,
4.     PRIMARY KEY (userId)
5. )

  一般使用一个用户ID来标识一个唯一的用户,可以使用数字,或者直接使用用户名作为主键(如果用户名不重复)。这里我们使用userId来唯一标识一个用户。
  有了用户以后,接下来需要确认这个用户所具有的能力,也就是权限,那么首先我们需要列一下我们的系统总共需要几个权限,比如增、删、改、查等。增加一张权限表:

 
1. CREATE TABLE permission (
2.     permissionId int(10) unsigned NOT NULL ,
3.     permissionName varchar(255) NOT NULL ,
4.     PRIMARY KEY (permissionId)
5. )

  同样的我们以permissionId作为主键来唯一标识一个权限,当然也可以使用permissionName来标识(如果你能确定唯一的话)。我们新增几条记录在这张表里:

 
1. +--------------+----------------+
2. | permissionId | permissionName |
3. +--------------+----------------+
4. |            1 | add            |
5. |            2 | del            |
6. |            3 | modify         |
7. |            4 | select         |
8. +--------------+----------------+

  这里列举了4个权限,简单的表示我们的用户在系统里可能具有的增、删、改、查4种不同的能力。
  接下来把这些能力赋给用户,需要一张对应表来保存:

 
1. CREATE TABLE userPermission (
2.     userId int(10) unsigned NOT NULL,
3.     permissionId int(10) unsigned NOT NULL,
4.     PRIMARY KEY (userId, permissionId)
5. )

  其中将userId和permissionId设置为主键,表示某个用户具有某种权限。表内容可能如下:

 
01. +--------+--------------+
02. | userId | permissionId |
03. +--------+--------------+
04. |      1 |            1 |
05. |      1 |            4 |
06. |      2 |            1 |
07. |      2 |            2 |
08. |      2 |            3 |
09. |      2 |            4 |
10. +--------+--------------+

  以上权限配置表明用户1具有增、查权限,用户2具有增、删、改、查权限(嗯可以猜想用户1是个普通用户,用户2是个管理员)。
  写到这里,我发现基本的用户权限系统雏型已经完成了。这么简单?看起来好像确实就是这么简单。一个用户拥有哪些权限,那么只需要勾选相应的权限分配给这个用户。在验证权限的时候,取出用户所拥有的所有权限,然后判断是否存在该权限即可。
  实际上的权限设计要比这个复杂一些,到底复杂在哪里呢?我们接下来分析。当用户比较多而且权限数量比较多的时候,你是不是要每个用户都去勾选一堆权限呢?如何简化这个操作?OK,用户组的概念推出。所谓用户组,就是具有某些权限的一类人的集合。我们赋予用户组某些权限,然后把这个用户加到这个用户组里即可,来看看用户组长什么样子:

 
1. CREATE TABLE group (
2.     groupId int(10) unsigned NOT NULL,
3.     groupName varchar(255) NOT NULL,
4.     PRIMARY KEY (groupId)
5. )

  和用户表类似,我们需要标识一个唯一的组,这里分配一个组ID作为主键来标识。内容可能如下:

 
1. +---------+-----------+
2. | groupId | groupName |
3. +---------+-----------+
4. |       1 | user      |
5. |       2 | admin     |
6. +---------+-----------+

  有了用户组表后,我们需要把一些权限赋给用户组,就需要一张用户组权限表:

 
1. CREATE TABLE groupPermission (
2.     groupId int(10) unsigned NOT NULL,
3.     permissionId int(10) unsigned NOT NULL,
4.     PRIMARY KEY (groupId, permissionId)
5. )

  我们分配增、查权限给user组,分配增、删、改、查权限给admin组:

 
01. +---------+--------------+
02. | groupId | permissionId |
03. +---------+--------------+
04. |       1 |            1 |
05. |       1 |            4 |
06. |       2 |            1 |
07. |       2 |            2 |
08. |       2 |            3 |
09. |       2 |            4 |
10. +---------+--------------+

  用户组表和用户组所对应的权限表有了,那么要把用户分配给一个用户组,就需要一张用户和组的对应关系表:

 
1. CREATE TABLE userGroup (
2.     userId int(10) unsigned NOT NULL,
3.     groupId int(10) unsigned NOT NULL,
4.     PRIMARY KEY (userId, groupId)
5. )

  把用户1赋给user组,把用户2赋给admin组,和一开始我们直接分配权限一样:

 
1. +--------+---------+
2. | userId | groupId |
3. +--------+---------+
4. |      1 |       1 |
5. |      2 |       2 |
6. +--------+---------+

  很明显这里的配置信息相对比userPermission表少很多,这里只需要记录用户属于什么组就可以了,那么检测用户权限,就需要查出用户所在的组,然后再查出这个组(或者这些组)所拥有的权限,就可以得出用户所具备的权限,再进行验证即可。当然会比直接查询userPermission表绕一点点,不过相对比维护成本,这点点消耗不算什么。更何况我们其实仍然可以保存userPermission表,在分配用户组的时候,同时更新userPermission表即可。
  这里可以看到,除了在分配用户权限方便以外,当你需要更改某类用户权限的时候,你只需要更改其所在组的权限,那么这个组下所有成员的权限也会随之更改,非常方便。
  到这里用户、用户组的权限构成基本完成。它能解决大部分问题,可是我发现它仍然有一些小的问题。比如如果某个用户只有查权限,我不得不再新增一个用户组,搞得用户组也很多,怎么办呢?如果这个用户属于普通用户组,其实可以考虑也分配普通用户组给这个用户,然后再从普通用户组里“扣掉”增权限。要达到这样的效果,怎么处理呢?
  其实很简单,我们刚才没有去掉的userPermission表派上用场,这个表存储了用户的实际单个权限,我们只需要增加一个字段标识用户是拥有这个权限,还是没有这个权限即可,这样可以解决两个问题:一是从现有用户组中扣掉某些权限,二是在现有用户组中,再给这个用户增加用户组以外的权限。来看一下userPermission表:

 
1. CREATE TABLE userPermission (
2.   userId int(10) unsigned NOT NULL,
3.   permissionId int(10) unsigned NOT NULL,
4.   has enum('yes','no') NOT NULL,
5.   PRIMARY KEY (userId, permissionId)
6. )

  把用户1的增权限去掉,那么内容可能像这样:

 
01. +--------+--------------+-----+
02. | userId | permissionId | has |
03. +--------+--------------+-----+
04. |      1 |            1 | no  |
05. |      1 |            4 | yes |
06. |      2 |            1 | yes |
07. |      2 |            2 | yes |
08. |      2 |            4 | yes |
09. |      2 |            3 | yes |
10. +--------+--------------+-----+

  这个用户依然在user组里,只不过user组所拥有的2个权限(add, select),他少了个add(ID为1被标记为no)权限而已。
  嗯,这样做有解决了这个小问题,不过这个功能增强会让分配权限代码更复杂一些,不仅要给用户分配组,还可能需要操作具体权限,让他有或者让他没有相应的权限。
  OK,简单的权限设计全部完成,只不过,细心的读者,你是否意识到,还少点什么呢?没错,即使到这里,整个权限验证还少了一块很重要的部分,那就是用户拥有这些权限,那么他能在哪里使用这些权限。考虑一个例子,一个论坛版主在他所管理的论坛版块里拥有删、改帖子的权限,他在其他非他所管理的版块就没有这些权限,可能在其他论坛版块他像是一个普通用户一样,我们上面讨论的权限设计如何做到这个验证呢?那么我个人觉得,权限设计到上面已经完成了,接下去这种情况,属于业务逻辑层的验证,我们从权限系统中已经获得用户的权限,那么在具体业务逻辑中,和权限进行绑定即可,以上例子可以用一个版块用户关系表来解决这个问题:

 
1. CREATE TABLE userBoard (
2.   userId int(10) unsigned NOT NULL,
3.   boardId int(10) unsigned NOT NULL,
4.   PRIMARY KEY (userId, boardId)
5. )

  标记用户拥有哪些版块的管理权限,那么在严重用户拥有管理权限的时候,还要看当前版块是否是用户管辖内的版块,最终确定用户是否有操作权限。那么我将这类和业务逻辑相关的权限分配归到用户角色里,同样可以创建一系列角色,来管理用户所管辖的范围,比如超级版主,他也是具有管理删、改权限,只不过他的权限作用于全论坛,那么普通版主就需要指定论坛,这样来区分用户组和角色组我想会使整个权限系统更加清晰。
  到这里,权限验证全部完成,以上没有具体实现代码,我相信这样已经足够了,具体的实现代码和业务逻辑由具体的应用实现吧。


本文转自PHP爱好者: http://www.phpfans.org/?cat=7
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值