还记的曾经写DISCUZ插件时,凡是要用到数据库的时候,必然会写一大堆的数据库处理语句。只有一个数据库的操作类 dbstuff,我们只能用到原始的操作方法query,fetch_array等去检索数据库。写数据库语句是一件头疼的事情。
而在X系列中,DISCUZ是如何操作数据库的呢?
我们打开一个文件,这里我打开index.php,会发现类似这个样子的代码:
C::t('common_domain')->fetch_by_domain_domainroot($_ENV['prefixdomain'], $_ENV['domainroot']);
这就是DISCUZ X系列的数据库操作了,完全不同于7.0以及以前的操作方式,那么它究竟是如何执行的呢?
按照从左到有的顺序,我们一步一步的拆分,首先是C,这个类在我之前的文章中提到过,他是core class的继承类,也可以认为就是core 类,我们看到他有一个静态的方法。
public static function t($name) {
return self::_make_obj($name, 'table', DISCUZ_TABLE_EXTENDABLE);
}
在这个方法中,又调用了_make_obj方法,继续跟进:
protected static function _make_obj($name, $type, $extendable = false, $p = array()) {
$pluginid = null;
if($name[0] === '#') {
list(, $pluginid, $name) = explode('#', $name);
}
$cname = $type.'_'.$name;
if(!isset(self::$_tables[$cname])) {
if(!class_exists($cname, false)) {
self::import(($pluginid ? 'plugin/'.$pluginid : 'class').'/'.$type.'/'.$name);
}
if($extendable) {
self::$_tables[$cname] = new discuz_container();
switch (count($p)) {
case 0: self::$_tables[$cname]->obj = new $cname();break;
case 1: self::$_tables[$cname]->obj = new $cname($p[1]);break;
case 2: self::$_tables[$cname]->obj = new $cname($p[1], $p[2]);break;
case 3: self::$_tables[$cname]->obj = new $cname($p[1], $p[2], $p[3]);break;
case 4: self::$_tables[$cname]->obj = new $cname($p[1], $p[2], $p[3], $p[4]);break;
case 5: self::$_tables[$cname]->obj = new $cname($p[1], $p[2], $p[3], $p[4], $p[5]);break;
default: $ref = new ReflectionClass($cname);self::$_tables[$cname]->obj = $ref->newInstanceArgs($p);unset($ref);break;
}
} else {
self::$_tables[$cname] = new $cname();
}
}
return self::$_tables[$cname];
}
这是一个protected静态方法,首先方法设置$pluginid为NULL,接着判断$name[0] === '#',(注意:===恒等计算符,和比较 运算符号“==”的区别是 “==”不会检查条件式的表达式的类型,恒等计算符会同时检查表达式的值与类型。比如 ( 1==true ) 为true 但是 (1 === true) 就是false了,因为1是整形,true为bool型)
这里$name 是common_domain,那么$name[1]就是‘c’,得到cname,table_common_domain,判断是否存在self::$_table[table_common_domain](这里就是为了防止重复加载),第一次执行,可定不存在,执行if(!class_exists(table_common_domain,false)),判断class是否不存在,然后取反,得到false,继续执行。
到这里if($extendable) ,$extendable默认为false(这里的$extendable大概为扩展表的意思吧),因此执行self::$_tables[$cname] = new $cname(); 创建table_common_domain的类,程序会调用_autoload(不懂的autoload的同学,可以看看我的上一篇文章 (菜鸟篇)从Discuz X系列中学PHP core )去加载 \source\class\table目录下的table_common_domain.php文件。
来到了table_common_domain类,这个类继承自discuz_table class table_common_domain extends discuz_table 我们再去打开 source\class\discuz\discuz_table.php,又发现了
discuz_table原来是从discuz_base类继承下来的,那么discuz_base类又写了什么呢?
打开source\class\discuz\discuz_base.php
abstract class discuz_base
{
private $_e;
private $_m;
public function __construct() {
}
public function __set($name, $value) {
$setter='set'.$name;
if(method_exists($this,$setter)) {
return $this->$setter($value);
} elseif($this->canGetProperty($name)) {
throw new Exception('The property "'.get_class($this).'->'.$name.'" is readonly');
} else {
throw new Exception('The property "'.get_class($this).'->'.$name.'" is not defined');
}
}
public function __get($name) {
$getter='get'.$name;
if(method_exists($this,$getter)) {
return $this->$getter();
} else {
throw new Exception('The property "'.get_class($this).'->'.$name.'" is not defined');
}
}
public function __call($name,$parameters) {
throw new Exception('Class "'.get_class($this).'" does not have a method named "'.$name.'".');
}
public function canGetProperty($name)
{
return method_exists($this,'get'.$name);
}
public function canSetProperty($name)
{
return method_exists($this,'set'.$name);
}
public function __toString() {
return get_class($this);
}
public function __invoke() {
return get_class($this);
}
}
?>
discuz_base原来是一个抽象类,这个类也是discuz的基类,设置了魔术方法____construct、__set、__get、__call、__tostring、__invoke,以及判断是否存在get,set方法的函数。
__construct 构造方法,没有具体内容,悄悄飘过。
__set方法,这个方法,当我们调用类的setXXXXXX方法的时候会触发,同样__get方法也是当我们调用getXXXXXX方法的时候会触发,注意是在get,set之前触发。这里是判断调用的setXXXXXX方法,或者getXXXXXX方法是否存在。__call方法会在调用该类的方法前调用,同样也是判断方法是否存在,若不存在 抛出异常。在__set,__get中变脸$name就是set或者get之后跟的字符串,而__call中,直接就是调用的方法名称。__tostring,过去该类的字符串描述,学java的同学经常使用,不多解释。而当尝试以调用函数的方式调用一个对象时,__invoke方法会被自动调用。比如 $a = new mysql(); $a()这样的方式就会触发__invoke函数。
get_class会返回当前调用该方法的类名,是子类调用的话,返回的就是子类的名称。
再回到discuz_table类:
首先看它的属性:
public $data = array();
public $methods = array();
protected $_table;
protected $_pk;
protected $_pre_cache_key;
protected $_cache_ttl;
protected $_allowmem;
从上到下分别是数据,方法,表,键名,缓存,以及是否允许缓存系统。
再返回看table_common_domain类,这下终于回到了这个”孙子“类了,哎,看类就是头大,一会儿就绕晕了,说不定哪个变量就在父类或者子类中。
首先会调用table_common_domain类的构造函数
public function __construct() {
$this->_table = 'common_domain';
$this->_pk = '';
parent::__construct();
}
这里两个赋值,然后调用父类的构造函数:discuz_table的构造函数
public function __construct($para = array()) {
if(!empty($para)) {
$this->_table = $para['table'];
$this->_pk = $para['pk'];
}
if(isset($this->_pre_cache_key) && (($ttl = getglobal('setting/memory/'.$this->_table)) !== null || ($ttl = $this->_cache_ttl) !== null) && memory('check')) {
$this->_cache_ttl = $ttl;
$this->_allowmem = true;
}
$this->_init_extend();
parent::__construct();
}
上边是基本的赋值,我们暂时不考虑,主要就是缓存极致了,主要看最后两句,$this->_init_extend(); 与 parent::__construct();,前者初始化了扩展信息,后者调用父类构造函数,在当前文件中_init_extend方法为空,但是不要以为就什么都不做了,因为在其子类中init_extend可能会被覆盖,所以有可能会调用到子类的这个方法。这里我们在写扩展表的时候 ,就可以把相关的初始化操作写到这个方法里边。
构造函数完毕,回到最上边的语句,该执行红色字体部分了:
C::t('common_domain')->fetch_by_domain_domainroot($_ENV['prefixdomain'], $_ENV['domainroot']);
下边这个方法:
public function fetch_by_domain_domainroot($domain, $droot) {
return DB::fetch_first('SELECT * FROM %t WHERE domain=%s AND domainroot=%s', array($this->_table, $domain, $droot));
}
我们又看到了一个新的类DB,其实也不算新类,这个类在core类中提到过,class_core.php文件中:class DB extends discuz_database {}
可以看到这个类是discuz_database类的一个子类,继续去看discuz_database
source\class\discuz\discuz_database.php
class discuz_database {
public static $db;
public static $driver;
public static function init($driver, $config) {
self::$driver = $driver;
self::$db = new $driver;
self::$db->set_config($config);
self::$db->connect();
}
public static function object() {
return self::$db;
}
public static function table($table) {
return self::$db->table_name($table);
}
public static function delete($table, $condition, $limit = 0, $unbuffered = true) {
if (empty($condition)) {
return false;
} elseif (is_array($condition)) {
if (count($condition) == 2 && isset($condition['where']) && isset($condition['arg'])) {
$where = self::format($condition['where'], $condition['arg']);
} else {
$where = self::implode_field_value($condition, ' AND ');
}
} else {
$where = $condition;
}
$limit = dintval($limit);
$sql = "DELETE FROM " . self::table($table) . " WHERE $where " . ($limit > 0 ? "LIMIT $limit" : '');
return self::query($sql, ($unbuffered ? 'UNBUFFERED' : ''));
}
public static function insert($table, $data, $return_insert_id = false, $replace = false, $silent = false) {
$sql = self::implode($data);
$cmd = $replace ? 'REPLACE INTO' : 'INSERT INTO';
$table = self::table($table);
$silent = $silent ? 'SILENT' : '';
return self::query("$cmd $table SET $sql", null, $silent, !$return_insert_id);
}
public static function update($table, $data, $condition, $unbuffered = false, $low_priority = false) {
$sql = self::implode($data);
if(empty($sql)) {
return false;
}
$cmd = "UPDATE " . ($low_priority ? 'LOW_PRIORITY' : '');
$table = self::table($table);
$where = '';
if (empty($condition)) {
$where = '1';
} elseif (is_array($condition)) {
$where = self::implode($condition, ' AND ');
} else {
$where = $condition;
}
$res = self::query("$cmd $table SET $sql WHERE $where", $unbuffered ? 'UNBUFFERED' : '');
return $res;
}
public static function insert_id() {
return self::$db->insert_id();
}
public static function fetch($resourceid, $type = 'MYSQL_ASSOC') {
return self::$db->fetch_array($resourceid, $type);
}
public static function fetch_first($sql, $arg = array(), $silent = false) {
$res = self::query($sql, $arg, $silent, false);
$ret = self::$db->fetch_array($res);
self::$db->free_result($res);
return $ret ? $ret : array();
}
public static function fetch_all($sql, $arg = array(), $keyfield = '', $silent=false) {
$data = array();
$query = self::query($sql, $arg, $silent, false);
while ($row = self::$db->fetch_array($query)) {
if ($keyfield && isset($row[$keyfield])) {
$data[$row[$keyfield]] = $row;
} else {
$data[] = $row;
}
}
self::$db->free_result($query);
return $data;
}
public static function result($resourceid, $row = 0) {
return self::$db->result($resourceid, $row);
}
public static function result_first($sql, $arg = array(), $silent = false) {
$res = self::query($sql, $arg, $silent, false);
$ret = self::$db->result($res, 0);
self::$db->free_result($res);
return $ret;
}
public static function query($sql, $arg = array(), $silent = false, $unbuffered = false) {
if (!empty($arg)) {
if (is_array($arg)) {
$sql = self::format($sql, $arg);
} elseif ($arg === 'SILENT') {
$silent = true;
} elseif ($arg === 'UNBUFFERED') {
$unbuffered = true;
}
}
self::checkquery($sql);
$ret = self::$db->query($sql, $silent, $unbuffered);
if (!$unbuffered && $ret) {
$cmd = trim(strtoupper(substr($sql, 0, strpos($sql, ' '))));
if ($cmd === 'SELECT') {
} elseif ($cmd === 'UPDATE' || $cmd === 'DELETE') {
$ret = self::$db->affected_rows();
} elseif ($cmd === 'INSERT') {
$ret = self::$db->insert_id();
}
}
return $ret;
}
public static function num_rows($resourceid) {
return self::$db->num_rows($resourceid);
}
public static function affected_rows() {
return self::$db->affected_rows();
}
public static function free_result($query) {
return self::$db->free_result($query);
}
public static function error() {
return self::$db->error();
}
public static function errno() {
return self::$db->errno();
}
public static function checkquery($sql) {
return discuz_database_safecheck::checkquery($sql);
}
public static function quote($str, $noarray = false) {
if (is_string($str))
return '\'' . addcslashes($str, "\n\r\\'\"\032") . '\'';
if (is_int($str) or is_float($str))
return '\'' . $str . '\'';
if (is_array($str)) {
if($noarray === false) {
foreach ($str as &$v) {
$v = self::quote($v, true);
}
return $str;
} else {
return '\'\'';
}
}
if (is_bool($str))
return $str ? '1' : '0';
return '\'\'';
}
public static function quote_field($field) {
if (is_array($field)) {
foreach ($field as $k => $v) {
$field[$k] = self::quote_field($v);
}
} else {
if (strpos($field, '`') !== false)
$field = str_replace('`', '', $field);
$field = '`' . $field . '`';
}
return $field;
}
public static function limit($start, $limit = 0) {
$limit = intval($limit > 0 ? $limit : 0);
$start = intval($start > 0 ? $start : 0);
if ($start > 0 && $limit > 0) {
return " LIMIT $start, $limit";
} elseif ($limit) {
return " LIMIT $limit";
} elseif ($start) {
return " LIMIT $start";
} else {
return '';
}
}
public static function order($field, $order = 'ASC') {
if(empty($field)) {
return '';
}
$order = strtoupper($order) == 'ASC' || empty($order) ? 'ASC' : 'DESC';
return self::quote_field($field) . ' ' . $order;
}
public static function field($field, $val, $glue = '=') {
$field = self::quote_field($field);
if (is_array($val)) {
$glue = $glue == 'notin' ? 'notin' : 'in';
} elseif ($glue == 'in') {
$glue = '=';
}
switch ($glue) {
case '=':
return $field . $glue . self::quote($val);
break;
case '-':
case '+':
return $field . '=' . $field . $glue . self::quote((string) $val);
break;
case '|':
case '&':
case '^':
return $field . '=' . $field . $glue . self::quote($val);
break;
case '>':
case '<':
case '<>':
case '<=':
case '>=':
return $field . $glue . self::quote($val);
break;
case 'like':
return $field . ' LIKE(' . self::quote($val) . ')';
break;
case 'in':
case 'notin':
$val = $val ? implode(',', self::quote($val)) : '\'\'';
return $field . ($glue == 'notin' ? ' NOT' : '') . ' IN(' . $val . ')';
break;
default:
throw new DbException('Not allow this glue between field and value: "' . $glue . '"');
}
}
public static function implode($array, $glue = ',') {
$sql = $comma = '';
$glue = ' ' . trim($glue) . ' ';
foreach ($array as $k => $v) {
$sql .= $comma . self::quote_field($k) . '=' . self::quote($v);
$comma = $glue;
}
return $sql;
}
public static function implode_field_value($array, $glue = ',') {
return self::implode($array, $glue);
}
public static function format($sql, $arg) {
$count = substr_count($sql, '%');
if (!$count) {
return $sql;
} elseif ($count > count($arg)) {
throw new DbException('SQL string format error! This SQL need "' . $count . '" vars to replace into.', 0, $sql);
}
$len = strlen($sql);
$i = $find = 0;
$ret = '';
while ($i <= $len && $find < $count) {
if ($sql{$i} == '%') {
$next = $sql{$i + 1};
if ($next == 't') {
$ret .= self::table($arg[$find]);
} elseif ($next == 's') {
$ret .= self::quote(is_array($arg[$find]) ? serialize($arg[$find]) : (string) $arg[$find]);
} elseif ($next == 'f') {
$ret .= sprintf('%F', $arg[$find]);
} elseif ($next == 'd') {
$ret .= dintval($arg[$find]);
} elseif ($next == 'i') {
$ret .= $arg[$find];
} elseif ($next == 'n') {
if (!empty($arg[$find])) {
$ret .= is_array($arg[$find]) ? implode(',', self::quote($arg[$find])) : self::quote($arg[$find]);
} else {
$ret .= '0';
}
} else {
$ret .= self::quote($arg[$find]);
}
$i++;
$find++;
} else {
$ret .= $sql{$i};
}
$i++;
}
if ($i < $len) {
$ret .= substr($sql, $i);
}
return $ret;
}
}
在这个类中,我们终于看到了熟悉的变量 $db,对了,这个变量就是我们在discuz 7.X时代经常使用的dbstuff类。
同时找到DB::fetch_first 方法,
public static function fetch_first($sql, $arg = array(), $silent = false) {
$res = self::query($sql, $arg, $silent, false);
$ret = self::$db->fetch_array($res);
self::$db->free_result($res);
return $ret ? $ret : array();
}
看到这里我们也大概能整理出一点思路:
Discuz X中 使用discuz_database类将以前的dbstuff类进行了新的封装,使其变成已经完全由静态方法操作数据库的类,并且添加了一些新的功能,比如SQL安全检查。该类执行基础的SQL语句,
然后再discuz_table类中有进行了第二次包装,使得discuz_table具有了一些简单的操作,比如updata,delete,比以前操作更简单,不用写语句,直接传入数组,以数组键名作为数据库键名,在函数内部重组SQL语句进行操作,大大简化了程序员二次开发的工作量,并且更加安全。
若是基本的操作不够用,那么discuz_table类进行继承,得到一个新类,在其中扩展该类,使其拥有自定义的更加复杂的操作,比如fetch_by_domain_domainroot方法。