From 49ee891da1108f1202a51b501107d34544e9d97f Mon Sep 17 00:00:00 2001 From: ThinkPHP Date: Wed, 17 Apr 2013 21:46:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Traits/Think/Model=20traits?= =?UTF-8?q?=20=E7=94=A8=E4=BA=8EModel=E7=BB=84=E8=A3=85(=E6=AD=A4=E4=B8=BA?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Traits/Think/Model/Auto.php | 338 ++++++++++ Traits/Think/Model/Curd.php | 1028 +++++++++++++++++++++++++++++++ Traits/Think/Model/Extend.php | 169 +++++ Traits/Think/Model/Query.php | 124 ++++ Traits/Think/Model/Relation.php | 346 +++++++++++ Traits/Think/Model/View.php | 239 +++++++ 6 files changed, 2244 insertions(+) create mode 100644 Traits/Think/Model/Auto.php create mode 100644 Traits/Think/Model/Curd.php create mode 100644 Traits/Think/Model/Extend.php create mode 100644 Traits/Think/Model/Query.php create mode 100644 Traits/Think/Model/Relation.php create mode 100644 Traits/Think/Model/View.php diff --git a/Traits/Think/Model/Auto.php b/Traits/Think/Model/Auto.php new file mode 100644 index 00000000..bdd190fe --- /dev/null +++ b/Traits/Think/Model/Auto.php @@ -0,0 +1,338 @@ + +// +---------------------------------------------------------------------- + +namespace Traits\Think\Model; + +trait Auto { + + protected $_validate = []; // 自动验证定义 + protected $_auto = []; // 自动完成定义 + + /** + * 创建数据对象 但不保存到数据库 + * @access public + * @param mixed $data 创建数据 + * @param string $type 状态 + * @return mixed + */ + public function create($data='',$type='') { + // 如果没有传值默认取POST数据 + if(empty($data)) { + $data = $_POST; + }elseif(is_object($data)){ + $data = get_object_vars($data); + } + // 验证数据 + if(empty($data) || !is_array($data)) { + $this->error = L('_DATA_TYPE_INVALID_'); + return false; + } + + // 状态 + $type = $type?$type:(!empty($data[$this->getPk()])?self::MODEL_UPDATE:self::MODEL_INSERT); + + // 检测提交字段的合法性 + if(isset($this->options['field'])) { // $this->field('field1,field2...')->create() + $fields = $this->options['field']; + unset($this->options['field']); + }elseif($type == self::MODEL_INSERT && isset($this->insertFields)) { + $fields = $this->insertFields; + }elseif($type == self::MODEL_UPDATE && isset($this->updateFields)) { + $fields = $this->updateFields; + } + if(isset($fields)) { + if(is_string($fields)) { + $fields = explode(',',$fields); + } + // 判断令牌验证字段 + if(C('TOKEN_ON')) $fields[] = C('TOKEN_NAME'); + foreach ($data as $key=>$val){ + if(!in_array($key,$fields)) { + unset($data[$key]); + } + } + } + + // 数据自动验证 + if(!$this->autoValidation($data,$type)) return false; + + + // 验证完成生成数据对象 + if($this->autoCheckFields) { // 开启字段检测 则过滤非法字段数据 + $fields = $this->getDbFields(); + foreach ($data as $key=>$val){ + if(!in_array($key,$fields)) { + unset($data[$key]); + }elseif(MAGIC_QUOTES_GPC && is_string($val)){ + $data[$key] = stripslashes($val); + } + } + } + + // 创建完成对数据进行自动处理 + $this->autoOperation($data,$type); + // 赋值当前数据对象 + $this->data = $data; + // 返回创建的数据以供其他调用 + return $data; + } + + /** + * 使用正则验证数据 + * @access public + * @param string $value 要验证的数据 + * @param string $rule 验证规则 + * @return boolean + */ + public function regex($value,$rule) { + $validate = array( + 'require' => '/.+/', + 'email' => '/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/', + 'url' => '/^http(s?):\/\/(?:[A-za-z0-9-]+\.)+[A-za-z]{2,4}(?:[\/\?#][\/=\?%\-&~`@[\]\':+!\.#\w]*)?$/', + 'currency' => '/^\d+(\.\d+)?$/', + 'number' => '/^\d+$/', + 'zip' => '/^\d{6}$/', + 'integer' => '/^[-\+]?\d+$/', + 'double' => '/^[-\+]?\d+(\.\d+)?$/', + 'english' => '/^[A-Za-z]+$/', + ); + // 检查是否有内置的正则表达式 + if(isset($validate[strtolower($rule)])) + $rule = $validate[strtolower($rule)]; + return preg_match($rule,$value)===1; + } + + /** + * 自动表单处理 + * @access public + * @param array $data 创建数据 + * @param string $type 创建类型 + * @return mixed + */ + private function autoOperation(&$data,$type) { + if(!empty($this->options['auto'])) { + $_auto = $this->options['auto']; + unset($this->options['auto']); + }elseif(!empty($this->_auto)){ + $_auto = $this->_auto; + } + // 自动填充 + if(isset($_auto)) { + foreach ($_auto as $auto){ + // 填充因子定义格式 + // array('field','填充内容','填充条件','附加规则',[额外参数]) + if(empty($auto[2])) $auto[2] = self::MODEL_INSERT; // 默认为新增的时候自动填充 + if( $type == $auto[2] || $auto[2] == self::MODEL_BOTH) { + switch(trim($auto[3])) { + case 'function': // 使用函数进行填充 字段的值作为参数 + case 'callback': // 使用回调方法 + $args = isset($auto[4])?(array)$auto[4]:[]; + if(isset($data[$auto[0]])) { + array_unshift($args,$data[$auto[0]]); + } + if('function'==$auto[3]) { + $data[$auto[0]] = call_user_func_array($auto[1], $args); + }else{ + $data[$auto[0]] = call_user_func_array(array(&$this,$auto[1]), $args); + } + break; + case 'field': // 用其它字段的值进行填充 + $data[$auto[0]] = $data[$auto[1]]; + break; + case 'ignore': // 为空忽略 + if(''===$data[$auto[0]]) + unset($data[$auto[0]]); + break; + case 'string': + default: // 默认作为字符串填充 + $data[$auto[0]] = $auto[1]; + } + if(false === $data[$auto[0]] ) unset($data[$auto[0]]); + } + } + } + return $data; + } + + /** + * 自动表单验证 + * @access protected + * @param array $data 创建数据 + * @param string $type 创建类型 + * @return boolean + */ + protected function autoValidation($data,$type) { + if(!empty($this->options['validate'])) { + $_validate = $this->options['validate']; + unset($this->options['validate']); + }elseif(!empty($this->_validate)){ + $_validate = $this->_validate; + } + // 属性验证 + if(isset($_validate)) { // 如果设置了数据自动验证则进行数据验证 + if($this->patchValidate) { // 重置验证错误信息 + $this->error = []; + } + foreach($_validate as $key=>$val) { + // 验证因子定义格式 + // array(field,rule,message,condition,type,when,params) + // 判断是否需要执行验证 + if(empty($val[5]) || $val[5]== self::MODEL_BOTH || $val[5]== $type ) { + if(0==strpos($val[2],'{%') && strpos($val[2],'}')) + // 支持提示信息的多语言 使用 {%语言定义} 方式 + $val[2] = L(substr($val[2],2,-1)); + $val[3] = isset($val[3])?$val[3]:self::EXISTS_VALIDATE; + $val[4] = isset($val[4])?$val[4]:'regex'; + // 判断验证条件 + switch($val[3]) { + case self::MUST_VALIDATE: // 必须验证 不管表单是否有设置该字段 + if(false === $this->_validationField($data,$val)) + return false; + break; + case self::VALUE_VALIDATE: // 值不为空的时候才验证 + if('' != trim($data[$val[0]])) + if(false === $this->_validationField($data,$val)) + return false; + break; + default: // 默认表单存在该字段就验证 + if(isset($data[$val[0]])) + if(false === $this->_validationField($data,$val)) + return false; + } + } + } + // 批量验证的时候最后返回错误 + if(!empty($this->error)) return false; + } + return true; + } + + /** + * 验证表单字段 支持批量验证 + * 如果批量验证返回错误的数组信息 + * @access protected + * @param array $data 创建数据 + * @param array $val 验证因子 + * @return boolean + */ + protected function _validationField($data,$val) { + if(false === $this->_validationFieldItem($data,$val)){ + if($this->patchValidate) { + $this->error[$val[0]] = $val[2]; + }else{ + $this->error = $val[2]; + return false; + } + } + return ; + } + + /** + * 根据验证因子验证字段 + * @access protected + * @param array $data 创建数据 + * @param array $val 验证因子 + * @return boolean + */ + protected function _validationFieldItem($data,$val) { + switch(strtolower(trim($val[4]))) { + case 'function':// 使用函数进行验证 + case 'callback':// 调用方法进行验证 + $args = isset($val[6])?(array)$val[6]:[]; + if(is_string($val[0]) && strpos($val[0], ',')) + $val[0] = explode(',', $val[0]); + if(is_array($val[0])){ + // 支持多个字段验证 + foreach($val[0] as $field) + $_data[$field] = $data[$field]; + array_unshift($args, $_data); + }else{ + array_unshift($args, $data[$val[0]]); + } + if('function'==$val[4]) { + return call_user_func_array($val[1], $args); + }else{ + return call_user_func_array(array(&$this, $val[1]), $args); + } + case 'confirm': // 验证两个字段是否相同 + return $data[$val[0]] == $data[$val[1]]; + case 'unique': // 验证某个值是否唯一 + if(is_string($val[0]) && strpos($val[0],',')) + $val[0] = explode(',',$val[0]); + $map = []; + if(is_array($val[0])) { + // 支持多个字段验证 + foreach ($val[0] as $field) + $map[$field] = $data[$field]; + }else{ + $map[$val[0]] = $data[$val[0]]; + } + if(!empty($data[$this->getPk()])) { // 完善编辑的时候验证唯一 + $map[$this->getPk()] = array('neq',$data[$this->getPk()]); + } + if($this->where($map)->find()) return false; + return true; + default: // 检查附加规则 + return $this->check($data[$val[0]],$val[1],$val[4]); + } + } + + /** + * 验证数据 支持 in between equal length regex expire ip_allow ip_deny + * @access public + * @param string $value 验证数据 + * @param mixed $rule 验证表达式 + * @param string $type 验证方式 默认为正则验证 + * @return boolean + */ + public function check($value,$rule,$type='regex'){ + $type = strtolower(trim($type)); + switch($type) { + case 'in': // 验证是否在某个指定范围之内 逗号分隔字符串或者数组 + case 'notin': + $range = is_array($rule)? $rule : explode(',',$rule); + return $type == 'in' ? in_array($value ,$range) : !in_array($value ,$range); + case 'between': // 验证是否在某个范围 + case 'notbetween': // 验证是否不在某个范围 + if (is_array($rule)){ + $min = $rule[0]; + $max = $rule[1]; + }else{ + list($min,$max) = explode(',',$rule); + } + return $type == 'between' ? $value>=$min && $value<=$max : $value<$min || $value>$max; + case 'equal': // 验证是否等于某个值 + case 'notequal': // 验证是否等于某个值 + return $type == 'equal' ? $value == $rule : $value != $rule; + case 'length': // 验证长度 + $length = mb_strlen($value,'utf-8'); // 当前数据长度 + if(strpos($rule,',')) { // 长度区间 + list($min,$max) = explode(',',$rule); + return $length >= $min && $length <= $max; + }else{// 指定长度 + return $length == $rule; + } + case 'expire': + list($start,$end) = explode(',',$rule); + if(!is_numeric($start)) $start = strtotime($start); + if(!is_numeric($end)) $end = strtotime($end); + return NOW_TIME >= $start && NOW_TIME <= $end; + case 'ip_allow': // IP 操作许可验证 + return in_array(get_client_ip(),explode(',',$rule)); + case 'ip_deny': // IP 操作禁止验证 + return !in_array(get_client_ip(),explode(',',$rule)); + case 'regex': + default: // 默认使用正则验证 可以使用验证类中定义的验证名称 + // 检查附加规则 + return $this->regex($value,$rule); + } + } +} \ No newline at end of file diff --git a/Traits/Think/Model/Curd.php b/Traits/Think/Model/Curd.php new file mode 100644 index 00000000..137be221 --- /dev/null +++ b/Traits/Think/Model/Curd.php @@ -0,0 +1,1028 @@ + +// +---------------------------------------------------------------------- + +namespace Traits\Think\Model; + +trait Curd { + + // 当前数据库操作对象 + protected $db = null; + // 主键名称 + protected $pk = 'id'; + // 数据表前缀 + protected $tablePrefix = ''; + // 模型名称 + protected $name = ''; + // 数据库名称 + protected $dbName = ''; + //数据库配置 + protected $connection = ''; + // 数据表名(不包含表前缀) + protected $tableName = ''; + // 实际数据表名(包含表前缀) + protected $trueTableName = ''; + // 最近错误信息 + protected $error = ''; + // 字段信息 + protected $fields = []; + // 数据信息 + protected $data = []; + // 查询表达式参数 + protected $options = []; + + protected $_map = []; // 字段映射定义 + protected $_scope = []; // 命名范围定义 + // 是否自动检测数据表字段信息 + protected $autoCheckFields = false; + // 链操作方法列表 + protected $methods = ['table','order','alias','having','group','lock','distinct','auto','filter','validate']; + + /** + * 架构函数 + * 取得DB类的实例对象 字段检查 + * @access public + * @param string $name 模型名称 + * @param array $config 模型配置 + */ + public function __construct($name='',$config=[]) { + // 模型初始化 + $this->_initialize(); + // 传入模型参数 + if(!empty($name)){ + $this->name = $name; + }elseif(empty($this->name)){ + $this->name = $this->getModelName(); + } + if(strpos($this->name,'.')) { // 支持 数据库名.模型名的 定义 + list($this->dbName,$this->name) = explode('.',$this->name); + } + + if(isset($config['table_prefix'])) { + $this->tablePrefix = $config['table_prefix']; + } + if(isset($config['connection'])) { + $this->connection = $config['connection']; + } + if(isset($config['table_name'])) { + $this->tableName = $config['table_name']; + } + if(isset($config['true_table_name'])) { + $this->trueTableName = $config['true_table_name']; + } + if(isset($config['db_name'])) { + $this->dbName = $config['db_name']; + } + + // 设置表前缀 + if(empty($this->tablePrefix)) { + $this->tablePrefix = is_null($this->tablePrefix)?'':C('db_prefix'); + } + + // 数据库初始化操作 + // 获取数据库操作对象 + // 当前模型有独立的数据库连接信息 + $this->db(0,$this->connection); + } + + /** + * 设置数据对象的值 + * @access public + * @param string $name 名称 + * @param mixed $value 值 + * @return void + */ + public function __set($name,$value) { + // 设置数据对象属性 + $this->data[$name] = $value; + } + + /** + * 获取数据对象的值 + * @access public + * @param string $name 名称 + * @return mixed + */ + public function __get($name) { + return isset($this->data[$name])?$this->data[$name]:null; + } + + /** + * 检测数据对象的值 + * @access public + * @param string $name 名称 + * @return boolean + */ + public function __isset($name) { + return isset($this->data[$name]); + } + + /** + * 销毁数据对象的值 + * @access public + * @param string $name 名称 + * @return void + */ + public function __unset($name) { + unset($this->data[$name]); + } + + /** + * 利用__call方法实现一些特殊的Model方法 + * @access public + * @param string $method 方法名称 + * @param array $args 调用参数 + * @return mixed + */ + public function __call($method,$args) { + if(in_array(strtolower($method),$this->methods,true)) { + // 连贯操作的实现 + $this->options[strtolower($method)] = $args[0]; + return $this; + }elseif(in_array(strtolower($method),['count','sum','min','max','avg'],true)){ + // 统计查询的实现 + $field = isset($args[0])?$args[0]:'*'; + return $this->getField(strtoupper($method).'('.$field.') AS tp_'.$method); + }elseif(strtolower(substr($method,0,5))=='getby') { + // 根据某个字段获取记录 + $field = parse_name(substr($method,5)); + $where[$field] = $args[0]; + return $this->where($where)->find(); + }elseif(strtolower(substr($method,0,10))=='getfieldby') { + // 根据某个字段获取记录的某个值 + $name = parse_name(substr($method,10)); + $where[$name] =$args[0]; + return $this->where($where)->getField($args[1]); + }elseif(isset($this->_scope[$method])){// 命名范围的单独调用支持 + return $this->scope($method,$args[0]); + }else{ + E(__CLASS__.':'.$method.L('_METHOD_NOT_EXIST_')); + return; + } + } + // 回调方法 初始化模型 + protected function _initialize() {} + + /** + * 对保存到数据库的数据进行处理 + * @access protected + * @param mixed $data 要操作的数据 + * @return boolean + */ + protected function _facade($data) { + // 检查非数据字段 + if(!empty($this->fields)) { + foreach ($data as $key=>$val){ + if(!in_array($key,$this->fields,true)){ + unset($data[$key]); + }elseif(is_scalar($val)) { + // 字段类型检查 + $this->_parseType($data,$key); + } + } + } + // 安全过滤 + if(!empty($this->options['filter'])) { + $data = array_map($this->options['filter'],$data); + unset($this->options['filter']); + } + $this->_before_write($data); + return $data; + } + + // 写入数据前的回调方法 包括新增和更新 + protected function _before_write(&$data) {} + + /** + * 新增数据 + * @access public + * @param mixed $data 数据 + * @param boolean $replace 是否replace + * @return mixed + */ + public function add($data='',$replace=false) { + if(empty($data)) { + // 没有传递数据,获取当前数据对象的值 + if(!empty($this->data)) { + $data = $this->data; + // 重置数据 + $this->data = []; + }else{ + $this->error = L('_DATA_TYPE_INVALID_'); + return false; + } + } + // 分析表达式 + $options = $this->_parseOptions(); + // 数据处理 + $data = $this->_facade($data); + if(false === $this->_before_insert($data,$options)) { + return false; + } + // 写入数据到数据库 + $result = $this->db->insert($data,$options,$replace); + if(false !== $result ) { + $insertId = $this->getLastInsID(); + if($insertId) { + // 自增主键返回插入ID + $data[$this->getPk()] = $insertId; + $this->_after_insert($data,$options); + return $insertId; + } + $this->_after_insert($data,$options); + } + return $result; + } + // 插入数据前的回调方法 + protected function _before_insert(&$data,$options) {} + // 插入成功后的回调方法 + protected function _after_insert($data,$options) {} + + /** + * 保存数据 + * @access public + * @param mixed $data 数据 + * @return boolean + */ + public function save($data='') { + if(empty($data)) { + // 没有传递数据,获取当前数据对象的值 + if(!empty($this->data)) { + $data = $this->data; + // 重置数据 + $this->data = []; + }else{ + $this->error = L('_DATA_TYPE_INVALID_'); + return false; + } + } + // 数据处理 + $data = $this->_facade($data); + // 分析表达式 + $options = $this->_parseOptions(); + if(false === $this->_before_update($data,$options)) { + return false; + } + if(!isset($options['where']) ) { + // 如果存在主键数据 则自动作为更新条件 + if(isset($data[$this->getPk()])) { + $pk = $this->getPk(); + $where[$pk] = $data[$pk]; + $options['where'] = $where; + $pkValue = $data[$pk]; + unset($data[$pk]); + }else{ + // 如果没有任何更新条件则不执行 + $this->error = Lang::get('_OPERATION_WRONG_'); + return false; + } + } + $result = $this->db->update($data,$options); + if(false !== $result) { + if(isset($pkValue)) $data[$pk] = $pkValue; + $this->_after_update($data,$options); + } + return $result; + } + // 更新数据前的回调方法 + protected function _before_update(&$data,$options) {} + // 更新成功后的回调方法 + protected function _after_update($data,$options) {} + + /** + * 删除数据 + * @access public + * @param mixed $options 表达式 + * @return mixed + */ + public function delete($options=[]) { + if(empty($options) && empty($this->options['where'])) { + // 如果删除条件为空 则删除当前数据对象所对应的记录 + if(!empty($this->data) && isset($this->data[$this->getPk()])) + return $this->delete($this->data[$this->getPk()]); + else + return false; + } + if(is_numeric($options) || is_string($options)) { + // 根据主键删除记录 + $pk = $this->getPk(); + if(strpos($options,',')) { + $where[$pk] = ['IN', $options]; + }else{ + $where[$pk] = $options; + } + $pkValue = $where[$pk]; + $options = []; + $options['where'] = $where; + } + // 分析表达式 + $options = $this->_parseOptions($options); + $result= $this->db->delete($options); + if(false !== $result) { + $data = []; + if(isset($pkValue)) $data[$pk] = $pkValue; + $this->_after_delete($data,$options); + } + // 返回删除记录个数 + return $result; + } + // 删除成功后的回调方法 + protected function _after_delete($data,$options) {} + + /** + * 查询数据集 + * @access public + * @param mixed $options 表达式参数 + * @return mixed + */ + public function select($options=[]) { + if(is_string($options) || is_numeric($options)) { + // 根据主键查询 + $pk = $this->getPk(); + if(strpos($options,',')) { + $where[$pk] = ['IN',$options]; + }else{ + $where[$pk] = $options; + } + $options = []; + $options['where'] = $where; + }elseif(false === $options){ // 用于子查询 不查询只返回SQL + $options = []; + // 分析表达式 + $options = $this->_parseOptions($options); + return '( '.$this->db->buildSelectSql($options).' )'; + } + // 分析表达式 + $options = $this->_parseOptions($options); + $resultSet = $this->db->select($options); + if(false === $resultSet) { + return false; + } + if(empty($resultSet)) { // 查询结果为空 + return null; + } + $this->_after_select($resultSet,$options); + return $resultSet; + } + // 查询成功后的回调方法 + protected function _after_select(&$resultSet,$options) {} + + /** + * 生成查询SQL 可用于子查询 + * @access public + * @param array $options 表达式参数 + * @return string + */ + public function buildSql($options=[]) { + // 分析表达式 + $options = $this->_parseOptions($options); + return '( '.$this->db->buildSelectSql($options).' )'; + } + + /** + * 分析表达式 + * @access protected + * @param array $options 表达式参数 + * @return array + */ + protected function _parseOptions($options=[]) { + if(is_array($options)) + $options = array_merge($this->options,$options); + // 查询过后清空sql表达式组装 避免影响下次查询 + $this->options = []; + + if(!empty($options['alias'])) { + $options['table'] .= ' '.$options['alias']; + } + // 记录操作的模型名称 + $options['model'] = $this->name; + + if(isset($options['table'])) {// 动态指定表名 + $fields = $this->db->getFields($this->options['table']); + $fields = $fields?array_keys($fields):false; + }else{ + $options['table'] = $this->getTableName(); + $fields = $this->getDbFields(); + } + // 字段类型验证 + if(isset($options['where']) && is_array($options['where']) && !empty($fields)) { + // 对数组查询条件进行字段类型检查 + foreach ($options['where'] as $key=>$val){ + $key = trim($key); + if(in_array($key,$fields,true)){ + if(is_scalar($val)) { + $this->_parseType($options['where'],$key); + } + }elseif('_' != substr($key,0,1) && false === strpos($key,'.') && false === strpos($key,'(') && false === strpos($key,'|') && false === strpos($key,'&')){ + unset($options['where'][$key]); + } + } + } + + + // 表达式过滤 + $this->_options_filter($options); + return $options; + } + // 表达式过滤回调方法 + protected function _options_filter(&$options) {} + + /** + * 数据类型检测 + * @access protected + * @param mixed $data 数据 + * @param string $key 字段名 + * @return void + */ + protected function _parseType(&$data,$key) { + if(isset($this->fields['_type'][$key])) { + $fieldType = strtolower($this->fields['_type'][$key]); + if(false === strpos($fieldType,'bigint') && false !== strpos($fieldType,'int')) { + $data[$key] = intval($data[$key]); + }elseif(false !== strpos($fieldType,'float') || false !== strpos($fieldType,'double')){ + $data[$key] = floatval($data[$key]); + }elseif(false !== strpos($fieldType,'bool')){ + $data[$key] = (bool)$data[$key]; + } + } + } + + /** + * 查询数据 + * @access public + * @param mixed $options 表达式参数 + * @return mixed + */ + public function find($options=[]) { + if(is_numeric($options) || is_string($options)) { + $where[$this->getPk()] = $options; + $options = []; + $options['where'] = $where; + } + // 总是查找一条记录 + $options['limit'] = 1; + // 分析表达式 + $options = $this->_parseOptions($options); + $resultSet = $this->db->select($options); + if(false === $resultSet) { + return false; + } + if(empty($resultSet)) {// 查询结果为空 + return null; + } + $this->data = $resultSet[0]; + $this->_after_find($this->data,$options); + return $this->data; + } + // 查询成功的回调方法 + protected function _after_find(&$result,$options) {} + + /** + * 处理字段映射 + * @access public + * @param array $data 当前数据 + * @param integer $type 类型 0 写入 1 读取 + * @return array + */ + public function parseFieldsMap($data,$type=1) { + // 检查字段映射 + if(!empty($this->_map)) { + foreach ($this->_map as $key=>$val){ + if($type==1) { // 读取 + if(isset($data[$val])) { + $data[$key] = $data[$val]; + unset($data[$val]); + } + }else{ + if(isset($data[$key])) { + $data[$val] = $data[$key]; + unset($data[$key]); + } + } + } + } + return $data; + } + + /** + * 设置记录的某个字段值 + * 支持使用数据库字段和方法 + * @access public + * @param string|array $field 字段名 + * @param string $value 字段值 + * @return boolean + */ + public function setField($field,$value='') { + if(is_array($field)) { + $data = $field; + }else{ + $data[$field] = $value; + } + return $this->save($data); + } + + /** + * 字段值增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @return boolean + */ + public function setInc($field,$step=1) { + return $this->setField($field,['exp',$field.'+'.$step]); + } + + /** + * 字段值减少 + * @access public + * @param string $field 字段名 + * @param integer $step 减少值 + * @return boolean + */ + public function setDec($field,$step=1) { + return $this->setField($field,['exp',$field.'-'.$step]); + } + + /** + * 获取一条记录的某个字段值 + * @access public + * @param string $field 字段名 + * @param string $spea 字段数据间隔符号 NULL返回数组 + * @return mixed + */ + public function getField($field,$sepa=null) { + $options['field'] = $field; + $options = $this->_parseOptions($options); + $field = trim($field); + if(strpos($field,',')) { // 多字段 + if(!isset($options['limit'])){ + $options['limit'] = is_numeric($sepa)?$sepa:''; + } + $resultSet = $this->db->select($options); + if(!empty($resultSet)) { + $_field = explode(',', $field); + $field = array_keys($resultSet[0]); + $key = array_shift($field); + $key2 = array_shift($field); + $cols = []; + $count = count($_field); + foreach ($resultSet as $result){ + $name = $result[$key]; + if(2==$count) { + $cols[$name] = $result[$key2]; + }else{ + $cols[$name] = is_string($sepa)?implode($sepa,$result):$result; + } + } + return $cols; + } + }else{ // 查找一条记录 + // 返回数据个数 + if(true !== $sepa) {// 当sepa指定为true的时候 返回所有数据 + $options['limit'] = is_numeric($sepa)?$sepa:1; + } + $result = $this->db->select($options); + if(!empty($result)) { + if(true !== $sepa && 1==$options['limit']) return reset($result[0]); + foreach ($result as $val){ + $array[] = $val[$field]; + } + return $array; + } + } + return null; + } + + /** + * 创建数据对象 但不保存到数据库 + * @access public + * @param mixed $data 创建数据 + * @param string $type 状态 + * @return mixed + */ + public function create($data='',$type='') { + // 如果没有传值默认取POST数据 + if(empty($data)) { + $data = $_POST; + }elseif(is_object($data)){ + $data = get_object_vars($data); + } + // 验证数据 + if(empty($data) || !is_array($data)) { + $this->error = L('_DATA_TYPE_INVALID_'); + return false; + } + + // 检查字段映射 + $data = $this->parseFieldsMap($data,0); + + // 状态 + $type = $type?$type:(!empty($data[$this->getPk()])?self::MODEL_UPDATE:self::MODEL_INSERT); + + // 检测提交字段的合法性 + if(isset($this->options['field'])) { // $this->field('field1,field2...')->create() + $fields = $this->options['field']; + unset($this->options['field']); + }elseif($type == self::MODEL_INSERT && isset($this->insertFields)) { + $fields = $this->insertFields; + }elseif($type == self::MODEL_UPDATE && isset($this->updateFields)) { + $fields = $this->updateFields; + } + if(isset($fields)) { + if(is_string($fields)) { + $fields = explode(',',$fields); + } + foreach ($data as $key=>$val){ + if(!in_array($key,$fields)) { + unset($data[$key]); + } + } + } + + // 验证完成生成数据对象 + if($this->autoCheckFields) { // 开启字段检测 则过滤非法字段数据 + $fields = $this->getDbFields(); + foreach ($data as $key=>$val){ + if(!in_array($key,$fields)) { + unset($data[$key]); + } + } + } + + // 赋值当前数据对象 + $this->data = $data; + // 返回创建的数据以供其他调用 + return $data; + } + + + + /** + * 切换当前的数据库连接 + * @access public + * @param integer $linkNum 连接序号 + * @param mixed $config 数据库连接信息 + * @param array $params 模型参数 + * @return Model + */ + public function db($linkNum='',$config='',$params=[]){ + if(''===$linkNum && $this->db) { + return $this->db; + } + static $_linkNum = []; + static $_db = []; + if(!isset($_db[$linkNum]) || (isset($_db[$linkNum]) && $config && $_linkNum[$linkNum]!=$config) ) { + // 创建一个新的实例 + if(!empty($config) && is_string($config) && false === strpos($config,'/')) { // 支持读取配置参数 + $config = C($config); + } + $_db[$linkNum] = Db::instance($config); + }elseif(NULL === $config){ + $_db[$linkNum]->close(); // 关闭数据库连接 + unset($_db[$linkNum]); + return ; + } + if(!empty($params)) { + if(is_string($params)) parse_str($params,$params); + foreach ($params as $name=>$value){ + $this->setProperty($name,$value); + } + } + // 记录连接信息 + $_linkNum[$linkNum] = $config; + // 切换数据库连接 + $this->db = $_db[$linkNum]; + $this->_after_db(); + return $this; + } + // 数据库切换后回调方法 + protected function _after_db() {} + + /** + * 得到当前的数据对象名称 + * @access public + * @return string + */ + public function getModelName() { + if(empty($this->name)) + $this->name = substr(get_class($this),0,-5); + return $this->name; + } + + /** + * 得到完整的数据表名 + * @access public + * @return string + */ + public function getTableName() { + if(empty($this->trueTableName)) { + $tableName = !empty($this->tablePrefix) ? $this->tablePrefix : ''; + if(!empty($this->tableName)) { + $tableName .= $this->tableName; + }else{ + $tableName .= parse_name($this->name); + } + $this->trueTableName = strtolower($tableName); + } + return (!empty($this->dbName)?$this->dbName.'.':'').$this->trueTableName; + } + + /** + * 启动事务 + * @access public + * @return void + */ + public function startTrans() { + $this->commit(); + $this->db->startTrans(); + return ; + } + + /** + * 提交事务 + * @access public + * @return boolean + */ + public function commit() { + return $this->db->commit(); + } + + /** + * 事务回滚 + * @access public + * @return boolean + */ + public function rollback() { + return $this->db->rollback(); + } + + + /** + * 获取主键名称 + * @access public + * @return string + */ + public function getPk() { + return isset($this->fields['_pk'])?$this->fields['_pk']:$this->pk; + } + + /** + * 获取数据表字段信息 + * @access public + * @return array + */ + public function getDbFields(){ + if($this->fields) { + $fields = $this->fields; + unset($fields['_pk'],$fields['_type']); + return $fields; + }else{ + $fields = Cache::get(md5($this->getTableName())); + if(!$fields) { + $fields = $this->db->getFields($this->getTableName()); + $this->fields = array_keys($fields); + foreach ($fields as $key=>$val){ + // 记录字段类型 + $type[$key] = $val['type']; + if($val['primary']) { + $this->fields['_pk'] = $key; + } + } + // 记录字段类型信息 + $this->fields['_type'] = $type; + Cache::set(md5($this->trueTableName),$this->fields); + return array_keys($fields); + }else{ + $this->fields = $fields; + unset($fields['_pk'],$fields['_type']); + return array_keys($fields); + } + } + } + + /** + * 设置数据对象值 + * @access public + * @param mixed $data 数据 + * @return Model + */ + public function data($data=''){ + if('' === $data && !empty($this->data)) { + return $this->data; + } + if(is_object($data)){ + $data = get_object_vars($data); + }elseif(is_string($data)){ + parse_str($data,$data); + }elseif(!is_array($data)){ + E(L('_DATA_TYPE_INVALID_')); + } + $this->data = $data; + return $this; + } + + /** + * 查询SQL组装 join + * @access public + * @param mixed $join + * @return Model + */ + public function join($join) { + if(is_array($join)) { + $this->options['join'] = $join; + }elseif(!empty($join)) { + $this->options['join'][] = $join; + } + return $this; + } + + /** + * 查询SQL组装 union + * @access public + * @param mixed $union + * @param boolean $all + * @return Model + */ + public function union($union,$all=false) { + if(empty($union)) return $this; + if($all) { + $this->options['union']['_all'] = true; + } + if(is_object($union)) { + $union = get_object_vars($union); + } + // 转换union表达式 + if(is_string($union) ) { + $options = $union; + }elseif(is_array($union)){ + if(isset($union[0])) { + $this->options['union'] = array_merge($this->options['union'],$union); + return $this; + }else{ + $options = $union; + } + }else{ + E(L('_DATA_TYPE_INVALID_')); + } + $this->options['union'][] = $options; + return $this; + } + + /** + * 查询缓存 + * @access public + * @param mixed $key + * @param integer $expire + * @param string $type + * @return Model + */ + public function cache($key=true,$expire=null,$type=''){ + if(false !== $key) + $this->options['cache'] = ['key'=>$key,'expire'=>$expire,'type'=>$type]; + return $this; + } + + /** + * 指定查询字段 支持字段排除 + * @access public + * @param mixed $field + * @param boolean $except 是否排除 + * @return Model + */ + public function field($field,$except=false){ + if(true === $field) {// 获取全部字段 + $fields = $this->getDbFields(); + $field = $fields?$fields:'*'; + }elseif($except) {// 字段排除 + if(is_string($field)) { + $field = explode(',',$field); + } + $fields = $this->getDbFields(); + $field = $fields?array_diff($fields,$field):$field; + } + $this->options['field'] = $field; + return $this; + } + + /** + * 调用命名范围 + * @access public + * @param mixed $scope 命名范围名称 支持多个 和直接定义 + * @param array $args 参数 + * @return Model + */ + public function scope($scope='',$args=NULL){ + if('' === $scope) { + if(isset($this->_scope['default'])) { + // 默认的命名范围 + $options = $this->_scope['default']; + }else{ + return $this; + } + }elseif(is_string($scope)){ // 支持多个命名范围调用 用逗号分割 + $scopes = explode(',',$scope); + $options = []; + foreach ($scopes as $name){ + if(!isset($this->_scope[$name])) continue; + $options = array_merge($options,$this->_scope[$name]); + } + if(!empty($args) && is_array($args)) { + $options = array_merge($options,$args); + } + }elseif(is_array($scope)){ // 直接传入命名范围定义 + $options = $scope; + } + + if(is_array($options) && !empty($options)){ + $this->options = array_merge($this->options,array_change_key_case($options)); + } + return $this; + } + + /** + * 指定查询条件 支持安全过滤 + * @access public + * @param mixed $where 条件表达式 + * @param mixed $parse 预处理参数 + * @return Model + */ + public function where($where,$parse=null){ + if(!is_null($parse) && is_string($where)) { + if(!is_array($parse)) { + $parse = func_get_args(); + array_shift($parse); + } + $parse = array_map([$this->db,'escapeString'],$parse); + $where = vsprintf($where,$parse); + }elseif(is_object($where)){ + $where = get_object_vars($where); + } + if(is_string($where) && '' != $where){ + $map = []; + $map['_string'] = $where; + $where = $map; + } + if(isset($this->options['where'])){ + $this->options['where'] = array_merge($this->options['where'],$where); + }else{ + $this->options['where'] = $where; + } + + return $this; + } + + /** + * 指定查询数量 + * @access public + * @param mixed $offset 起始位置 + * @param mixed $length 查询数量 + * @return Model + */ + public function limit($offset,$length=null){ + $this->options['limit'] = is_null($length)?$offset:$offset.','.$length; + return $this; + } + + /** + * 指定分页 + * @access public + * @param mixed $page 页数 + * @param mixed $listRows 每页数量 + * @return Model + */ + public function page($page,$listRows=null){ + $this->options['page'] = is_null($listRows)?$page:$page.','.$listRows; + return $this; + } + + /** + * 查询注释 + * @access public + * @param string $comment 注释 + * @return Model + */ + public function comment($comment){ + $this->options['comment'] = $comment; + return $this; + } + + /** + * 设置模型的属性值 + * @access public + * @param string $name 名称 + * @param mixed $value 值 + * @return Model + */ + public function setProperty($name,$value) { + if(property_exists($this,$name)) + $this->$name = $value; + return $this; + } +} \ No newline at end of file diff --git a/Traits/Think/Model/Extend.php b/Traits/Think/Model/Extend.php new file mode 100644 index 00000000..db34eedc --- /dev/null +++ b/Traits/Think/Model/Extend.php @@ -0,0 +1,169 @@ + +// +---------------------------------------------------------------------- + +namespace Traits\Think; + +trait Extend { + + protected $partition = []; + + /** + * 字段值延迟增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @param integer $lazyTime 延时时间(s) + * @return boolean + */ + public function setLazyInc($field,$step=1,$lazyTime=0) { + $condition = $this->options['where']; + if(empty($condition)) { // 没有条件不做任何更新 + return false; + } + if($lazyTime>0) {// 延迟写入 + $guid = md5($this->name.'_'.$field.'_'.serialize($condition)); + $step = $this->lazyWrite($guid,$step,$lazyTime); + if(false === $step ) return true; // 等待下次写入 + } + return $this->setField($field,array('exp',$field.'+'.$step)); + } + + /** + * 字段值延迟减少 + * @access public + * @param string $field 字段名 + * @param integer $step 减少值 + * @param integer $lazyTime 延时时间(s) + * @return boolean + */ + public function setLazyDec($field,$step=1,$lazyTime=0) { + $condition = $this->options['where']; + if(empty($condition)) { // 没有条件不做任何更新 + return false; + } + if($lazyTime>0) {// 延迟写入 + $guid = md5($this->name.'_'.$field.'_'.serialize($condition)); + $step = $this->lazyWrite($guid,$step,$lazyTime); + if(false === $step ) return true; // 等待下次写入 + } + return $this->setField($field,array('exp',$field.'-'.$step)); + } + + /** + * 延时更新检查 返回false表示需要延时 + * 否则返回实际写入的数值 + * @access public + * @param string $guid 写入标识 + * @param integer $step 写入步进值 + * @param integer $lazyTime 延时时间(s) + * @return false|integer + */ + protected function lazyWrite($guid,$step,$lazyTime) { + if(false !== ($value = F($guid))) { // 存在缓存写入数据 + if(time()>S($guid.'_time')+$lazyTime) { + // 延时更新时间到了,删除缓存数据 并实际写入数据库 + S($guid,NULL); + S($guid.'_time',NULL); + return $value+$step; + }else{ + // 追加数据到缓存 + S($guid,$value+$step); + return false; + } + }else{ // 没有缓存数据 + S($guid,$step); + // 计时开始 + S($guid.'_time',time()); + return false; + } + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param array $sql SQL批处理指令 + * @return boolean + */ + public function patchQuery($sql=[]) { + if(!is_array($sql)) return false; + // 自动启动事务支持 + $this->startTrans(); + try{ + foreach ($sql as $_sql){ + $result = $this->execute($_sql); + if(false === $result) { + // 发生错误自动回滚事务 + $this->rollback(); + return false; + } + } + // 提交事务 + $this->commit(); + } catch (\Think\Exception $e) { + $this->rollback(); + } + return true; + } + + /** + * 得到分表的的数据表名 + * @access public + * @param array $data 操作的数据 + * @return string + */ + public function getPartitionTableName($data=[]) { + // 对数据表进行分区 + if(isset($data[$this->partition['field']])) { + $field = $data[$this->partition['field']]; + switch($this->partition['type']) { + case 'id': + // 按照id范围分表 + $step = $this->partition['expr']; + $seq = floor($field / $step)+1; + break; + case 'year': + // 按照年份分表 + if(!is_numeric($field)) { + $field = strtotime($field); + } + $seq = date('Y',$field)-$this->partition['expr']+1; + break; + case 'mod': + // 按照id的模数分表 + $seq = ($field % $this->partition['num'])+1; + break; + case 'md5': + // 按照md5的序列分表 + $seq = (ord(substr(md5($field),0,1)) % $this->partition['num'])+1; + break; + default : + if(function_exists($this->partition['type'])) { + // 支持指定函数哈希 + $fun = $this->partition['type']; + $seq = (ord(substr($fun($field),0,1)) % $this->partition['num'])+1; + }else{ + // 按照字段的首字母的值分表 + $seq = (ord($field{0}) % $this->partition['num'])+1; + } + } + return $this->getTableName().'_'.$seq; + }else{ + // 当设置的分表字段不在查询条件或者数据中 + // 进行联合查询,必须设定 partition['num'] + $tableName = []; + for($i=0;$i<$this->partition['num'];$i++) + $tableName[] = 'SELECT * FROM '.$this->getTableName().'_'.($i+1); + $tableName = '( '.implode(" UNION ",$tableName).') AS '.$this->name; + return $tableName; + } + } +} \ No newline at end of file diff --git a/Traits/Think/Model/Query.php b/Traits/Think/Model/Query.php new file mode 100644 index 00000000..3c19b4e8 --- /dev/null +++ b/Traits/Think/Model/Query.php @@ -0,0 +1,124 @@ + +// +---------------------------------------------------------------------- + +namespace Traits\Think\Model; + +trait Query { + + /** + * 启动事务 + * @access public + * @return void + */ + public function startTrans() { + $this->commit(); + $this->db->startTrans(); + return ; + } + + /** + * 提交事务 + * @access public + * @return boolean + */ + public function commit() { + return $this->db->commit(); + } + + /** + * 事务回滚 + * @access public + * @return boolean + */ + public function rollback() { + return $this->db->rollback(); + } + + /** + * SQL查询 + * @access public + * @param string $sql SQL指令 + * @param mixed $parse 是否需要解析SQL + * @return mixed + */ + public function query($sql,$parse=false) { + if(!is_bool($parse) && !is_array($parse)) { + $parse = func_get_args(); + array_shift($parse); + } + $sql = $this->parseSql($sql,$parse); + return $this->db->query($sql); + } + + /** + * 执行SQL语句 + * @access public + * @param string $sql SQL指令 + * @param mixed $parse 是否需要解析SQL + * @return false | integer + */ + public function execute($sql,$parse=false) { + if(!is_bool($parse) && !is_array($parse)) { + $parse = func_get_args(); + array_shift($parse); + } + $sql = $this->parseSql($sql,$parse); + return $this->db->execute($sql); + } + + /** + * 解析SQL语句 + * @access public + * @param string $sql SQL指令 + * @param boolean $parse 是否需要解析SQL + * @return string + */ + protected function parseSql($sql,$parse) { + // 分析表达式 + if(true === $parse) { + $options = $this->_parseOptions(); + $sql = $this->db->parseSql($sql,$options); + }elseif(is_array($parse)){ // SQL预处理 + $sql = vsprintf($sql,$parse); + }else{ + $sql = strtr($sql,['__TABLE__'=>$this->getTableName(),'__PREFIX__'=>$this->tablePrefix]); + } + return $sql; + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param array $sql SQL批处理指令 + * @return boolean + */ + public function patchQuery($sql=[]) { + if(!is_array($sql)) return false; + // 自动启动事务支持 + $this->startTrans(); + try{ + foreach ($sql as $_sql){ + $result = $this->execute($_sql); + if(false === $result) { + // 发生错误自动回滚事务 + $this->rollback(); + return false; + } + } + // 提交事务 + $this->commit(); + } catch (\Think\Exception $e) { + $this->rollback(); + } + return true; + } +} \ No newline at end of file diff --git a/Traits/Think/Model/Relation.php b/Traits/Think/Model/Relation.php new file mode 100644 index 00000000..4f4dbf2d --- /dev/null +++ b/Traits/Think/Model/Relation.php @@ -0,0 +1,346 @@ + +// +---------------------------------------------------------------------- + +namespace Traits\Think\Model; + +define('HAS_ONE',1); +define('BELONGS_TO',2); +define('HAS_MANY',3); +define('MANY_TO_MANY',4); + +trait Relation { + // 关联定义 + protected $_link = array(); + + /** + * 得到关联的数据表名 + * @access public + * @return string + */ + public function getRelationTableName($relation) { + $relationTable = !empty($this->tablePrefix) ? $this->tablePrefix : ''; + $relationTable .= $this->tableName?$this->tableName:$this->name; + $relationTable .= '_'.$relation->getModelName(); + return strtolower($relationTable); + } + + // 查询成功后的回调方法 + protected function _after_find(&$result,$options) { + // 获取关联数据 并附加到结果中 + if(!empty($options['link'])) + $this->getRelation($result,$options['link']); + } + + // 查询数据集成功后的回调方法 + protected function _after_select(&$result,$options) { + // 获取关联数据 并附加到结果中 + if(!empty($options['link'])) + $this->getRelations($result,$options['link']); + } + + // 写入成功后的回调方法 + protected function _after_insert($data,$options) { + // 关联写入 + if(!empty($options['link'])) + $this->opRelation('ADD',$data,$options['link']); + } + + // 更新成功后的回调方法 + protected function _after_update($data,$options) { + // 关联更新 + if(!empty($options['link'])) + $this->opRelation('SAVE',$data,$options['link']); + } + + // 删除成功后的回调方法 + protected function _after_delete($data,$options) { + // 关联删除 + if(!empty($options['link'])) + $this->opRelation('DEL',$data,$options['link']); + } + + /** + * 对保存到数据库的数据进行处理 + * @access protected + * @param mixed $data 要操作的数据 + * @return boolean + */ + protected function _facade($data) { + $this->_before_write($data); + return $data; + } + + /** + * 获取返回数据集的关联记录 + * @access protected + * @param array $resultSet 返回数据 + * @param string|array $name 关联名称 + * @return array + */ + protected function getRelations(&$resultSet,$name='') { + // 获取记录集的主键列表 + foreach($resultSet as $key=>$val) { + $val = $this->getRelation($val,$name); + $resultSet[$key] = $val; + } + return $resultSet; + } + + /** + * 获取返回数据的关联记录 + * @access protected + * @param mixed $result 返回数据 + * @param string|array $name 关联名称 + * @param boolean $return 是否返回关联数据本身 + * @return array + */ + protected function getRelation(&$result,$name='',$return=false) { + if(!empty($this->_link)) { + foreach($this->_link as $key=>$val) { + $mappingName = !empty($val['mapping_name'])?$val['mapping_name']:$key; // 映射名称 + if(empty($name) || true === $name || $mappingName == $name || (is_array($name) && in_array($mappingName,$name))) { + $mappingType = !empty($val['mapping_type'])?$val['mapping_type']:$val; // 关联类型 + $mappingClass = !empty($val['class_name'])?$val['class_name']:$key; // 关联类名 + $mappingFields = !empty($val['mapping_fields'])?$val['mapping_fields']:'*'; // 映射字段 + $mappingCondition = !empty($val['condition'])?$val['condition']:'1=1'; // 关联条件 + $mappingKey =!empty($val['mapping_key'])? $val['mapping_key'] : $this->getPk(); // 关联键名 + if(strtoupper($mappingClass)==strtoupper($this->name)) { + // 自引用关联 获取父键名 + $mappingFk = !empty($val['parent_key'])? $val['parent_key'] : 'parent_id'; + }else{ + $mappingFk = !empty($val['foreign_key'])?$val['foreign_key']:strtolower($this->name).'_id'; // 关联外键 + } + // 获取关联模型对象 + $model = D($mappingClass); + switch($mappingType) { + case HAS_ONE: + $pk = $result[$mappingKey]; + $mappingCondition .= " AND {$mappingFk}='{$pk}'"; + $relationData = $model->where($mappingCondition)->field($mappingFields)->find(); + break; + case BELONGS_TO: + if(strtoupper($mappingClass)==strtoupper($this->name)) { + // 自引用关联 获取父键名 + $mappingFk = !empty($val['parent_key'])? $val['parent_key'] : 'parent_id'; + }else{ + $mappingFk = !empty($val['foreign_key'])?$val['foreign_key']:strtolower($model->getModelName()).'_id'; // 关联外键 + } + $fk = $result[$mappingFk]; + $mappingCondition .= " AND {$model->getPk()}='{$fk}'"; + $relationData = $model->where($mappingCondition)->field($mappingFields)->find(); + break; + case HAS_MANY: + $pk = $result[$mappingKey]; + $mappingCondition .= " AND {$mappingFk}='{$pk}'"; + $mappingOrder = !empty($val['mapping_order'])?$val['mapping_order']:''; + $mappingLimit = !empty($val['mapping_limit'])?$val['mapping_limit']:''; + // 延时获取关联记录 + $relationData = $model->where($mappingCondition)->field($mappingFields)->order($mappingOrder)->limit($mappingLimit)->select(); + break; + case MANY_TO_MANY: + $pk = $result[$mappingKey]; + $mappingCondition = " {$mappingFk}='{$pk}'"; + $mappingOrder = $val['mapping_order']; + $mappingLimit = $val['mapping_limit']; + $mappingRelationFk = $val['relation_foreign_key']?$val['relation_foreign_key']:$model->getModelName().'_id'; + $mappingRelationTable = $val['relation_table']?$val['relation_table']:$this->getRelationTableName($model); + $sql = "SELECT b.{$mappingFields} FROM {$mappingRelationTable} AS a, ".$model->getTableName()." AS b WHERE a.{$mappingRelationFk} = b.{$model->getPk()} AND a.{$mappingCondition}"; + if(!empty($val['condition'])) { + $sql .= ' AND '.$val['condition']; + } + if(!empty($mappingOrder)) { + $sql .= ' ORDER BY '.$mappingOrder; + } + if(!empty($mappingLimit)) { + $sql .= ' LIMIT '.$mappingLimit; + } + $relationData = $this->query($sql); + break; + } + if(!$return){ + if(isset($val['as_fields']) && in_array($mappingType,array(HAS_ONE,BELONGS_TO)) ) { + // 支持直接把关联的字段值映射成数据对象中的某个字段 + // 仅仅支持HAS_ONE BELONGS_TO + $fields = explode(',',$val['as_fields']); + foreach ($fields as $field){ + if(strpos($field,':')) { + list($relationName,$nick) = explode(':',$field); + $result[$nick] = $relationData[$relationName]; + }else{ + $result[$field] = $relationData[$field]; + } + } + }else{ + $result[$mappingName] = $relationData; + } + unset($relationData); + }else{ + return $relationData; + } + } + } + } + return $result; + } + + /** + * 操作关联数据 + * @access protected + * @param string $opType 操作方式 ADD SAVE DEL + * @param mixed $data 数据对象 + * @param string $name 关联名称 + * @return mixed + */ + protected function opRelation($opType,$data='',$name='') { + $result = false; + if(empty($data) && !empty($this->data)){ + $data = $this->data; + }elseif(!is_array($data)){ + // 数据无效返回 + return false; + } + if(!empty($this->_link)) { + // 遍历关联定义 + foreach($this->_link as $key=>$val) { + // 操作制定关联类型 + $mappingName = $val['mapping_name']?$val['mapping_name']:$key; // 映射名称 + if(empty($name) || true === $name || $mappingName == $name || (is_array($name) && in_array($mappingName,$name)) ) { + // 操作制定的关联 + $mappingType = !empty($val['mapping_type'])?$val['mapping_type']:$val; // 关联类型 + $mappingClass = !empty($val['class_name'])?$val['class_name']:$key; // 关联类名 + $mappingKey =!empty($val['mapping_key'])? $val['mapping_key'] : $this->getPk(); // 关联键名 + // 当前数据对象主键值 + $pk = $data[$mappingKey]; + if(strtoupper($mappingClass)==strtoupper($this->name)) { + // 自引用关联 获取父键名 + $mappingFk = !empty($val['parent_key'])? $val['parent_key'] : 'parent_id'; + }else{ + $mappingFk = !empty($val['foreign_key'])?$val['foreign_key']:strtolower($this->name).'_id'; // 关联外键 + } + if(!empty($val['condition'])) { + $mappingCondition = $val['condition']; + }else{ + $mappingCondition = array(); + $mappingCondition[$mappingFk] = $pk; + } + // 获取关联model对象 + $model = D($mappingClass); + $mappingData = isset($data[$mappingName])?$data[$mappingName]:false; + if(!empty($mappingData) || $opType == 'DEL') { + switch($mappingType) { + case HAS_ONE: + switch (strtoupper($opType)){ + case 'ADD': // 增加关联数据 + $mappingData[$mappingFk] = $pk; + $result = $model->add($mappingData); + break; + case 'SAVE': // 更新关联数据 + $result = $model->where($mappingCondition)->save($mappingData); + break; + case 'DEL': // 根据外键删除关联数据 + $result = $model->where($mappingCondition)->delete(); + break; + } + break; + case BELONGS_TO: + break; + case HAS_MANY: + switch (strtoupper($opType)){ + case 'ADD' : // 增加关联数据 + $model->startTrans(); + foreach ($mappingData as $val){ + $val[$mappingFk] = $pk; + $result = $model->add($val); + } + $model->commit(); + break; + case 'SAVE' : // 更新关联数据 + $model->startTrans(); + $pk = $model->getPk(); + foreach ($mappingData as $vo){ + if(isset($vo[$pk])) {// 更新数据 + $mappingCondition = "$pk ={$vo[$pk]}"; + $result = $model->where($mappingCondition)->save($vo); + }else{ // 新增数据 + $vo[$mappingFk] = $data[$mappingKey]; + $result = $model->add($vo); + } + } + $model->commit(); + break; + case 'DEL' : // 删除关联数据 + $result = $model->where($mappingCondition)->delete(); + break; + } + break; + case MANY_TO_MANY: + $mappingRelationFk = $val['relation_foreign_key']?$val['relation_foreign_key']:$model->getModelName().'_id';// 关联 + $mappingRelationTable = $val['relation_table']?$val['relation_table']:$this->getRelationTableName($model); + if(is_array($mappingData)) { + $ids = array(); + foreach ($mappingData as $vo) + $ids[] = $vo[$mappingKey]; + $relationId = implode(',',$ids); + } + switch (strtoupper($opType)){ + case 'ADD': // 增加关联数据 + case 'SAVE': // 更新关联数据 + if(isset($relationId)) { + $this->startTrans(); + // 删除关联表数据 + $this->table($mappingRelationTable)->where($mappingCondition)->delete(); + // 插入关联表数据 + $sql = 'INSERT INTO '.$mappingRelationTable.' ('.$mappingFk.','.$mappingRelationFk.') SELECT a.'.$this->getPk().',b.'.$model->getPk().' FROM '.$this->getTableName().' AS a ,'.$model->getTableName()." AS b where a.".$this->getPk().' ='. $pk.' AND b.'.$model->getPk().' IN ('.$relationId.") "; + $result = $model->execute($sql); + if(false !== $result) + // 提交事务 + $this->commit(); + else + // 事务回滚 + $this->rollback(); + } + break; + case 'DEL': // 根据外键删除中间表关联数据 + $result = $this->table($mappingRelationTable)->where($mappingCondition)->delete(); + break; + } + break; + } + } + } + } + } + return $result; + } + + /** + * 进行关联查询 + * @access public + * @param mixed $name 关联名称 + * @return Model + */ + public function relation($name) { + $this->options['link'] = $name; + return $this; + } + + /** + * 关联数据获取 仅用于查询后 + * @access public + * @param string $name 关联名称 + * @return array + */ + public function relationGet($name) { + if(empty($this->data)) + return false; + return $this->getRelation($this->data,$name,true); + } +} \ No newline at end of file diff --git a/Traits/Think/Model/View.php b/Traits/Think/Model/View.php new file mode 100644 index 00000000..1f5589a2 --- /dev/null +++ b/Traits/Think/Model/View.php @@ -0,0 +1,239 @@ + +// +---------------------------------------------------------------------- + +namespace Traits\Think\Model; + +trait View { + + protected $viewFields = []; + + /** + * 自动检测数据表信息 + * @access protected + * @return void + */ + protected function _checkTableInfo() {} + + /** + * 得到完整的数据表名 + * @access public + * @return string + */ + public function getTableName() { + if(empty($this->trueTableName)) { + $tableName = ''; + foreach ($this->viewFields as $key=>$view){ + // 获取数据表名称 + if(isset($view['_table'])) { // 2011/10/17 添加实际表名定义支持 可以实现同一个表的视图 + $tableName .= $view['_table']; + }else{ + $class = $key.'Model'; + $Model = class_exists($class)?new $class():M($key); + $tableName .= $Model->getTableName(); + } + // 表别名定义 + $tableName .= !empty($view['_as'])?' '.$view['_as']:' '.$key; + // 支持ON 条件定义 + $tableName .= !empty($view['_on'])?' ON '.$view['_on']:''; + // 指定JOIN类型 例如 RIGHT INNER LEFT 下一个表有效 + $type = !empty($view['_type'])?$view['_type']:''; + $tableName .= ' '.strtoupper($type).' JOIN '; + $len = strlen($type.'_JOIN '); + } + $tableName = substr($tableName,0,-$len); + $this->trueTableName = $tableName; + } + return $this->trueTableName; + } + + /** + * 表达式过滤方法 + * @access protected + * @param string $options 表达式 + * @return void + */ + protected function _options_filter(&$options) { + if(isset($options['field'])) + $options['field'] = $this->checkFields($options['field']); + else + $options['field'] = $this->checkFields(); + if(isset($options['group'])) + $options['group'] = $this->checkGroup($options['group']); + if(isset($options['where'])) + $options['where'] = $this->checkCondition($options['where']); + if(isset($options['order'])) + $options['order'] = $this->checkOrder($options['order']); + } + + /** + * 检查是否定义了所有字段 + * @access protected + * @param string $name 模型名称 + * @param array $fields 字段数组 + * @return array + */ + private function _checkFields($name,$fields) { + if(false !== $pos = array_search('*',$fields)) {// 定义所有字段 + $fields = array_merge($fields,M($name)->getDbFields()); + unset($fields[$pos]); + } + return $fields; + } + + /** + * 检查条件中的视图字段 + * @access protected + * @param mixed $data 条件表达式 + * @return array + */ + protected function checkCondition($where) { + if(is_array($where)) { + $view = []; + // 检查视图字段 + foreach ($this->viewFields as $key=>$val){ + $k = isset($val['_as'])?$val['_as']:$key; + $val = $this->_checkFields($key,$val); + foreach ($where as $name=>$value){ + if(false !== $field = array_search($name,$val,true)) { + // 存在视图字段 + $_key = is_numeric($field)? $k.'.'.$name : $k.'.'.$field; + $view[$_key] = $value; + unset($where[$name]); + } + } + } + $where = array_merge($where,$view); + } + return $where; + } + + /** + * 检查Order表达式中的视图字段 + * @access protected + * @param string $order 字段 + * @return string + */ + protected function checkOrder($order='') { + if(is_string($order) && !empty($order)) { + $orders = explode(',',$order); + $_order = []; + foreach ($orders as $order){ + $array = explode(' ',$order); + $field = $array[0]; + $sort = isset($array[1])?$array[1]:'ASC'; + // 解析成视图字段 + foreach ($this->viewFields as $name=>$val){ + $k = isset($val['_as'])?$val['_as']:$name; + $val = $this->_checkFields($name,$val); + if(false !== $_field = array_search($field,$val,true)) { + // 存在视图字段 + $field = is_numeric($_field)?$k.'.'.$field:$k.'.'.$_field; + break; + } + } + $_order[] = $field.' '.$sort; + } + $order = implode(',',$_order); + } + return $order; + } + + /** + * 检查Group表达式中的视图字段 + * @access protected + * @param string $group 字段 + * @return string + */ + protected function checkGroup($group='') { + if(!empty($group)) { + $groups = explode(',',$group); + $_group = []; + foreach ($groups as $field){ + // 解析成视图字段 + foreach ($this->viewFields as $name=>$val){ + $k = isset($val['_as'])?$val['_as']:$name; + $val = $this->_checkFields($name,$val); + if(false !== $_field = array_search($field,$val,true)) { + // 存在视图字段 + $field = is_numeric($_field)?$k.'.'.$field:$k.'.'.$_field; + break; + } + } + $_group[] = $field; + } + $group = implode(',',$_group); + } + return $group; + } + + /** + * 检查fields表达式中的视图字段 + * @access protected + * @param string $fields 字段 + * @return string + */ + protected function checkFields($fields='') { + if(empty($fields) || '*'==$fields ) { + // 获取全部视图字段 + $fields = []; + foreach ($this->viewFields as $name=>$val){ + $k = isset($val['_as'])?$val['_as']:$name; + $val = $this->_checkFields($name,$val); + foreach ($val as $key=>$field){ + if(is_numeric($key)) { + $fields[] = $k.'.'.$field.' AS '.$field; + }elseif('_' != substr($key,0,1)) { + // 以_开头的为特殊定义 + if( false !== strpos($key,'*') || false !== strpos($key,'(') || false !== strpos($key,'.')) { + //如果包含* 或者 使用了sql方法 则不再添加前面的表名 + $fields[] = $key.' AS '.$field; + }else{ + $fields[] = $k.'.'.$key.' AS '.$field; + } + } + } + } + $fields = implode(',',$fields); + }else{ + if(!is_array($fields)) + $fields = explode(',',$fields); + // 解析成视图字段 + $array = []; + foreach ($fields as $key=>$field){ + if(strpos($field,'(') || strpos(strtolower($field),' as ')){ + // 使用了函数或者别名 + $array[] = $field; + unset($fields[$key]); + } + } + foreach ($this->viewFields as $name=>$val){ + $k = isset($val['_as'])?$val['_as']:$name; + $val = $this->_checkFields($name,$val); + foreach ($fields as $key=>$field){ + if(false !== $_field = array_search($field,$val,true)) { + // 存在视图字段 + if(is_numeric($_field)) { + $array[] = $k.'.'.$field.' AS '.$field; + }elseif('_' != substr($_field,0,1)){ + if( false !== strpos($_field,'*') || false !== strpos($_field,'(') || false !== strpos($_field,'.')) + //如果包含* 或者 使用了sql方法 则不再添加前面的表名 + $array[] = $_field.' AS '.$field; + else + $array[] = $k.'.'.$_field.' AS '.$field; + } + } + } + } + $fields = implode(',',$array); + } + return $fields; + } +} \ No newline at end of file