From 6e5658b0b91c1c7fe420bfaf2298b9d1f0469266 Mon Sep 17 00:00:00 2001 From: thinkphp Date: Mon, 4 Apr 2016 22:33:58 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84Model=E7=B1=BB=E5=92=8CDriver?= =?UTF-8?q?=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- library/think/Model.php | 2224 ++++++++--------------------------- library/think/db/Driver.php | 1699 +++++++++++++++++++------- 2 files changed, 1783 insertions(+), 2140 deletions(-) diff --git a/library/think/Model.php b/library/think/Model.php index 1385db62..c5f38c2a 100644 --- a/library/think/Model.php +++ b/library/think/Model.php @@ -11,103 +11,115 @@ namespace think; -class Model +use think\Cache; + +abstract class Model implements \JsonSerializable, \ArrayAccess { - // 操作状态 - const MODEL_INSERT = 1; // 新增 - const MODEL_UPDATE = 2; // 更新 - const MODEL_BOTH = 3; // 全部 - const EXISTS_VALIDATE = 0; // 存在就验证 - const MUST_VALIDATE = 1; // 必须验证 - const VALUE_VALIDATE = 2; // 有值就验证 - // 当前数据库操作对象 - protected $db = null; + // 当前实例 + private static $instance; // 数据库对象池 - private $links = []; - // 主键名称 - protected $pk = null; - // 数据表前缀 - protected $tablePrefix = null; - // 模型名称 - protected $name = ''; - // 数据库名称 - protected $dbName = ''; - // 数据表字段大小写 - protected $attrCase = null; - //数据库配置 - protected $connection = []; - // 数据表名(不包含表前缀) - protected $tableName = ''; - // 实际数据表名(包含表前缀) - protected $trueTableName = ''; - // 最近错误信息 - protected $error = ''; - // 字段信息 - protected $fields = []; + private static $links = []; + // 数据库配置 + protected static $connection; + // 数据表名称 + protected static $tableName; + // 回调事件 + protected static $event = []; + // 数据信息 protected $data = []; - // 数据副本 - protected $duplicate = []; - // 查询表达式参数 - protected $options = []; - // 命名范围定义 - protected $scope = []; - // 字段映射定义 - protected $map = []; + // 缓存数据 + protected $cache = []; + // 记录改变字段 + protected $change = []; + // 数据表主键 + protected $pk = 'id'; + // 错误信息 + protected $error; + // 当前模型名称 + protected $name; + // 字段验证规则 + protected $validate; + // 字段完成规则 + protected $auto = []; + // 新增的字段完成 + protected $insert = []; + // 更新的字段完成 + protected $update = []; + // 关联 + protected $relation = []; /** * 架构函数 * @access public - * @param string $name 模型名称 - * @param array $config 模型配置 + * @param array $data 数据 */ - public function __construct($name = '', array $config = []) + public function __construct($data = []) { - // 模型初始化 - $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); - } + $this->data = $data; + $this->name = basename(str_replace('\\', '/', get_class($this))); + } - if (!empty($config['prefix'])) { - $this->tablePrefix = $config['prefix']; - } elseif (isset($config['prefix']) && '' === $config['prefix']) { - $this->tablePrefix = ''; - } elseif (is_null($this->tablePrefix)) { - $this->tablePrefix = Config::get('database.prefix'); - } - if (!empty($config['connection'])) { - $this->connection = $config['connection']; - } - if (!empty($config['table_name'])) { - $this->tableName = $config['table_name']; - } - if (!empty($config['true_table_name'])) { - $this->trueTableName = $config['true_table_name']; - } - if (!empty($config['db_name'])) { - $this->dbName = $config['db_name']; - } + // JsonSerializable + public function jsonSerialize() + { + return $this->data; + } - if (is_null($this->attrCase)) { - $this->attrCase = Config::get('db_attr_case'); - } + // ArrayAccess + public function offsetSet($name, $value) + { + $this->__set($name, $value); + } - // 数据库初始化操作 - // 获取数据库操作对象 - // 当前模型有独立的数据库连接信息 - $this->db(0, $this->connection); + public function offsetExists($name) + { + return $this->__isset($name); + } + + public function offsetUnset($name) + { + $this->__unset($name); + } + + public function offsetGet($name) + { + return $this->__get($name); } /** - * 设置数据对象的值 + * 设置数据对象值 + * @access public + * @param mixed $data 数据 + * @return Model + */ + public function data($data = '') + { + if (is_object($data)) { + $data = get_object_vars($data); + } elseif (!is_array($data)) { + throw new Exception('data type invalid', 10300); + } + $this->data = $data; + return $this; + } + + /** + * 转换为数组 + * @access public + * @return Model + */ + public function toArray() + { + if (!empty($this->data)) { + return $this->data; + } else { + return []; + } + } + + /** + * 设置数据对象的值 修改器 * @access public * @param string $name 名称 * @param mixed $value 值 @@ -115,19 +127,42 @@ class Model */ public function __set($name, $value) { + // 检测修改器 + $method = 'set' . Loader::parseName($name, 1) . 'Attr'; + if (method_exists($this, $method)) { + $value = $this->$method($value, $this->data); + } + // 设置数据对象属性 + if (isset($this->data[$name]) && $this->data[$name] != $value && !in_array($name, $this->change)) { + $this->change[] = $name; + } $this->data[$name] = $value; + } /** - * 获取数据对象的值 + * 获取数据对象的值 获取器 * @access public * @param string $name 名称 * @return mixed */ public function __get($name) { - return isset($this->data[$name]) ? $this->data[$name] : null; + $value = isset($this->data[$name]) ? $this->data[$name] : null; + // 检测获取器 + $method = 'get' . Loader::parseName($name, 1) . 'Attr'; + if (method_exists($this, $method)) { + return $this->$method($value, $this->data); + } + if (is_null($value)) { + // 检测关联数据 + $method = 'getRelation' . Loader::parseName($name, 1); + if (method_exists($this, $method)) { + return $this->$method(); + } + } + return $value; } /** @@ -153,831 +188,205 @@ class Model } /** - * 利用__call方法实现一些特殊的Model方法 + * 判断一个字段名是否为主键字段 * @access public - * @param string $method 方法名称 - * @param array $args 调用参数 - * @return mixed - * @throws \think\Exception + * @param string $key 名称 + * @return void */ - public function __call($method, $args) + protected function isPk($key) { - if (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 = Loader::parseName(substr($method, 5)); - $where[$field] = $args[0]; - return $this->where($where)->find(); - } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') { - // 根据某个字段获取记录的某个值 - $name = Loader::parseName(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 { - throw new Exception(__CLASS__ . ':' . $method . ' method not exist'); + $pk = $this->pk; + if (is_string($pk) && $pk == $key) { + return true; + } elseif (is_array($pk) && in_array($key, $pk)) { + return true; } + return false; } - // 回调方法 初始化模型 - protected function _initialize() - {} - /** - * 对写入到数据库的数据进行处理 - * @access protected - * @param mixed $data 要操作的数据 - * @param string $type insert 或者 update - * @return array - * @throws \think\Exception + * 保存当前数据对象的值 无需任何参数(自动识别新增或者更新) + * @access public + * @return void */ - protected function _write_data($data, $type) + public function save() { - if (empty($data)) { - if (!empty($this->data)) { - // 没有传递数据,获取当前数据对象的值 - $data = $this->data; - $find = true; - // 重置数据 - $this->data = []; - } else { - throw new Exception('invalid data'); - } - } - if (isset($find) && !empty($this->duplicate) && 'update' == $type) { - // 存在数据副本 - foreach ($data as $key => $val) { - // 去除相同数据 - if (isset($this->duplicate[$key]) && $val == $this->duplicate[$key]) { - unset($data[$key]); - } - } - if (empty($data)) { - // 没有数据变化 - return []; - } else { - // 更新操作保留主键信息 - $pk = $this->getPk(); - if (is_array($pk)) { - foreach ($pk as $key) { - if (isset($this->duplicate[$key])) { - $data[$key] = $this->duplicate[$key]; - } - } - } elseif (isset($this->duplicate[$pk])) { - $data[$pk] = $this->duplicate[$pk]; - } - } - // 重置副本 - $this->duplicate = []; - } - // 检查字段映射 - if (!empty($this->map)) { - foreach ($this->map as $key => $val) { - if (isset($data[$key])) { - $data[$val] = $data[$key]; - unset($data[$key]); - } - } - } + $data = $this->data; // 数据自动验证 if (!$this->dataValidate($data)) { return false; } - // 数据自动填充 - $this->dataFill($data); + // 数据自动完成 + foreach ($this->auto as $name => $rule) { + if (!in_array($name, $this->change)) { + $this->change[] = $name; + } + $data[$name] = is_callable($rule) ? call_user_func_array($rule, [ & $data]) : $rule; + } - $fields = $this->getFields(); - // 检查非数据字段 - if (!empty($fields)) { - foreach ($data as $key => $val) { - if (!in_array($key, $fields, true)) { - if (Config::get('db_fields_strict')) { - throw new Exception(' fields not exists :[ ' . $key . ' ]'); - } - unset($data[$key]); - } elseif (is_scalar($val) && !isset($this->options['bind'][$key])) { - // 字段类型检查 - $this->_parseType($data, $key, $this->options['bind']); - } - } - } - // 安全过滤 - if (!empty($this->options['filter'])) { - $data = array_map($this->options['filter'], $data); - unset($this->options['filter']); - } - // 回调方法 - $this->_before_write($data); - if (empty($data)) { - // 没有数据则不执行 - throw new Exception('no data to write'); - } - return $data; - } - // 写入数据前的回调方法 包括新增和更新 - protected function _before_write(&$data) - {} + if ($this->checkPkExists($data)) { - /** - * 新增数据 - * @access public - * @param mixed $data 数据 - * @param boolean $replace 是否replace - * @return mixed - * @throws \think\Exception - */ - public function add($data = '', $replace = false) - { - // 数据处理 - $data = $this->_write_data($data, 'insert'); - if (false === $data) { - return false; - } - // 分析表达式 - $options = $this->_parseOptions(); - if (false === $this->_before_insert($data, $options)) { - return false; - } - // 写入数据到数据库 - $result = $this->db->insert($data, $options, $replace); - if (false !== $result && is_numeric($result)) { - $pk = $this->getPk($options['table']); - // 增加复合主键支持 - if (is_array($pk)) { - return $result; - } - $insertId = $this->getLastInsID(); - if ($insertId) { - // 自增主键返回插入ID - $data[$pk] = $insertId; - } - if (false === $this->_after_insert($data, $options)) { + if (false === self::trigger('before_update', $data)) { return false; } - } - return !empty($insertId) ? $insertId : $result; - } - // 插入数据前的回调方法 - protected function _before_insert(&$data, $options = []) - {} - // 插入成功后的回调方法 - protected function _after_insert($data, $options = []) - {} - - public function addAll($dataList, $options = [], $replace = false) - { - if (empty($dataList)) { - throw new Exception('no data to write'); - } - // 数据处理 - foreach ($dataList as &$data) { - $data = $this->_write_data($data, 'insert'); - if (false === $data) { - return false; - } - } - // 分析表达式 - $options = $this->_parseOptions($options); - // 写入数据到数据库 - $result = $this->db->insertAll($dataList, $options, $replace); - if (false !== $result) { - $insertId = $this->getLastInsID(); - } - return !empty($insertId) ? $insertId : $result; - } - - /** - * 保存数据 - * @access public - * @param mixed $data 数据 - * @return boolean - * @throws \think\Exception - */ - public function save($data = '') - { - // 数据处理 - $data = $this->_write_data($data, 'update'); - if (false == $data) { - return false; - } - // 分析表达式 - $options = $this->_parseOptions(); - $pk = $this->getPk($options['table']); - if (!isset($options['where'])) { - // 如果存在主键数据 则自动作为更新条件 - if (is_string($pk) && isset($data[$pk])) { - $where[$pk] = $data[$pk]; - unset($data[$pk]); - } elseif (is_array($pk)) { - // 增加复合主键支持 - foreach ($pk as $field) { - if (isset($data[$field])) { - $where[$field] = $data[$field]; - } else { - // 如果缺少复合主键数据则不执行 - throw new Exception('miss complex primary data'); - } - unset($data[$field]); - } - } - if (!isset($where)) { - // 如果没有任何更新条件则不执行 - throw new Exception('no data to update without where'); - } else { - $options['where'] = $where; - } - } - if (is_string($pk) && isset($options['where'][$pk])) { - $pkValue = $options['where'][$pk]; - } - if (false === $this->_before_update($data, $options)) { - return false; - } - $result = $this->db->update($data, $options); - if (false !== $result && is_numeric($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 - * @throws \think\Exception - */ - public function delete($options = []) - { - $pk = $this->getPk(); - if (empty($options) && empty($this->options['where'])) { - // 如果删除条件为空 则删除当前数据对象所对应的记录 - if (!empty($this->data) && is_string($pk) && isset($this->data[$pk])) { - return $this->delete($this->data[$pk]); - } else { - throw new Exception('no data to delete without where'); - } - } - if (!empty($options) && empty($options['where'])) { - // AR模式分析主键条件 - $this->parsePkWhere($options); - } - - // 分析表达式 - $options = $this->_parseOptions($options); - if (empty($options['where'])) { - // 如果条件为空 不进行删除操作 除非设置 1=1 - throw new Exception('no data to delete without where'); - } - if (is_string($pk) && isset($options['where'][$pk])) { - $pkValue = $options['where'][$pk]; - } - $result = $this->db->delete($options); - if (false !== $result && is_numeric($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 - * @throws \think\Exception - */ - public function select($options = []) - { - if (false === $options) { - // 用于子查询 不查询只返回SQL - $options['fetch_sql'] = true; - } elseif (!empty($options) && empty($options['where'])) { - // AR模式主键条件分析 - $this->parsePkWhere($options); - } - - // 分析表达式 - $options = $this->_parseOptions($options); - // 判断查询缓存 - if (isset($options['cache'])) { - $cache = $options['cache']; - if (!isset($cache['key']) || !is_string($cache['key'])) { - $cache['key'] = md5(serialize($options)); - } - $cache['expire'] = isset($cache['expire']) ? $cache['expire'] : null; - $data = Cache::get($cache['key']); - if (false !== $data) { - return $data; - } - } - $resultSet = $this->db->select($options); - - if (!empty($resultSet)) { - // 有查询结果 - if (is_string($resultSet)) { - return $resultSet; - } - - // 数据列表读取后的处理 - $resultSet = $this->_read_data_list($resultSet, $options); - // 回调 - $this->_after_select($resultSet, $options); - if (isset($options['index'])) { - // 对数据集进行索引 - $index = explode(',', $options['index']); - foreach ($resultSet as $result) { - $_key = $result[$index[0]]; - if (isset($index[1]) && isset($result[$index[1]])) { - $cols[$_key] = $result[$index[1]]; - } else { - $cols[$_key] = $result; + // 更新的时候检测字段更改 + if (!empty($this->change)) { + foreach ($data as $key => $val) { + if (!in_array($key, $this->change) && !$this->isPk($key) && !isset($this->relation[$key])) { + unset($data[$key]); } } - $resultSet = $cols; } - } - if (isset($cache)) { - Cache::set($cache['key'], $resultSet, $cache['expire']); - } - return $resultSet; - } + // 自动更新 + foreach ($this->update as $name => $rule) { + $data[$name] = is_callable($rule) ? call_user_func_array($rule, [ & $data]) : $rule; + } - /** - * 把主键值转换为查询条件 支持复合主键 - * @access public - * @param mixed $options 表达式参数 - * @return void - * @throws \think\Exception - */ - protected function parsePkWhere(&$options) - { - $pk = $this->getPk(); - if (is_string($pk)) { - // 根据主键查询 - if (is_array($options)) { - // 判断是否索引数组 - if (0 === key($options)) { - $where[$pk] = ['in', $options]; - } else { - return; - } - } else { - $where[$pk] = strpos($options, ',') ? ['IN', $options] : $options; - } - $options = []; - $options['where'] = $where; - } elseif (is_array($pk) && is_array($options) && !empty($options)) { - // 根据复合主键查询 - $array = array_intersect_key($options, $pk); - if (count($pk) == count($array)) { - $options = array_diff_key($options, $array); - $options['where'] = array_combine($pk, $array); - } else { - throw new Exception('miss complex primary data'); - } - } - return; - } + $result = self::db()->update($data); - /** - * 数据列表读取后的处理 - * @access protected - * @param array $resultSet 当前数据 - * @return array - */ - protected function _read_data_list($resultSet, $options) - { - $resultSet = array_map([$this, '_read_data'], $resultSet); - return $resultSet; - } - // 查询成功后的回调方法 - protected function _after_select(&$resultSet, $options = []) - {} - - /** - * 获取一条记录的某个字段值 - * @access public - * @param string $field 字段名 - * @param null $sepa 字段数据间隔符号 NULL返回数组 - * @return array|mixed|null - */ - public function getField($field, $sepa = null) - { - $options['field'] = $field; - $options = $this->_parseOptions($options); - $field = trim($field); - // 判断查询缓存 - if (isset($options['cache'])) { - $cache = $options['cache']; - $cache['key'] = is_string($cache['key']) ? $cache['key'] : md5($sepa . serialize($options)); - $data = Cache::get($cache['key']); - if (false !== $data) { - return $data; - } - } - if (strpos($field, ',')) { - // 多字段 - if (!isset($options['limit'])) { - $options['limit'] = is_numeric($sepa) ? $sepa : ''; - } - $resultSet = $this->db->select($options); - if (!empty($resultSet)) { - if (is_string($resultSet)) { - return $resultSet; - } - $field = array_keys($resultSet[0]); - $cols = []; - $count = count($field); - foreach ($resultSet as $result) { - $name = $result[$field[0]]; - if (2 == $count) { - $cols[$name] = $result[$field[1]]; - } else { - $cols[$name] = is_string($sepa) ? implode($sepa, array_slice($result, 1)) : $result; + // 关联更新 + if (!empty($this->relation)) { + foreach ($this->relation as $key => $val) { + if (isset($data[$key])) { + $this->relationUpdate($key, $data[$key], $val); } } - if (isset($cache)) { - Cache::set($cache['key'], $cols, $cache['expire']); - } - return $cols; } + self::trigger('after_update', $data); + return $result; } else { - // 查找一条记录 - // 返回数据个数 - if (true !== $sepa) { - // 当sepa指定为true的时候 返回所有数据 - $options['limit'] = is_numeric($sepa) ? $sepa : 1; + + if (false === self::trigger('before_insert', $data)) { + return false; } - $result = $this->db->select($options); - if (!empty($result)) { - if (is_string($result)) { - return $result; - } - if (true !== $sepa && 1 == $options['limit']) { - $data = reset($result[0]); - if (isset($cache)) { - Cache::set($cache['key'], $data, $cache['expire']); + + // 自动写入 + foreach ($this->insert as $name => $rule) { + $data[$name] = is_callable($rule) ? call_user_func_array($rule, [ & $data]) : $rule; + } + + $result = self::db()->insert($data); + + // 获取自动增长主键 + $insertId = self::db()->getLastInsID(); + if (is_string($this->pk) && $insertId) { + $data[$this->pk] = $insertId; + $this->data[$this->pk] = $insertId; + } + + // 关联写入 + if (!empty($this->relation)) { + foreach ($this->relation as $key => $val) { + if (isset($data[$key])) { + $this->relationInsert($key, $data[$key], $val); } - return $data; } - $array = []; - foreach ($result as $val) { - $array[] = $val[$field]; - } - if (isset($cache)) { - Cache::set($cache['key'], $array, $cache['expire']); - } - return $array; } + self::trigger('after_insert', $data); + return $insertId ?: $result; } - return null; } /** - * 设置记录的某个字段值 - * 支持使用数据库字段和方法 + * 删除当前的记录 * @access public - * @param string|array $field 字段名 - * @param string $value 字段值 - * @return boolean + * @return integer */ - public function setField($field, $value = '') + public function delete() + { + $data = $this->data; + + if (false === self::trigger('before_delete', $data)) { + return false; + } + + $result = self::db()->delete($data); + + // 关联删除 + if ($result) { + if (!empty($this->relation)) { + foreach ($this->relation as $key => $val) { + $this->relationDelete($key, $data, $val); + } + } + } + self::trigger('after_delete', $data); + return $result; + } + + /** + * 设置字段验证 + * @access public + * @param array|bool $rule 验证规则 true表示自动读取验证器类 + * @param array $msg 提示信息 + * @return Model + */ + public function validate($rule = true, $msg = null) + { + if (true === $rule) { + $this->validate = $this->name; + } elseif (is_array($rule)) { + $this->validate = [ + 'rule' => $rule, + 'msg' => is_array($msg) ? $msg : [], + ]; + } + return $this; + } + + /** + * 设置字段自动完成(包括新增和更新) + * @access public + * @param string $field 字段名或者数组规则 + * @param array|null $rule 完成规则 + * @return Model + */ + public function auto($field, $rule = null) { if (is_array($field)) { - $data = $field; + $this->auto = array_merge($this->auto, $field); } else { - $data[$field] = $value; + $this->auto[$field] = $rule; } - // 更新某个字段的时候 忽略数据副本 - $this->duplicate = []; - return $this->save($data); + return $this; } /** - * 字段值(延迟)增长 + * 设置写入自动完成 * @access public - * @param string $field 字段名 - * @param integer $step 增长值 - * @param integer $lazyTime 延时时间(s) - * @return boolean - * @throws \think\Exception + * @param string $field 字段名或者数组规则 + * @param array|null $rule 完成规则 + * @return Model */ - public function setInc($field, $step = 1, $lazyTime = 0) + public function autoInsert($field, $rule = null) { - $condition = !empty($this->options['where']) ? $this->options['where'] : []; - if (empty($condition)) { - // 没有条件不做任何更新 - throw new Exception('no data to update'); - } - if ($lazyTime > 0) { - // 延迟写入 - $guid = md5($this->name . '_' . $field . '_' . serialize($condition)); - $step = $this->lazyWrite($guid, $step, $lazyTime); - if (empty($step)) { - return true; // 等待下次写入 - } - } - return $this->setField($field, ['exp', $field . '+' . $step]); - } - - /** - * 字段值(延迟)减少 - * @access public - * @param string $field 字段名 - * @param integer $step 减少值 - * @param integer $lazyTime 延时时间(s) - * @return boolean - * @throws \think\Exception - */ - public function setDec($field, $step = 1, $lazyTime = 0) - { - $condition = !empty($this->options['where']) ? $this->options['where'] : []; - if (empty($condition)) { - // 没有条件不做任何更新 - throw new Exception('no data to update'); - } - if ($lazyTime > 0) { - // 延迟写入 - $guid = md5($this->name . '_' . $field . '_' . serialize($condition)); - $step = $this->lazyWrite($guid, -$step, $lazyTime); - if (empty($step)) { - return true; // 等待下次写入 - } - } - return $this->setField($field, ['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 = Cache::get($guid))) { - // 存在缓存写入数据 - if (NOW_TIME > Cache::get($guid . '_time') + $lazyTime) { - // 延时更新时间到了,删除缓存数据 并实际写入数据库 - Cache::rm($guid); - Cache::rm($guid . '_time'); - return $value + $step; - } else { - // 追加数据到缓存 - Cache::set($guid, $value + $step, 0); - return false; - } + if (is_array($field)) { + $this->insert = array_merge($this->insert, $field); } else { - // 没有缓存数据 - Cache::set($guid, $step, 0); - // 计时开始 - Cache::set($guid . '_time', NOW_TIME, 0); - return false; + $this->insert[$field] = $rule; } + return $this; } /** - * 生成查询SQL 可用于子查询 + * 设置更新自动完成 * @access public - * @return string + * @param string $field 字段名或者数组规则 + * @param array|null $rule 完成规则 + * @return Model */ - public function buildSql() + public function autoUpdate($field, $rule = null) { - return '( ' . $this->fetchSql(true)->select() . ' )'; - } - - /** - * 分析表达式(可用于查询或者写入操作) - * @access protected - * @param array $options 表达式参数 - * @return array - */ - protected function _parseOptions($options = []) - { - if (is_array($options)) { - $options = array_merge($this->options, $options); - } - - // 记录操作的模型名称 - $options['model'] = $this->name; - - if (isset($options['table'])) { - // 动态指定表名 - $fields = $this->getTableInfo('fields', $options['table']); + if (is_array($field)) { + $this->update = array_merge($this->update, $field); } else { - $options['table'] = $this->getTableName(); - $fields = $this->getFields(); + $this->update[$field] = $rule; } - if (!empty($options['alias'])) { - $options['table'] .= ' ' . $options['alias']; - } - // 字段类型验证 - 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) && is_scalar($val) && empty($options['bind'][$key])) { - $this->_parseType($options['where'], $key, $options['bind'], $options['table']); - } - } - } - // 查询过后清空sql表达式组装 避免影响下次查询 - $this->options = []; - // 表达式过滤 - $this->_options_filter($options); - return $options; + return $this; } - // 表达式过滤回调方法 - protected function _options_filter(&$options) - {} - - /** - * 数据类型检测和自动转换 - * @access protected - * @param array $data 数据 - * @param string $key 字段名 - * @param array $bind 参数绑定列表 - * @param string $tableName 表名 - * @return void - */ - protected function _parseType(&$data, $key, &$bind, $tableName = '') - { - $binds = $this->getTableInfo('bind', $tableName); - $type = $this->getTableInfo('type', $tableName); - // 强制类型转换 - if (false !== strpos($type[$key], 'int')) { - $data[$key] = (int) $data[$key]; - } elseif (false !== strpos($type[$key], 'float') || false !== strpos($type[$key], 'double')) { - $data[$key] = (float) $data[$key]; - } elseif (false !== strpos($type[$key], 'bool')) { - $data[$key] = (bool) $data[$key]; - } - if (':' == substr($data[$key], 0, 1) && isset($bind[substr($data[$key], 1)])) { - // 已经绑定 无需再次绑定 请确保bind方法优先执行 - return; - } - $bind[$key] = [$data[$key], isset($binds[$key]) ? $binds[$key] : \PDO::PARAM_STR]; - $data[$key] = ':' . $key; - } - - /** - * 查询数据 - * @access public - * @param mixed $options 表达式参数 - * @return mixed - * @throws \think\Exception - */ - public function find($options = []) - { - if (!empty($options) && empty($options['where'])) { - // AR模式主键条件分析 - $this->parsePkWhere($options); - } - // 总是查找一条记录 - $options['limit'] = 1; - // 分析表达式 - $options = $this->_parseOptions($options); - // 判断查询缓存 - if (isset($options['cache'])) { - $cache = $options['cache']; - if (!isset($cache['key']) || !is_string($cache['key'])) { - $cache['key'] = md5(serialize($options)); - } - $cache['expire'] = isset($cache['expire']) ? $cache['expire'] : null; - $data = Cache::get($cache['key']); - if (false !== $data) { - $this->data = $data; - return $data; - } - } - $resultSet = $this->db->select($options); - - if (empty($resultSet)) { - // 查询结果为空 - return null; - } - if (is_string($resultSet)) { - return $resultSet; - } - // 数据处理 - $data = $this->_read_data($resultSet[0], $options); - // 回调 - $this->_after_find($data, $options); - if (isset($cache)) { - Cache::set($cache['key'], $data, $cache['expire']); - } - // 数据对象赋值 - $this->data = $data; - // 数据副本 - $this->duplicate = $data; - return $this->data; - } - - /** - * 数据读取后的处理 - * @access protected - * @param array $data 当前数据 - * @return array - */ - protected function _read_data($data, $options = []) - { - // 检查字段映射 - if (!empty($this->map)) { - foreach ($this->map as $key => $val) { - if (isset($data[$val])) { - $data[$key] = $data[$val]; - unset($data[$val]); - } - } - } - return $data; - } - // 数据读取成功后的回调方法 - protected function _after_find(&$result, $options = []) - {} - - /** - * 创建数据对象 但不保存到数据库 - * @access public - * @param mixed $data 创建数据 - * @return mixed - * @throws \think\Exception - */ - public function create($data = '') - { - // 如果没有传值默认取POST数据 - if (empty($data)) { - $data = \think\Input::post(); - } elseif (is_object($data)) { - $data = get_object_vars($data); - } - // 验证数据 - if (empty($data) || !is_array($data)) { - throw new Exception('invalid data type'); - } - - // 检测提交字段的合法性 - if (isset($this->options['field'])) { - // $this->field('field1,field2...')->create() - $fields = $this->options['field']; - unset($this->options['field']); - if (is_string($fields)) { - $fields = explode(',', $fields); - } - foreach ($data as $key => $val) { - if (!in_array($key, $fields)) { - unset($data[$key]); - } - } - } - - // 数据自动验证 - if (!$this->dataValidate($data)) { - return false; - } - - // 数据自动填充 - $this->dataFill($data); - - // 过滤创建的数据 - $this->_create_filter($data); - // 赋值当前数据对象 - $this->data = $data; - // 返回创建的数据以供其他调用 - return $data; - } - // 数据对象创建后的回调方法 - protected function _create_filter(&$data) - {} /** * 数据自动验证 @@ -987,8 +396,8 @@ class Model */ protected function dataValidate(&$data) { - if (!empty($this->options['validate'])) { - $info = $this->options['validate']; + if (!empty($this->validate)) { + $info = $this->validate; if (is_array($info)) { $validate = Loader::validate(Config::get('default_validate')); $validate->rule($info['rule']); @@ -1007,225 +416,33 @@ class Model $this->error = $validate->getError(); return false; } - $this->options['validate'] = null; + $this->validate = null; } return true; } /** - * 数据自动填充 - * @access protected - * @param array $data 数据 - * @return void + * 检查数据中是否存在主键值 + * @access public + * @param mixed $data 数据 + * @return bool */ - protected function dataFill(&$data) + public function checkPkExists($data = []) { - if (!empty($this->options['auto'])) { - // 获取自动完成规则 - list($rules, $options, $scene) = $this->getDataRule($this->options['auto']); - - foreach ($rules as $key => $val) { - if (is_numeric($key) && is_array($val)) { - $key = array_shift($val); + $data = $data ?: $this->data; + $pk = $this->pk; + // 如果存在主键数据 则自动作为更新条件 + if (is_string($pk) && isset($data[$pk])) { + return true; + } elseif (is_array($pk)) { + // 增加复合主键支持 + foreach ($pk as $field) { + if (isset($data[$field])) { + return true; } - if (!empty($scene) && !in_array($key, $scene)) { - continue; - } - // 数据自动填充 - $this->fillItem($key, $val, $data, $options); - } - $this->options['auto'] = null; - } - } - - /** - * 获取数据自动完成的规则定义 - * @access protected - * @param mixed $rules 数据规则 - * @return array - */ - protected function getDataRule($rules) - { - if (is_string($rules)) { - // 读取配置定义 - $config = Config::get('auto'); - if (strpos($rules, '.')) { - list($name, $group) = explode('.', $rules); - } else { - $name = $rules; - } - $rules = isset($config[$name]) ? $config[$name] : []; - if (isset($config['__all__'])) { - $rules = array_merge($config['__all__'], $rules); } } - if (isset($rules['__option__'])) { - // 参数设置 - $options = $rules['__option__']; - unset($rules['__option__']); - } else { - $options = []; - } - if (isset($group) && isset($options['scene'][$group])) { - // 如果设置了适用场景 - $scene = $options['scene'][$group]; - if (is_string($scene)) { - $scene = explode(',', $scene); - } - } else { - $scene = []; - } - return [$rules, $options, $scene]; - } - - /** - * 数据自动填充 - * @access protected - * @param string $key 字段名 - * @param mixed $val 填充规则 - * @param array $data 数据 - * @param array $options 参数 - * @return void - */ - protected function fillItem($key, $val, &$data, $options = []) - { - // 获取数据 支持二维数组 - if (strpos($key, '.')) { - list($name1, $name2) = explode('.', $key); - $value = isset($data[$name1][$name2]) ? $data[$name1][$name2] : null; - } else { - $value = isset($data[$key]) ? $data[$key] : null; - } - if ((isset($options['value_fill']) && in_array($key, is_string($options['value_fill']) ? explode(',', $options['value_fill']) : $options['value_fill']) && '' == $value) - || (isset($options['exists_fill']) && in_array($key, is_string($options['exists_fill']) ? explode(',', $options['exists_fill']) : $options['exists_fill']) && is_null($value))) { - // 不满足自动填充条件 - return; - } - if ($val instanceof \Closure) { - $result = call_user_func_array($val, [$value, &$data]); - } elseif (isset($val[0]) && $val[0] instanceof \Closure) { - $result = call_user_func_array($val[0], [$value, &$data]); - } elseif (!is_array($val)) { - $result = $val; - } else { - $rule = isset($val[0]) ? $val[0] : $val; - $type = isset($val[1]) ? $val[1] : 'value'; - $params = isset($val[2]) ? (array) $val[2] : []; - switch ($type) { - case 'behavior': - Hook::exec($rule, '', $data); - return; - case 'callback': - array_unshift($params, $value); - $result = call_user_func_array($rule, $params); - break; - case 'serialize': - if (is_string($rule)) { - $rule = explode(',', $rule); - } - $serialize = []; - foreach ($rule as $name) { - if (strpos($name, '.')) { - list($name1, $name2) = explode('.', $name); - if (isset($data[$name1][$name2])) { - $serialize[$name] = $data[$name1][$name2]; - unset($data[$name1][$name2]); - } - } elseif (isset($data[$name])) { - $serialize[$name] = $data[$name]; - unset($data[$name]); - } - } - $fun = !empty($params['type']) ? $params['type'] : 'serialize'; - $result = $fun($serialize); - break; - case 'ignore': - if ($rule === $value) { - if (strpos($key, '.')) { - unset($data[$name1][$name2]); - } else { - unset($data[$key]); - } - } - return; - case 'value': - default: - $result = $rule; - break; - } - } - if (strpos($key, '.')) { - $data[$name1][$name2] = $result; - } else { - $data[$key] = $result; - } - } - - /** - * 切换当前的数据库连接 - * @access public - * @param mixed $linkId 连接标识 - * @param mixed $config 数据库连接信息 - * @return Model - */ - public function db($linkId = '', $config = '') - { - if ('' === $linkId && $this->db) { - return $this->db; - } - - if (!isset($this->links[$linkId])) { - // 创建一个新的实例 - if (is_string($linkId) && '' == $config) { - $config = Config::get($linkId); - } - $this->links[$linkId] = Db::connect($config); - } elseif (null === $config) { - $this->links[$linkId]->close(); // 关闭数据库连接 - unset($this->links[$linkId]); - return; - } - - // 切换数据库连接 - $this->db = $this->links[$linkId]; - $this->_after_db(); - return $this; - } - // 数据库切换后回调方法 - protected function _after_db() - {} - - /** - * 得到当前的数据对象名称 - * @access public - * @return string - */ - public function getModelName() - { - if (empty($this->name)) { - // 解决非windows环境下获取不到basename的bug(xiaobo.sun modify 20160215) - $this->name = basename(str_replace('\\', '/', get_class($this))); - } - 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 .= Loader::parseName($this->name); - } - $this->trueTableName = strtolower($tableName); - } - return (!empty($this->dbName) ? $this->dbName . '.' : '') . $this->trueTableName; + return false; } /** @@ -1239,741 +456,292 @@ class Model } /** - * 返回数据库的错误信息 + * 注册回调方法 * @access public - * @return string + * @param string $event 事件名 + * @param callable $callback 回调方法 */ - public function getDbError() + public static function event($event, $callback) { - return $this->db->getError(); + self::$event[$event] = $callback; } /** - * 返回最后插入的ID + * 触发事件 * @access public - * @return string + * @param string $event 事件名 + * @param mixed $params 传入参数(引用) */ - public function getLastInsID() + protected static function trigger($event, &$params) { - return $this->db->getLastInsID(); + if (isset(self::$event[$event]) && is_callable(self::$event[$event])) { + $result = call_user_func_array(self::$event[$event], [ & $params]); + return false === $result ? false : true; + } + return true; } /** - * 返回最后执行的sql语句 + * 创建数据并写入 * @access public - * @return string + * @param mixed $data 数据 支持数组或者对象 + * @return void */ - public function getLastSql() + public static function create($data = []) { - return $this->db->getLastSql($this->name); + $model = new static(); + return $model->data($data)->save(); } /** - * 获取当前主键名称 + * 查找单条记录 * @access public - * @param string $tableName 数据表名 留空自动获取 + * @param array $data 主键值 + * @param bool $cache 是否缓存 * @return mixed */ - public function getPk($tableName = '') + public static function get($data = '', $cache = false) { - if (is_null($this->pk)) { - $this->pk = $this->getTableInfo('pk', $tableName); + if ($cache) { + // 查找是否存在缓存 + $name = basename(str_replace('\\', '/', get_called_class())); + $guid = 'model_' . $name . '_' . $data; + $result = Cache::get($guid); + if ($result) { + return new static($result); + } } - return $this->pk; + + $result = self::db()->find($data); + + if ($cache) { + // 缓存模型数据 + Cache::set($guid, $result->toArray()); + } + return $result; } /** - * 获取当前字段信息 + * 查找多条记录 * @access public - * @param string $tableName 数据表名 留空自动获取 - * @return array + * @param mixed $data 主键列表 + * @return array|string */ - public function getFields($tableName = '') + public static function all($data = []) { - if (empty($this->fields)) { - $this->fields = $this->getTableInfo('fields', $tableName); - } - return $this->fields; + return self::db()->select($data); } /** - * 获取数据表信息 + * 删除记录 * @access public - * @param string $fetch 获取信息类型 包括 fields type bind pk - * @param string $tableName 数据表名 留空自动获取 - * @return mixed + * @param mixed $data 主键列表 + * @param bool $findFirst 是否查询 + * @return integer */ - public function getTableInfo($fetch = '', $tableName = '') + public static function destroy($data, $findFirst = false) { - if (!$tableName) { - $tableName = isset($this->options['table']) ? $this->options['table'] : $this->getTableName(); + // 保留删除的数据备用 + if ($findFirst) { + $resultSet = self::all($data); } - if (is_array($tableName)) { - $tableName = key($tableName) ?: current($tableName); - } - if (strpos($tableName, ',')) { - // 多表不获取字段信息 + + if (false === self::trigger('before_delete', $findFirst ? $resultSet : $data)) { return false; } - $guid = md5($tableName); - $result = Cache::get($guid); - if (!$result) { - $info = $this->db->getFields($tableName); - // 字段大小写转换 - switch ($this->attrCase) { - case \PDO::CASE_LOWER: - $info = array_change_key_case($info); - break; - case \PDO::CASE_UPPER: - $info = array_change_key_case($info, CASE_UPPER); - break; - case \PDO::CASE_NATURAL: - default: - // 不做转换 - } - $fields = array_keys($info); - $bind = $type = []; - foreach ($info as $key => $val) { - // 记录字段类型 - $type[$key] = $val['type']; - if (preg_match('/(int|double|float|decimal|real|numeric|serial)/is', $val['type'])) { - $bind[$key] = \PDO::PARAM_INT; - } elseif (preg_match('/bool/is', $val['type'])) { - $bind[$key] = \PDO::PARAM_BOOL; + $result = self::db()->delete($data); + + // 关联删除 + + // 删除回调 + self::trigger('after_delete', $findFirst ? $resultSet : $data); + return $result; + } + + // 解析模型的完整命名空间 + protected function parseModel($model) + { + if (false === strpos($model, '\\')) { + $path = explode('\\', get_called_class()); + array_pop($path); + array_push($path, $model); + $model = implode('\\', $path); + } + return $model; + } + + // 指定关联 + public function relation($name, $relation = '') + { + if (is_array($name)) { + $this->relation = array_merge($this->relation, $name); + } else { + $this->relation[$name] = $relation; + } + return $this; + } + + // HAS ONE + public function hasOne($model, $foreignKey = '', $localKey = '') + { + $model = $this->parseModel($model); + $localKey = $localKey ?: $this->pk; + $foreignKey = $foreignKey ?: $this->name . '_id'; + return $model::where($foreignKey, $this->data[$localKey])->find(); + } + + // BELONGS TO + public function belongsTo($model, $localKey = '', $foreignKey = '') + { + $model = $this->parseModel($model); + $foreignKey = $foreignKey ?: $this->pk; + $localKey = $localKey ?: basename(str_replace('\\', '/', $model)) . '_id'; + return $model::where($foreignKey, $this->data[$localKey])->find(); + } + + // HAS MANY + public function hasMany($model, $foreignKey = '', $localKey = '') + { + $model = $this->parseModel($model); + $localKey = $localKey ?: $this->pk; + $foreignKey = $foreignKey ?: $this->name . '_id'; + return $model::where($foreignKey, $this->data[$localKey])->select(); + } + + // BELONGS TO MANY + public function belongsToMany($model, $localKey = '', $foreignKey = '') + { + $model = $this->parseModel($model); + $foreignKey = $foreignKey ?: $this->pk; + $localKey = $localKey ?: basename(str_replace('\\', '/', $model)) . '_id'; + return $model::where($foreignKey, $this->data[$localKey])->select(); + } + + // 关联写入 + public function relationInsert($className, $data, $relation = []) + { + if (empty($relation) && isset($this->relation[$className])) { + $relation = $this->relation[$className]; + } + $type = isset($relation['relation_type']) ? $relation['relation_type'] : $relation; + $foreignKey = isset($relation['foreign_key']) ? $relation['foreign_key'] : strtolower($this->name) . '_id'; + $className = isset($relation['class_name']) ? $relation['class_name'] : $className; + + $model = $this->parseModel(ucfirst($className)); + switch ($type) { + case 'has_one': + $data[$foreignKey] = $this->data[$this->pk]; + $model::create($data); + break; + case 'belongs_to': + break; + case 'has_many': + foreach ($data as $key => &$val) { + $val[$foreignKey] = $this->data[$this->pk]; + } + $model::insertAll($data); + break; + } + return $this; + } + + // 关联更新 + public function relationUpdate($className, $data, $relation = []) + { + if (empty($relation) && isset($this->relation[$className])) { + $relation = $this->relation[$className]; + } + $type = isset($relation['relation_type']) ? $relation['relation_type'] : $relation; + $foreignKey = isset($relation['foreign_key']) ? $relation['foreign_key'] : strtolower($this->name) . '_id'; + $className = isset($relation['class_name']) ? $relation['class_name'] : $className; + + $model = $this->parseModel(ucfirst($className)); + switch ($type) { + case 'has_one': + $class = new $model; + if ($class->checkPkExists($data)) { + $class::update($data); } else { - $bind[$key] = \PDO::PARAM_STR; + $class::where($foreignKey, $this->data[$this->pk])->update($data); } - if (!empty($val['primary'])) { - $pk[] = $key; + break; + case 'belongs_to': + break; + case 'has_many': + $class = new $model; + foreach ($data as $key => $val) { + $class::update($val); } - } - if (isset($pk)) { - // 设置主键 - $pk = count($pk) > 1 ? $pk : $pk[0]; - } else { - $pk = null; - } - // 记录字段类型信息 - $result = ['fields' => $fields, 'bind' => $bind, 'type' => $type, 'pk' => $pk]; - !APP_DEBUG && Cache::set($guid, $result, 0); + break; } - return $fetch ? $result[$fetch] : $result; - } - - /** - * SQL查询 - * @access public - * @param string $sql SQL指令 - * @param array $bind 参数绑定 - * @return mixed - */ - public function query($sql, $bind = []) - { - $sql = $this->parseSql($sql); - return $this->db->query($sql, $bind); - } - - /** - * 执行SQL语句 - * @access public - * @param string $sql SQL指令 - * @param array $bind 参数绑定 - * @return false | integer - */ - public function execute($sql, $bind = []) - { - $sql = $this->parseSql($sql); - return $this->db->execute($sql, $bind); - } - - /** - * 解析SQL语句 - * @access public - * @param string $sql SQL指令 - * @return string - */ - protected function parseSql($sql) - { - // 分析表达式 - $sql = strtr($sql, ['__TABLE__' => $this->getTableName(), '__PREFIX__' => $this->tablePrefix]); - $sql = $this->parseSqlTable($sql); - $this->db->setModel($this->name); - return $sql; - } - - /** - * 设置数据对象值 - * @access public - * @param mixed $data 数据 - * @return Model - * @throws \think\Exception - */ - 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)) { - throw new Exception('data type invalid', 10300); - } - $this->data = $data; return $this; } - /** - * 查询SQL组装 join - * @access public - * @param mixed $join - * @param string $type JOIN类型 - * @return Model - */ - public function _join($join, $type = 'INNER') + // 关联删除 + public function relationDelete($className, $data = '', $relation = []) { - if (is_array($join)) { - foreach ($join as $key => &$_join) { - $_join = $this->parseSqlTable($_join); - $_join = false !== stripos($_join, 'JOIN') ? $_join : $type . ' JOIN ' . $_join; - } - $this->options['join'] = $join; - } elseif (!empty($join)) { - $join = $this->parseSqlTable($join); - $this->options['join'][] = false !== stripos($join, 'JOIN') ? $join : $type . ' JOIN ' . $join; + if (empty($relation) && isset($this->relation[$className])) { + $relation = $this->relation[$className]; + } + $type = isset($relation['relation_type']) ? $relation['relation_type'] : $relation; + $foreignKey = isset($relation['foreign_key']) ? $relation['foreign_key'] : strtolower($this->name) . '_id'; + $className = isset($relation['class_name']) ? $relation['class_name'] : $className; + + $model = $this->parseModel(ucfirst($className)); + $id = $data ? $data[$this->pk] : $this->data[$this->pk]; + switch ($type) { + case 'has_one': + $model::where($foreignKey, $id)->delete(); + break; + case 'belongs_to': + break; + case 'has_many': + $model::where($foreignKey, $id)->delete(); + break; } return $this; } /** - * 查询SQL组装 join + * 实例化模型 * @access public - * @param mixed $join 关联的表名 - * @param mixed $condition 条件 - * @param string $type JOIN类型 - * @return Model + * @param array $config 配置参数 + * @return object */ - public function join($join, $condition = null, $type = 'INNER') + public static function instance() { - if (empty($join)) { - return $this; + if (is_null(self::$instance)) { + self::$instance = new static(); } - - if (empty($condition)) { - if (is_array($join) && is_array(current($join))) { - // 如果为组数,则循环调用join - foreach ($join as $key => $value) { - if (is_array($value) && 2 <= count($value)) { - $this->join($value[0], $value[1], isset($value[2]) ? $value[2] : $type); - } - } - } else { - $this->_join($join, $condition); // 兼容原来的join写法 - } - } elseif (in_array(strtoupper($condition), ['INNER', 'LEFT', 'RIGHT', 'ALL'])) { - $this->_join($join, $condition); // 兼容原来的join写法 - } else { - $prefix = $this->tablePrefix; - // 传入的表名为数组 - if (is_array($join)) { - if (0 !== $key = key($join)) { - // 设置了键名则键名为表名,键值作为表的别名 - $table = $key . ' ' . array_shift($join); - } else { - $table = array_shift($join); - } - if (count($join)) { - // 有设置第二个元素则把第二元素作为表前缀 - $table = (string) current($join) . $table; - } else { - // 加上默认的表前缀 - $table = $prefix . $table; - } - } else { - $join = trim($join); - if (0 === strpos($join, '__')) { - $table = $this->parseSqlTable($join); - } elseif (false === strpos($join, '(') && !empty($prefix) && 0 !== strpos($join, $prefix)) { - // 传入的表名中不带有'('并且不以默认的表前缀开头时加上默认的表前缀 - $table = $prefix . $join; - } else { - $table = $join; - } - } - if (is_array($condition)) { - $condition = implode(' AND ', $condition); - } - $this->options['join'][] = strtoupper($type) . ' JOIN ' . $table . ' ON ' . $condition; - } - return $this; + return self::$instance; } /** - * 查询SQL组装 union - * @access public - * @param mixed $union - * @param boolean $all - * @return Model - * @throws \think\Exception + * 初始化数据库对象 + * @access private + * @return object */ - public function union($union, $all = false) + private static function db() { - if (empty($union)) { - return $this; - } - - if ($all) { - $this->options['union']['_all'] = true; - } - if (is_object($union)) { - $union = get_object_vars($union); - } elseif (is_string($union)) { - // 转换union表达式 - $union = (array) $union; - } - if (is_array($union)) { - if (isset($union[0])) { - foreach ($union as &$val) { - $val = $this->parseSqlTable($val); - } - $this->options['union'] = isset($this->options['union']) ? array_merge($this->options['union'], $union) : $union; - } else { - $this->options['union'][] = $union; - } - } else { - throw new Exception('data type invalid', 10300); - } - return $this; - } - - /** - * 查询缓存 - * @access public - * @param mixed $key - * @param integer $expire - * @param string $type - * @return Model - */ - public function cache($key = true, $expire = null, $type = '') - { - // 增加快捷调用方式 cache(10) 等同于 cache(true, 10) - if (is_numeric($key) && is_null($expire)) { - $expire = $key; - $key = true; - } - 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->getFields(); - $field = $fields ?: '*'; - } elseif ($except) { - // 字段排除 - if (is_string($field)) { - $field = explode(',', $field); - } - $fields = $this->getFields(); - $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); - } - } else { - // 直接传入命名范围定义 - $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 条件表达式 - * @return Model - */ - public function where($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) - { - if (is_null($length) && strpos($offset, ',')) { - list($offset, $length) = explode(',', $offset); - } - $this->options['limit'] = intval($offset) . ($length ? ',' . intval($length) : ''); - return $this; - } - - /** - * 指定分页 - * @access public - * @param mixed $page 页数 - * @param mixed $listRows 每页数量 - * @return Model - */ - public function page($page, $listRows = null) - { - if (is_null($listRows) && strpos($page, ',')) { - list($page, $listRows) = explode(',', $page); - } - $this->options['page'] = [intval($page), intval($listRows)]; - return $this; - } - - /** - * 指定数据表 - * @access public - * @param string $table 表名 - * @return Model - */ - public function table($table) - { - if (is_array($table)) { - $this->options['table'] = $table; - } elseif (!empty($table)) { - $this->options['table'] = $this->parseSqlTable($table); - } - return $this; - } - - /** - * USING支持 用于多表删除 - * @access public - * @param mixed $using - * @return Model - */ - public function using($using) - { - if (is_array($using)) { - $this->options['using'] = $using; - } elseif (!empty($using)) { - $this->options['using'] = $this->parseSqlTable($using); - } - return $this; - } - - /** - * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc']) - * @access public - * @param string|array $field 排序字段 - * @param string $order 排序 - * @return Model - */ - public function order($field, $order = null) - { - if (!empty($field)) { - if (is_string($field)) { - $field = empty($order) ? $field : [$field => $order]; - } - $this->options['order'] = $field; - } - return $this; - } - - /** - * 指定group查询 - * @access public - * @param string $group GROUP - * @return Model - */ - public function group($group) - { - $this->options['group'] = $group; - return $this; - } - - /** - * 指定having查询 - * @access public - * @param string $having having - * @return Model - */ - public function having($having) - { - $this->options['having'] = $having; - return $this; - } - - /** - * 指定查询lock - * @access public - * @param boolean $lock 是否lock - * @return Model - */ - public function lock($lock = false) - { - $this->options['lock'] = $lock; - return $this; - } - - /** - * 指定distinct查询 - * @access public - * @param string $distinct 是否唯一 - * @return Model - */ - public function distinct($distinct) - { - $this->options['distinct'] = $distinct; - return $this; - } - - /** - * 指定数据表别名 - * @access public - * @param string $alias 数据表别名 - * @return Model - */ - public function alias($alias) - { - $this->options['alias'] = $alias; - return $this; - } - - /** - * 指定写入过滤方法 - * @access public - * @param string $filter 指定过滤方法 - * @return Model - */ - public function filter($filter) - { - $this->options['filter'] = $filter; - return $this; - } - - /** - * 对数据集进行索引 - * @access public - * @param string $index 索引名称 - * @return Model - */ - public function index($index) - { - $this->options['index'] = $index; - return $this; - } - - /** - * 指定强制索引 - * @access public - * @param string $force 索引名称 - * @return Model - */ - public function force($force) - { - $this->options['force'] = $force; - return $this; - } - - /** - * 参数绑定 - * @access public - * @param mixed $key 参数名 - * @param mixed $value 绑定的变量及绑定参数 - * @return Model - */ - public function bind($key, $value = false) - { - if (is_array($key)) { - $this->options['bind'] = $key; - } else { - $num = func_num_args(); - if ($num > 2) { - $params = func_get_args(); - array_shift($params); - $this->options['bind'][$key] = $params; - } else { - $this->options['bind'][$key] = $value; + $model = get_called_class(); + $name = basename(str_replace('\\', '/', $model)); + if (!isset(self::$links[$model])) { + self::$links[$model] = Db::connect(static::$connection); + self::$links[$model]->name($name); + if (isset(static::$tableName)) { + self::$links[$model]->setTable(static::$tableName); } } - return $this; + // 设置当前模型 确保查询返回模型对象 + self::$links[$model]->model($model); + // 返回当前数据库对象 + return self::$links[$model]; } - /** - * 查询注释 - * @access public - * @param string $comment 注释 - * @return Model - */ - public function comment($comment) + public static function __callStatic($method, $params) { - $this->options['comment'] = $comment; - return $this; + return call_user_func_array([self::db(), $method], $params); } - /** - * 获取执行的SQL语句 - * @access public - * @param boolean $fetch 是否返回sql - * @return Model - */ - public function fetchSql($fetch = true) - { - $this->options['fetch_sql'] = $fetch; - return $this; - } - - /** - * 设置字段映射 - * @access public - * @param mixed $map 映射名称或者映射数据 - * @param string $name 映射的字段 - * @return Model - */ - public function map($map, $name = '') - { - if (is_array($map)) { - $this->map = array_merge($this->map, $map); - } else { - $this->map[$map] = $name; - } - return $this; - } - - /** - * 设置字段验证 - * @access public - * @param array|bool $rule 验证规则 true表示自动读取验证器类 - * @param array $msg 提示信息 - * @return Model - */ - public function validate($rule = true, $msg = null) - { - if (true === $rule) { - $this->options['validate'] = $this->name; - } elseif (is_array($rule)) { - $this->options['validate'] = [ - 'rule' => $rule, - 'msg' => is_array($msg) ? $msg : [], - ]; - } - return $this; - } - - /** - * 设置字段完成 - * @access public - * @param mixed $field 字段名或者自动完成规则 true 表示自动读取 - * @param array|null $rule 完成规则 - * @return Model - */ - public function auto($field = true, $rule = null) - { - if (is_array($field) || is_null($rule)) { - $this->options['auto'] = true === $field ? $this->name : $field; - } else { - $this->options['auto'][$field] = $rule; - } - return $this; - } - - /** - * 设置从主服务器读取数据 - * @access public - * @return Model - */ - public function master() - { - $this->options['master'] = true; - return $this; - } - - /** - * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写) - * @access protected - * @param string $sql sql语句 - * @return string - */ - protected function parseSqlTable($sql) - { - if (false !== strpos($sql, '__')) { - $prefix = $this->tablePrefix; - $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) use ($prefix) { - return $prefix . strtolower($match[1]); - }, $sql); - } - return $sql; - } - - /** - * 获取属性值 - * @access protected - * @param string $property 属性名 - * @return mixed - */ - public function getProperty($property) - { - if (property_exists($this, $property)) { - return $this->$property; - } - return null; - } } diff --git a/library/think/db/Driver.php b/library/think/db/Driver.php index cac385cb..6eda67f5 100644 --- a/library/think/db/Driver.php +++ b/library/think/db/Driver.php @@ -12,23 +12,26 @@ namespace think\db; use PDO; +use think\Cache; use think\Config; use think\Db; use think\Debug; +use think\Exception; use think\exception\DbBindParamException; -use think\exception\DbException; use think\exception\PDOException; +use think\Loader; use think\Log; abstract class Driver { // PDO操作实例 protected $PDOStatement = null; - // 当前操作所属的模型名 - protected $model = '_think_'; + // 当前操作的数据表名 + protected $table = ''; + // 当前操作的数据对象名 + protected $name = ''; // 当前SQL指令 protected $queryStr = ''; - protected $modelSql = []; // 最后插入ID protected $lastInsID = null; // 返回或者影响记录数 @@ -41,47 +44,52 @@ abstract class Driver protected $links = []; // 当前连接ID protected $linkID = null; + // 查询参数 + protected $options = []; + // 数据库连接参数配置 protected $config = [ // 数据库类型 - 'type' => '', + 'type' => '', // 服务器地址 - 'hostname' => '127.0.0.1', + 'hostname' => '', // 数据库名 - 'database' => '', + 'database' => '', // 用户名 - 'username' => '', + 'username' => '', // 密码 - 'password' => '', + 'password' => '', // 端口 - 'hostport' => '', - 'dsn' => '', + 'hostport' => '', + 'dsn' => '', // 数据库连接参数 - 'params' => [], + 'params' => [], // 数据库编码默认采用utf8 - 'charset' => 'utf8', + 'charset' => 'utf8', // 数据库表前缀 - 'prefix' => '', + 'prefix' => '', + // 数据库调试模式 + 'debug' => false, // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) - 'deploy' => 0, + 'deploy' => 0, // 数据库读写是否分离 主从式有效 - 'rw_separate' => false, + 'rw_separate' => false, // 读写分离后 主服务器数量 - 'master_num' => 1, + 'master_num' => 1, // 指定从服务器序号 - 'slave_no' => '', + 'slave_no' => '', // like字段自动替换为%%包裹 - 'db_like_fields' => '', - // 是否开启数据库调试 - 'debug' => false, + 'like_fields' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, ]; // 数据库表达式 - protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'like' => 'LIKE', 'in' => 'IN', 'notin' => 'NOT IN', 'not in' => 'NOT IN', 'between' => 'BETWEEN', 'not between' => 'NOT BETWEEN', 'notbetween' => 'NOT BETWEEN']; + protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'like' => 'LIKE', 'in' => 'IN', 'exp' => 'EXP', 'notin' => 'NOT IN', 'not in' => 'NOT IN', 'between' => 'BETWEEN', 'not between' => 'NOT BETWEEN', 'notbetween' => 'NOT BETWEEN', 'exists' => 'EXISTS', 'notexists' => 'NOT EXISTS']; // 查询表达式 protected $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%'; // PDO连接参数 - protected $options = [ + protected $params = [ PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, @@ -100,16 +108,50 @@ abstract class Driver if (!empty($config)) { $this->config = array_merge($this->config, $config); if (is_array($this->config['params'])) { - $this->options = $this->config['params'] + $this->options; + $this->params = $this->config['params'] + $this->params; } } } + /** + * 利用__call方法实现一些特殊的Model方法 + * @access public + * @param string $method 方法名称 + * @param array $args 调用参数 + * @return mixed + */ + public function __call($method, $args) + { + if (strtolower(substr($method, 0, 5)) == 'getby') { + // 根据某个字段获取记录 + $field = Loader::parseName(substr($method, 5)); + $where[$field] = $args[0]; + return $this->where($where)->find(); + } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') { + // 根据某个字段获取记录的某个值 + $name = Loader::parseName(substr($method, 10)); + $where[$name] = $args[0]; + return $this->where($where)->get($args[1]); + } elseif (isset($this->scope[$method])) { + // 命名范围的单独调用支持 + return $this->scope($method, $args[0]); + } else { + throw new Exception(__CLASS__ . ':' . $method . ' method not exist'); + } + } + + /** + * 指定当前数据表 + * @access public + */ + public function setTable($table) + { + $this->table = $table; + } + /** * 连接数据库方法 * @access public - * @return resource - * @throws \think\Exception */ public function connect($config = '', $linkNum = 0, $autoConnection = false) { @@ -122,7 +164,7 @@ abstract class Driver if (empty($config['dsn'])) { $config['dsn'] = $this->parseDsn($config); } - $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $this->options); + $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $this->params); // 记录数据库连接信息 APP_DEBUG && Log::record('[ DB ] CONNECT: ' . $config['dsn'], 'info'); } catch (\PDOException $e) { @@ -130,7 +172,7 @@ abstract class Driver Log::record($e->getMessage(), 'error'); return $this->connect($autoConnection, $linkNum); } else { - throw new PDOException($e, $this->config, $this->queryStr); + throw new Exception($e->getMessage()); } } } @@ -155,6 +197,19 @@ abstract class Driver $this->PDOStatement = null; } + /** + * 获取PDO对象 + * @access public + */ + public function getPdo() + { + if (!$this->linkID) { + return false; + } else { + return $this->linkID; + } + } + /** * 执行查询 返回数据集 * @access public @@ -162,10 +217,10 @@ abstract class Driver * @param array $bind 参数绑定 * @param boolean $fetch 不执行只是获取SQL * @param boolean $master 是否在主服务器读操作 - * @return array|bool|string - * @throws \think\Exception + * @param boolean $mode fetch Mode false表示不返回数据只返回 PDOStatement 对象 + * @return mixed */ - public function query($sql, $bind = [], $fetch = false, $master = false) + public function query($sql, $bind = [], $fetch = false, $master = false, $mode = true) { $this->initConnect($master); if (!$this->linkID) { @@ -195,7 +250,7 @@ abstract class Driver $result = $this->PDOStatement->execute(); // 调试结束 $this->debug(false); - return $this->getResult(); + return $mode ? $this->getResult($mode) : $this->PDOStatement; } catch (\PDOException $e) { throw new PDOException($e, $this->config, $this->queryStr); } @@ -208,7 +263,6 @@ abstract class Driver * @param array $bind 参数绑定 * @param boolean $fetch 不执行只是获取SQL * @return integer - * @throws \think\Exception */ public function execute($sql, $bind = [], $fetch = false) { @@ -216,7 +270,6 @@ abstract class Driver if (!$this->linkID) { return false; } - // 根据参数绑定组装最终的SQL语句 $this->queryStr = $this->getBindSql($sql, $bind); @@ -266,7 +319,7 @@ abstract class Driver // 判断占位符 $sql = is_numeric($key) ? substr_replace($sql, $val, strpos($sql, '?'), 1) : - str_replace(':' . $key . ' ', $val . ' ', $sql . ' '); + str_replace([':' . $key . ')', ':' . $key . ' '], [$val . ')', $val . ' '], $sql . ' '); } } return $sql; @@ -302,10 +355,30 @@ abstract class Driver } } + /** + * 获得数据集 + * @access private + * @return array + */ + private function getResult($mode) + { + if (true === $mode) { + $mode = PDO::FETCH_ASSOC; + } + // 根据fetchMode返回数据集 + if (is_array($mode)) { + $result = $this->PDOStatement->fetchAll($mode[0], $mode[1]); + } else { + $result = $this->PDOStatement->fetchAll($mode); + } + $this->numRows = count($result); + return $result; + } + /** * 启动事务 * @access public - * @return void|false + * @return void */ public function startTrans() { @@ -325,8 +398,7 @@ abstract class Driver /** * 用于非自动提交状态下面的查询提交 * @access public - * @return boolean - * @throws \think\Exception + * @return boolen */ public function commit() { @@ -344,8 +416,7 @@ abstract class Driver /** * 事务回滚 * @access public - * @return boolean - * @throws \think\Exception + * @return boolen */ public function rollback() { @@ -361,16 +432,853 @@ abstract class Driver } /** - * 获得所有的查询数据 - * @access private + * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写) + * @access protected + * @param string $sql sql语句 + * @return string + */ + protected function parseSqlTable($sql) + { + if (false !== strpos($sql, '__')) { + $prefix = $this->tablePrefix; + $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) use ($prefix) { + return $prefix . strtolower($match[1]); + }, $sql); + } + return $sql; + } + + /** + * 查询SQL组装 join + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param string $type JOIN类型 + * @return Model + */ + public function join($join, $condition = null, $type = 'INNER') + { + if (empty($condition)) { + if (is_array($join) && is_array($join[0])) { + // 如果为组数,则循环调用join + foreach ($join as $key => $value) { + if (is_array($value) && 2 <= count($value)) { + $this->join($value[0], $value[1], isset($value[2]) ? $value[2] : $type); + } + } + } + } else { + $prefix = $this->config['prefix']; + // 传入的表名为数组 + if (is_array($join)) { + if (0 !== $key = key($join)) { + // 设置了键名则键名为表名,键值作为表的别名 + $table = $key . ' ' . array_shift($join); + } else { + $table = array_shift($join); + } + if (count($join)) { + // 有设置第二个元素则把第二元素作为表前缀 + $table = (string) current($join) . $table; + } else { + // 加上默认的表前缀 + $table = $prefix . $table; + } + } else { + $join = trim($join); + if (0 === strpos($join, '__')) { + $table = $this->parseSqlTable($join); + } elseif (false === strpos($join, '(') && !empty($prefix) && 0 !== strpos($join, $prefix)) { + // 传入的表名中不带有'('并且不以默认的表前缀开头时加上默认的表前缀 + $table = $prefix . $join; + } else { + $table = $join; + } + } + if (is_array($condition)) { + $condition = implode(' AND ', $condition); + } + $this->options['join'][] = strtoupper($type) . ' JOIN ' . $table . ' ON ' . $condition; + } + return $this; + } + + /** + * 查询SQL组装 union + * @access public + * @param mixed $union + * @param boolean $all + * @return Model + */ + public function union($union, $all = false) + { + $this->options['union']['type'] = $all?'UNION ALL':'UNION'; + + if(is_array($union)){ + $this->options['union'] = array_merge($this->options['union'],$union); + }else{ + $this->options['union'][] = $union; + } + return $this; + } + + /** + * 获取数据表信息 + * @access public + * @param string $fetch 获取信息类型 包括 fields type bind pk + * @param string $tableName 数据表名 留空自动获取 + * @return mixed + */ + public function getTableInfo($tableName = '', $fetch = '') + { + static $_info = []; + if (!$tableName) { + $tableName = isset($this->options['table']) ? $this->options['table'] : $this->getTableName(); + } + if (is_array($tableName)) { + $tableName = key($tableName) ?: current($tableName); + } + if (strpos($tableName, ',')) { + // 多表不获取字段信息 + return false; + } + $guid = md5($tableName); + if (!isset($_info[$guid])) { + $info = $this->getFields($tableName); + // 字段大小写转换 + switch ($this->params[PDO::ATTR_CASE]) { + case \PDO::CASE_LOWER: + $info = array_change_key_case($info); + break; + case \PDO::CASE_UPPER: + $info = array_change_key_case($info, CASE_UPPER); + break; + case \PDO::CASE_NATURAL: + default: + // 不做转换 + } + + $fields = array_keys($info); + $bind = $type = []; + foreach ($info as $key => $val) { + // 记录字段类型 + $type[$key] = $val['type']; + if (preg_match('/(int|double|float|decimal|real|numeric|serial)/is', $val['type'])) { + $bind[$key] = \PDO::PARAM_INT; + } elseif (preg_match('/bool/is', $val['type'])) { + $bind[$key] = \PDO::PARAM_BOOL; + } else { + $bind[$key] = \PDO::PARAM_STR; + } + if (!empty($val['primary'])) { + $pk[] = $key; + } + } + if (isset($pk)) { + // 设置主键 + $pk = count($pk) > 1 ? $pk : $pk[0]; + } else { + $pk = null; + } + $result = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk]; + $_info[$guid] = $result; + } + return $fetch ? $_info[$guid][$fetch] : $_info[$guid]; + } + + /** + * 指定查询字段 支持字段排除 + * @access public + * @param mixed $field + * @param boolean $except 是否排除 + * @return Model + */ + public function field($field, $except = false) + { + if (true === $field) { + // 获取全部字段 + $fields = $this->getTableInfo('', 'fields'); + $field = $fields ?: '*'; + } elseif ($except) { + // 字段排除 + if (is_string($field)) { + $field = explode(',', $field); + } + $fields = $this->getTableInfo('', 'fields'); + $field = $fields ? array_diff($fields, $field) : $field; + } + $this->options['field'] = $field; + return $this; + } + + /** + * 指定查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return Model + */ + public function where($field, $op = null, $condition = null) + { + if ($field instanceof Query) { + // 使用查询对象 + $this->options['where'] = $field; + return $this; + } elseif ($field instanceof \Closure) { + // 闭包查询 + $where[] = $field; + } elseif (is_null($op) && is_null($condition)) { + if (is_array($field)) { + // 数组批量查询 + $where = $field; + } else { + // 字符串查询 + $where[] = ['exp', $field]; + } + } elseif (is_array($op)) { + // 字段多条件查询 + $param = func_get_args(); + array_shift($param); + $where[$field] = $param; + } elseif (is_null($condition)) { + // 字段相等查询 + $where[$field] = ['eq', $op]; + } else { + // 字段表达式查询 + $where[$field] = [$op, $condition]; + } + if (!isset($this->options['where']['AND'])) { + $this->options['where']['AND'] = []; + } + $this->options['where']['AND'] = array_merge($this->options['where']['AND'], $where); + return $this; + } + + /** + * 指定查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return Model + */ + public function or($field, $op = null, $condition = null) + { + if ($field instanceof \Closure) { + $where[] = $field; + } elseif (is_null($op) && is_null($condition)) { + if (is_array($field)) { + // 数组批量查询 + $where = $field; + } else { + // 字符串查询 + $where[] = ['exp', $field]; + } + } elseif (is_array($op)) { + $param = func_get_args(); + array_shift($param); + $where[$field] = $param; + } elseif (is_null($condition)) { + $where[$field] = ['eq', $op]; + } else { + $where[$field] = [$op, $condition]; + } + if (!isset($this->options['where']['OR'])) { + $this->options['where']['OR'] = []; + } + $this->options['where']['OR'] = array_merge($this->options['where']['OR'], $where); + return $this; + } + + /** + * 指定查询条件 + * @access public + * @param mixed $where 条件表达式 + * @return Model + */ + public function whereExist($where) + { + $this->options['where']['AND'][] = ['EXISTS', $where]; + return $this; + } + + /** + * 指定查询条件 + * @access public + * @param mixed $where 条件表达式 + * @return Model + */ + public function whereOrExist($where) + { + $this->options['where']['OR'][] = ['EXISTS', $where]; + return $this; + } + + /** + * 指定查询条件 + * @access public + * @param mixed $where 条件表达式 + * @return Model + */ + public function whereNotExist($where) + { + $this->options['where']['AND'][] = ['NOT EXISTS', $where]; + return $this; + } + + /** + * 指定查询条件 + * @access public + * @param mixed $where 条件表达式 + * @return Model + */ + public function whereOrNotExist($where) + { + $this->options['where']['OR'][] = ['NOT EXISTS', $where]; + return $this; + } + + /** + * 指定查询数量 + * @access public + * @param mixed $offset 起始位置 + * @param mixed $length 查询数量 + * @return Model + */ + public function limit($offset, $length = null) + { + if (is_null($length) && strpos($offset, ',')) { + list($offset, $length) = explode(',', $offset); + } + $this->options['limit'] = intval($offset) . ($length ? ',' . intval($length) : ''); + return $this; + } + + /** + * 指定分页 + * @access public + * @param mixed $page 页数 + * @param mixed $listRows 每页数量 + * @return Model + */ + public function page($page, $listRows = null) + { + if (is_null($listRows) && strpos($page, ',')) { + list($page, $listRows) = explode(',', $page); + } + $this->options['page'] = [intval($page), intval($listRows)]; + return $this; + } + + /** + * 指定数据表 + * @access public + * @param string $table 表名 + * @return Model + */ + public function table($table) + { + if (is_array($table)) { + $this->options['table'] = $table; + } elseif (!empty($table)) { + $this->options['table'] = $this->parseSqlTable($table); + } + return $this; + } + + /** + * USING支持 用于多表删除 + * @access public + * @param mixed $using + * @return Model + */ + public function using($using) + { + if (is_array($using)) { + $this->options['using'] = $using; + } elseif (!empty($using)) { + $this->options['using'] = $this->parseSqlTable($using); + } + return $this; + } + + /** + * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc']) + * @access public + * @param string|array $field 排序字段 + * @param string $order 排序 + * @return Model + */ + public function order($field, $order = null) + { + if (!empty($field)) { + if (is_string($field)) { + $field = empty($order) ? $field : [$field => $order]; + } + $this->options['order'] = $field; + } + return $this; + } + + /** + * 指定group查询 + * @access public + * @param string $group GROUP + * @return Model + */ + public function group($group) + { + $this->options['group'] = $group; + return $this; + } + + /** + * 指定having查询 + * @access public + * @param string $having having + * @return Model + */ + public function having($having) + { + $this->options['having'] = $having; + return $this; + } + + /** + * 指定查询lock + * @access public + * @param boolean $lock 是否lock + * @return Model + */ + public function lock($lock = false) + { + $this->options['lock'] = $lock; + return $this; + } + + /** + * 指定distinct查询 + * @access public + * @param string $distinct 是否唯一 + * @return Model + */ + public function distinct($distinct) + { + $this->options['distinct'] = $distinct; + return $this; + } + + /** + * 指定数据表别名 + * @access public + * @param string $alias 数据表别名 + * @return Model + */ + public function alias($alias) + { + $this->options['alias'] = $alias; + return $this; + } + + /** + * 指定强制索引 + * @access public + * @param string $force 索引名称 + * @return Model + */ + public function force($force) + { + $this->options['force'] = $force; + return $this; + } + + /** + * 查询注释 + * @access public + * @param string $comment 注释 + * @return Model + */ + public function comment($comment) + { + $this->options['comment'] = $comment; + return $this; + } + + /** + * 获取执行的SQL语句 + * @access public + * @param boolean $fetch 是否返回sql + * @return Model + */ + public function fetchSql($fetch = true) + { + $this->options['fetch_sql'] = $fetch; + return $this; + } + + /** + * 不主动获取数据集 + * @access public + * @param mixed $fetch fetch mode + * @return Model + */ + public function fetchMode($fetch = true) + { + $this->options['fetch_mode'] = $fetch; + return $this; + } + + /** + * 设置从主服务器读取数据 + * @access public + * @return Model + */ + public function master() + { + $this->options['master'] = true; + return $this; + } + + /** + * 指定当前模型 + * @access public + * @param string $model 模型类名称 + * @return object + */ + public function model($model) + { + $this->options['model'] = $model; + return $this; + } + + /** + * 参数绑定 + * @access public + * @param mixed $key 参数名 + * @param mixed $value 绑定变量值 + * @param integer $type 绑定类型 + * @return Model + */ + public function bind($key, $value = false, $type = PDO::PARAM_STR) + { + if (is_array($key)) { + $this->options['bind'] = $key; + } else { + $this->options['bind'][$key] = [$value, $type]; + } + 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); + } + } else { + // 直接传入命名范围定义 + $options = $scope; + } + + if (is_array($options) && !empty($options)) { + $this->options = array_merge($this->options, array_change_key_case($options)); + } + return $this; + } + + /** + * 得到某个字段的值 或者多个字段列数组 + * @access public + * @return string + */ + public function get($field, $resultSet = false) + { + $options['field'] = $field; + // 返回数据个数 + if (!$resultSet) { + $options['limit'] = 1; + } + $result = $this->options($options)->select(); + if (1 == $options['limit']) { + $data = reset($result[0]); + return $data; + } + $fields = array_keys($result[0]); + $count = count($fields); + $key1 = array_shift($fields); + $key2 = $fields ? array_shift($fields) : ''; + foreach ($result as $val) { + if ($count > 2) { + $array[$val[$key1]] = $val; + } elseif (2 == $count) { + $array[$val[$key1]] = $val[$key2]; + } else { + $array[] = $val[$key1]; + } + } + return $array; + } + + public function count($field = '*') + { + return $this->get('COUNT(' . $field . ') AS tp_count'); + } + + public function sum($field = '*') + { + return $this->get('SUM(' . $field . ') AS tp_sum'); + } + + public function min($field = '*') + { + return $this->get('MIN(' . $field . ') AS tp_min'); + } + + public function max($field = '*') + { + return $this->get('MAX(' . $field . ') AS tp_max'); + } + + public function avg($field = '*') + { + return $this->get('AVG(' . $field . ') AS tp_avg'); + } + + /** + * 设置记录的某个字段值 + * 支持使用数据库字段和方法 + * @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 增长值 + * @param integer $lazyTime 延时时间(s) + * @return boolean + * @throws \think\Exception + */ + public function setInc($field, $step = 1, $lazyTime = 0) + { + $condition = !empty($this->options['where']) ? $this->options['where'] : []; + if (empty($condition)) { + // 没有条件不做任何更新 + throw new Exception('no data to update'); + } + if ($lazyTime > 0) { + // 延迟写入 + $guid = md5($this->name . '_' . $field . '_' . serialize($condition)); + $step = $this->lazyWrite($guid, $step, $lazyTime); + if (empty($step)) { + return true; // 等待下次写入 + } + } + return $this->setField($field, ['exp', $field . '+' . $step]); + } + + /** + * 字段值(延迟)减少 + * @access public + * @param string $field 字段名 + * @param integer $step 减少值 + * @param integer $lazyTime 延时时间(s) + * @return boolean + * @throws \think\Exception + */ + public function setDec($field, $step = 1, $lazyTime = 0) + { + $condition = !empty($this->options['where']) ? $this->options['where'] : []; + if (empty($condition)) { + // 没有条件不做任何更新 + throw new Exception('no data to update'); + } + if ($lazyTime > 0) { + // 延迟写入 + $guid = md5($this->name . '_' . $field . '_' . serialize($condition)); + $step = $this->lazyWrite($guid, -$step, $lazyTime); + if (empty($step)) { + return true; // 等待下次写入 + } + } + return $this->setField($field, ['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 = Cache::get($guid))) { + // 存在缓存写入数据 + if (NOW_TIME > Cache::get($guid . '_time') + $lazyTime) { + // 延时更新时间到了,删除缓存数据 并实际写入数据库 + Cache::rm($guid); + Cache::rm($guid . '_time'); + return $value + $step; + } else { + // 追加数据到缓存 + Cache::set($guid, $value + $step, 0); + return false; + } + } else { + // 没有缓存数据 + Cache::set($guid, $step, 0); + // 计时开始 + Cache::set($guid . '_time', NOW_TIME, 0); + return false; + } + } + + /** + * 得到完整的数据表名 + * @access protected + * @return string + */ + protected function getTableName() + { + if (!$this->table) { + $tableName = $this->config['prefix']; + $tableName .= Loader::parseName($this->name); + } else { + $tableName = $this->table; + } + return $tableName; + } + + public function name($name) + { + $this->name = $name; + return $this; + } + + public function options(array $options) + { + $this->options = array_merge($this->options, $options); + return $this; + } + + /** + * 分析表达式(可用于查询或者写入操作) + * @access protected + * @param array $options 表达式参数 * @return array */ - private function getResult() + private function _parseOptions() { - //返回数据集 - $result = $this->PDOStatement->fetchAll(PDO::FETCH_ASSOC); - $this->numRows = count($result); - return $result; + $options = $this->options; + + // 获取数据表 + if (empty($options['table'])) { + $options['table'] = $this->getTableName(); + } + + // 获取字段信息 + $fields = $this->getTableInfo($options['table'], 'fields'); + + // 字段类型检查 + if (isset($options['where']) && is_array($options['where']) && !empty($fields)) { + // 对数组查询条件进行字段类型检查 + if (isset($options['where']['AND'])) { + foreach ($options['where']['AND'] as $key => $val) { + $key = trim($key); + if (in_array($key, $fields, true) && is_scalar($val) && empty($options['bind'][$key])) { + $this->_parseType($options['where']['AND'], $key, $options['bind'], $options['table']); + } + } + } + if (isset($options['where']['OR'])) { + foreach ($options['where']['OR'] as $key => $val) { + $key = trim($key); + if (in_array($key, $fields, true) && is_scalar($val) && empty($options['bind'][$key])) { + $this->_parseType($options['where']['OR'], $key, $options['bind'], $options['table']); + } + } + } + } + + // 表别名 + if (!empty($options['alias'])) { + $options['table'] .= ' ' . $options['alias']; + } + + // 参数绑定 全局化 + $this->bind = !empty($options['bind']) ? $options['bind'] : []; + + // 查询过后清空sql表达式组装 避免影响下次查询 + $this->options = []; + return $options; + } + + /** + * 数据类型检测和自动转换 + * @access protected + * @param array $data 数据 + * @param string $key 字段名 + * @param array $bind 参数绑定列表 + * @param string $tableName 表名 + * @return void + */ + protected function _parseType(&$data, $key, &$bind, $tableName = '') + { + if (':' == substr($data[$key], 0, 1) && isset($bind[substr($data[$key], 1)])) { + // 已经绑定 无需再次绑定 请确保bind方法优先执行 + return; + } + $binds = $this->getTableInfo($tableName, 'bind'); + $type = $this->getTableInfo($tableName, 'type'); + // 强制类型转换 + if (false !== strpos($type[$key], 'int') && false !== strpos($type[$key], 'int')) { + $data[$key] = (int) $data[$key]; + } elseif (false !== strpos($type[$key], 'float') || false !== strpos($type[$key], 'double')) { + $data[$key] = (float) $data[$key]; + } elseif (false !== strpos($type[$key], 'bool')) { + $data[$key] = (bool) $data[$key]; + } + $bind[$key] = [$data[$key], isset($binds[$key]) ? $binds[$key] : \PDO::PARAM_STR]; + $data[$key] = ':' . $key; } /** @@ -406,7 +1314,6 @@ abstract class Driver /** * 设置锁机制 * @access protected - * @param bool $lock * @return string */ protected function parseLock($lock = false) @@ -415,58 +1322,37 @@ abstract class Driver } /** - * set分析 + * 数据分析 * @access protected - * @param array $data - * @return string + * @param array $data 数据 + * @param array $bind 参数绑定类型 + * @param string $type insert update + * @return array */ - protected function parseSet($data) + protected function parseData($data, $bind) { + $fields = array_keys($bind); foreach ($data as $key => $val) { - if (isset($val[0]) && 'exp' == $val[0]) { - $set[] = $this->parseKey($key) . '=' . $val[1]; - } elseif (is_null($val)) { - $set[] = $this->parseKey($key) . '=NULL'; - } elseif (is_scalar($val)) { - // 过滤非标量数据 - if (0 === strpos($val, ':') && isset($this->bind[substr($val, 1)])) { - $set[] = $this->parseKey($key) . '=' . $val; - } else { - $name = count($this->bind); - $set[] = $this->parseKey($key) . '=:' . $key . $_SERVER['REQUEST_TIME'] . '_' . $name; - $this->bindParam($key . $_SERVER['REQUEST_TIME'] . '_' . $name, $val); + if (!in_array($key, $fields, true)) { + if ($this->config['fields_strict']) { + throw new Exception(' fields not exists :[' . $key . ']'); + } + } else { + $item = $this->parseKey($key); + if (isset($val[0]) && 'exp' == $val[0]) { + $result[$item] = $val[1]; + } elseif (is_null($val)) { + $result[$item] = 'NULL'; + } elseif (is_scalar($val)) { + // 过滤非标量数据 + $this->_parseType($data, $key, $this->bind); + $result[$item] = $data[$key]; } } } - return ' SET ' . implode(',', $set); + return $result; } - /** - * 参数绑定 - * @access protected - * @param string $name 绑定参数名 - * @param mixed $value 绑定值 - * @return void - */ - protected function bindParam($name, $value) - { - $this->bind[$name] = $value; - } - - /** - * 获取参数绑定信息并清空 - * @access protected - * @param bool $reset 获取后清空 - * @return array - */ - protected function getBindParams($reset = false) - { - $bind = $this->bind; - if ($reset) { - $this->bind = []; - } - return $bind; - } /** * 字段名分析 * @access protected @@ -512,7 +1398,6 @@ abstract class Driver $fields = explode(',', $fields); } if (is_array($fields)) { - // 完善数组方式传字段名的支持 // 支持 'field1'=>'field2' 这样的字段别名定义 $array = []; foreach ($fields as $key => $field) { @@ -542,13 +1427,10 @@ abstract class Driver { if (is_array($tables)) { // 支持别名定义 - $array = []; foreach ($tables as $table => $alias) { - if (!is_numeric($table)) { - $array[] = $this->parseKey($table) . ' ' . $this->parseKey($alias); - } else { - $array[] = $this->parseKey($alias); - } + $array[] = !is_numeric($table) ? + $this->parseKey($table) . ' ' . $this->parseKey($alias) : + $this->parseKey($alias); } $tables = $array; } elseif (is_string($tables)) { @@ -565,174 +1447,131 @@ abstract class Driver */ protected function parseWhere($where) { - $whereStr = ''; - if (is_string($where)) { - // 直接使用字符串条件 - $whereStr = $where; - } else { - // 使用数组表达式 - $operate = isset($where['_logic']) ? strtoupper($where['_logic']) : ''; - if (in_array($operate, ['AND', 'OR', 'XOR'])) { - // 定义逻辑运算规则 例如 OR XOR AND NOT - $operate = ' ' . $operate . ' '; - unset($where['_logic']); - } else { - // 默认进行 AND 运算 - $operate = ' AND '; - } - foreach ($where as $key => $val) { - if (is_numeric($key)) { - $key = '_complex'; - } - if (0 === strpos($key, '_')) { - // 解析特殊条件表达式 - $whereStr .= $this->parseThinkWhere($key, $val); - } else { - // 多条件支持 - $multi = is_array($val) && isset($val['_multi']); - $key = trim($key); - if (strpos($key, '|')) { - // 支持 name|title|nickname 方式定义查询字段 - $array = explode('|', $key); - $str = []; - foreach ($array as $m => $k) { - $v = $multi ? $val[$m] : $val; - $str[] = $this->parseWhereItem($this->parseKey($k), $v); - } - $whereStr .= '( ' . implode(' OR ', $str) . ' )'; - } elseif (strpos($key, '&')) { - $array = explode('&', $key); - $str = []; - foreach ($array as $m => $k) { - $v = $multi ? $val[$m] : $val; - $str[] = '(' . $this->parseWhereItem($this->parseKey($k), $v) . ')'; - } - $whereStr .= '( ' . implode(' AND ', $str) . ' )'; - } else { - $whereStr .= $this->parseWhereItem($this->parseKey($key), $val); - } - } - $whereStr .= $operate; - } - $whereStr = substr($whereStr, 0, -strlen($operate)); - } + $whereStr = $this->buildWhere($where); return empty($whereStr) ? '' : ' WHERE ' . $whereStr; } - // where子单元分析 - protected function parseWhereItem($key, $val) + /** + * 生成查询条件SQL + * @access public + * @param mixed $where + * @return string + */ + public function buildWhere($where = []) { + if (empty($where) && isset($this->options['where'])) { + $where = $this->options['where']; + } elseif (empty($where)) { + $where = []; + } + if ($where instanceof Query) { + // 使用查询对象 + return $where->buildWhere(); + } $whereStr = ''; - if (is_array($val)) { - if (is_string($val[0])) { - $exp = strtolower($val[0]); - if (preg_match('/^(eq|neq|gt|egt|lt|elt)$/', $exp)) { - // 比较运算 - $whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]); - } elseif (preg_match('/^(notlike|like)$/', $exp)) { - // 模糊查找 - if (is_array($val[1])) { - $likeLogic = isset($val[2]) ? strtoupper($val[2]) : 'OR'; - if (in_array($likeLogic, ['AND', 'OR', 'XOR'])) { - $like = []; - foreach ($val[1] as $item) { - $like[] = $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($item); - } - $whereStr .= '(' . implode(' ' . $likeLogic . ' ', $like) . ')'; - } - } else { - $whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]); - } - } elseif ('exp' == $exp) { - // 使用表达式 - $whereStr .= $key . ' ' . $val[1]; - } elseif (preg_match('/^(notin|not in|in)$/', $exp)) { - // IN 运算 - if (isset($val[2]) && 'exp' == $val[2]) { - $whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $val[1]; - } else { - if (is_string($val[1])) { - $val[1] = explode(',', $val[1]); - } - $zone = implode(',', $this->parseValue($val[1])); - $whereStr .= $key . ' ' . $this->exp[$exp] . ' (' . $zone . ')'; - } - } elseif (preg_match('/^(notbetween|not between|between)$/', $exp)) { - // BETWEEN运算 - $data = is_string($val[1]) ? explode(',', $val[1]) : $val[1]; - $whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($data[0]) . ' AND ' . $this->parseValue($data[1]); + foreach ($where as $key => $val) { + $str = []; + foreach ($val as $field => $value) { + if ($value instanceof \Closure) { + // 使用闭包查询 + $class = clone $this; + call_user_func_array($value, [ & $class]); + $str[] = ' ' . $key . ' ( ' . $class->buildWhere() . ' )'; } else { - throw new DbException("The WHERE express error: {$val[0]}", $this->config, '', 10503); - } - } else { - $count = count($val); - $rule = isset($val[$count - 1]) ? (is_array($val[$count - 1]) ? strtoupper($val[$count - 1][0]) : strtoupper($val[$count - 1])) : ''; - if (in_array($rule, ['AND', 'OR', 'XOR'])) { - --$count; - } else { - $rule = 'AND'; - } - for ($i = 0; $i < $count; $i++) { - $data = is_array($val[$i]) ? $val[$i][1] : $val[$i]; - if ('exp' == strtolower($val[$i][0])) { - $whereStr .= $key . ' ' . $data . ' ' . $rule . ' '; + if (strpos($field, '|')) { + // 不同字段使用相同查询条件(OR) + $array = explode('|', $field); + $item = []; + foreach ($array as $k) { + $item[] = $this->parseWhereItem($k, $value); + } + $str[] = ' ' . $key . ' ( ' . implode(' OR ', $item) . ' )'; + } elseif (strpos($field, '&')) { + // 不同字段使用相同查询条件(AND) + $array = explode('&', $field); + $item = []; + foreach ($array as $k) { + $item[] = $this->parseWhereItem($k, $value); + } + $str[] = ' ' . $key . ' ( ' . implode(' AND ', $item) . ' )'; } else { - $whereStr .= $this->parseWhereItem($key, $val[$i]) . ' ' . $rule . ' '; + // 对字段使用表达式查询 + $field = is_string($field) ? $field : ''; + $str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key); } } - $whereStr = '( ' . substr($whereStr, 0, -4) . ' )'; - } - } else { - //对字符串类型字段采用模糊匹配 - $likeFields = $this->config['db_like_fields']; - if ($likeFields && preg_match('/^(' . $likeFields . ')$/i', $key)) { - $whereStr .= $key . ' LIKE ' . $this->parseValue('%' . $val . '%'); - } else { - $whereStr .= $key . ' = ' . $this->parseValue($val); } + $whereStr .= empty($whereStr) ? substr(implode('', $str), strlen($key) + 1) : implode('', $str); } return $whereStr; } - /** - * 特殊条件分析 - * @access protected - * @param string $key - * @param mixed $val - * @return string - */ - protected function parseThinkWhere($key, $val) + // where子单元分析 + protected function parseWhereItem($key, $val, $rule = '') { - $whereStr = ''; - switch ($key) { - case '_string': - // 字符串模式查询条件 - $whereStr = $val; - break; - case '_complex': - // 复合查询条件 - $whereStr = substr($this->parseWhere($val), 6); - break; - case '_query': - // 字符串模式查询条件 - parse_str($val, $where); - if (isset($where['_logic'])) { - $op = ' ' . strtoupper($where['_logic']) . ' '; - unset($where['_logic']); - } else { - $op = ' AND '; - } - $array = []; - foreach ($where as $field => $data) { - $array[] = $this->parseKey($field) . ' = ' . $this->parseValue($data); - } + if ($key) { + // 字段分析 + $key = $this->parseKey($key); + } - $whereStr = implode($op, $array); - break; + // 查询规则和条件 + if (!is_array($val)) { + $val = ['=', $val]; + } + list($exp, $value) = $val; + + // 对一个字段使用多个查询条件 + if (is_array($exp)) { + foreach ($val as $item) { + $str[] = $this->parseWhereItem($key, $item); + } + return '( ' . implode(' ' . $rule . ' ', $str) . ' )'; + } + + // 检测操作符 + if (!in_array($exp, $this->exp)) { + $exp = strtolower($exp); + if (isset($this->exp[$exp])) { + $exp = $this->exp[$exp]; + } else { + throw new Exception('where express error:' . $exp); + } + } + + $whereStr = ''; + if (in_array($exp, ['=', '<>', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE'])) { + // 比较运算 及 模糊匹配 + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value); + } elseif ('EXP' == $exp) { + // 表达式查询 + $whereStr .= $key . ' ' . $value; + } elseif (in_array($exp, ['NOT IN', 'IN'])) { + // IN 查询 + if($value instanceof \Closure){ + $whereStr .= $key . ' '.$exp . ' ' . $this->parseClosure($value); + }else{ + $value = is_string($value) ? explode(',', $value) : $value; + $zone = implode(',', $this->parseValue($value)); + $whereStr .= $key . ' ' . $exp . ' (' . $zone . ')'; + } + } elseif (in_array($exp, ['NOT BETWEEN', 'BETWEEN'])) { + // BETWEEN 查询 + $data = is_string($value) ? explode(',', $value) : $value; + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($data[0]) . ' AND ' . $this->parseValue($data[1]); + } elseif (in_array($exp, ['NOT EXISTS', 'EXISTS'])) { + // EXISTS 查询 + $whereStr .= $exp . ' ' . $this->parseClosure($value); } return $whereStr; } + // 执行闭包子查询 + protected function parseClosure($call,$show=true){ + $class = clone $this; + call_user_func_array($call, [ & $class]); + return $class->buildSql($show); + } + /** * limit分析 * @access protected @@ -838,18 +1677,17 @@ abstract class Driver */ protected function parseUnion($union) { - if (empty($union)) { + if(empty($union)){ return ''; } - - if (isset($union['_all'])) { - $str = 'UNION ALL '; - unset($union['_all']); - } else { - $str = 'UNION '; - } + $type = $union['type']; + unset($union['type']); foreach ($union as $u) { - $sql[] = $str . (is_array($u) ? $this->buildSelectSql($u) : $u); + if($u instanceof \Closure){ + $sql[] = $type . ' ' . $this->parseClosure($u,false); + }elseif(is_string($u)){ + $sql[] = $type .' '.$this->parseSqlTable($u); + } } return implode(' ', $sql); } @@ -873,141 +1711,50 @@ abstract class Driver return sprintf(" FORCE INDEX ( %s ) ", $index); } - /** - * ON DUPLICATE KEY UPDATE 分析 - * @access protected - * @param mixed $duplicate - * @return string - */ - protected function parseDuplicate($duplicate) - { - return ''; - } - /** * 插入记录 * @access public * @param mixed $data 数据 - * @param array $options 参数表达式 * @param boolean $replace 是否replace - * @return false | integer + * @return integer */ - public function insert($data, $options = [], $replace = false) + public function insert(array $data, $replace = false) { - $values = $fields = []; - $this->model = $options['model']; - $this->bind = array_merge($this->bind, !empty($options['bind']) ? $options['bind'] : []); + $options = $this->_parseOptions(); + $bind = $this->getTableInfo($options['table'], 'bind'); - foreach ($data as $key => $val) { - if (isset($val[0]) && 'exp' == $val[0]) { - $fields[] = $this->parseKey($key); - $values[] = $val[1]; - } elseif (is_null($val)) { - $fields[] = $this->parseKey($key); - $values[] = 'NULL'; - } elseif (is_scalar($val)) { - // 过滤非标量数据 - $fields[] = $this->parseKey($key); - if (0 === strpos($val, ':') && isset($this->bind[substr($val, 1)])) { - $values[] = $val; - } else { - $name = count($this->bind); - $values[] = ':' . $key . $_SERVER['REQUEST_TIME'] . '_' . $name; - $this->bindParam($key . $_SERVER['REQUEST_TIME'] . '_' . $name, $val); - } - } - } + $data = $this->parseData($data, $bind); + $fields = array_keys($data); + $values = array_values($data); // 兼容数字传入方式 - $replace = (is_numeric($replace) && $replace > 0) ? true : $replace; - $sql = (true === $replace ? 'REPLACE' : 'INSERT') . ' INTO ' . $this->parseTable($options['table']) . ' (' . implode(',', $fields) . ') VALUES (' . implode(',', $values) . ')' . $this->parseDuplicate($replace); + $sql = ($replace ? 'REPLACE' : 'INSERT') . ' INTO ' . $this->parseTable($options['table']) . ' (' . implode(',', $fields) . ') VALUES (' . implode(',', $values) . ')'; $sql .= $this->parseComment(!empty($options['comment']) ? $options['comment'] : ''); - return $this->execute($sql, $this->getBindParams(true), !empty($options['fetch_sql']) ? true : false); - } - - /** - * 批量更新某字段 - * @access public - * @param mixed $field 字段名 - * @param mixed $pk 主键名 - * @param mixed $dataSet 数据集 - * @param mixed $operator 运算符 - * @param array $options 参数表达式 - **/ - public function updateFieldAll($field, $pk, $dataSet, $operator = '=', $options = []) - { - $values = []; - $this->bind = array_merge($this->bind, !empty($options['bind']) ? $options['bind'] : []); - $field = $this->parseKey($field); - $pk = $this->parseKey($pk); - if (in_array($operator, ['+', '-'])) { - $operator = '= ' . $field . $operator; - } - - $value = ''; - foreach ($dataSet as $key => $val) { - if (is_array($val) && 'exp' == $val[0]) { - $value = $val[1]; - } elseif (is_null($val)) { - $value = 'NULL'; - } elseif (is_scalar($val)) { - if (0 === strpos($val, ':') && isset($this->bind[substr($val, 1)])) { - $value = $val; - } else { - $name = count($this->bind); - $value = ':' . $_SERVER['REQUEST_TIME'] . '_' . $name; - $this->bindParam($_SERVER['REQUEST_TIME'] . '_' . $name, $val); - } - } - //没使用过非数字主键,怎么处理比较合适? - $values[] = " WHEN " . $key . " THEN " . $value; - } - - $sql = 'UPDATE ' . $this->parseTable($options['table']) . ' SET ' . $field . $operator . ' CASE ' . $pk . implode(' ', $values) . ' END '; - //查询条件需和WHEN THEN对一致 - $sql .= ' WHERE ' . $pk . ' in (' . implode(',', array_map([$this, 'parseValue'], array_keys($dataSet))) . ')'; - $sql .= $this->parseComment(!empty($options['comment']) ? $options['comment'] : ''); - return $this->execute($sql, $this->getBindParams(true), !empty($options['fetch_sql']) ? true : false); + $result = $this->execute($sql, isset($options['bind'])?$options['bind']:[], !empty($options['fetch_sql']) ? true : false); + return $result; } /** * 批量插入记录 * @access public * @param mixed $dataSet 数据集 - * @param array $options 参数表达式 - * @param boolean $replace 是否replace - * @return false | integer + * @return integer */ - public function insertAll($dataSet, $options = [], $replace = false) + public function insertAll(array $dataSet) { - $values = []; - $this->model = $options['model']; + $options = $this->_parseOptions(); if (!is_array($dataSet[0])) { return false; } - $this->bind = array_merge($this->bind, !empty($options['bind']) ? $options['bind'] : []); - $fields = array_map([$this, 'parseKey'], array_keys($dataSet[0])); + $bind = $this->getTableInfo($options['table'], 'bind'); + $fields = array_map([$this, 'parseKey'], array_keys($dataSet[0])); foreach ($dataSet as $data) { - $value = []; - foreach ($data as $key => $val) { - if (is_array($val) && 'exp' == $val[0]) { - $value[] = $val[1]; - } elseif (is_null($val)) { - $value[] = 'NULL'; - } elseif (is_scalar($val)) { - if (0 === strpos($val, ':') && isset($this->bind[substr($val, 1)])) { - $value[] = $val; - } else { - $name = count($this->bind); - $value[] = ':' . $key . $_SERVER['REQUEST_TIME'] . '_' . $name; - $this->bindParam($key . $_SERVER['REQUEST_TIME'] . '_' . $name, $val); - } - } - } + //$data = $this->parseData($data, $bind); + $value = array_values($data); $values[] = 'SELECT ' . implode(',', $value); } - $sql = (true === $replace ? 'REPLACE' : 'INSERT') . ' INTO ' . $this->parseTable($options['table']) . ' (' . implode(',', $fields) . ') ' . implode(' UNION ALL ', $values); + $sql = 'INSERT INTO ' . $this->parseTable($options['table']) . ' (' . implode(',', $fields) . ') ' . implode(' UNION ALL ', $values); $sql .= $this->parseComment(!empty($options['comment']) ? $options['comment'] : ''); - return $this->execute($sql, $this->getBindParams(true), !empty($options['fetch_sql']) ? true : false); + return $this->execute($sql, isset($options['bind'])?$options['bind']:[], !empty($options['fetch_sql']) ? true : false); } /** @@ -1015,13 +1762,12 @@ abstract class Driver * @access public * @param string $fields 要插入的数据表字段名 * @param string $table 要插入的数据表名 - * @param array $options 查询数据参数 - * @return false | integer + * @param array $option 查询数据参数 + * @return integer */ - public function selectInsert($fields, $table, $options = []) + public function selectInsert($fields, $table) { - $this->model = $options['model']; - $this->bind = array_merge($this->bind, !empty($options['bind']) ? $options['bind'] : []); + $options = $this->_parseOptions(); if (is_string($fields)) { $fields = explode(',', $fields); } @@ -1029,22 +1775,52 @@ abstract class Driver $fields = array_map([$this, 'parseKey'], $fields); $sql = 'INSERT INTO ' . $this->parseTable($table) . ' (' . implode(',', $fields) . ') '; $sql .= $this->buildSelectSql($options); - return $this->execute($sql, $this->getBindParams(true), !empty($options['fetch_sql']) ? true : false); + return $this->execute($sql, isset($options['bind'])?$options['bind']:[], !empty($options['fetch_sql']) ? true : false); } /** * 更新记录 * @access public * @param mixed $data 数据 - * @param array $options 表达式 - * @return false | integer + * @return integer */ - public function update($data, $options) + public function update(array $data) { - $this->model = $options['model']; - $this->bind = array_merge($this->bind, !empty($options['bind']) ? $options['bind'] : []); - $table = $this->parseTable($options['table']); - $sql = 'UPDATE ' . $table . $this->parseSet($data); + $options = $this->_parseOptions(); + if (!isset($options['where'])) { + $pk = $this->getTableInfo($options['table'], 'pk'); + // 如果存在主键数据 则自动作为更新条件 + if (is_string($pk) && isset($data[$pk])) { + $where[$pk] = $data[$pk]; + unset($data[$pk]); + } elseif (is_array($pk)) { + // 增加复合主键支持 + foreach ($pk as $field) { + if (isset($data[$field])) { + $where[$field] = $data[$field]; + } else { + // 如果缺少复合主键数据则不执行 + throw new Exception('miss pk data'); + } + unset($data[$field]); + } + } + if (!isset($where)) { + // 如果没有任何更新条件则不执行 + throw new Exception('miss update condition'); + } else { + $options['where']['AND'] = $where; + } + } + + $bind = $this->getTableInfo($options['table'], 'bind'); + $table = $this->parseTable($options['table']); + $data = $this->parseData($data, $bind); + + foreach ($data as $key => $val) { + $set[] = $key . '=' . $val; + } + $sql = 'UPDATE ' . $table . ' SET ' . implode(',', $set); if (strpos($table, ',')) { // 多表更新支持JOIN操作 $sql .= $this->parseJoin(!empty($options['join']) ? $options['join'] : ''); @@ -1056,21 +1832,30 @@ abstract class Driver . $this->parseLimit(!empty($options['limit']) ? $options['limit'] : ''); } $sql .= $this->parseComment(!empty($options['comment']) ? $options['comment'] : ''); - return $this->execute($sql, $this->getBindParams(true), !empty($options['fetch_sql']) ? true : false); + return $this->execute($sql, isset($options['bind'])?$options['bind']:[], !empty($options['fetch_sql']) ? true : false); } /** * 删除记录 * @access public - * @param array $options 表达式 - * @return false | integer + * @param array $data 表达式 + * @return integer */ - public function delete($options = []) + public function delete($data = []) { - $this->model = $options['model']; - $this->bind = array_merge($this->bind, !empty($options['bind']) ? $options['bind'] : []); - $table = $this->parseTable($options['table']); - $sql = 'DELETE FROM ' . $table; + if (!empty($data)) { + // AR模式分析主键条件 + $this->parsePkWhere($data); + } + $options = $this->_parseOptions(); + + if (empty($options['where'])) { + // 如果条件为空 不进行删除操作 除非设置 1=1 + throw new Exception('no data to delete without where'); + } + + $table = $this->parseTable($options['table']); + $sql = 'DELETE FROM ' . $table; if (strpos($table, ',')) { // 多表删除支持USING和JOIN操作 if (!empty($options['using'])) { @@ -1085,22 +1870,128 @@ abstract class Driver . $this->parseLimit(!empty($options['limit']) ? $options['limit'] : ''); } $sql .= $this->parseComment(!empty($options['comment']) ? $options['comment'] : ''); - return $this->execute($sql, $this->getBindParams(true), !empty($options['fetch_sql']) ? true : false); + return $this->execute($sql, isset($options['bind'])?$options['bind']:[], !empty($options['fetch_sql']) ? true : false); + } + + public function buildSql($sub=true) + { + return $sub? '( ' . $this->select(false) . ' )' : $this->select(false); } /** * 查找记录 * @access public * @param array $options 表达式 + * @return array|string + */ + public function select($data = []) + { + if (false === $data) { + // 用于子查询 不查询只返回SQL + $this->options['fetch_sql'] = true; + } elseif (!empty($data)) { + // AR模式主键条件分析 + $this->parsePkWhere($data); + } + + $options = $this->_parseOptions(); + $sql = $this->buildSelectSql($options); + $resultSet = $this->query($sql, isset($options['bind'])?$options['bind']:[], !empty($options['fetch_sql']) ? true : false, !empty($options['master']) ? true : false, isset($options['fetch_mode']) ? $options['fetch_mode'] : true); + + if (!empty($resultSet)) { + if (is_string($resultSet)) { + // 返回SQL + return $resultSet; + } + if ($resultSet instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $resultSet; + } + + // 数据列表读取后的处理 + if (!empty($options['model'])) { + foreach ($resultSet as $key => $result) { + if (!empty($options['model'])) { + // 返回模型对象 + $result = new $options['model']($result); + } + $resultSet[$key] = $result; + } + } + } + return $resultSet; + } + + /** + * 把主键值转换为查询条件 支持复合主键 + * @access public + * @param mixed $options 表达式参数 + * @return void + * @throws \think\Exception + */ + protected function parsePkWhere($data) + { + $pk = $this->getTableInfo('', 'pk'); + if (is_string($pk)) { + // 根据主键查询 + if (is_array($data)) { + $where[$pk] = ['in', $data]; + } else { + $where[$pk] = strpos($data, ',') ? ['IN', $data] : $data; + } + $this->options['where']['AND'] = $where; + } elseif (is_array($pk) && is_array($data) && !empty($data)) { + // 根据复合主键查询 + foreach ($pk as $key) { + if (isset($data[$key])) { + $where[$key] = $data[$key]; + } else { + throw new Exception('miss complex primary data'); + } + } + $this->options['where']['AND'] = $where; + } + return; + } + + /** + * 查找单条记录 + * @access public + * @param array $options 表达式 * @return mixed */ - public function select($options = []) + public function find($data = []) { - $this->model = $options['model']; - $this->bind = array_merge($this->bind, !empty($options['bind']) ? $options['bind'] : []); - $sql = $this->buildSelectSql($options); - $result = $this->query($sql, $this->getBindParams(true), !empty($options['fetch_sql']) ? true : false, !empty($options['master']) ? true : false); - return $result; + if (!empty($data)) { + // AR模式分析主键条件 + $this->parsePkWhere($data); + } + $options = $this->_parseOptions(); + $options['limit'] = 1; + $sql = $this->buildSelectSql($options); + $result = $this->query($sql, isset($options['bind'])?$options['bind']:[], !empty($options['fetch_sql']) ? true : false, !empty($options['master']) ? true : false, isset($options['fetch_mode']) ? $options['fetch_mode'] : true); + + // 数据处理 + if (!empty($result)) { + if (is_string($result)) { + // 返回SQL + return $result; + } + + if ($result instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $result; + } + + $data = $result[0]; + if (!empty($options['model'])) { + // 返回模型对象 + $data = new $options['model']($data); + } + } else { + $data = false; + } + return $data; } /** @@ -1153,13 +2044,12 @@ abstract class Driver /** * 获取最近一次查询的sql语句 - * @param string $model 模型名 * @access public * @return string */ - public function getLastSql($model = '') + public function getLastSql() { - return ($model && isset($this->modelSql[$model])) ? $this->modelSql[$model] : $this->queryStr; + return $this->queryStr; } /** @@ -1185,11 +2075,9 @@ abstract class Driver } else { $error = ''; } - if ('' != $this->queryStr) { $error .= "\n [ SQL语句 ] : " . $this->queryStr; } - return $error; } @@ -1205,17 +2093,6 @@ abstract class Driver return $this->linkID ? $this->linkID->quote($str) : $str; } - /** - * 设置当前操作模型 - * @access public - * @param string $model 模型名 - * @return void - */ - public function setModel($model) - { - $this->model = $model; - } - /** * 数据库调试 记录当前SQL * @access protected @@ -1228,7 +2105,6 @@ abstract class Driver if ($start) { Debug::remark('queryStartTime', 'time'); } else { - $this->modelSql[$this->model] = $this->queryStr; // 记录操作结束时间 Debug::remark('queryEndTime', 'time'); $log = $this->queryStr . ' [ RunTime:' . Debug::getRangeTime('queryStartTime', 'queryEndTime') . 's ]'; @@ -1257,14 +2133,13 @@ abstract class Driver // 默认单数据库 $this->linkID = $this->connect(); } - } /** * 连接分布式服务器 * @access protected * @param boolean $master 主服务器 - * @return resource + * @return void */ protected function multiConnect($master = false) {