diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..36c9cd1242ff819a09fa0e5be4779d30e9bb13ed
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+/.github export-ignore
+/tests export-ignore
+/phpunit.xml export-ignore
\ No newline at end of file
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
new file mode 100644
index 0000000000000000000000000000000000000000..798cef4ca14739fe98f23eac8af18665bf5e8bb8
--- /dev/null
+++ b/.github/workflows/codecov.yml
@@ -0,0 +1,68 @@
+name: codecov
+
+on: [push, pull_request]
+
+jobs:
+ phpunit:
+ runs-on: ubuntu-latest
+ services:
+ mysql:
+ image: mysql:5.7
+ env:
+ MYSQL_ROOT_PASSWORD: password
+ MYSQL_DATABASE: testing
+ ports:
+ - 3306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 7.4
+ extensions: pdo, pdo_mysql, mbstring #optional, setup extensions
+ coverage: xdebug #optional, setup coverage driver
+
+ - name: Check Version
+ run: |
+ php -v
+ php -m
+ composer -V
+
+ - name: Validate composer.json and composer.lock
+ run: composer validate
+
+ - name: Get composer cache directory
+ id: composercache
+ run: echo "::set-output name=dir::$(composer config cache-files-dir)"
+
+ - name: Cache vendor
+ uses: actions/cache@v2
+ env:
+ cache-name: composer-cache
+ with:
+ path: ${{ steps.composercache.outputs.dir }}
+ key: ${{ runner.os }}-build-${{ env.cache-name }}
+
+ - name: Install dependencies (composer.lock)
+ run: composer install --prefer-dist --no-progress --no-suggest
+
+ - name: Run test suite
+ run: composer exec -- phpunit --coverage-clover=coverage.xml -v
+ env:
+ TESTS_DB_MYSQL_HOST: 127.0.0.1
+ TESTS_DB_MYSQL_PORT: 3306
+ TESTS_DB_MYSQL_USERNAME: root
+ TESTS_DB_MYSQL_PASSWORD: password
+ TESTS_DB_MYSQL_DATABASE: testing
+
+ - name: Codecov
+ uses: codecov/codecov-action@v1
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }} #required
+ file: ./coverage.xml #optional
+ flags: unittests #optional
+ name: codecov-umbrella #optional
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000000000000000000000000000000000000..33ac60aff7dbc48e88a3bc71cd908ba6238beeb6
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,73 @@
+name: tests
+
+on: [push, pull_request]
+
+jobs:
+ phpunit:
+ runs-on: ubuntu-latest
+ continue-on-error: ${{ matrix.experimental }}
+ strategy:
+ fail-fast: false
+ matrix:
+ php:
+ - 7.1
+ - 7.2
+ - 7.3
+ - 7.4
+ experimental: [false]
+ include:
+ - php: 8.0
+ experimental: true
+ services:
+ mysql:
+ image: mysql:5.7
+ env:
+ MYSQL_ROOT_PASSWORD: password
+ MYSQL_DATABASE: testing
+ ports:
+ - 3306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: pdo, pdo_mysql, mbstring #optional, setup extensions
+ coverage: none #optional, setup coverage driver
+
+ - name: Check Version
+ run: |
+ php -v
+ php -m
+ composer -V
+
+ - name: Validate composer.json and composer.lock
+ run: composer validate
+
+ - name: Get composer cache directory
+ id: composercache
+ run: echo "::set-output name=dir::$(composer config cache-files-dir)"
+
+ - name: Cache vendor
+ uses: actions/cache@v2
+ env:
+ cache-name: composer-cache
+ with:
+ path: ${{ steps.composercache.outputs.dir }}
+ key: ${{ runner.os }}-${{ matrix.php }}-build-${{ env.cache-name }}
+
+ - name: Install dependencies (composer.lock)
+ run: composer install --prefer-dist --no-progress --no-suggest
+
+ - name: Run test suite
+ run: composer exec -- phpunit -v
+ env:
+ TESTS_DB_MYSQL_HOST: 127.0.0.1
+ TESTS_DB_MYSQL_PORT: 3306
+ TESTS_DB_MYSQL_USERNAME: root
+ TESTS_DB_MYSQL_PASSWORD: password
+ TESTS_DB_MYSQL_DATABASE: testing
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 485dee64bcfb48793379b200a1afd14e85a8aaf4..82cfc4e957ca1cae69fb8686c6b3bc10f222f24e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
.idea
+composer.lock
+vendor
diff --git a/README.md b/README.md
index 392269f167ea22de30f236aaedc714ab56fff11d..c65afe9d65c1c18e71d16e29cede003db50bea14 100644
--- a/README.md
+++ b/README.md
@@ -1,207 +1,27 @@
-# think-orm
-
-基于PHP5.6+ 的ORM实现,主要特性:
-
-- 基于ThinkPHP5.1的ORM独立封装
-- 支持Mysql、Pgsql、Sqlite、SqlServer、Oracle和Mongodb
-- 支持Db类和查询构造器
-- 支持事务
-- 支持模型和关联
-
-适用于不使用ThinkPHP框架的开发者。
-
-安装
+# ThinkORM
+
+基于PHP7.1+ 和PDO实现的ORM,支持多数据库,2.0版本主要特性包括:
+
+* 基于PDO和PHP强类型实现
+* 支持原生查询和查询构造器
+* 自动参数绑定和预查询
+* 简洁易用的查询功能
+* 强大灵活的模型用法
+* 支持预载入关联查询和延迟关联查询
+* 支持多数据库及动态切换
+* 支持`MongoDb`
+* 支持分布式及事务
+* 支持断点重连
+* 支持`JSON`查询
+* 支持数据库日志
+* 支持`PSR-16`缓存及`PSR-3`日志规范
+
+
+## 安装
~~~
composer require topthink/think-orm
~~~
-Db类用法:
-~~~php
-use think\Db;
-// 数据库配置信息设置(全局有效)
-Db::setConfig(['数据库配置参数(数组)']);
-// 进行CURD操作
-Db::table('user')
- ->data(['name'=>'thinkphp','email'=>'thinkphp@qq.com'])
- ->insert();
-Db::table('user')->find();
-Db::table('user')
- ->where('id','>',10)
- ->order('id','desc')
- ->limit(10)
- ->select();
-Db::table('user')
- ->where('id',10)
- ->update(['name'=>'test']);
-Db::table('user')
- ->where('id',10)
- ->delete();
-~~~
-
-Db类增加的(静态)方法包括:
-- `setConfig` 设置全局配置信息
-- `getConfig` 获取数据库配置信息
-- `setQuery` 设置数据库Query类名称
-- `setCacheHandler` 设置缓存对象Handler(必须支持get、set及rm方法)
-- `getSqlLog` 用于获取当前请求的SQL日志信息(包含连接信息)
-
-其它操作参考TP5.1的完全开发手册[数据库](https://www.kancloud.cn/manual/thinkphp5_1/353998)章节
-
-定义模型:
-~~~php
-namespace app\index\model;
-use think\Model;
-class User extends Model
-{
-}
-~~~
-
-代码调用:
-
-~~~php
-use app\index\model\User;
-
-$user = User::get(1);
-$user->name = 'thinkphp';
-$user->save();
-~~~
-
-## Db类和模型对比使用
-#### :white_check_mark: 创建Create
-* Db用法
-
- ```php
- Db::table('user')
- ->insert([
- 'name' => 'thinkphp',
- 'email' => 'thinkphp@qq.com',
- ]);
- ```
-* 模型用法
-
- ```php
- $user = new User;
- $user->name = 'thinkphp';
- $user->email = 'thinkphp@qq.com';
- $user->save();
- ```
-* 或者批量设置
-
- ```php
- $user = new User;
- $user->save([
- 'name' => 'thinkphp',
- 'email' => 'thinkphp@qq.com',
- ]);
- ```
-#### :white_check_mark: 读取Read
-* Db用法
-
- ```php
- $user = Db::table('user')
- ->where('id', 1)
- ->find();
- // 或者
- $user = Db::table('user')
- ->find(1);
- echo $user['id'];
- echo $user['name'];
- ```
-* 模型用法
-
- ```php
- $user = User::get(1);
- echo $user->id;
- echo $user->name;
- ```
-* 模型实现读取多个记录
-
- ```php
- // 查询用户数据集
- $users = User::where('id', '>', 1)
- ->limit(5)
- ->select();
-
- // 遍历读取用户数据
- foreach ($users as $user) {
- echo $user->id;
- echo $user->name;
- }
- ```
-#### :white_check_mark: 更新Update
-* Db用法
-
- ```php
- Db::table('user')
- ->where('id', 1)
- ->update([
- 'name' => 'topthink',
- 'email' => 'topthink@qq.com',
- ]);
- ```
-* 模型用法
-
- ```php
- $user = User::get(1);
- $user->name = 'topthink';
- $user->email = 'topthink@qq.com';
- $user->save();
- ```
-* 或者使用
-
- ```php
- $user = User::get(1);
- $user->save([
- 'name' => 'topthink',
- 'email' => 'topthink@qq.com',
- ]);
- ```
-* 静态调用
-
- ```php
- User::update([
- 'name' => 'topthink',
- 'email' => 'topthink@qq.com',
- ], ['id' => 1]);
- ```
-#### :white_check_mark: 删除Delete
-* Db用法
-
- ```php
- Db::table('user')->delete(1);
- ```
-* 模型用法
-
- ```php
- $user = User::get(1);
- $user->delete();
- ```
-* 或者静态实现
-
- ```php
- User::destroy(1);
- ```
-* 静态调用
-
- ```php
- User::update([
- 'name' => 'topthink',
- 'email' => 'topthink@qq.com',
- ], ['id' => 1]);
- ```
-* destroy方法支持删除指定主键或者查询条件的数据
+## 文档
- ```php
- // 根据主键删除多个数据
- User::destroy([1, 2, 3]);
- // 指定条件删除数据
- User::destroy([
- 'status' => 0,
- ]);
- // 使用闭包条件
- User::destroy(function ($query) {
- $query->where('id', '>', 0)
- ->where('status', 0);
- });
- ```
-更多模型用法可以参考5.1完全开发手册的[模型](https://www.kancloud.cn/manual/thinkphp5_1/354041)章节
+详细参考 [ThinkORM开发指南](https://www.kancloud.cn/manual/think-orm/content)
diff --git a/composer.json b/composer.json
index ef5057346717605a08995a2257c9c62faf2a75f7..097d9244beef8910e55e82331b4d92ffd5f75ce5 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,10 @@
{
"name": "topthink/think-orm",
"description": "think orm",
+ "keywords": [
+ "orm",
+ "database"
+ ],
"license": "Apache-2.0",
"authors": [
{
@@ -9,14 +13,30 @@
}
],
"require": {
- "php": ">=5.6.0"
+ "php": ">=7.1.0",
+ "ext-json": "*",
+ "ext-pdo": "*",
+ "psr/simple-cache": "^1.0|^2.0",
+ "psr/log": "^1.0|^2.0",
+ "topthink/think-helper":"^3.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7|^8|^9.5"
},
"autoload": {
"psr-4": {
"think\\": "src"
},
"files": [
- "src/config.php"
+ "stubs/load_stubs.php"
]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "tests\\": "tests"
+ }
+ },
+ "config": {
+ "sort-packages": true
}
-}
\ No newline at end of file
+}
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000000000000000000000000000000000000..97f50abffb32729a421eb72c24c0bc6481f518ab
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ src
+
+
+
+
+ tests
+
+
+
+
+
+
+
+
+
diff --git a/src/CacheInterface.php b/src/CacheInterface.php
deleted file mode 100644
index 782ee2458c6adc1d4e3f0c061b1466903019d0d1..0000000000000000000000000000000000000000
--- a/src/CacheInterface.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- * @package think
- */
-interface CacheInterface
-{
- function get($name, $default = false);
-
- function set($name, $value, $expire = null);
-
- function rm($name);
-}
\ No newline at end of file
diff --git a/src/Collection.php b/src/Collection.php
deleted file mode 100644
index dcf15e085c69a4ef8a791455664001de5c252fa6..0000000000000000000000000000000000000000
--- a/src/Collection.php
+++ /dev/null
@@ -1,520 +0,0 @@
-
-// +----------------------------------------------------------------------
-
-namespace think;
-
-use ArrayAccess;
-use ArrayIterator;
-use Countable;
-use IteratorAggregate;
-use JsonSerializable;
-
-class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
-{
- protected $items = [];
-
- public function __construct($items = [])
- {
- $this->items = $this->convertToArray($items);
- }
-
- public static function make($items = [])
- {
- return new static($items);
- }
-
- /**
- * 是否为空
- * @return bool
- */
- public function isEmpty()
- {
- return empty($this->items);
- }
-
- public function toArray()
- {
- return array_map(function ($value) {
- return ($value instanceof Model || $value instanceof self) ? $value->toArray() : $value;
- }, $this->items);
- }
-
- public function all()
- {
- return $this->items;
- }
-
- /**
- * 合并数组
- *
- * @param mixed $items
- * @return static
- */
- public function merge($items)
- {
- return new static(array_merge($this->items, $this->convertToArray($items)));
- }
-
- /**
- * 交换数组中的键和值
- *
- * @return static
- */
- public function flip()
- {
- return new static(array_flip($this->items));
- }
-
- /**
- * 按指定键整理数据
- *
- * @access public
- * @param mixed $items 数据
- * @param string $indexKey 键名
- * @return array
- */
- public function dictionary($items = null, &$indexKey = null)
- {
- if ($items instanceof self || $items instanceof Paginator) {
- $items = $items->all();
- }
-
- $items = is_null($items) ? $this->items : $items;
-
- if ($items && empty($indexKey)) {
- $indexKey = is_array($items[0]) ? 'id' : $items[0]->getPk();
- }
-
- if (isset($indexKey) && is_string($indexKey)) {
- return array_column($items, null, $indexKey);
- }
-
- return $items;
- }
-
- /**
- * 比较数组,返回差集
- *
- * @access public
- * @param mixed $items 数据
- * @param string $indexKey 指定比较的键名
- * @return static
- */
- public function diff($items, $indexKey = null)
- {
- if ($this->isEmpty() || is_scalar($this->items[0])) {
- return new static(array_diff($this->items, $this->convertToArray($items)));
- }
-
- $diff = [];
- $dictionary = $this->dictionary($items, $indexKey);
-
- if (is_string($indexKey)) {
- foreach ($this->items as $item) {
- if (!isset($dictionary[$item[$indexKey]])) {
- $diff[] = $item;
- }
- }
- }
-
- return new static($diff);
- }
-
- /**
- * 比较数组,返回交集
- *
- * @access public
- * @param mixed $items 数据
- * @param string $indexKey 指定比较的键名
- * @return static
- */
- public function intersect($items, $indexKey = null)
- {
- if ($this->isEmpty() || is_scalar($this->items[0])) {
- return new static(array_diff($this->items, $this->convertToArray($items)));
- }
-
- $intersect = [];
- $dictionary = $this->dictionary($items, $indexKey);
-
- if (is_string($indexKey)) {
- foreach ($this->items as $item) {
- if (isset($dictionary[$item[$indexKey]])) {
- $intersect[] = $item;
- }
- }
- }
-
- return new static($intersect);
- }
-
- /**
- * 返回数组中所有的键名
- *
- * @access public
- * @return array
- */
- public function keys()
- {
- $current = current($this->items);
-
- if (is_scalar($current)) {
- $array = $this->items;
- } elseif (is_array($current)) {
- $array = $current;
- } else {
- $array = $current->toArray();
- }
-
- return array_keys($array);
- }
-
- /**
- * 删除数组的最后一个元素(出栈)
- *
- * @return mixed
- */
- public function pop()
- {
- return array_pop($this->items);
- }
-
- /**
- * 通过使用用户自定义函数,以字符串返回数组
- *
- * @param callable $callback
- * @param mixed $initial
- * @return mixed
- */
- public function reduce(callable $callback, $initial = null)
- {
- return array_reduce($this->items, $callback, $initial);
- }
-
- /**
- * 以相反的顺序返回数组。
- *
- * @return static
- */
- public function reverse()
- {
- return new static(array_reverse($this->items));
- }
-
- /**
- * 删除数组中首个元素,并返回被删除元素的值
- *
- * @return mixed
- */
- public function shift()
- {
- return array_shift($this->items);
- }
-
- /**
- * 在数组结尾插入一个元素
- * @param mixed $value
- * @param mixed $key
- * @return void
- */
- public function push($value, $key = null)
- {
- if (is_null($key)) {
- $this->items[] = $value;
- } else {
- $this->items[$key] = $value;
- }
- }
-
- /**
- * 把一个数组分割为新的数组块.
- *
- * @param int $size
- * @param bool $preserveKeys
- * @return static
- */
- public function chunk($size, $preserveKeys = false)
- {
- $chunks = [];
-
- foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) {
- $chunks[] = new static($chunk);
- }
-
- return new static($chunks);
- }
-
- /**
- * 在数组开头插入一个元素
- * @param mixed $value
- * @param mixed $key
- * @return void
- */
- public function unshift($value, $key = null)
- {
- if (is_null($key)) {
- array_unshift($this->items, $value);
- } else {
- $this->items = [$key => $value] + $this->items;
- }
- }
-
- /**
- * 给每个元素执行个回调
- *
- * @param callable $callback
- * @return $this
- */
- public function each(callable $callback)
- {
- foreach ($this->items as $key => $item) {
- $result = $callback($item, $key);
-
- if (false === $result) {
- break;
- } elseif (!is_object($item)) {
- $this->items[$key] = $result;
- }
- }
-
- return $this;
- }
-
- /**
- * 用回调函数过滤数组中的元素
- * @param callable|null $callback
- * @return static
- */
- public function filter(callable $callback = null)
- {
- if ($callback) {
- return new static(array_filter($this->items, $callback));
- }
-
- return new static(array_filter($this->items));
- }
-
- /**
- * 根据字段条件过滤数组中的元素
- * @access public
- * @param string $field 字段名
- * @param mixed $operator 操作符
- * @param mixed $value 数据
- * @return static
- */
- public function where($field, $operator, $value = null)
- {
- if (is_null($value)) {
- $value = $operator;
- $operator = '=';
- }
-
- return $this->filter(function ($data) use ($field, $operator, $value) {
- if (strpos($field, '.')) {
- list($field, $relation) = explode('.', $field);
-
- $result = isset($data[$field][$relation]) ? $data[$field][$relation] : null;
- } else {
- $result = isset($data[$field]) ? $data[$field] : null;
- }
-
- switch ($operator) {
- case '===':
- return $result === $value;
- case '!==':
- return $result !== $value;
- case '!=':
- case '<>':
- return $result != $value;
- case '>':
- return $result > $value;
- case '>=':
- return $result >= $value;
- case '<':
- return $result < $value;
- case '<=':
- return $result <= $value;
- case 'like':
- return is_string($result) && false !== strpos($result, $value);
- case 'not like':
- return is_string($result) && false === strpos($result, $value);
- case 'in':
- return is_scalar($result) && in_array($result, $value, true);
- case 'not in':
- return is_scalar($result) && !in_array($result, $value, true);
- case 'between':
- list($min, $max) = is_string($value) ? explode(',', $value) : $value;
- return is_scalar($result) && $result >= $min && $result <= $max;
- case 'not between':
- list($min, $max) = is_string($value) ? explode(',', $value) : $value;
- return is_scalar($result) && $result > $max || $result < $min;
- case '==':
- case '=':
- default:
- return $result == $value;
- }
- });
- }
-
- /**
- * 返回数组中指定的一列
- * @param mixed $column_key
- * @param mixed $index_key
- * @return array
- */
- public function column($column_key, $index_key = null)
- {
- return array_column($this->items, $column_key, $index_key);
- }
-
- /**
- * 对数组排序
- *
- * @access public
- * @param callable|null $callback
- * @return static
- */
- public function sort(callable $callback = null)
- {
- $items = $this->items;
-
- $callback = $callback ?: function ($a, $b) {
- return $a == $b ? 0 : (($a < $b) ? -1 : 1);
-
- };
-
- uasort($items, $callback);
-
- return new static($items);
- }
-
- /**
- * 指定字段排序
- * @access public
- * @param string $field 排序字段
- * @param string $order 排序
- * @param bool $intSort 是否为数字排序
- * @return $this
- */
- public function order($field, $order = null, $intSort = true)
- {
- return $this->sort(function ($a, $b) use ($field, $order, $intSort) {
- $fieldA = isset($a[$field]) ? $a[$field] : null;
- $fieldB = isset($b[$field]) ? $b[$field] : null;
-
- if ($intSort) {
- return 'desc' == strtolower($order) ? $fieldB >= $fieldA : $fieldA >= $fieldB;
- } else {
- return 'desc' == strtolower($order) ? strcmp($fieldB, $fieldA) : strcmp($fieldA, $fieldB);
- }
- });
- }
-
- /**
- * 将数组打乱
- *
- * @return static
- */
- public function shuffle()
- {
- $items = $this->items;
-
- shuffle($items);
-
- return new static($items);
- }
-
- /**
- * 截取数组
- *
- * @param int $offset
- * @param int $length
- * @param bool $preserveKeys
- * @return static
- */
- public function slice($offset, $length = null, $preserveKeys = false)
- {
- return new static(array_slice($this->items, $offset, $length, $preserveKeys));
- }
-
- // ArrayAccess
- public function offsetExists($offset)
- {
- return array_key_exists($offset, $this->items);
- }
-
- public function offsetGet($offset)
- {
- return $this->items[$offset];
- }
-
- public function offsetSet($offset, $value)
- {
- if (is_null($offset)) {
- $this->items[] = $value;
- } else {
- $this->items[$offset] = $value;
- }
- }
-
- public function offsetUnset($offset)
- {
- unset($this->items[$offset]);
- }
-
- //Countable
- public function count()
- {
- return count($this->items);
- }
-
- //IteratorAggregate
- public function getIterator()
- {
- return new ArrayIterator($this->items);
- }
-
- //JsonSerializable
- public function jsonSerialize()
- {
- return $this->toArray();
- }
-
- /**
- * 转换当前数据集为JSON字符串
- * @access public
- * @param integer $options json参数
- * @return string
- */
- public function toJson($options = JSON_UNESCAPED_UNICODE)
- {
- return json_encode($this->toArray(), $options);
- }
-
- public function __toString()
- {
- return $this->toJson();
- }
-
- /**
- * 转换成数组
- *
- * @param mixed $items
- * @return array
- */
- protected function convertToArray($items)
- {
- if ($items instanceof self) {
- return $items->all();
- }
- return (array) $items;
- }
-}
diff --git a/src/Db.php b/src/Db.php
deleted file mode 100644
index b69695a285377d92da3d3f937856bd8b1c61419a..0000000000000000000000000000000000000000
--- a/src/Db.php
+++ /dev/null
@@ -1,152 +0,0 @@
-
-// +----------------------------------------------------------------------
-
-namespace think;
-
-use think\db\Query;
-
-/**
- * Class Db
- * @package think
- * @method Query table(string $table) static 指定数据表(含前缀)
- * @method Query name(string $name) static 指定数据表(不含前缀)
- * @method Query where(mixed $field, string $op = null, mixed $condition = null) static 查询条件
- * @method Query join(mixed $join, mixed $condition = null, string $type = 'INNER') static JOIN查询
- * @method Query union(mixed $union, boolean $all = false) static UNION查询
- * @method Query limit(mixed $offset, integer $length = null) static 查询LIMIT
- * @method Query order(mixed $field, string $order = null) static 查询ORDER
- * @method Query cache(mixed $key = null , integer $expire = null) static 设置查询缓存
- * @method mixed value(string $field) static 获取某个字段的值
- * @method array column(string $field, string $key = '') static 获取某个列的值
- * @method Query view(mixed $join, mixed $field = null, mixed $on = null, string $type = 'INNER') static 视图查询
- * @method mixed find(mixed $data = null) static 查询单个记录
- * @method mixed select(mixed $data = null) static 查询多个记录
- * @method integer insert(array $data, boolean $replace = false, boolean $getLastInsID = false, string $sequence = null) static 插入一条记录
- * @method integer insertGetId(array $data, boolean $replace = false, string $sequence = null) static 插入一条记录并返回自增ID
- * @method integer insertAll(array $dataSet) static 插入多条记录
- * @method integer update(array $data) static 更新记录
- * @method integer delete(mixed $data = null) static 删除记录
- * @method boolean chunk(integer $count, callable $callback, string $column = null) static 分块获取数据
- * @method \Generator cursor(mixed $data = null) static 使用游标查找记录
- * @method mixed query(string $sql, array $bind = [], boolean $master = false, bool $pdo = false) static SQL查询
- * @method integer execute(string $sql, array $bind = [], boolean $fetch = false, boolean $getLastInsID = false, string $sequence = null) static SQL执行
- * @method Paginator paginate(integer $listRows = 15, mixed $simple = null, array $config = []) static 分页查询
- * @method mixed transaction(callable $callback) static 执行数据库事务
- * @method void startTrans() static 启动事务
- * @method void commit() static 用于非自动提交状态下面的查询提交
- * @method void rollback() static 事务回滚
- * @method boolean batchQuery(array $sqlArray) static 批处理执行SQL语句
- * @method string getLastInsID($sequence = null) static 获取最近插入的ID
- */
-class Db
-{
- /**
- * 数据库配置
- * @var array
- */
- protected static $config = [];
-
- /**
- * 查询类名
- * @var string
- */
- protected static $query;
-
- /**
- * 查询类自动映射
- * @var array
- */
- protected static $queryMap = [
- 'mongo' => '\\think\\db\Mongo',
- ];
-
- /**
- * 查询次数
- * @var integer
- */
- public static $queryTimes = 0;
-
- /**
- * 执行次数
- * @var integer
- */
- public static $executeTimes = 0;
-
- /**
- * 缓存对象
- * @var object
- */
- protected static $cacheHandler;
-
- public static function setConfig($config = [])
- {
- self::$config = array_merge(self::$config, $config);
- }
-
- public static function getConfig($name = null)
- {
- if ($name) {
- return isset(self::$config[$name]) ? self::$config[$name] : null;
- } else {
- return self::$config;
- }
- }
-
- public static function setQuery($query)
- {
- self::$query = $query;
- }
-
- /**
- * 字符串命名风格转换
- * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格
- * @param string $name 字符串
- * @param integer $type 转换类型
- * @param bool $ucfirst 首字母是否大写(驼峰规则)
- * @return string
- */
- public static function parseName($name, $type = 0, $ucfirst = true)
- {
- if ($type) {
- $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) {
- return strtoupper($match[1]);
- }, $name);
- return $ucfirst ? ucfirst($name) : lcfirst($name);
- } else {
- return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_"));
- }
- }
-
- public static function setCacheHandler($cacheHandler)
- {
- self::$cacheHandler = $cacheHandler;
- }
-
- public static function getCacheHandler()
- {
- return self::$cacheHandler;
- }
-
- public static function __callStatic($method, $args)
- {
- if (!self::$query) {
- $type = strtolower(self::getConfig('type'));
-
- $class = isset(self::$queryMap[$type]) ? self::$queryMap[$type] : '\\think\\db\\Query';
-
- self::$query = $class;
- }
-
- $class = self::$query;
-
- return call_user_func_array([new $class, $method], $args);
- }
-}
diff --git a/src/DbManager.php b/src/DbManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..1c1314e88b1ee82f3d15fb659225abcc8e536132
--- /dev/null
+++ b/src/DbManager.php
@@ -0,0 +1,386 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use InvalidArgumentException;
+use Psr\Log\LoggerInterface;
+use Psr\SimpleCache\CacheInterface;
+use think\db\BaseQuery;
+use think\db\ConnectionInterface;
+use think\db\Query;
+use think\db\Raw;
+
+/**
+ * Class DbManager
+ * @package think
+ * @mixin BaseQuery
+ * @mixin Query
+ */
+class DbManager
+{
+ /**
+ * 数据库连接实例
+ * @var array
+ */
+ protected $instance = [];
+
+ /**
+ * 数据库配置
+ * @var array
+ */
+ protected $config = [];
+
+ /**
+ * Event对象或者数组
+ * @var array|object
+ */
+ protected $event;
+
+ /**
+ * SQL监听
+ * @var array
+ */
+ protected $listen = [];
+
+ /**
+ * SQL日志
+ * @var array
+ */
+ protected $dbLog = [];
+
+ /**
+ * 查询次数
+ * @var int
+ */
+ protected $queryTimes = 0;
+
+ /**
+ * 查询缓存对象
+ * @var CacheInterface
+ */
+ protected $cache;
+
+ /**
+ * 查询日志对象
+ * @var LoggerInterface
+ */
+ protected $log;
+
+ /**
+ * 架构函数
+ * @access public
+ */
+ public function __construct()
+ {
+ $this->modelMaker();
+ }
+
+ /**
+ * 注入模型对象
+ * @access public
+ * @return void
+ */
+ protected function modelMaker()
+ {
+ Model::setDb($this);
+
+ if (is_object($this->event)) {
+ Model::setEvent($this->event);
+ }
+
+ Model::maker(function (Model $model) {
+ $isAutoWriteTimestamp = $model->getAutoWriteTimestamp();
+
+ if (is_null($isAutoWriteTimestamp)) {
+ // 自动写入时间戳
+ $model->isAutoWriteTimestamp($this->getConfig('auto_timestamp', true));
+ }
+
+ $dateFormat = $model->getDateFormat();
+
+ if (is_null($dateFormat)) {
+ // 设置时间戳格式
+ $model->setDateFormat($this->getConfig('datetime_format', 'Y-m-d H:i:s'));
+ }
+ });
+ }
+
+ /**
+ * 监听SQL
+ * @access protected
+ * @return void
+ */
+ public function triggerSql(): void
+ {}
+
+ /**
+ * 初始化配置参数
+ * @access public
+ * @param array $config 连接配置
+ * @return void
+ */
+ public function setConfig($config): void
+ {
+ $this->config = $config;
+ }
+
+ /**
+ * 设置缓存对象
+ * @access public
+ * @param CacheInterface $cache 缓存对象
+ * @return void
+ */
+ public function setCache(CacheInterface $cache): void
+ {
+ $this->cache = $cache;
+ }
+
+ /**
+ * 设置日志对象
+ * @access public
+ * @param LoggerInterface $log 日志对象
+ * @return void
+ */
+ public function setLog(LoggerInterface $log): void
+ {
+ $this->log = $log;
+ }
+
+ /**
+ * 记录SQL日志
+ * @access protected
+ * @param string $log SQL日志信息
+ * @param string $type 日志类型
+ * @return void
+ */
+ public function log(string $log, string $type = 'sql')
+ {
+ if ($this->log) {
+ $this->log->log($type, $log);
+ } else {
+ $this->dbLog[$type][] = $log;
+ }
+ }
+
+ /**
+ * 获得查询日志(没有设置日志对象使用)
+ * @access public
+ * @param bool $clear 是否清空
+ * @return array
+ */
+ public function getDbLog(bool $clear = false): array
+ {
+ $logs = $this->dbLog;
+ if ($clear) {
+ $this->dbLog = [];
+ }
+
+ return $logs;
+ }
+
+ /**
+ * 获取配置参数
+ * @access public
+ * @param string $name 配置参数
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function getConfig(string $name = '', $default = null)
+ {
+ if ('' === $name) {
+ return $this->config;
+ }
+
+ return $this->config[$name] ?? $default;
+ }
+
+ /**
+ * 创建/切换数据库连接查询
+ * @access public
+ * @param string|null $name 连接配置标识
+ * @param bool $force 强制重新连接
+ * @return ConnectionInterface
+ */
+ public function connect(string $name = null, bool $force = false)
+ {
+ return $this->instance($name, $force);
+ }
+
+ /**
+ * 创建数据库连接实例
+ * @access protected
+ * @param string|null $name 连接标识
+ * @param bool $force 强制重新连接
+ * @return ConnectionInterface
+ */
+ protected function instance(string $name = null, bool $force = false): ConnectionInterface
+ {
+ if (empty($name)) {
+ $name = $this->getConfig('default', 'mysql');
+ }
+
+ if ($force || !isset($this->instance[$name])) {
+ $this->instance[$name] = $this->createConnection($name);
+ }
+
+ return $this->instance[$name];
+ }
+
+ /**
+ * 获取连接配置
+ * @param string $name
+ * @return array
+ */
+ protected function getConnectionConfig(string $name): array
+ {
+ $connections = $this->getConfig('connections');
+ if (!isset($connections[$name])) {
+ throw new InvalidArgumentException('Undefined db config:' . $name);
+ }
+
+ return $connections[$name];
+ }
+
+ /**
+ * 创建连接
+ * @param $name
+ * @return ConnectionInterface
+ */
+ protected function createConnection(string $name): ConnectionInterface
+ {
+ $config = $this->getConnectionConfig($name);
+
+ $type = !empty($config['type']) ? $config['type'] : 'mysql';
+
+ if (false !== strpos($type, '\\')) {
+ $class = $type;
+ } else {
+ $class = '\\think\\db\\connector\\' . ucfirst($type);
+ }
+
+ /** @var ConnectionInterface $connection */
+ $connection = new $class($config);
+ $connection->setDb($this);
+
+ if ($this->cache) {
+ $connection->setCache($this->cache);
+ }
+
+ return $connection;
+ }
+
+ /**
+ * 使用表达式设置数据
+ * @access public
+ * @param string $value 表达式
+ * @return Raw
+ */
+ public function raw(string $value): Raw
+ {
+ return new Raw($value);
+ }
+
+ /**
+ * 更新查询次数
+ * @access public
+ * @return void
+ */
+ public function updateQueryTimes(): void
+ {
+ $this->queryTimes++;
+ }
+
+ /**
+ * 重置查询次数
+ * @access public
+ * @return void
+ */
+ public function clearQueryTimes(): void
+ {
+ $this->queryTimes = 0;
+ }
+
+ /**
+ * 获得查询次数
+ * @access public
+ * @return integer
+ */
+ public function getQueryTimes(): int
+ {
+ return $this->queryTimes;
+ }
+
+ /**
+ * 监听SQL执行
+ * @access public
+ * @param callable $callback 回调方法
+ * @return void
+ */
+ public function listen(callable $callback): void
+ {
+ $this->listen[] = $callback;
+ }
+
+ /**
+ * 获取监听SQL执行
+ * @access public
+ * @return array
+ */
+ public function getListen(): array
+ {
+ return $this->listen;
+ }
+
+ /**
+ * 获取所有连接实列
+ * @access public
+ * @return array
+ */
+ public function getInstance(): array
+ {
+ return $this->instance;
+ }
+
+ /**
+ * 注册回调方法
+ * @access public
+ * @param string $event 事件名
+ * @param callable $callback 回调方法
+ * @return void
+ */
+ public function event(string $event, callable $callback): void
+ {
+ $this->event[$event][] = $callback;
+ }
+
+ /**
+ * 触发事件
+ * @access public
+ * @param string $event 事件名
+ * @param mixed $params 传入参数
+ * @return mixed
+ */
+ public function trigger(string $event, $params = null)
+ {
+ if (isset($this->event[$event])) {
+ foreach ($this->event[$event] as $callback) {
+ call_user_func_array($callback, [$params]);
+ }
+ }
+ }
+
+ public function __call($method, $args)
+ {
+ return call_user_func_array([$this->connect(), $method], $args);
+ }
+}
diff --git a/src/Model.php b/src/Model.php
index fb50f9bcec9f4a3d6ee50eb52ac6810b0c63685c..b3a99ea1037c3e3240f9ada8d8811ee089406602 100644
--- a/src/Model.php
+++ b/src/Model.php
@@ -1,1065 +1,1069 @@
-
-// +----------------------------------------------------------------------
-
-namespace think;
-
-use think\db\Query;
-
-/**
- * Class Model
- * @package think
- * @mixin Query
- * @method \think\Model withAttr(array $name,\Closure $closure) 动态定义获取器
- */
-abstract class Model implements \JsonSerializable, \ArrayAccess
-{
- use model\concern\Attribute;
- use model\concern\RelationShip;
- use model\concern\ModelEvent;
- use model\concern\TimeStamp;
- use model\concern\Conversion;
-
- /**
- * 数据是否存在
- * @var bool
- */
- private $exists = false;
-
- /**
- * 是否强制更新所有数据
- * @var bool
- */
- private $force = false;
-
- /**
- * 是否Replace
- * @var bool
- */
- private $replace = false;
-
- /**
- * 更新条件
- * @var array
- */
- private $updateWhere;
-
- /**
- * 数据库配置信息
- * @var array|string
- */
- protected $connection = [];
-
- /**
- * 数据库查询对象类名
- * @var string
- */
- protected $query;
-
- /**
- * 模型名称
- * @var string
- */
- protected $name;
-
- /**
- * 数据表名称
- * @var string
- */
- protected $table;
-
- /**
- * 写入自动完成定义
- * @var array
- */
- protected $auto = [];
-
- /**
- * 新增自动完成定义
- * @var array
- */
- protected $insert = [];
-
- /**
- * 更新自动完成定义
- * @var array
- */
- protected $update = [];
-
- /**
- * 初始化过的模型.
- * @var array
- */
- protected static $initialized = [];
-
- /**
- * 是否从主库读取(主从分布式有效)
- * @var array
- */
- protected static $readMaster;
-
- /**
- * 查询对象实例
- * @var Query
- */
- protected $queryInstance;
-
- /**
- * 错误信息
- * @var mixed
- */
- protected $error;
-
- /**
- * 软删除字段默认值
- * @var mixed
- */
- protected $defaultSoftDelete;
-
- /**
- * 全局查询范围
- * @var array
- */
- protected $globalScope = [];
-
- /**
- * 架构函数
- * @access public
- * @param array|object $data 数据
- */
- public function __construct($data = [])
- {
- if (is_object($data)) {
- $this->data = get_object_vars($data);
- } else {
- $this->data = $data;
- }
-
- if ($this->disuse) {
- // 废弃字段
- foreach ((array) $this->disuse as $key) {
- if (array_key_exists($key, $this->data)) {
- unset($this->data[$key]);
- }
- }
- }
-
- // 记录原始数据
- $this->origin = $this->data;
-
- $config = Db::getConfig();
-
- if (empty($this->name)) {
- // 当前模型名
- $name = str_replace('\\', '/', static::class);
- $this->name = basename($name);
- if (!empty($config['class_suffix'])) {
- $suffix = basename(dirname($name));
- $this->name = substr($this->name, 0, -strlen($suffix));
- }
- }
-
- if (is_null($this->autoWriteTimestamp)) {
- // 自动写入时间戳
- $this->autoWriteTimestamp = $config['auto_timestamp'];
- }
-
- if (is_null($this->dateFormat)) {
- // 设置时间戳格式
- $this->dateFormat = $config['datetime_format'];
- }
-
- if (is_null($this->resultSetType)) {
- $this->resultSetType = $config['resultset_type'];
- }
-
- if (is_null($this->query)) {
- // 设置查询对象
- $this->query = $config['query'];
- }
-
- if (!empty($this->connection) && is_array($this->connection)) {
- // 设置模型的数据库连接
- $this->connection = array_merge($config, $this->connection);
- }
-
- if ($this->observerClass) {
- // 注册模型观察者
- static::observe($this->observerClass);
- }
-
- // 执行初始化操作
- $this->initialize();
- }
-
- /**
- * 是否从主库读取数据(主从分布有效)
- * @access public
- * @param bool $all 是否所有模型有效
- * @return $this
- */
- public function readMaster($all = false)
- {
- $model = $all ? '*' : static::class;
-
- static::$readMaster[$model] = true;
-
- return $this;
- }
-
- /**
- * 创建新的模型实例
- * @access public
- * @param array|object $data 数据
- * @param bool $isUpdate 是否为更新
- * @param mixed $where 更新条件
- * @return Model
- */
- public function newInstance($data = [], $isUpdate = false, $where = null)
- {
- return (new static($data))->isUpdate($isUpdate, $where);
- }
-
- /**
- * 创建模型的查询对象
- * @access protected
- * @return Query
- */
- protected function buildQuery()
- {
- // 设置当前模型 确保查询返回模型对象
- $class = $this->query;
- $query = (new $class())->connect($this->connection)
- ->model($this)
- ->json($this->json, $this->jsonAssoc)
- ->setJsonFieldType($this->jsonType);
-
- if (isset(static::$readMaster['*']) || isset(static::$readMaster[static::class])) {
- $query->master(true);
- }
-
- // 设置当前数据表和模型名
- if (!empty($this->table)) {
- $query->table($this->table);
- } else {
- $query->name($this->name);
- }
-
- if (!empty($this->pk)) {
- $query->pk($this->pk);
- }
-
- return $query;
- }
-
- /**
- * 获取当前模型的数据库查询对象
- * @access public
- * @param Query $query 查询对象实例
- * @return $this
- */
- public function setQuery($query)
- {
- $this->queryInstance = $query;
- return $this;
- }
-
- /**
- * 获取当前模型的数据库查询对象
- * @access public
- * @param bool|array $useBaseQuery 是否调用全局查询范围(或者指定查询范围名称)
- * @return Query
- */
- public function db($useBaseQuery = true)
- {
- if ($this->queryInstance) {
- return $this->queryInstance;
- }
-
- $query = $this->buildQuery();
-
- // 软删除
- if (property_exists($this, 'withTrashed') && !$this->withTrashed) {
- $this->withNoTrashed($query);
- }
-
- // 全局作用域
- if (true === $useBaseQuery && method_exists($this, 'base')) {
- call_user_func_array([$this, 'base'], [ & $query]);
- }
-
- $globalScope = is_array($useBaseQuery) && $useBaseQuery ? $useBaseQuery : $this->globalScope;
-
- if ($globalScope && false !== $useBaseQuery) {
- $query->scope($globalScope);
- }
-
- // 返回当前模型的数据库查询对象
- return $query;
- }
-
- /**
- * 初始化模型
- * @access protected
- * @return void
- */
- protected function initialize()
- {
- if (!isset(static::$initialized[static::class])) {
- static::$initialized[static::class] = true;
- static::init();
- }
- }
-
- /**
- * 初始化处理
- * @access protected
- * @return void
- */
- protected static function init()
- {}
-
- /**
- * 更新是否强制写入数据 而不做比较
- * @access public
- * @param bool $force
- * @return $this
- */
- public function force($force = true)
- {
- $this->force = $force;
- return $this;
- }
-
- /**
- * 判断force
- * @access public
- * @return bool
- */
- public function isForce()
- {
- return $this->force;
- }
-
- /**
- * 新增数据是否使用Replace
- * @access public
- * @param bool $replace
- * @return $this
- */
- public function replace($replace = true)
- {
- $this->replace = $replace;
- return $this;
- }
-
- /**
- * 设置数据是否存在
- * @access public
- * @param bool $exists
- * @return $this
- */
- public function exists($exists)
- {
- $this->exists = $exists;
- return $this;
- }
-
- /**
- * 判断数据是否存在数据库
- * @access public
- * @return bool
- */
- public function isExists()
- {
- return $this->exists;
- }
-
- /**
- * 数据自动完成
- * @access protected
- * @param array $auto 要自动更新的字段列表
- * @return void
- */
- protected function autoCompleteData($auto = [])
- {
- foreach ($auto as $field => $value) {
- if (is_integer($field)) {
- $field = $value;
- $value = null;
- }
-
- if (!isset($this->data[$field])) {
- $default = null;
- } else {
- $default = $this->data[$field];
- }
-
- $this->setAttr($field, !is_null($value) ? $value : $default);
- }
- }
-
- /**
- * 保存当前数据对象
- * @access public
- * @param array $data 数据
- * @param array $where 更新条件
- * @param string $sequence 自增序列名
- * @return false
- */
- public function save($data = [], $where = [], $sequence = null)
- {
- if (is_string($data)) {
- $sequence = $data;
- $data = [];
- }
-
- if (!$this->checkBeforeSave($data, $where)) {
- return false;
- }
-
- $result = $this->exists ? $this->updateData($where) : $this->insertData($sequence);
-
- if (false === $result) {
- return false;
- }
-
- // 写入回调
- $this->trigger('after_write');
-
- // 重新记录原始数据
- $this->origin = $this->data;
- $this->set = [];
-
- return true;
- }
-
- /**
- * 解析查询条件
- * @access protected
- * @param array|null $where 保存条件
- * @return array|null
- */
- protected static function parseWhere($where)
- {
- if (is_array($where) && key($where) !== 0) {
- $item = [];
- foreach ($where as $key => $val) {
- $item[] = [$key, '=', $val];
- }
- return $item;
- }
- return $where;
- }
-
- /**
- * 写入之前检查数据
- * @access protected
- * @param array $data 数据
- * @param array $where 保存条件
- * @return bool
- */
- protected function checkBeforeSave($data, $where)
- {
- if (!empty($data)) {
-
- // 数据对象赋值
- foreach ($data as $key => $value) {
- $this->setAttr($key, $value, $data);
- }
-
- if (!empty($where)) {
- $this->exists = true;
- $this->updateWhere = self::parseWhere($where);
- }
- }
-
- // 数据自动完成
- $this->autoCompleteData($this->auto);
-
- // 事件回调
- if (false === $this->trigger('before_write')) {
- return false;
- }
-
- return true;
- }
-
- /**
- * 检查数据是否允许写入
- * @access protected
- * @param array $autoFields 自动完成的字段列表
- * @return array
- */
- protected function checkAllowFields($append = [])
- {
- // 检测字段
- if (empty($this->field) || true === $this->field) {
- $query = $this->db(false);
- $table = $this->table ?: $query->getTable();
-
- $this->field = $query->getConnection()->getTableFields($table);
-
- $field = $this->field;
- } else {
- $field = array_merge($this->field, $append);
-
- if ($this->autoWriteTimestamp) {
- array_push($field, $this->createTime, $this->updateTime);
- }
- }
-
- if ($this->disuse) {
- // 废弃字段
- $field = array_diff($field, (array) $this->disuse);
- }
- return $field;
- }
-
- /**
- * 保存写入数据
- * @access protected
- * @param array $where 保存条件
- * @return int|false
- */
- protected function updateData($where)
- {
- // 自动更新
- $this->autoCompleteData($this->update);
-
- // 事件回调
- if (false === $this->trigger('before_update')) {
- return false;
- }
-
- // 获取有更新的数据
- $data = $this->getChangedData();
-
- if (empty($data)) {
- // 关联更新
- if (isset($this->relationWrite)) {
- $this->autoRelationUpdate();
- }
-
- return 0;
- } elseif ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {
- // 自动写入更新时间
- $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
-
- $this->data[$this->updateTime] = $data[$this->updateTime];
- }
-
- if (empty($where) && !empty($this->updateWhere)) {
- $where = $this->updateWhere;
- }
-
- // 检查允许字段
- $allowFields = $this->checkAllowFields(array_merge($this->auto, $this->update));
-
- // 保留主键数据
- foreach ($this->data as $key => $val) {
- if ($this->isPk($key)) {
- $data[$key] = $val;
- }
- }
-
- $pk = $this->getPk();
-
- foreach ((array) $pk as $key) {
- if (isset($data[$key])) {
- $array[] = [$key, '=', $data[$key]];
- unset($data[$key]);
- }
- }
-
- if (!empty($array)) {
- $where = $array;
- }
-
- if ($this->relationWrite) {
- foreach ($this->relationWrite as $name => $val) {
- if (is_array($val)) {
- foreach ($val as $key) {
- if (isset($data[$key])) {
- unset($data[$key]);
- }
- }
- }
- }
- }
-
- $db = $this->db(false);
- $db->startTrans();
-
- try {
- // 模型更新
- $result = $db->where($where)
- ->strict(false)
- ->field($allowFields)
- ->update($data);
-
- // 关联更新
- if (isset($this->relationWrite)) {
- $this->autoRelationUpdate();
- }
-
- $db->commit();
-
- // 更新回调
- $this->trigger('after_update');
-
- return $result;
- } catch (\Exception $e) {
- $db->rollback();
- throw $e;
- }
- }
-
- /**
- * 新增写入数据
- * @access protected
- * @param string $sequence 自增名
- * @return int|false
- */
- protected function insertData($sequence)
- {
- // 自动写入
- $this->autoCompleteData($this->insert);
-
- // 时间戳自动写入
- $this->checkTimeStampWrite();
-
- if (false === $this->trigger('before_insert')) {
- return false;
- }
-
- // 检查允许字段
- $allowFields = $this->checkAllowFields(array_merge($this->auto, $this->insert));
-
- $db = $this->db(false);
- $db->startTrans();
-
- try {
- $result = $db->strict(false)
- ->field($allowFields)
- ->insert($this->data, $this->replace, false, $sequence);
-
- // 获取自动增长主键
- if ($result && $insertId = $db->getLastInsID($sequence)) {
- $pk = $this->getPk();
-
- foreach ((array) $pk as $key) {
- if (!isset($this->data[$key]) || '' == $this->data[$key]) {
- $this->data[$key] = $insertId;
- }
- }
- }
-
- // 关联写入
- if (isset($this->relationWrite)) {
- $this->autoRelationInsert();
- }
-
- $db->commit();
-
- // 标记为更新
- $this->exists = true;
-
- // 新增回调
- $this->trigger('after_insert');
-
- return $result;
- } catch (\Exception $e) {
- $db->rollback();
- throw $e;
- }
- }
-
- /**
- * 字段值(延迟)增长
- * @access public
- * @param string $field 字段名
- * @param integer $step 增长值
- * @param integer $lazyTime 延时时间(s)
- * @return integer|true
- * @throws Exception
- */
- public function setInc($field, $step = 1, $lazyTime = 0)
- {
- // 读取更新条件
- $where = $this->getWhere();
-
- $result = $this->db(false)->where($where)->setInc($field, $step, $lazyTime);
-
- if (true !== $result) {
- $this->data[$field] += $step;
- }
-
- return $result;
- }
-
- /**
- * 字段值(延迟)增长
- * @access public
- * @param string $field 字段名
- * @param integer $step 增长值
- * @param integer $lazyTime 延时时间(s)
- * @return integer|true
- * @throws Exception
- */
- public function setDec($field, $step = 1, $lazyTime = 0)
- {
- // 读取更新条件
- $where = $this->getWhere();
-
- $result = $this->db(false)->where($where)->setDec($field, $step, $lazyTime);
-
- if (true !== $result) {
- $this->data[$field] -= $step;
- }
-
- return $result;
- }
-
- /**
- * 获取当前的更新条件
- * @access protected
- * @return mixed
- */
- protected function getWhere()
- {
- // 删除条件
- $pk = $this->getPk();
-
- if (is_string($pk) && isset($this->data[$pk])) {
- $where[] = [$pk, '=', $this->data[$pk]];
- } elseif (!empty($this->updateWhere)) {
- $where = $this->updateWhere;
- } else {
- $where = null;
- }
-
- return $where;
- }
-
- /**
- * 保存多个数据到当前数据对象
- * @access public
- * @param array $dataSet 数据
- * @param boolean $replace 是否自动识别更新和写入
- * @return Collection|false
- * @throws \Exception
- */
- public function saveAll($dataSet, $replace = true)
- {
- $result = [];
-
- $db = $this->db(false);
- $db->startTrans();
-
- try {
- $pk = $this->getPk();
-
- if (is_string($pk) && $replace) {
- $auto = true;
- }
-
- foreach ($dataSet as $key => $data) {
- if (!empty($auto) && isset($data[$pk])) {
- $result[$key] = self::update($data, [], $this->field);
- } else {
- $result[$key] = self::create($data, $this->field, $this->replace);
- }
- }
-
- $db->commit();
-
- return $this->toCollection($result);
- } catch (\Exception $e) {
- $db->rollback();
- throw $e;
- }
- }
-
- /**
- * 是否为更新数据
- * @access public
- * @param mixed $update
- * @param mixed $where
- * @return $this
- */
- public function isUpdate($update = true, $where = null)
- {
- if (is_bool($update)) {
- $this->exists = $update;
-
- if (!empty($where)) {
- $this->updateWhere = $where;
- }
- } else {
- $this->exists = true;
- $this->updateWhere = $update;
- }
-
- return $this;
- }
-
- /**
- * 删除当前的记录
- * @access public
- * @return bool
- */
- public function delete()
- {
- if (!$this->exists || false === $this->trigger('before_delete')) {
- return false;
- }
-
- // 读取更新条件
- $where = $this->getWhere();
-
- $db = $this->db(false);
- $db->startTrans();
-
- try {
- // 删除当前模型数据
- $db->where($where)->delete();
-
- // 关联删除
- if (!empty($this->relationWrite)) {
- $this->autoRelationDelete();
- }
-
- $db->commit();
-
- $this->trigger('after_delete');
-
- $this->exists = false;
-
- return true;
- } catch (\Exception $e) {
- $db->rollback();
- throw $e;
- }
- }
-
- /**
- * 设置自动完成的字段( 规则通过修改器定义)
- * @access public
- * @param array $fields 需要自动完成的字段
- * @return $this
- */
- public function auto($fields)
- {
- $this->auto = $fields;
-
- return $this;
- }
-
- /**
- * 写入数据
- * @access public
- * @param array $data 数据数组
- * @param array|true $field 允许字段
- * @param bool $replace 使用Replace
- * @return static
- */
- public static function create($data = [], $field = null, $replace = false)
- {
- $model = new static();
-
- if (!empty($field)) {
- $model->allowField($field);
- }
-
- $model->isUpdate(false)->replace($replace)->save($data, []);
-
- return $model;
- }
-
- /**
- * 更新数据
- * @access public
- * @param array $data 数据数组
- * @param array $where 更新条件
- * @param array|true $field 允许字段
- * @return $this
- */
- public static function update($data = [], $where = [], $field = null)
- {
- $model = new static();
-
- if (!empty($field)) {
- $model->allowField($field);
- }
-
- $result = $model->isUpdate(true)->save($data, $where);
-
- return $model;
- }
-
- /**
- * 删除记录
- * @access public
- * @param mixed $data 主键列表 支持闭包查询条件
- * @return bool
- */
- public static function destroy($data)
- {
- $model = new static();
-
- $query = $model->db();
-
- if (empty($data) && 0 !== $data) {
- return false;
- } elseif (is_array($data) && key($data) !== 0) {
- $query->where(self::parseWhere($data));
- $data = null;
- } elseif ($data instanceof \Closure) {
- $data($query);
- $data = null;
- }
-
- $resultSet = $query->select($data);
-
- if ($resultSet) {
- foreach ($resultSet as $data) {
- $data->delete();
- }
- }
-
- return true;
- }
-
- /**
- * 获取错误信息
- * @access public
- * @return mixed
- */
- public function getError()
- {
- return $this->error;
- }
-
- /**
- * 解序列化后处理
- */
- public function __wakeup()
- {
- $this->initialize();
- }
-
- public function __debugInfo()
- {
- return [
- 'data' => $this->data,
- 'relation' => $this->relation,
- ];
- }
-
- /**
- * 修改器 设置数据对象的值
- * @access public
- * @param string $name 名称
- * @param mixed $value 值
- * @return void
- */
- public function __set($name, $value)
- {
- $this->setAttr($name, $value);
- }
-
- /**
- * 获取器 获取数据对象的值
- * @access public
- * @param string $name 名称
- * @return mixed
- */
- public function __get($name)
- {
- return $this->getAttr($name);
- }
-
- /**
- * 检测数据对象的值
- * @access public
- * @param string $name 名称
- * @return boolean
- */
- public function __isset($name)
- {
- try {
- return !is_null($this->getAttr($name));
- } catch (InvalidArgumentException $e) {
- return false;
- }
- }
-
- /**
- * 销毁数据对象的值
- * @access public
- * @param string $name 名称
- * @return void
- */
- public function __unset($name)
- {
- unset($this->data[$name], $this->relation[$name]);
- }
-
- // ArrayAccess
- public function offsetSet($name, $value)
- {
- $this->setAttr($name, $value);
- }
-
- public function offsetExists($name)
- {
- return $this->__isset($name);
- }
-
- public function offsetUnset($name)
- {
- $this->__unset($name);
- }
-
- public function offsetGet($name)
- {
- return $this->getAttr($name);
- }
-
- /**
- * 设置是否使用全局查询范围
- * @param bool|array $use 是否启用全局查询范围(或者用数组指定查询范围名称)
- * @access public
- * @return Query
- */
- public static function useGlobalScope($use)
- {
- $model = new static();
-
- return $model->db($use);
- }
-
- public function __call($method, $args)
- {
- if ('withattr' == strtolower($method)) {
- return call_user_func_array([$this, 'withAttribute'], $args);
- }
-
- return call_user_func_array([$this->db(), $method], $args);
- }
-
- public static function __callStatic($method, $args)
- {
- $model = new static();
-
- return call_user_func_array([$model->db(), $method], $args);
- }
-}
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use ArrayAccess;
+use Closure;
+use JsonSerializable;
+use think\contract\Arrayable;
+use think\contract\Jsonable;
+use think\db\BaseQuery as Query;
+
+/**
+ * Class Model
+ * @package think
+ * @mixin Query
+ * @method void onAfterRead(Model $model) static after_read事件定义
+ * @method mixed onBeforeInsert(Model $model) static before_insert事件定义
+ * @method void onAfterInsert(Model $model) static after_insert事件定义
+ * @method mixed onBeforeUpdate(Model $model) static before_update事件定义
+ * @method void onAfterUpdate(Model $model) static after_update事件定义
+ * @method mixed onBeforeWrite(Model $model) static before_write事件定义
+ * @method void onAfterWrite(Model $model) static after_write事件定义
+ * @method mixed onBeforeDelete(Model $model) static before_write事件定义
+ * @method void onAfterDelete(Model $model) static after_delete事件定义
+ * @method void onBeforeRestore(Model $model) static before_restore事件定义
+ * @method void onAfterRestore(Model $model) static after_restore事件定义
+ */
+abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable
+{
+ use model\concern\Attribute;
+ use model\concern\RelationShip;
+ use model\concern\ModelEvent;
+ use model\concern\TimeStamp;
+ use model\concern\Conversion;
+
+ /**
+ * 数据是否存在
+ * @var bool
+ */
+ private $exists = false;
+
+ /**
+ * 是否强制更新所有数据
+ * @var bool
+ */
+ private $force = false;
+
+ /**
+ * 是否Replace
+ * @var bool
+ */
+ private $replace = false;
+
+ /**
+ * 数据表后缀
+ * @var string
+ */
+ protected $suffix;
+
+ /**
+ * 更新条件
+ * @var array
+ */
+ private $updateWhere;
+
+ /**
+ * 数据库配置
+ * @var string
+ */
+ protected $connection;
+
+ /**
+ * 模型名称
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * 主键值
+ * @var string
+ */
+ protected $key;
+
+ /**
+ * 数据表名称
+ * @var string
+ */
+ protected $table;
+
+ /**
+ * 初始化过的模型.
+ * @var array
+ */
+ protected static $initialized = [];
+
+ /**
+ * 软删除字段默认值
+ * @var mixed
+ */
+ protected $defaultSoftDelete;
+
+ /**
+ * 全局查询范围
+ * @var array
+ */
+ protected $globalScope = [];
+
+ /**
+ * 延迟保存信息
+ * @var bool
+ */
+ private $lazySave = false;
+
+ /**
+ * Db对象
+ * @var DbManager
+ */
+ protected static $db;
+
+ /**
+ * 容器对象的依赖注入方法
+ * @var callable
+ */
+ protected static $invoker;
+
+ /**
+ * 服务注入
+ * @var Closure[]
+ */
+ protected static $maker = [];
+
+ /**
+ * 方法注入
+ * @var Closure[][]
+ */
+ protected static $macro = [];
+
+ /**
+ * 设置服务注入
+ * @access public
+ * @param Closure $maker
+ * @return void
+ */
+ public static function maker(Closure $maker)
+ {
+ static::$maker[] = $maker;
+ }
+
+ /**
+ * 设置方法注入
+ * @access public
+ * @param string $method
+ * @param Closure $closure
+ * @return void
+ */
+ public static function macro(string $method, Closure $closure)
+ {
+ if (!isset(static::$macro[static::class])) {
+ static::$macro[static::class] = [];
+ }
+ static::$macro[static::class][$method] = $closure;
+ }
+
+ /**
+ * 设置Db对象
+ * @access public
+ * @param DbManager $db Db对象
+ * @return void
+ */
+ public static function setDb(DbManager $db)
+ {
+ self::$db = $db;
+ }
+
+ /**
+ * 设置容器对象的依赖注入方法
+ * @access public
+ * @param callable $callable 依赖注入方法
+ * @return void
+ */
+ public static function setInvoker(callable $callable): void
+ {
+ self::$invoker = $callable;
+ }
+
+ /**
+ * 调用反射执行模型方法 支持参数绑定
+ * @access public
+ * @param mixed $method
+ * @param array $vars 参数
+ * @return mixed
+ */
+ public function invoke($method, array $vars = [])
+ {
+ if (self::$invoker) {
+ $call = self::$invoker;
+ return $call($method instanceof Closure ? $method : Closure::fromCallable([$this, $method]), $vars);
+ }
+
+ return call_user_func_array($method instanceof Closure ? $method : [$this, $method], $vars);
+ }
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $data 数据
+ */
+ public function __construct(array $data = [])
+ {
+ $this->data = $data;
+
+ if (!empty($this->data)) {
+ // 废弃字段
+ foreach ((array) $this->disuse as $key) {
+ if (array_key_exists($key, $this->data)) {
+ unset($this->data[$key]);
+ }
+ }
+ }
+
+ // 记录原始数据
+ $this->origin = $this->data;
+
+ if (empty($this->name)) {
+ // 当前模型名
+ $name = str_replace('\\', '/', static::class);
+ $this->name = basename($name);
+ }
+
+ if (!empty(static::$maker)) {
+ foreach (static::$maker as $maker) {
+ call_user_func($maker, $this);
+ }
+ }
+
+ // 执行初始化操作
+ $this->initialize();
+ }
+
+ /**
+ * 获取当前模型名称
+ * @access public
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * 创建新的模型实例
+ * @access public
+ * @param array $data 数据
+ * @param mixed $where 更新条件
+ * @param array $options 参数
+ * @return Model
+ */
+ public function newInstance(array $data = [], $where = null, array $options = []): Model
+ {
+ $model = new static($data);
+
+ if ($this->connection) {
+ $model->setConnection($this->connection);
+ }
+
+ if ($this->suffix) {
+ $model->setSuffix($this->suffix);
+ }
+
+ if (empty($data)) {
+ return $model;
+ }
+
+ $model->exists(true);
+
+ $model->setUpdateWhere($where);
+
+ $model->trigger('AfterRead');
+
+ return $model;
+ }
+
+ /**
+ * 设置模型的更新条件
+ * @access protected
+ * @param mixed $where 更新条件
+ * @return void
+ */
+ protected function setUpdateWhere($where): void
+ {
+ $this->updateWhere = $where;
+ }
+
+ /**
+ * 设置当前模型的数据库连接
+ * @access public
+ * @param string $connection 数据表连接标识
+ * @return $this
+ */
+ public function setConnection(string $connection)
+ {
+ $this->connection = $connection;
+ return $this;
+ }
+
+ /**
+ * 获取当前模型的数据库连接标识
+ * @access public
+ * @return string
+ */
+ public function getConnection(): string
+ {
+ return $this->connection ?: '';
+ }
+
+ /**
+ * 设置当前模型数据表的后缀
+ * @access public
+ * @param string $suffix 数据表后缀
+ * @return $this
+ */
+ public function setSuffix(string $suffix)
+ {
+ $this->suffix = $suffix;
+ return $this;
+ }
+
+ /**
+ * 获取当前模型的数据表后缀
+ * @access public
+ * @return string
+ */
+ public function getSuffix(): string
+ {
+ return $this->suffix ?: '';
+ }
+
+ /**
+ * 获取当前模型的数据库查询对象
+ * @access public
+ * @param array $scope 设置不使用的全局查询范围
+ * @return Query
+ */
+ public function db($scope = []): Query
+ {
+ /** @var Query $query */
+ $query = self::$db->connect($this->connection)
+ ->name($this->name . $this->suffix)
+ ->pk($this->pk);
+
+ if (!empty($this->table)) {
+ $query->table($this->table . $this->suffix);
+ }
+
+ $query->model($this)
+ ->json($this->json, $this->jsonAssoc)
+ ->setFieldType(array_merge($this->schema, $this->jsonType));
+
+ // 软删除
+ if (property_exists($this, 'withTrashed') && !$this->withTrashed) {
+ $this->withNoTrashed($query);
+ }
+
+ // 全局作用域
+ if (is_array($scope)) {
+ $globalScope = array_diff($this->globalScope, $scope);
+ $query->scope($globalScope);
+ }
+
+ // 返回当前模型的数据库查询对象
+ return $query;
+ }
+
+ /**
+ * 初始化模型
+ * @access private
+ * @return void
+ */
+ private function initialize(): void
+ {
+ if (!isset(static::$initialized[static::class])) {
+ static::$initialized[static::class] = true;
+ static::init();
+ }
+ }
+
+ /**
+ * 初始化处理
+ * @access protected
+ * @return void
+ */
+ protected static function init()
+ {
+ }
+
+ protected function checkData(): void
+ {
+ }
+
+ protected function checkResult($result): void
+ {
+ }
+
+ /**
+ * 更新是否强制写入数据 而不做比较(亦可用于软删除的强制删除)
+ * @access public
+ * @param bool $force
+ * @return $this
+ */
+ public function force(bool $force = true)
+ {
+ $this->force = $force;
+ return $this;
+ }
+
+ /**
+ * 判断force
+ * @access public
+ * @return bool
+ */
+ public function isForce(): bool
+ {
+ return $this->force;
+ }
+
+ /**
+ * 新增数据是否使用Replace
+ * @access public
+ * @param bool $replace
+ * @return $this
+ */
+ public function replace(bool $replace = true)
+ {
+ $this->replace = $replace;
+ return $this;
+ }
+
+ /**
+ * 刷新模型数据
+ * @access public
+ * @param bool $relation 是否刷新关联数据
+ * @return $this
+ */
+ public function refresh(bool $relation = false)
+ {
+ if ($this->exists) {
+ $this->data = $this->db()->find($this->getKey())->getData();
+ $this->origin = $this->data;
+ $this->get = [];
+
+ if ($relation) {
+ $this->relation = [];
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置数据是否存在
+ * @access public
+ * @param bool $exists
+ * @return $this
+ */
+ public function exists(bool $exists = true)
+ {
+ $this->exists = $exists;
+ return $this;
+ }
+
+ /**
+ * 判断数据是否存在数据库
+ * @access public
+ * @return bool
+ */
+ public function isExists(): bool
+ {
+ return $this->exists;
+ }
+
+ /**
+ * 判断模型是否为空
+ * @access public
+ * @return bool
+ */
+ public function isEmpty(): bool
+ {
+ return empty($this->data);
+ }
+
+ /**
+ * 延迟保存当前数据对象
+ * @access public
+ * @param array|bool $data 数据
+ * @return void
+ */
+ public function lazySave($data = []): void
+ {
+ if (false === $data) {
+ $this->lazySave = false;
+ } else {
+ if (is_array($data)) {
+ $this->setAttrs($data);
+ }
+
+ $this->lazySave = true;
+ }
+ }
+
+ /**
+ * 保存当前数据对象
+ * @access public
+ * @param array $data 数据
+ * @param string $sequence 自增序列名
+ * @return bool
+ */
+ public function save(array $data = [], string $sequence = null): bool
+ {
+ // 数据对象赋值
+ $this->setAttrs($data);
+
+ if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
+ return false;
+ }
+
+ $result = $this->exists ? $this->updateData() : $this->insertData($sequence);
+
+ if (false === $result) {
+ return false;
+ }
+
+ // 写入回调
+ $this->trigger('AfterWrite');
+
+ // 重新记录原始数据
+ $this->origin = $this->data;
+ $this->get = [];
+ $this->lazySave = false;
+
+ return true;
+ }
+
+ /**
+ * 检查数据是否允许写入
+ * @access protected
+ * @return array
+ */
+ protected function checkAllowFields(): array
+ {
+ // 检测字段
+ if (empty($this->field)) {
+ if (!empty($this->schema)) {
+ $this->field = array_keys(array_merge($this->schema, $this->jsonType));
+ } else {
+ $query = $this->db();
+ $table = $this->table ? $this->table . $this->suffix : $query->getTable();
+
+ $this->field = $query->getConnection()->getTableFields($table);
+ }
+
+ return $this->field;
+ }
+
+ $field = $this->field;
+
+ if ($this->autoWriteTimestamp) {
+ array_push($field, $this->createTime, $this->updateTime);
+ }
+
+ if (!empty($this->disuse)) {
+ // 废弃字段
+ $field = array_diff($field, $this->disuse);
+ }
+
+ return $field;
+ }
+
+ /**
+ * 保存写入数据
+ * @access protected
+ * @return bool
+ */
+ protected function updateData(): bool
+ {
+ // 事件回调
+ if (false === $this->trigger('BeforeUpdate')) {
+ return false;
+ }
+
+ $this->checkData();
+
+ // 获取有更新的数据
+ $data = $this->getChangedData();
+
+ if (empty($data)) {
+ // 关联更新
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationUpdate();
+ }
+
+ return true;
+ }
+
+ if ($this->autoWriteTimestamp && $this->updateTime) {
+ // 自动写入更新时间
+ $data[$this->updateTime] = $this->autoWriteTimestamp();
+ $this->data[$this->updateTime] = $data[$this->updateTime];
+ }
+
+ // 检查允许字段
+ $allowFields = $this->checkAllowFields();
+
+ foreach ($this->relationWrite as $name => $val) {
+ if (!is_array($val)) {
+ continue;
+ }
+
+ foreach ($val as $key) {
+ if (isset($data[$key])) {
+ unset($data[$key]);
+ }
+ }
+ }
+
+ // 模型更新
+ $db = $this->db();
+
+ $db->transaction(function () use ($data, $allowFields, $db) {
+ $this->key = null;
+ $where = $this->getWhere();
+
+ $result = $db->where($where)
+ ->strict(false)
+ ->cache(true)
+ ->setOption('key', $this->key)
+ ->field($allowFields)
+ ->update($data);
+
+ $this->checkResult($result);
+
+ // 关联更新
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationUpdate();
+ }
+ });
+
+ // 更新回调
+ $this->trigger('AfterUpdate');
+
+ return true;
+ }
+
+ /**
+ * 新增写入数据
+ * @access protected
+ * @param string $sequence 自增名
+ * @return bool
+ */
+ protected function insertData(string $sequence = null): bool
+ {
+ if (false === $this->trigger('BeforeInsert')) {
+ return false;
+ }
+
+ $this->checkData();
+ $data = $this->data;
+
+ // 时间戳自动写入
+ if ($this->autoWriteTimestamp) {
+ if ($this->createTime && !isset($data[$this->createTime])) {
+ $data[$this->createTime] = $this->autoWriteTimestamp();
+ $this->data[$this->createTime] = $data[$this->createTime];
+ }
+
+ if ($this->updateTime && !isset($data[$this->updateTime])) {
+ $data[$this->updateTime] = $this->autoWriteTimestamp();
+ $this->data[$this->updateTime] = $data[$this->updateTime];
+ }
+ }
+
+ // 检查允许字段
+ $allowFields = $this->checkAllowFields();
+
+ $db = $this->db();
+
+ $db->transaction(function () use ($data, $sequence, $allowFields, $db) {
+ $result = $db->strict(false)
+ ->field($allowFields)
+ ->replace($this->replace)
+ ->sequence($sequence)
+ ->insert($data, true);
+
+ // 获取自动增长主键
+ if ($result) {
+ $pk = $this->getPk();
+
+ if (is_string($pk) && (!isset($this->data[$pk]) || '' == $this->data[$pk])) {
+ unset($this->get[$pk]);
+ $this->data[$pk] = $result;
+ }
+ }
+
+ // 关联写入
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationInsert();
+ }
+ });
+
+ // 标记数据已经存在
+ $this->exists = true;
+ $this->origin = $this->data;
+
+ // 新增回调
+ $this->trigger('AfterInsert');
+
+ return true;
+ }
+
+ /**
+ * 获取当前的更新条件
+ * @access public
+ * @return mixed
+ */
+ public function getWhere()
+ {
+ $pk = $this->getPk();
+
+ if (is_string($pk) && isset($this->origin[$pk])) {
+ $where = [[$pk, '=', $this->origin[$pk]]];
+ $this->key = $this->origin[$pk];
+ } elseif (is_array($pk)) {
+ foreach ($pk as $field) {
+ if (isset($this->origin[$field])) {
+ $where[] = [$field, '=', $this->origin[$field]];
+ }
+ }
+ }
+
+ if (empty($where)) {
+ $where = empty($this->updateWhere) ? null : $this->updateWhere;
+ }
+
+ return $where;
+ }
+
+ /**
+ * 保存多个数据到当前数据对象
+ * @access public
+ * @param iterable $dataSet 数据
+ * @param boolean $replace 是否自动识别更新和写入
+ * @return Collection
+ * @throws \Exception
+ */
+ public function saveAll(iterable $dataSet, bool $replace = true): Collection
+ {
+ $db = $this->db();
+
+ $result = $db->transaction(function () use ($replace, $dataSet) {
+
+ $pk = $this->getPk();
+
+ if (is_string($pk) && $replace) {
+ $auto = true;
+ }
+
+ $result = [];
+
+ $suffix = $this->getSuffix();
+
+ foreach ($dataSet as $key => $data) {
+ if ($this->exists || (!empty($auto) && isset($data[$pk]))) {
+ $result[$key] = static::update($data, [], [], $suffix);
+ } else {
+ $result[$key] = static::create($data, $this->field, $this->replace, $suffix);
+ }
+ }
+
+ return $result;
+ });
+
+ return $this->toCollection($result);
+ }
+
+ /**
+ * 删除当前的记录
+ * @access public
+ * @return bool
+ */
+ public function delete(): bool
+ {
+ if (!$this->exists || $this->isEmpty() || false === $this->trigger('BeforeDelete')) {
+ return false;
+ }
+
+ // 读取更新条件
+ $where = $this->getWhere();
+
+ $db = $this->db();
+
+ $db->transaction(function () use ($where, $db) {
+ // 删除当前模型数据
+ $db->where($where)->delete();
+
+ // 关联删除
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationDelete();
+ }
+ });
+
+ $this->trigger('AfterDelete');
+
+ $this->exists = false;
+ $this->lazySave = false;
+
+ return true;
+ }
+
+ /**
+ * 写入数据
+ * @access public
+ * @param array $data 数据数组
+ * @param array $allowField 允许字段
+ * @param bool $replace 使用Replace
+ * @param string $suffix 数据表后缀
+ * @return static
+ */
+ public static function create(array $data, array $allowField = [], bool $replace = false, string $suffix = ''): Model
+ {
+ $model = new static();
+
+ if (!empty($allowField)) {
+ $model->allowField($allowField);
+ }
+
+ if (!empty($suffix)) {
+ $model->setSuffix($suffix);
+ }
+
+ $model->replace($replace)->save($data);
+
+ return $model;
+ }
+
+ /**
+ * 更新数据
+ * @access public
+ * @param array $data 数据数组
+ * @param mixed $where 更新条件
+ * @param array $allowField 允许字段
+ * @param string $suffix 数据表后缀
+ * @return static
+ */
+ public static function update(array $data, $where = [], array $allowField = [], string $suffix = '')
+ {
+ $model = new static();
+
+ if (!empty($allowField)) {
+ $model->allowField($allowField);
+ }
+
+ if (!empty($where)) {
+ $model->setUpdateWhere($where);
+ }
+
+ if (!empty($suffix)) {
+ $model->setSuffix($suffix);
+ }
+
+ $model->exists(true)->save($data);
+
+ return $model;
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param mixed $data 主键列表 支持闭包查询条件
+ * @param bool $force 是否强制删除
+ * @return bool
+ */
+ public static function destroy($data, bool $force = false): bool
+ {
+ if (empty($data) && 0 !== $data) {
+ return false;
+ }
+
+ $model = new static();
+
+ $query = $model->db();
+
+ if (is_array($data) && key($data) !== 0) {
+ $query->where($data);
+ $data = null;
+ } elseif ($data instanceof \Closure) {
+ $data($query);
+ $data = null;
+ }
+
+ $resultSet = $query->select($data);
+
+ foreach ($resultSet as $result) {
+ $result->force($force)->delete();
+ }
+
+ return true;
+ }
+
+ /**
+ * 解序列化后处理
+ */
+ public function __wakeup()
+ {
+ $this->initialize();
+ }
+
+ /**
+ * 修改器 设置数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @param mixed $value 值
+ * @return void
+ */
+ public function __set(string $name, $value): void
+ {
+ $this->setAttr($name, $value);
+ }
+
+ /**
+ * 获取器 获取数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @return mixed
+ */
+ public function __get(string $name)
+ {
+ return $this->getAttr($name);
+ }
+
+ /**
+ * 检测数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @return bool
+ */
+ public function __isset(string $name): bool
+ {
+ return !is_null($this->getAttr($name));
+ }
+
+ /**
+ * 销毁数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @return void
+ */
+ public function __unset(string $name): void
+ {
+ unset($this->data[$name],
+ $this->get[$name],
+ $this->relation[$name]);
+ }
+
+ // ArrayAccess
+ #[\ReturnTypeWillChange]
+ public function offsetSet($name, $value)
+ {
+ $this->setAttr($name, $value);
+ }
+
+ #[\ReturnTypeWillChange]
+ public function offsetExists($name): bool
+ {
+ return $this->__isset($name);
+ }
+
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($name)
+ {
+ $this->__unset($name);
+ }
+
+ #[\ReturnTypeWillChange]
+ public function offsetGet($name)
+ {
+ return $this->getAttr($name);
+ }
+
+ /**
+ * 设置不使用的全局查询范围
+ * @access public
+ * @param array $scope 不启用的全局查询范围
+ * @return Query
+ */
+ public static function withoutGlobalScope(array $scope = null)
+ {
+ $model = new static();
+
+ return $model->db($scope);
+ }
+
+ /**
+ * 切换后缀进行查询
+ * @access public
+ * @param string $suffix 切换的表后缀
+ * @return Model
+ */
+ public static function suffix(string $suffix)
+ {
+ $model = new static();
+ $model->setSuffix($suffix);
+
+ return $model;
+ }
+
+ /**
+ * 切换数据库连接进行查询
+ * @access public
+ * @param string $connection 数据库连接标识
+ * @return Model
+ */
+ public static function connect(string $connection)
+ {
+ $model = new static();
+ $model->setConnection($connection);
+
+ return $model;
+ }
+
+ public function __call($method, $args)
+ {
+ if (isset(static::$macro[static::class][$method])) {
+ return call_user_func_array(static::$macro[static::class][$method]->bindTo($this, static::class), $args);
+ }
+
+ return call_user_func_array([$this->db(), $method], $args);
+ }
+
+ public static function __callStatic($method, $args)
+ {
+ if (isset(static::$macro[static::class][$method])) {
+ return call_user_func_array(static::$macro[static::class][$method]->bindTo(null, static::class), $args);
+ }
+
+ $model = new static();
+
+ return call_user_func_array([$model->db(), $method], $args);
+ }
+
+ /**
+ * 析构方法
+ * @access public
+ */
+ public function __destruct()
+ {
+ if ($this->lazySave) {
+ $this->save();
+ }
+ }
+}
diff --git a/src/Paginator.php b/src/Paginator.php
index cbc6568246f563767c6075b90a4cacf7a04f4d54..eace6ba8a46634edd0ba27892f29930dff70c373 100644
--- a/src/Paginator.php
+++ b/src/Paginator.php
@@ -2,46 +2,78 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: zhangyajun <448901948@qq.com>
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think;
use ArrayAccess;
use ArrayIterator;
+use Closure;
use Countable;
+use DomainException;
use IteratorAggregate;
use JsonSerializable;
+use think\paginator\driver\Bootstrap;
use Traversable;
+/**
+ * 分页基础类
+ * @mixin Collection
+ */
abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
{
- /** @var bool 是否为简洁模式 */
+ /**
+ * 是否简洁模式
+ * @var bool
+ */
protected $simple = false;
- /** @var Collection 数据集 */
+ /**
+ * 数据集
+ * @var Collection
+ */
protected $items;
- /** @var integer 当前页 */
+ /**
+ * 当前页
+ * @var int
+ */
protected $currentPage;
- /** @var integer 最后一页 */
+ /**
+ * 最后一页
+ * @var int
+ */
protected $lastPage;
- /** @var integer|null 数据总数 */
+ /**
+ * 数据总数
+ * @var integer|null
+ */
protected $total;
- /** @var integer 每页的数量 */
+ /**
+ * 每页数量
+ * @var int
+ */
protected $listRows;
- /** @var bool 是否有下一页 */
+ /**
+ * 是否有下一页
+ * @var bool
+ */
protected $hasMore;
- /** @var array 一些配置 */
+ /**
+ * 分页配置
+ * @var array
+ */
protected $options = [
'var_page' => 'page',
'path' => '/',
@@ -49,7 +81,24 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
'fragment' => '',
];
- public function __construct($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = [])
+ /**
+ * 获取当前页码
+ * @var Closure
+ */
+ protected static $currentPageResolver;
+
+ /**
+ * 获取当前路径
+ * @var Closure
+ */
+ protected static $currentPathResolver;
+
+ /**
+ * @var Closure
+ */
+ protected static $maker;
+
+ public function __construct($items, int $listRows, int $currentPage = 1, int $total = null, bool $simple = false, array $options = [])
{
$this->options = array_merge($this->options, $options);
@@ -76,20 +125,30 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
}
/**
- * @param $items
- * @param $listRows
- * @param null $currentPage
+ * @access public
+ * @param mixed $items
+ * @param int $listRows
+ * @param int $currentPage
+ * @param int $total
* @param bool $simple
- * @param null $total
* @param array $options
* @return Paginator
*/
- public static function make($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = [])
+ public static function make($items, int $listRows, int $currentPage = 1, int $total = null, bool $simple = false, array $options = [])
+ {
+ if (isset(static::$maker)) {
+ return call_user_func(static::$maker, $items, $listRows, $currentPage, $total, $simple, $options);
+ }
+
+ return new Bootstrap($items, $listRows, $currentPage, $total, $simple, $options);
+ }
+
+ public static function maker(Closure $resolver)
{
- return new static($items, $listRows, $currentPage, $total, $simple, $options);
+ static::$maker = $resolver;
}
- protected function setCurrentPage($currentPage)
+ protected function setCurrentPage(int $currentPage): int
{
if (!$this->simple && $currentPage > $this->lastPage) {
return $this->lastPage > 0 ? $this->lastPage : 1;
@@ -101,10 +160,11 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* 获取页码对应的链接
*
- * @param $page
+ * @access protected
+ * @param int $page
* @return string
*/
- protected function url($page)
+ protected function url(int $page): string
{
if ($page <= 0) {
$page = 1;
@@ -115,7 +175,7 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
$path = $this->options['path'];
} else {
$parameters = [];
- $path = str_replace('[PAGE]', $page, $this->options['path']);
+ $path = str_replace('[PAGE]', (string) $page, $this->options['path']);
}
if (count($this->options['query']) > 0) {
@@ -124,7 +184,7 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
$url = $path;
if (!empty($parameters)) {
- $url .= '?' . urldecode(http_build_query($parameters, null, '&'));
+ $url .= '?' . http_build_query($parameters, '', '&');
}
return $url . $this->buildFragment();
@@ -132,63 +192,92 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* 自动获取当前页码
+ * @access public
* @param string $varPage
* @param int $default
* @return int
*/
- public static function getCurrentPage($varPage = 'page', $default = 1)
+ public static function getCurrentPage(string $varPage = 'page', int $default = 1): int
{
- $page = isset($_REQUEST[$varPage]) ? $_REQUEST[$varPage] : 1;
-
- if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
- return $page;
+ if (isset(static::$currentPageResolver)) {
+ return call_user_func(static::$currentPageResolver, $varPage);
}
return $default;
}
+ /**
+ * 设置获取当前页码闭包
+ * @param Closure $resolver
+ */
+ public static function currentPageResolver(Closure $resolver)
+ {
+ static::$currentPageResolver = $resolver;
+ }
+
/**
* 自动获取当前的path
+ * @access public
+ * @param string $default
* @return string
*/
- public static function getCurrentPath()
+ public static function getCurrentPath($default = '/'): string
{
- if (isset($_SERVER['HTTP_X_REWRITE_URL'])) {
- $url = $_SERVER['HTTP_X_REWRITE_URL'];
- } elseif (isset($_SERVER['REQUEST_URI'])) {
- $url = $_SERVER['REQUEST_URI'];
- } elseif (isset($_SERVER['ORIG_PATH_INFO'])) {
- $url = $_SERVER['ORIG_PATH_INFO'] . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
- } else {
- $url = '';
+ if (isset(static::$currentPathResolver)) {
+ return call_user_func(static::$currentPathResolver);
}
- return strpos($url, '?') ? strstr($url, '?', true) : $url;
+ return $default;
}
- public function total()
+ /**
+ * 设置获取当前路径闭包
+ * @param Closure $resolver
+ */
+ public static function currentPathResolver(Closure $resolver)
+ {
+ static::$currentPathResolver = $resolver;
+ }
+
+ /**
+ * 获取数据总条数
+ * @return int
+ */
+ public function total(): int
{
if ($this->simple) {
- throw new \DomainException('not support total');
+ throw new DomainException('not support total');
}
return $this->total;
}
- public function listRows()
+ /**
+ * 获取每页数量
+ * @return int
+ */
+ public function listRows(): int
{
return $this->listRows;
}
- public function currentPage()
+ /**
+ * 获取当前页页码
+ * @return int
+ */
+ public function currentPage(): int
{
return $this->currentPage;
}
- public function lastPage()
+ /**
+ * 获取最后一页页码
+ * @return int
+ */
+ public function lastPage(): int
{
if ($this->simple) {
- throw new \DomainException('not support last');
+ throw new DomainException('not support last');
}
return $this->lastPage;
@@ -196,9 +285,10 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* 数据是否足够分页
- * @return boolean
+ * @access public
+ * @return bool
*/
- public function hasPages()
+ public function hasPages(): bool
{
return !(1 == $this->currentPage && !$this->hasMore);
}
@@ -206,11 +296,12 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* 创建一组分页链接
*
- * @param int $start
- * @param int $end
+ * @access public
+ * @param int $start
+ * @param int $end
* @return array
*/
- public function getUrlRange($start, $end)
+ public function getUrlRange(int $start, int $end): array
{
$urls = [];
@@ -224,10 +315,11 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* 设置URL锚点
*
- * @param string|null $fragment
+ * @access public
+ * @param string|null $fragment
* @return $this
*/
- public function fragment($fragment)
+ public function fragment(string $fragment = null)
{
$this->options['fragment'] = $fragment;
@@ -237,19 +329,13 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* 添加URL参数
*
- * @param array|string $key
- * @param string|null $value
+ * @access public
+ * @param array $append
* @return $this
*/
- public function appends($key, $value = null)
+ public function appends(array $append)
{
- if (!is_array($key)) {
- $queries = [$key => $value];
- } else {
- $queries = $key;
- }
-
- foreach ($queries as $k => $v) {
+ foreach ($append as $k => $v) {
if ($k !== $this->options['var_page']) {
$this->options['query'][$k] = $v;
}
@@ -261,15 +347,17 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* 构造锚点字符串
*
+ * @access public
* @return string
*/
- protected function buildFragment()
+ protected function buildFragment(): string
{
return $this->options['fragment'] ? '#' . $this->options['fragment'] : '';
}
/**
* 渲染分页html
+ * @access public
* @return mixed
*/
abstract public function render();
@@ -279,12 +367,30 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
return $this->items->all();
}
+ /**
+ * 获取数据集
+ *
+ * @return Collection|\think\model\Collection
+ */
public function getCollection()
{
return $this->items;
}
- public function isEmpty()
+ /**
+ * 设置数据集
+ *
+ * @param Collection $items
+ * @return $this
+ */
+ public function setCollection(Collection $items)
+ {
+ $this->items = $items;
+
+ return $this;
+ }
+
+ public function isEmpty(): bool
{
return $this->items->isEmpty();
}
@@ -292,7 +398,8 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* 给每个元素执行个回调
*
- * @param callable $callback
+ * @access public
+ * @param callable $callback
* @return $this
*/
public function each(callable $callback)
@@ -312,29 +419,35 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* Retrieve an external iterator
+ * @access public
* @return Traversable An instance of an object implementing Iterator or
* Traversable
*/
- public function getIterator()
+ #[\ReturnTypeWillChange]
+ public function getIterator(): Traversable
{
return new ArrayIterator($this->items->all());
}
/**
* Whether a offset exists
+ * @access public
* @param mixed $offset
* @return bool
*/
- public function offsetExists($offset)
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset): bool
{
return $this->items->offsetExists($offset);
}
/**
* Offset to retrieve
+ * @access public
* @param mixed $offset
* @return mixed
*/
+ #[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->items->offsetGet($offset);
@@ -342,9 +455,11 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* Offset to set
+ * @access public
* @param mixed $offset
* @param mixed $value
*/
+ #[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
$this->items->offsetSet($offset, $value);
@@ -352,19 +467,22 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* Offset to unset
+ * @access public
* @param mixed $offset
* @return void
- * @since 5.0.0
+ * @since 5.0.0
*/
+ #[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
$this->items->offsetUnset($offset);
}
/**
- * Count elements of an object
+ * 统计数据集条数
+ * @return int
*/
- public function count()
+ public function count(): int
{
return $this->items->count();
}
@@ -374,11 +492,15 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
return (string) $this->render();
}
- public function toArray()
+ /**
+ * 转换为数组
+ * @return array
+ */
+ public function toArray(): array
{
try {
$total = $this->total();
- } catch (\DomainException $e) {
+ } catch (DomainException $e) {
$total = null;
}
@@ -394,6 +516,7 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
/**
* Specify data which should be serialized to JSON
*/
+ #[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
@@ -401,7 +524,14 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J
public function __call($name, $arguments)
{
- return call_user_func_array([$this->getCollection(), $name], $arguments);
+ $result = call_user_func_array([$this->items, $name], $arguments);
+
+ if ($result instanceof Collection) {
+ $this->items = $result;
+ return $this;
+ }
+
+ return $result;
}
}
diff --git a/src/config.php b/src/config.php
deleted file mode 100644
index 3a925bba62457a07c8af665cb65c8c1fb6f63955..0000000000000000000000000000000000000000
--- a/src/config.php
+++ /dev/null
@@ -1,66 +0,0 @@
-
-// +----------------------------------------------------------------------
-namespace think;
-
-Db::setConfig([
- // 数据库类型
- 'type' => '',
- // 服务器地址
- 'hostname' => '',
- // 数据库名
- 'database' => '',
- // 用户名
- 'username' => '',
- // 密码
- 'password' => '',
- // 端口
- 'hostport' => '',
- // 连接dsn
- 'dsn' => '',
- // 数据库连接参数
- 'params' => [],
- // 数据库编码默认采用utf8
- 'charset' => 'utf8',
- // 数据库表前缀
- 'prefix' => '',
- // 数据库调试模式
- 'debug' => false,
- // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
- 'deploy' => 0,
- // 数据库读写是否分离 主从式有效
- 'rw_separate' => false,
- // 读写分离后 主服务器数量
- 'master_num' => 1,
- // 指定从服务器序号
- 'slave_no' => '',
- // 是否严格检查字段是否存在
- 'fields_strict' => true,
- // 数据集返回类型
- 'resultset_type' => '',
- // 自动写入时间戳字段
- 'auto_timestamp' => false,
- // 时间字段取出后的默认时间格式
- 'datetime_format' => 'Y-m-d H:i:s',
- // 是否需要进行SQL性能分析
- 'sql_explain' => false,
- // Builder类
- 'builder' => '',
- // Query类
- 'query' => '\\think\\db\\Query',
- // 是否需要断线重连
- 'break_reconnect' => false,
- // 默认分页设置
- 'paginate' => [
- 'type' => 'bootstrap',
- 'var_page' => 'page',
- 'list_rows' => 15,
- ]
-]);
diff --git a/src/db/BaseQuery.php b/src/db/BaseQuery.php
new file mode 100644
index 0000000000000000000000000000000000000000..460a730dc9b16fbf31e6be0ceda2168a038b1c49
--- /dev/null
+++ b/src/db/BaseQuery.php
@@ -0,0 +1,1313 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use think\Collection;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException as Exception;
+use think\db\exception\ModelNotFoundException;
+use think\helper\Str;
+use think\Model;
+use think\Paginator;
+
+/**
+ * 数据查询基础类
+ */
+abstract class BaseQuery
+{
+ use concern\TimeFieldQuery;
+ use concern\AggregateQuery;
+ use concern\ModelRelationQuery;
+ use concern\ResultOperation;
+ use concern\Transaction;
+ use concern\WhereQuery;
+
+ /**
+ * 当前数据库连接对象
+ * @var Connection
+ */
+ protected $connection;
+
+ /**
+ * 当前数据表名称(不含前缀)
+ * @var string
+ */
+ protected $name = '';
+
+ /**
+ * 当前数据表主键
+ * @var string|array
+ */
+ protected $pk;
+
+ /**
+ * 当前数据表自增主键
+ * @var string
+ */
+ protected $autoinc;
+
+ /**
+ * 当前数据表前缀
+ * @var string
+ */
+ protected $prefix = '';
+
+ /**
+ * 当前查询参数
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param ConnectionInterface $connection 数据库连接对象
+ */
+ public function __construct(ConnectionInterface $connection)
+ {
+ $this->connection = $connection;
+
+ $this->prefix = $this->connection->getConfig('prefix');
+ }
+
+ /**
+ * 利用__call方法实现一些特殊的Model方法
+ * @access public
+ * @param string $method 方法名称
+ * @param array $args 调用参数
+ * @return mixed
+ * @throws Exception
+ */
+ public function __call(string $method, array $args)
+ {
+ if (strtolower(substr($method, 0, 5)) == 'getby') {
+ // 根据某个字段获取记录
+ $field = Str::snake(substr($method, 5));
+ return $this->where($field, '=', $args[0])->find();
+ } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') {
+ // 根据某个字段获取记录的某个值
+ $name = Str::snake(substr($method, 10));
+ return $this->where($name, '=', $args[0])->value($args[1]);
+ } elseif (strtolower(substr($method, 0, 7)) == 'whereor') {
+ $name = Str::snake(substr($method, 7));
+ array_unshift($args, $name);
+ return call_user_func_array([$this, 'whereOr'], $args);
+ } elseif (strtolower(substr($method, 0, 5)) == 'where') {
+ $name = Str::snake(substr($method, 5));
+ array_unshift($args, $name);
+ return call_user_func_array([$this, 'where'], $args);
+ } elseif ($this->model && method_exists($this->model, 'scope' . $method)) {
+ // 动态调用命名范围
+ $method = 'scope' . $method;
+ array_unshift($args, $this);
+
+ call_user_func_array([$this->model, $method], $args);
+ return $this;
+ } else {
+ throw new Exception('method not exist:' . static::class . '->' . $method);
+ }
+ }
+
+ /**
+ * 创建一个新的查询对象
+ * @access public
+ * @return BaseQuery
+ */
+ public function newQuery(): BaseQuery
+ {
+ $query = new static($this->connection);
+
+ if ($this->model) {
+ $query->model($this->model);
+ }
+
+ if (isset($this->options['table'])) {
+ $query->table($this->options['table']);
+ } else {
+ $query->name($this->name);
+ }
+
+ if (!empty($this->options['json'])) {
+ $query->json($this->options['json'], $this->options['json_assoc']);
+ }
+
+ if (isset($this->options['field_type'])) {
+ $query->setFieldType($this->options['field_type']);
+ }
+
+ return $query;
+ }
+
+ /**
+ * 获取当前的数据库Connection对象
+ * @access public
+ * @return ConnectionInterface
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * 指定当前数据表名(不含前缀)
+ * @access public
+ * @param string $name 不含前缀的数据表名字
+ * @return $this
+ */
+ public function name(string $name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * 获取当前的数据表名称
+ * @access public
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name ?: $this->model->getName();
+ }
+
+ /**
+ * 获取数据库的配置参数
+ * @access public
+ * @param string $name 参数名称
+ * @return mixed
+ */
+ public function getConfig(string $name = '')
+ {
+ return $this->connection->getConfig($name);
+ }
+
+ /**
+ * 得到当前或者指定名称的数据表
+ * @access public
+ * @param string $name 不含前缀的数据表名字
+ * @return mixed
+ */
+ public function getTable(string $name = '')
+ {
+ if (empty($name) && isset($this->options['table'])) {
+ return $this->options['table'];
+ }
+
+ $name = $name ?: $this->name;
+
+ return $this->prefix . Str::snake($name);
+ }
+
+ /**
+ * 设置字段类型信息
+ * @access public
+ * @param array $type 字段类型信息
+ * @return $this
+ */
+ public function setFieldType(array $type)
+ {
+ $this->options['field_type'] = $type;
+ return $this;
+ }
+
+ /**
+ * 获取最近一次查询的sql语句
+ * @access public
+ * @return string
+ */
+ public function getLastSql(): string
+ {
+ return $this->connection->getLastSql();
+ }
+
+ /**
+ * 获取返回或者影响的记录数
+ * @access public
+ * @return integer
+ */
+ public function getNumRows(): int
+ {
+ return $this->connection->getNumRows();
+ }
+
+ /**
+ * 获取最近插入的ID
+ * @access public
+ * @param string $sequence 自增序列名
+ * @return mixed
+ */
+ public function getLastInsID(string $sequence = null)
+ {
+ return $this->connection->getLastInsID($this, $sequence);
+ }
+
+ /**
+ * 得到某个字段的值
+ * @access public
+ * @param string $field 字段名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function value(string $field, $default = null)
+ {
+ $result = $this->connection->value($this, $field, $default);
+
+ $array[$field] = $result;
+ $this->result($array);
+
+ return $array[$field];
+ }
+
+ /**
+ * 得到某个列的数组
+ * @access public
+ * @param string|array $field 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
+ */
+ public function column($field, string $key = ''): array
+ {
+ $result = $this->connection->column($this, $field, $key);
+
+ if (count($result) != count($result, 1)) {
+ $this->resultSet($result, false);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 查询SQL组装 union
+ * @access public
+ * @param mixed $union UNION
+ * @param boolean $all 是否适用UNION ALL
+ * @return $this
+ */
+ public function union($union, bool $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;
+ }
+
+ /**
+ * 查询SQL组装 union all
+ * @access public
+ * @param mixed $union UNION数据
+ * @return $this
+ */
+ public function unionAll($union)
+ {
+ return $this->union($union, true);
+ }
+
+ /**
+ * 指定查询字段
+ * @access public
+ * @param mixed $field 字段信息
+ * @return $this
+ */
+ public function field($field)
+ {
+ if (empty($field)) {
+ return $this;
+ } elseif ($field instanceof Raw) {
+ $this->options['field'][] = $field;
+ return $this;
+ }
+
+ if (is_string($field)) {
+ if (preg_match('/[\<\'\"\(]/', $field)) {
+ return $this->fieldRaw($field);
+ }
+
+ $field = array_map('trim', explode(',', $field));
+ }
+
+ if (true === $field) {
+ // 获取全部字段
+ $fields = $this->getTableFields();
+ $field = $fields ?: ['*'];
+ }
+
+ if (isset($this->options['field'])) {
+ $field = array_merge((array) $this->options['field'], $field);
+ }
+
+ $this->options['field'] = array_unique($field, SORT_REGULAR);
+
+ return $this;
+ }
+
+ /**
+ * 指定要排除的查询字段
+ * @access public
+ * @param array|string $field 要排除的字段
+ * @return $this
+ */
+ public function withoutField($field)
+ {
+ if (empty($field)) {
+ return $this;
+ }
+
+ if (is_string($field)) {
+ $field = array_map('trim', explode(',', $field));
+ }
+
+ // 字段排除
+ $fields = $this->getTableFields();
+ $field = $fields ? array_diff($fields, $field) : $field;
+
+ if (isset($this->options['field'])) {
+ $field = array_merge((array) $this->options['field'], $field);
+ }
+
+ $this->options['field'] = array_unique($field, SORT_REGULAR);
+
+ return $this;
+ }
+
+ /**
+ * 指定其它数据表的查询字段
+ * @access public
+ * @param mixed $field 字段信息
+ * @param string $tableName 数据表名
+ * @param string $prefix 字段前缀
+ * @param string $alias 别名前缀
+ * @return $this
+ */
+ public function tableField($field, string $tableName, string $prefix = '', string $alias = '')
+ {
+ if (empty($field)) {
+ return $this;
+ }
+
+ if (is_string($field)) {
+ $field = array_map('trim', explode(',', $field));
+ }
+
+ if (true === $field) {
+ // 获取全部字段
+ $fields = $this->getTableFields($tableName);
+ $field = $fields ?: ['*'];
+ }
+
+ // 添加统一的前缀
+ $prefix = $prefix ?: $tableName;
+ foreach ($field as $key => &$val) {
+ if (is_numeric($key) && $alias) {
+ $field[$prefix . '.' . $val] = $alias . $val;
+ unset($field[$key]);
+ } elseif (is_numeric($key)) {
+ $val = $prefix . '.' . $val;
+ }
+ }
+
+ if (isset($this->options['field'])) {
+ $field = array_merge((array) $this->options['field'], $field);
+ }
+
+ $this->options['field'] = array_unique($field, SORT_REGULAR);
+
+ return $this;
+ }
+
+ /**
+ * 设置数据
+ * @access public
+ * @param array $data 数据
+ * @return $this
+ */
+ public function data(array $data)
+ {
+ $this->options['data'] = $data;
+
+ return $this;
+ }
+
+ /**
+ * 去除查询参数
+ * @access public
+ * @param string $option 参数名 留空去除所有参数
+ * @return $this
+ */
+ public function removeOption(string $option = '')
+ {
+ if ('' === $option) {
+ $this->options = [];
+ $this->bind = [];
+ } elseif (isset($this->options[$option])) {
+ unset($this->options[$option]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 指定查询数量
+ * @access public
+ * @param int $offset 起始位置
+ * @param int $length 查询数量
+ * @return $this
+ */
+ public function limit(int $offset, int $length = null)
+ {
+ $this->options['limit'] = $offset . ($length ? ',' . $length : '');
+
+ return $this;
+ }
+
+ /**
+ * 指定分页
+ * @access public
+ * @param int $page 页数
+ * @param int $listRows 每页数量
+ * @return $this
+ */
+ public function page(int $page, int $listRows = null)
+ {
+ $this->options['page'] = [$page, $listRows];
+
+ return $this;
+ }
+
+ /**
+ * 指定当前操作的数据表
+ * @access public
+ * @param mixed $table 表名
+ * @return $this
+ */
+ public function table($table)
+ {
+ if (is_string($table)) {
+ if (strpos($table, ')')) {
+ // 子查询
+ } elseif (false === strpos($table, ',')) {
+ if (strpos($table, ' ')) {
+ [$item, $alias] = explode(' ', $table);
+ $table = [];
+ $this->alias([$item => $alias]);
+ $table[$item] = $alias;
+ }
+ } else {
+ $tables = explode(',', $table);
+ $table = [];
+
+ foreach ($tables as $item) {
+ $item = trim($item);
+ if (strpos($item, ' ')) {
+ [$item, $alias] = explode(' ', $item);
+ $this->alias([$item => $alias]);
+ $table[$item] = $alias;
+ } else {
+ $table[] = $item;
+ }
+ }
+ }
+ } elseif (is_array($table)) {
+ $tables = $table;
+ $table = [];
+
+ foreach ($tables as $key => $val) {
+ if (is_numeric($key)) {
+ $table[] = $val;
+ } else {
+ $this->alias([$key => $val]);
+ $table[$key] = $val;
+ }
+ }
+ }
+
+ $this->options['table'] = $table;
+
+ return $this;
+ }
+
+ /**
+ * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc'])
+ * @access public
+ * @param string|array|Raw $field 排序字段
+ * @param string $order 排序
+ * @return $this
+ */
+ public function order($field, string $order = '')
+ {
+ if (empty($field)) {
+ return $this;
+ } elseif ($field instanceof Raw) {
+ $this->options['order'][] = $field;
+ return $this;
+ }
+
+ if (is_string($field)) {
+ if (!empty($this->options['via'])) {
+ $field = $this->options['via'] . '.' . $field;
+ }
+ if (strpos($field, ',')) {
+ $field = array_map('trim', explode(',', $field));
+ } else {
+ $field = empty($order) ? $field : [$field => $order];
+ }
+ } elseif (!empty($this->options['via'])) {
+ foreach ($field as $key => $val) {
+ if (is_numeric($key)) {
+ $field[$key] = $this->options['via'] . '.' . $val;
+ } else {
+ $field[$this->options['via'] . '.' . $key] = $val;
+ unset($field[$key]);
+ }
+ }
+ }
+
+ if (!isset($this->options['order'])) {
+ $this->options['order'] = [];
+ }
+
+ if (is_array($field)) {
+ $this->options['order'] = array_merge($this->options['order'], $field);
+ } else {
+ $this->options['order'][] = $field;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 分页查询
+ * @access public
+ * @param int|array $listRows 每页数量 数组表示配置参数
+ * @param int|bool $simple 是否简洁模式或者总记录数
+ * @return Paginator
+ * @throws Exception
+ */
+ public function paginate($listRows = null, $simple = false): Paginator
+ {
+ if (is_int($simple)) {
+ $total = $simple;
+ $simple = false;
+ }
+
+ $defaultConfig = [
+ 'query' => [], //url额外参数
+ 'fragment' => '', //url锚点
+ 'var_page' => 'page', //分页变量
+ 'list_rows' => 15, //每页数量
+ ];
+
+ if (is_array($listRows)) {
+ $config = array_merge($defaultConfig, $listRows);
+ $listRows = intval($config['list_rows']);
+ } else {
+ $config = $defaultConfig;
+ $listRows = intval($listRows ?: $config['list_rows']);
+ }
+
+ $page = isset($config['page']) ? (int) $config['page'] : Paginator::getCurrentPage($config['var_page']);
+
+ $page = $page < 1 ? 1 : $page;
+
+ $config['path'] = $config['path'] ?? Paginator::getCurrentPath();
+
+ if (!isset($total) && !$simple) {
+ $options = $this->getOptions();
+
+ unset($this->options['order'], $this->options['cache'], $this->options['limit'], $this->options['page'], $this->options['field']);
+
+ $bind = $this->bind;
+ $total = $this->count();
+ if ($total > 0) {
+ $results = $this->options($options)->bind($bind)->page($page, $listRows)->select();
+ } else {
+ if (!empty($this->model)) {
+ $results = new \think\model\Collection([]);
+ } else {
+ $results = new \think\Collection([]);
+ }
+ }
+ } elseif ($simple) {
+ $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select();
+ $total = null;
+ } else {
+ $results = $this->page($page, $listRows)->select();
+ }
+
+ $this->removeOption('limit');
+ $this->removeOption('page');
+
+ return Paginator::make($results, $listRows, $page, $total, $simple, $config);
+ }
+
+ /**
+ * 根据数字类型字段进行分页查询(大数据)
+ * @access public
+ * @param int|array $listRows 每页数量或者分页配置
+ * @param string $key 分页索引键
+ * @param string $sort 索引键排序 asc|desc
+ * @return Paginator
+ * @throws Exception
+ */
+ public function paginateX($listRows = null, string $key = null, string $sort = null): Paginator
+ {
+ $defaultConfig = [
+ 'query' => [], //url额外参数
+ 'fragment' => '', //url锚点
+ 'var_page' => 'page', //分页变量
+ 'list_rows' => 15, //每页数量
+ ];
+
+ $config = is_array($listRows) ? array_merge($defaultConfig, $listRows) : $defaultConfig;
+ $listRows = is_int($listRows) ? $listRows : (int) $config['list_rows'];
+ $page = isset($config['page']) ? (int) $config['page'] : Paginator::getCurrentPage($config['var_page']);
+ $page = $page < 1 ? 1 : $page;
+
+ $config['path'] = $config['path'] ?? Paginator::getCurrentPath();
+
+ $key = $key ?: $this->getPk();
+ $options = $this->getOptions();
+
+ if (is_null($sort)) {
+ $order = $options['order'] ?? '';
+ if (!empty($order)) {
+ $sort = $order[$key] ?? 'desc';
+ } else {
+ $this->order($key, 'desc');
+ $sort = 'desc';
+ }
+ } else {
+ $this->order($key, $sort);
+ }
+
+ $newOption = $options;
+ unset($newOption['field'], $newOption['page']);
+
+ $data = $this->newQuery()
+ ->options($newOption)
+ ->field($key)
+ ->where(true)
+ ->order($key, $sort)
+ ->limit(1)
+ ->find();
+
+ $result = $data[$key] ?? 0;
+
+ if (is_numeric($result)) {
+ $lastId = 'asc' == $sort ? ($result - 1) + ($page - 1) * $listRows : ($result + 1) - ($page - 1) * $listRows;
+ } else {
+ throw new Exception('not support type');
+ }
+
+ $results = $this->when($lastId, function ($query) use ($key, $sort, $lastId) {
+ $query->where($key, 'asc' == $sort ? '>' : '<', $lastId);
+ })
+ ->limit($listRows)
+ ->select();
+
+ $this->options($options);
+
+ return Paginator::make($results, $listRows, $page, null, true, $config);
+ }
+
+ /**
+ * 根据最后ID查询更多N个数据
+ * @access public
+ * @param int $limit LIMIT
+ * @param int|string $lastId LastId
+ * @param string $key 分页索引键 默认为主键
+ * @param string $sort 索引键排序 asc|desc
+ * @return array
+ * @throws Exception
+ */
+ public function more(int $limit, $lastId = null, string $key = null, string $sort = null): array
+ {
+ $key = $key ?: $this->getPk();
+
+ if (is_null($sort)) {
+ $order = $this->getOptions('order');
+ if (!empty($order)) {
+ $sort = $order[$key] ?? 'desc';
+ } else {
+ $this->order($key, 'desc');
+ $sort = 'desc';
+ }
+ } else {
+ $this->order($key, $sort);
+ }
+
+ $result = $this->when($lastId, function ($query) use ($key, $sort, $lastId) {
+ $query->where($key, 'asc' == $sort ? '>' : '<', $lastId);
+ })->limit($limit)->select();
+
+ $last = $result->last();
+
+ $result->first();
+
+ return [
+ 'data' => $result,
+ 'lastId' => $last ? $last[$key] : null,
+ ];
+ }
+
+ /**
+ * 查询缓存 数据为空不缓存
+ * @access public
+ * @param mixed $key 缓存key
+ * @param integer|\DateTime $expire 缓存有效期
+ * @param string|array $tag 缓存标签
+ * @param bool $always 始终缓存
+ * @return $this
+ */
+ public function cache($key = true, $expire = null, $tag = null, bool $always = false)
+ {
+ if (false === $key || !$this->getConnection()->getCache()) {
+ return $this;
+ }
+
+ if ($key instanceof \DateTimeInterface || $key instanceof \DateInterval || (is_int($key) && is_null($expire))) {
+ $expire = $key;
+ $key = true;
+ }
+
+ $this->options['cache'] = [$key, $expire, $tag];
+ $this->options['cache_always'] = $always;
+
+ return $this;
+ }
+
+ /**
+ * 查询缓存 允许缓存空数据
+ * @access public
+ * @param mixed $key 缓存key
+ * @param integer|\DateTime $expire 缓存有效期
+ * @param string|array $tag 缓存标签
+ * @return $this
+ */
+ public function cacheAlways($key = true, $expire = null, $tag = null)
+ {
+ return $this->cache($key, $expire, $tag, true);
+ }
+
+ /**
+ * 指定查询lock
+ * @access public
+ * @param bool|string $lock 是否lock
+ * @return $this
+ */
+ public function lock($lock = false)
+ {
+ $this->options['lock'] = $lock;
+
+ if ($lock) {
+ $this->options['master'] = true;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 指定数据表别名
+ * @access public
+ * @param array|string $alias 数据表别名
+ * @return $this
+ */
+ public function alias($alias)
+ {
+ if (is_array($alias)) {
+ $this->options['alias'] = $alias;
+ } else {
+ $table = $this->getTable();
+
+ $this->options['alias'][$table] = $alias;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置从主服务器读取数据
+ * @access public
+ * @param bool $readMaster 是否从主服务器读取
+ * @return $this
+ */
+ public function master(bool $readMaster = true)
+ {
+ $this->options['master'] = $readMaster;
+ return $this;
+ }
+
+ /**
+ * 设置是否严格检查字段名
+ * @access public
+ * @param bool $strict 是否严格检查字段
+ * @return $this
+ */
+ public function strict(bool $strict = true)
+ {
+ $this->options['strict'] = $strict;
+ return $this;
+ }
+
+ /**
+ * 设置自增序列名
+ * @access public
+ * @param string $sequence 自增序列名
+ * @return $this
+ */
+ public function sequence(string $sequence = null)
+ {
+ $this->options['sequence'] = $sequence;
+ return $this;
+ }
+
+ /**
+ * 设置JSON字段信息
+ * @access public
+ * @param array $json JSON字段
+ * @param bool $assoc 是否取出数组
+ * @return $this
+ */
+ public function json(array $json = [], bool $assoc = false)
+ {
+ $this->options['json'] = $json;
+ $this->options['json_assoc'] = $assoc;
+
+ return $this;
+ }
+
+ /**
+ * 指定数据表主键
+ * @access public
+ * @param string|array $pk 主键
+ * @return $this
+ */
+ public function pk($pk)
+ {
+ $this->pk = $pk;
+ return $this;
+ }
+
+ /**
+ * 查询参数批量赋值
+ * @access protected
+ * @param array $options 表达式参数
+ * @return $this
+ */
+ protected function options(array $options)
+ {
+ $this->options = $options;
+ return $this;
+ }
+
+ /**
+ * 获取当前的查询参数
+ * @access public
+ * @param string $name 参数名
+ * @return mixed
+ */
+ public function getOptions(string $name = '')
+ {
+ if ('' === $name) {
+ return $this->options;
+ }
+
+ return $this->options[$name] ?? null;
+ }
+
+ /**
+ * 设置当前的查询参数
+ * @access public
+ * @param string $option 参数名
+ * @param mixed $value 参数值
+ * @return $this
+ */
+ public function setOption(string $option, $value)
+ {
+ $this->options[$option] = $value;
+ return $this;
+ }
+
+ /**
+ * 设置当前字段添加的表别名
+ * @access public
+ * @param string $via 临时表别名
+ * @return $this
+ */
+ public function via(string $via = '')
+ {
+ $this->options['via'] = $via;
+
+ return $this;
+ }
+
+ /**
+ * 保存记录 自动判断insert或者update
+ * @access public
+ * @param array $data 数据
+ * @param bool $forceInsert 是否强制insert
+ * @return integer
+ */
+ public function save(array $data = [], bool $forceInsert = false)
+ {
+ if ($forceInsert) {
+ return $this->insert($data);
+ }
+
+ $this->options['data'] = array_merge($this->options['data'] ?? [], $data);
+
+ if (!empty($this->options['where'])) {
+ $isUpdate = true;
+ } else {
+ $isUpdate = $this->parseUpdateData($this->options['data']);
+ }
+
+ return $isUpdate ? $this->update() : $this->insert();
+ }
+
+ /**
+ * 插入记录
+ * @access public
+ * @param array $data 数据
+ * @param boolean $getLastInsID 返回自增主键
+ * @return integer|string
+ */
+ public function insert(array $data = [], bool $getLastInsID = false)
+ {
+ if (!empty($data)) {
+ $this->options['data'] = $data;
+ }
+
+ return $this->connection->insert($this, $getLastInsID);
+ }
+
+ /**
+ * 插入记录并获取自增ID
+ * @access public
+ * @param array $data 数据
+ * @return integer|string
+ */
+ public function insertGetId(array $data)
+ {
+ return $this->insert($data, true);
+ }
+
+ /**
+ * 批量插入记录
+ * @access public
+ * @param array $dataSet 数据集
+ * @param integer $limit 每次写入数据限制
+ * @return integer
+ */
+ public function insertAll(array $dataSet = [], int $limit = 0): int
+ {
+ if (empty($dataSet)) {
+ $dataSet = $this->options['data'] ?? [];
+ }
+
+ if (empty($limit) && !empty($this->options['limit']) && is_numeric($this->options['limit'])) {
+ $limit = (int) $this->options['limit'];
+ }
+
+ return $this->connection->insertAll($this, $dataSet, $limit);
+ }
+
+ /**
+ * 通过Select方式插入记录
+ * @access public
+ * @param array $fields 要插入的数据表字段名
+ * @param string $table 要插入的数据表名
+ * @return integer
+ */
+ public function selectInsert(array $fields, string $table): int
+ {
+ return $this->connection->selectInsert($this, $fields, $table);
+ }
+
+ /**
+ * 更新记录
+ * @access public
+ * @param mixed $data 数据
+ * @return integer
+ * @throws Exception
+ */
+ public function update(array $data = []): int
+ {
+ if (!empty($data)) {
+ $this->options['data'] = array_merge($this->options['data'] ?? [], $data);
+ }
+
+ if (empty($this->options['where'])) {
+ $this->parseUpdateData($this->options['data']);
+ }
+
+ if (empty($this->options['where']) && $this->model) {
+ $this->where($this->model->getWhere());
+ }
+
+ if (empty($this->options['where'])) {
+ // 如果没有任何更新条件则不执行
+ throw new Exception('miss update condition');
+ }
+
+ return $this->connection->update($this);
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param mixed $data 表达式 true 表示强制删除
+ * @return int
+ * @throws Exception
+ */
+ public function delete($data = null): int
+ {
+ if (!is_null($data) && true !== $data) {
+ // AR模式分析主键条件
+ $this->parsePkWhere($data);
+ }
+
+ if (empty($this->options['where']) && $this->model) {
+ $this->where($this->model->getWhere());
+ }
+
+ if (true !== $data && empty($this->options['where'])) {
+ // 如果条件为空 不进行删除操作 除非设置 1=1
+ throw new Exception('delete without condition');
+ }
+
+ if (!empty($this->options['soft_delete'])) {
+ // 软删除
+ list($field, $condition) = $this->options['soft_delete'];
+ if ($condition) {
+ unset($this->options['soft_delete']);
+ $this->options['data'] = [$field => $condition];
+
+ return $this->connection->update($this);
+ }
+ }
+
+ $this->options['data'] = $data;
+
+ return $this->connection->delete($this);
+ }
+
+ /**
+ * 查找记录
+ * @access public
+ * @param mixed $data 数据
+ * @return Collection|array|static[]
+ * @throws Exception
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ public function select($data = null): Collection
+ {
+ if (!is_null($data)) {
+ // 主键条件分析
+ $this->parsePkWhere($data);
+ }
+
+ $resultSet = $this->connection->select($this);
+
+ // 返回结果处理
+ if (!empty($this->options['fail']) && count($resultSet) == 0) {
+ $this->throwNotFound();
+ }
+
+ // 数据列表读取后的处理
+ if (!empty($this->model)) {
+ // 生成模型对象
+ $resultSet = $this->resultSetToModelCollection($resultSet);
+ } else {
+ $this->resultSet($resultSet);
+ }
+
+ return $resultSet;
+ }
+
+ /**
+ * 查找单条记录
+ * @access public
+ * @param mixed $data 查询数据
+ * @return array|Model|null|static|mixed
+ * @throws Exception
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ public function find($data = null)
+ {
+ if (!is_null($data)) {
+ // AR模式分析主键条件
+ $this->parsePkWhere($data);
+ }
+
+ if (empty($this->options['where']) && empty($this->options['order'])) {
+ $result = [];
+ } else {
+ $result = $this->connection->find($this);
+ }
+
+ // 数据处理
+ if (empty($result)) {
+ return $this->resultToEmpty();
+ }
+
+ if (!empty($this->model)) {
+ // 返回模型对象
+ $this->resultToModel($result);
+ } else {
+ $this->result($result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 分析表达式(可用于查询或者写入操作)
+ * @access public
+ * @return array
+ */
+ public function parseOptions(): array
+ {
+ $options = $this->getOptions();
+
+ // 获取数据表
+ if (empty($options['table'])) {
+ $options['table'] = $this->getTable();
+ }
+
+ if (!isset($options['where'])) {
+ $options['where'] = [];
+ } elseif (isset($options['view'])) {
+ // 视图查询条件处理
+ $this->parseView($options);
+ }
+
+ foreach (['data', 'order', 'join', 'union', 'filter', 'json', 'with_attr', 'with_relation_attr'] as $name) {
+ if (!isset($options[$name])) {
+ $options[$name] = [];
+ }
+ }
+
+ if (!isset($options['strict'])) {
+ $options['strict'] = $this->connection->getConfig('fields_strict');
+ }
+
+ foreach (['master', 'lock', 'fetch_sql', 'array', 'distinct', 'procedure', 'with_cache'] as $name) {
+ if (!isset($options[$name])) {
+ $options[$name] = false;
+ }
+ }
+
+ foreach (['group', 'having', 'limit', 'force', 'comment', 'partition', 'duplicate', 'extra'] as $name) {
+ if (!isset($options[$name])) {
+ $options[$name] = '';
+ }
+ }
+
+ if (isset($options['page'])) {
+ // 根据页数计算limit
+ [$page, $listRows] = $options['page'];
+ $page = $page > 0 ? $page : 1;
+ $listRows = $listRows ?: (is_numeric($options['limit']) ? $options['limit'] : 20);
+ $offset = $listRows * ($page - 1);
+ $options['limit'] = $offset . ',' . $listRows;
+ }
+
+ $this->options = $options;
+
+ return $options;
+ }
+
+ /**
+ * 分析数据是否存在更新条件
+ * @access public
+ * @param array $data 数据
+ * @return bool
+ * @throws Exception
+ */
+ public function parseUpdateData(&$data): bool
+ {
+ $pk = $this->getPk();
+ $isUpdate = false;
+ // 如果存在主键数据 则自动作为更新条件
+ if (is_string($pk) && isset($data[$pk])) {
+ $this->where($pk, '=', $data[$pk]);
+ $this->options['key'] = $data[$pk];
+ unset($data[$pk]);
+ $isUpdate = true;
+ } elseif (is_array($pk)) {
+ foreach ($pk as $field) {
+ if (isset($data[$field])) {
+ $this->where($field, '=', $data[$field]);
+ $isUpdate = true;
+ } else {
+ // 如果缺少复合主键数据则不执行
+ throw new Exception('miss complex primary data');
+ }
+ unset($data[$field]);
+ }
+ }
+
+ return $isUpdate;
+ }
+
+ /**
+ * 把主键值转换为查询条件 支持复合主键
+ * @access public
+ * @param array|string $data 主键数据
+ * @return void
+ * @throws Exception
+ */
+ public function parsePkWhere($data): void
+ {
+ $pk = $this->getPk();
+
+ if (is_string($pk)) {
+ // 获取数据表
+ if (empty($this->options['table'])) {
+ $this->options['table'] = $this->getTable();
+ }
+
+ $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table'];
+
+ if (!empty($this->options['alias'][$table])) {
+ $alias = $this->options['alias'][$table];
+ }
+
+ $key = isset($alias) ? $alias . '.' . $pk : $pk;
+ // 根据主键查询
+ if (is_array($data)) {
+ $this->where($key, 'in', $data);
+ } else {
+ $this->where($key, '=', $data);
+ $this->options['key'] = $data;
+ }
+ }
+ }
+
+ /**
+ * 获取模型的更新条件
+ * @access protected
+ * @param array $options 查询参数
+ */
+ protected function getModelUpdateCondition(array $options)
+ {
+ return $options['where']['AND'] ?? null;
+ }
+}
diff --git a/src/db/Builder.php b/src/db/Builder.php
index 3810dfefc4f36716daa610fcfa466690fcfe0d68..938ed4db3b2710881a5edf4e1f0a1f49cafcce93 100644
--- a/src/db/Builder.php
+++ b/src/db/Builder.php
@@ -2,27 +2,41 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db;
+use Closure;
use PDO;
-use think\Exception;
+use think\db\exception\DbException as Exception;
+/**
+ * Db Builder
+ */
abstract class Builder
{
- // connection对象实例
+ /**
+ * Connection对象
+ * @var ConnectionInterface
+ */
protected $connection;
- // 查询表达式映射
- protected $exp = ['EQ' => '=', 'NEQ' => '<>', 'GT' => '>', 'EGT' => '>=', 'LT' => '<', 'ELT' => '<=', 'NOTLIKE' => 'NOT LIKE', 'NOTIN' => 'NOT IN', 'NOTBETWEEN' => 'NOT BETWEEN', 'NOTEXISTS' => 'NOT EXISTS', 'NOTNULL' => 'NOT NULL', 'NOTBETWEEN TIME' => 'NOT BETWEEN TIME'];
+ /**
+ * 查询表达式映射
+ * @var array
+ */
+ protected $exp = ['NOTLIKE' => 'NOT LIKE', 'NOTIN' => 'NOT IN', 'NOTBETWEEN' => 'NOT BETWEEN', 'NOTEXISTS' => 'NOT EXISTS', 'NOTNULL' => 'NOT NULL', 'NOTBETWEEN TIME' => 'NOT BETWEEN TIME'];
- // 查询表达式解析
+ /**
+ * 查询表达式解析
+ * @var array
+ */
protected $parser = [
'parseCompare' => ['=', '<>', '>', '>=', '<', '<='],
'parseLike' => ['LIKE', 'NOT LIKE'],
@@ -36,23 +50,42 @@ abstract class Builder
'parseColumn' => ['COLUMN'],
];
- // SQL表达式
- protected $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT%%LOCK%%COMMENT%';
+ /**
+ * SELECT SQL表达式
+ * @var string
+ */
+ protected $selectSql = 'SELECT%DISTINCT%%EXTRA% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT% %LOCK%%COMMENT%';
- protected $insertSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
+ /**
+ * INSERT SQL表达式
+ * @var string
+ */
+ protected $insertSql = '%INSERT%%EXTRA% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
- protected $insertAllSql = '%INSERT% INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
+ /**
+ * INSERT ALL SQL表达式
+ * @var string
+ */
+ protected $insertAllSql = '%INSERT%%EXTRA% INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
- protected $updateSql = 'UPDATE %TABLE% SET %SET% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%';
+ /**
+ * UPDATE SQL表达式
+ * @var string
+ */
+ protected $updateSql = 'UPDATE%EXTRA% %TABLE% SET %SET%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%';
- protected $deleteSql = 'DELETE FROM %TABLE% %USING% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%';
+ /**
+ * DELETE SQL表达式
+ * @var string
+ */
+ protected $deleteSql = 'DELETE%EXTRA% FROM %TABLE%%USING%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%';
/**
* 架构函数
* @access public
- * @param Connection $connection 数据库连接对象实例
+ * @param ConnectionInterface $connection 数据库连接对象实例
*/
- public function __construct(Connection $connection)
+ public function __construct(ConnectionInterface $connection)
{
$this->connection = $connection;
}
@@ -60,9 +93,9 @@ abstract class Builder
/**
* 获取当前的连接对象实例
* @access public
- * @return Connection
+ * @return ConnectionInterface
*/
- public function getConnection()
+ public function getConnection(): ConnectionInterface
{
return $this->connection;
}
@@ -70,11 +103,11 @@ abstract class Builder
/**
* 注册查询表达式解析
* @access public
- * @param string $name 解析方法
- * @param array $parser 匹配表达式数据
+ * @param string $name 解析方法
+ * @param array $parser 匹配表达式数据
* @return $this
*/
- public function bindParser($name, $parser)
+ public function bindParser(string $name, array $parser)
{
$this->parser[$name] = $parser;
return $this;
@@ -83,13 +116,13 @@ abstract class Builder
/**
* 数据分析
* @access protected
- * @param Query $query 查询对象
- * @param array $data 数据
- * @param array $fields 字段信息
- * @param array $bind 参数绑定
+ * @param Query $query 查询对象
+ * @param array $data 数据
+ * @param array $fields 字段信息
+ * @param array $bind 参数绑定
* @return array
*/
- protected function parseData(Query $query, $data = [], $fields = [], $bind = [])
+ protected function parseData(Query $query, array $data = [], array $fields = [], array $bind = []): array
{
if (empty($data)) {
return [];
@@ -99,11 +132,11 @@ abstract class Builder
// 获取绑定信息
if (empty($bind)) {
- $bind = $this->connection->getFieldsBind($options['table']);
+ $bind = $query->getFieldsBindType();
}
if (empty($fields)) {
- if ('*' == $options['field']) {
+ if (empty($options['field']) || '*' == $options['field']) {
$fields = array_keys($bind);
} else {
$fields = $options['field'];
@@ -113,33 +146,26 @@ abstract class Builder
$result = [];
foreach ($data as $key => $val) {
- if ('*' != $options['field'] && !in_array($key, $fields, true)) {
- continue;
- }
-
$item = $this->parseKey($query, $key, true);
- if ($val instanceof Expression) {
- $result[$item] = $val->getValue();
+ if ($val instanceof Raw) {
+ $result[$item] = $this->parseRaw($query, $val);
continue;
- } elseif (!is_scalar($val) && (in_array($key, (array) $query->getOptions('json')) || 'json' == $this->connection->getFieldsType($options['table'], $key))) {
- $val = json_encode($val, JSON_UNESCAPED_UNICODE);
- } elseif (is_object($val) && method_exists($val, '__toString')) {
- // 对象数据写入
- $val = $val->__toString();
+ } elseif (!is_scalar($val) && (in_array($key, (array) $query->getOptions('json')) || 'json' == $query->getFieldType($key))) {
+ $val = json_encode($val);
}
if (false !== strpos($key, '->')) {
- list($key, $name) = explode('->', $key);
- $item = $this->parseKey($query, $key);
- $result[$item] = 'json_set(' . $item . ', \'$.' . $name . '\', ' . $this->parseDataBind($query, $key, $val, $bind) . ')';
+ [$key, $name] = explode('->', $key, 2);
+ $item = $this->parseKey($query, $key);
+ $result[$item] = 'json_set(' . $item . ', \'$.' . $name . '\', ' . $this->parseDataBind($query, $key . '->' . $name, $val, $bind) . ')';
} elseif (false === strpos($key, '.') && !in_array($key, $fields, true)) {
if ($options['strict']) {
throw new Exception('fields not exists:[' . $key . ']');
}
} elseif (is_null($val)) {
$result[$item] = 'NULL';
- } elseif (is_array($val) && !empty($val)) {
+ } elseif (is_array($val) && !empty($val) && is_string($val[0])) {
switch (strtoupper($val[0])) {
case 'INC':
$result[$item] = $item . ' + ' . floatval($val[1]);
@@ -147,8 +173,6 @@ abstract class Builder
case 'DEC':
$result[$item] = $item . ' - ' . floatval($val[1]);
break;
- case 'EXP':
- throw new Exception('not support data:[' . $val[0] . ']');
}
} elseif (is_scalar($val)) {
// 过滤非标量数据
@@ -162,19 +186,19 @@ abstract class Builder
/**
* 数据绑定处理
* @access protected
- * @param Query $query 查询对象
- * @param string $key 字段名
- * @param mixed $data 数据
- * @param array $bind 绑定数据
+ * @param Query $query 查询对象
+ * @param string $key 字段名
+ * @param mixed $data 数据
+ * @param array $bind 绑定数据
* @return string
*/
- protected function parseDataBind(Query $query, $key, $data, $bind = [])
+ protected function parseDataBind(Query $query, string $key, $data, array $bind = []): string
{
- if ($data instanceof Expression) {
- return $data->getValue();
+ if ($data instanceof Raw) {
+ return $this->parseRaw($query, $data);
}
- $name = $query->bind($data, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
+ $name = $query->bindValue($data, $bind[$key] ?? PDO::PARAM_STR);
return ':' . $name;
}
@@ -187,28 +211,40 @@ abstract class Builder
* @param bool $strict 严格检测
* @return string
*/
- public function parseKey(Query $query, $key, $strict = false)
+ public function parseKey(Query $query, $key, bool $strict = false): string
{
- return $key instanceof Expression ? $key->getValue() : $key;
+ return $key;
+ }
+
+ /**
+ * 查询额外参数分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $extra 额外参数
+ * @return string
+ */
+ protected function parseExtra(Query $query, string $extra): string
+ {
+ return preg_match('/^[\w]+$/i', $extra) ? ' ' . strtoupper($extra) : '';
}
/**
* field分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $fields
+ * @param Query $query 查询对象
+ * @param mixed $fields 字段名
* @return string
*/
- protected function parseField(Query $query, $fields)
+ protected function parseField(Query $query, $fields): string
{
- if ('*' == $fields || empty($fields)) {
- $fieldsStr = '*';
- } elseif (is_array($fields)) {
+ if (is_array($fields)) {
// 支持 'field1'=>'field2' 这样的字段别名定义
$array = [];
foreach ($fields as $key => $field) {
- if (!is_numeric($key)) {
+ if ($field instanceof Raw) {
+ $array[] = $this->parseRaw($query, $field);
+ } elseif (!is_numeric($key)) {
$array[] = $this->parseKey($query, $key) . ' AS ' . $this->parseKey($query, $field, true);
} else {
$array[] = $this->parseKey($query, $field);
@@ -216,6 +252,8 @@ abstract class Builder
}
$fieldsStr = implode(',', $array);
+ } else {
+ $fieldsStr = '*';
}
return $fieldsStr;
@@ -224,27 +262,24 @@ abstract class Builder
/**
* table分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $tables
+ * @param Query $query 查询对象
+ * @param mixed $tables 表名
* @return string
*/
- protected function parseTable(Query $query, $tables)
+ protected function parseTable(Query $query, $tables): string
{
$item = [];
$options = $query->getOptions();
foreach ((array) $tables as $key => $table) {
- if (!is_numeric($key)) {
- $key = $this->connection->parseSqlTable($key);
+ if ($table instanceof Raw) {
+ $item[] = $this->parseRaw($query, $table);
+ } elseif (!is_numeric($key)) {
$item[] = $this->parseKey($query, $key) . ' ' . $this->parseKey($query, $table);
+ } elseif (isset($options['alias'][$table])) {
+ $item[] = $this->parseKey($query, $table) . ' ' . $this->parseKey($query, $options['alias'][$table]);
} else {
- $table = $this->connection->parseSqlTable($table);
-
- if (isset($options['alias'][$table])) {
- $item[] = $this->parseKey($query, $table) . ' ' . $this->parseKey($query, $options['alias'][$table]);
- } else {
- $item[] = $this->parseKey($query, $table);
- }
+ $item[] = $this->parseKey($query, $table);
}
}
@@ -254,22 +289,22 @@ abstract class Builder
/**
* where分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $where 查询条件
+ * @param Query $query 查询对象
+ * @param mixed $where 查询条件
* @return string
*/
- protected function parseWhere(Query $query, $where)
+ protected function parseWhere(Query $query, array $where): string
{
$options = $query->getOptions();
$whereStr = $this->buildWhere($query, $where);
if (!empty($options['soft_delete'])) {
// 附加软删除条件
- list($field, $condition) = $options['soft_delete'];
+ [$field, $condition] = $options['soft_delete'];
- $binds = $this->connection->getFieldsBind($options['table']);
+ $binds = $query->getFieldsBindType();
$whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : '';
- $whereStr = $whereStr . $this->parseWhereItem($query, $field, $condition, '', $binds);
+ $whereStr = $whereStr . $this->parseWhereItem($query, $field, $condition, $binds);
}
return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
@@ -278,181 +313,249 @@ abstract class Builder
/**
* 生成查询条件SQL
* @access public
- * @param Query $query 查询对象
- * @param mixed $where
- * @param array $options
+ * @param Query $query 查询对象
+ * @param mixed $where 查询条件
* @return string
*/
- public function buildWhere(Query $query, $where)
+ public function buildWhere(Query $query, array $where): string
{
if (empty($where)) {
$where = [];
}
$whereStr = '';
- $binds = $this->connection->getFieldsBind($query->getOptions('table'));
+
+ $binds = $query->getFieldsBindType();
foreach ($where as $logic => $val) {
- $str = [];
+ $str = $this->parseWhereLogic($query, $logic, $val, $binds);
- foreach ($val as $value) {
- if ($value instanceof Expression) {
- $str[] = ' ' . $logic . ' ( ' . $value->getValue() . ' )';
- continue;
- }
+ $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($logic) + 1) : implode(' ', $str);
+ }
+
+ return $whereStr;
+ }
- if (is_array($value)) {
- if (key($value) !== 0) {
- throw new Exception('where express error:' . var_export($value, true));
- }
- $field = array_shift($value);
- } elseif (!($value instanceof \Closure)) {
+ /**
+ * 不同字段使用相同查询条件(AND)
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $logic Logic
+ * @param array $val 查询条件
+ * @param array $binds 参数绑定
+ * @return array
+ */
+ protected function parseWhereLogic(Query $query, string $logic, array $val, array $binds = []): array
+ {
+ $where = [];
+ foreach ($val as $value) {
+ if ($value instanceof Raw) {
+ $where[] = ' ' . $logic . ' ( ' . $this->parseRaw($query, $value) . ' )';
+ continue;
+ }
+
+ if (is_array($value)) {
+ if (key($value) !== 0) {
throw new Exception('where express error:' . var_export($value, true));
}
+ $field = array_shift($value);
+ } elseif (true === $value) {
+ $where[] = ' ' . $logic . ' 1 ';
+ continue;
+ } elseif (!($value instanceof Closure)) {
+ throw new Exception('where express error:' . var_export($value, true));
+ }
- if ($value instanceof \Closure) {
- // 使用闭包查询
- $newQuery = $query->newQuery()->setConnection($this->connection);
- $value($newQuery);
- $whereClause = $this->buildWhere($query, $newQuery->getOptions('where'));
-
- if (!empty($whereClause)) {
- $str[] = ' ' . $logic . ' ( ' . $whereClause . ' )';
- }
- } elseif (is_array($field)) {
- array_unshift($value, $field);
- $str2 = [];
- foreach ($value as $item) {
- $str2[] = $this->parseWhereItem($query, array_shift($item), $item, $logic, $binds);
- }
-
- $str[] = ' ' . $logic . ' ( ' . implode(' AND ', $str2) . ' )';
- } elseif (strpos($field, '|')) {
- // 不同字段使用相同查询条件(OR)
- $array = explode('|', $field);
- $item = [];
-
- foreach ($array as $k) {
- $item[] = $this->parseWhereItem($query, $k, $value, '', $binds);
- }
-
- $str[] = ' ' . $logic . ' ( ' . implode(' OR ', $item) . ' )';
- } elseif (strpos($field, '&')) {
- // 不同字段使用相同查询条件(AND)
- $array = explode('&', $field);
- $item = [];
-
- foreach ($array as $k) {
- $item[] = $this->parseWhereItem($query, $k, $value, '', $binds);
- }
-
- $str[] = ' ' . $logic . ' ( ' . implode(' AND ', $item) . ' )';
- } else {
- // 对字段使用表达式查询
- $field = is_string($field) ? $field : '';
- $str[] = ' ' . $logic . ' ' . $this->parseWhereItem($query, $field, $value, $logic, $binds);
+ if ($value instanceof Closure) {
+ // 使用闭包查询
+ $whereClosureStr = $this->parseClosureWhere($query, $value, $logic);
+ if ($whereClosureStr) {
+ $where[] = $whereClosureStr;
}
+ } elseif (is_array($field)) {
+ $where[] = $this->parseMultiWhereField($query, $value, $field, $logic, $binds);
+ } elseif ($field instanceof Raw) {
+ $where[] = ' ' . $logic . ' ' . $this->parseWhereItem($query, $field, $value, $binds);
+ } elseif (strpos($field, '|')) {
+ $where[] = $this->parseFieldsOr($query, $value, $field, $logic, $binds);
+ } elseif (strpos($field, '&')) {
+ $where[] = $this->parseFieldsAnd($query, $value, $field, $logic, $binds);
+ } else {
+ // 对字段使用表达式查询
+ $field = is_string($field) ? $field : '';
+ $where[] = ' ' . $logic . ' ' . $this->parseWhereItem($query, $field, $value, $binds);
}
+ }
- $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($logic) + 1) : implode(' ', $str);
+ return $where;
+ }
+
+ /**
+ * 不同字段使用相同查询条件(AND)
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $value 查询条件
+ * @param string $field 查询字段
+ * @param string $logic Logic
+ * @param array $binds 参数绑定
+ * @return string
+ */
+ protected function parseFieldsAnd(Query $query, $value, string $field, string $logic, array $binds): string
+ {
+ $item = [];
+
+ foreach (explode('&', $field) as $k) {
+ $item[] = $this->parseWhereItem($query, $k, $value, $binds);
}
- return $whereStr;
+ return ' ' . $logic . ' ( ' . implode(' AND ', $item) . ' )';
}
- // where子单元分析
- protected function parseWhereItem(Query $query, $field, $val, $rule = '', $binds = [])
+ /**
+ * 不同字段使用相同查询条件(OR)
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $value 查询条件
+ * @param string $field 查询字段
+ * @param string $logic Logic
+ * @param array $binds 参数绑定
+ * @return string
+ */
+ protected function parseFieldsOr(Query $query, $value, string $field, string $logic, array $binds): string
{
- // 字段分析
- $key = $field ? $this->parseKey($query, $field, true) : '';
+ $item = [];
- // 查询规则和条件
- if (!is_array($val)) {
- $val = is_null($val) ? ['NULL', ''] : ['=', $val];
+ foreach (explode('|', $field) as $k) {
+ $item[] = $this->parseWhereItem($query, $k, $value, $binds);
}
- list($exp, $value) = $val;
+ return ' ' . $logic . ' ( ' . implode(' OR ', $item) . ' )';
+ }
- // 对一个字段使用多个查询条件
- if (is_array($exp)) {
- $item = array_pop($val);
+ /**
+ * 闭包查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param Closure $value 查询条件
+ * @param string $logic Logic
+ * @return string
+ */
+ protected function parseClosureWhere(Query $query, Closure $value, string $logic): string
+ {
+ $newQuery = $query->newQuery();
+ $value($newQuery);
+ $whereClosure = $this->buildWhere($newQuery, $newQuery->getOptions('where') ?: []);
- // 传入 or 或者 and
- if (is_string($item) && in_array($item, ['AND', 'and', 'OR', 'or'])) {
- $rule = $item;
- } else {
- array_push($val, $item);
- }
+ if (!empty($whereClosure)) {
+ $query->bind($newQuery->getBind(false));
+ $where = ' ' . $logic . ' ( ' . $whereClosure . ' )';
+ }
- foreach ($val as $k => $item) {
- $str[] = $this->parseWhereItem($query, $field, $item, $rule, $binds);
- }
+ return $where ?? '';
+ }
- return '( ' . implode(' ' . $rule . ' ', $str) . ' )';
+ /**
+ * 复合条件查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $value 查询条件
+ * @param mixed $field 查询字段
+ * @param string $logic Logic
+ * @param array $binds 参数绑定
+ * @return string
+ */
+ protected function parseMultiWhereField(Query $query, $value, $field, string $logic, array $binds): string
+ {
+ array_unshift($value, $field);
+
+ $where = [];
+ foreach ($value as $item) {
+ $where[] = $this->parseWhereItem($query, array_shift($item), $item, $binds);
}
+ return ' ' . $logic . ' ( ' . implode(' AND ', $where) . ' )';
+ }
+
+ /**
+ * where子单元分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $field 查询字段
+ * @param array $val 查询条件
+ * @param array $binds 参数绑定
+ * @return string
+ */
+ protected function parseWhereItem(Query $query, $field, array $val, array $binds = []): string
+ {
+ // 字段分析
+ $key = $field ? $this->parseKey($query, $field, true) : '';
+
+ [$exp, $value] = $val;
+
// 检测操作符
+ if (!is_string($exp)) {
+ throw new Exception('where express error:' . var_export($exp, true));
+ }
+
$exp = strtoupper($exp);
if (isset($this->exp[$exp])) {
$exp = $this->exp[$exp];
}
- if ($value instanceof Expression) {
+ if (is_string($field) && 'LIKE' != $exp) {
+ $bindType = $binds[$field] ?? PDO::PARAM_STR;
+ } else {
+ $bindType = PDO::PARAM_STR;
+ }
+
+ if ($value instanceof Raw) {
} elseif (is_object($value) && method_exists($value, '__toString')) {
// 对象数据写入
$value = $value->__toString();
}
- if (strpos($field, '->')) {
- $jsonType = $query->getJsonFieldType($field);
- $bindType = $this->connection->getFieldBindType($jsonType);
- } else {
- $bindType = isset($binds[$field]) ? $binds[$field] : PDO::PARAM_STR;
- }
-
if (is_scalar($value) && !in_array($exp, ['EXP', 'NOT NULL', 'NULL', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) && strpos($exp, 'TIME') === false) {
- $name = $query->bind($value, $bindType);
- $value = ':' . $name;
+ if (is_string($value) && 0 === strpos($value, ':') && $query->isBind(substr($value, 1))) {
+ } else {
+ $name = $query->bindValue($value, $bindType);
+ $value = ':' . $name;
+ }
}
// 解析查询表达式
foreach ($this->parser as $fun => $parse) {
if (in_array($exp, $parse)) {
- $whereStr = $this->$fun($query, $key, $exp, $value, $field, $bindType, isset($val[2]) ? $val[2] : 'AND');
- break;
+ return $this->$fun($query, $key, $exp, $value, $field, $bindType, $val[2] ?? 'AND');
}
}
- if (!isset($whereStr)) {
- throw new Exception('where express error:' . $exp);
- }
-
- return $whereStr;
+ throw new Exception('where express error:' . $exp);
}
/**
* 模糊查询
* @access protected
- * @param Query $query 查询对象
- * @param string $key
- * @param string $exp
- * @param mixed $value
- * @param string $field
- * @param integer $bindType
- * @param string $logic
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param array $value
+ * @param string $field
+ * @param integer $bindType
+ * @param string $logic
* @return string
*/
- protected function parseLike(Query $query, $key, $exp, $value, $field, $bindType, $logic)
+ protected function parseLike(Query $query, string $key, string $exp, $value, $field, int $bindType, string $logic): string
{
// 模糊匹配
if (is_array($value)) {
+ $array = [];
foreach ($value as $item) {
- $name = $query->bind($item, $bindType);
+ $name = $query->bindValue($item, PDO::PARAM_STR);
$array[] = $key . ' ' . $exp . ' :' . $name;
}
- $whereStr = '(' . implode($array, ' ' . strtoupper($logic) . ' ') . ')';
+ $whereStr = '(' . implode(' ' . strtoupper($logic) . ' ', $array) . ')';
} else {
$whereStr = $key . ' ' . $exp . ' ' . $value;
}
@@ -463,55 +566,55 @@ abstract class Builder
/**
* 表达式查询
* @access protected
- * @param Query $query 查询对象
- * @param string $key
- * @param string $exp
- * @param Expression $value
- * @param string $field
- * @param integer $bindType
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param array $value
+ * @param string $field
+ * @param integer $bindType
* @return string
*/
- protected function parseExp(Query $query, $key, $exp, Expression $value, $field, $bindType)
+ protected function parseExp(Query $query, string $key, string $exp, Raw $value, string $field, int $bindType): string
{
// 表达式查询
- return '( ' . $key . ' ' . $value->getValue() . ' )';
+ return '( ' . $key . ' ' . $this->parseRaw($query, $value) . ' )';
}
/**
* 表达式查询
* @access protected
- * @param Query $query 查询对象
- * @param string $key
- * @param string $exp
- * @param array $value
- * @param string $field
- * @param integer $bindType
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param array $value
+ * @param string $field
+ * @param integer $bindType
* @return string
*/
- protected function parseColumn(Query $query, $key, $exp, array $value, $field, $bindType)
+ protected function parseColumn(Query $query, string $key, $exp, array $value, string $field, int $bindType): string
{
// 字段比较查询
- list($op, $field2) = $value;
+ [$op, $field] = $value;
- if (!in_array($op, ['=', '<>', '>', '>=', '<', '<='])) {
+ if (!in_array(trim($op), ['=', '<>', '>', '>=', '<', '<='])) {
throw new Exception('where express error:' . var_export($value, true));
}
- return '( ' . $key . ' ' . $op . ' ' . $this->parseKey($query, $field2, true) . ' )';
+ return '( ' . $key . ' ' . $op . ' ' . $this->parseKey($query, $field, true) . ' )';
}
/**
* Null查询
* @access protected
- * @param Query $query 查询对象
- * @param string $key
- * @param string $exp
- * @param mixed $value
- * @param string $field
- * @param integer $bindType
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
* @return string
*/
- protected function parseNull(Query $query, $key, $exp, $value, $field, $bindType)
+ protected function parseNull(Query $query, string $key, string $exp, $value, $field, int $bindType): string
{
// NULL 查询
return $key . ' IS ' . $exp;
@@ -520,21 +623,21 @@ abstract class Builder
/**
* 范围查询
* @access protected
- * @param Query $query 查询对象
- * @param string $key
- * @param string $exp
- * @param mixed $value
- * @param string $field
- * @param integer $bindType
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
* @return string
*/
- protected function parseBetween(Query $query, $key, $exp, $value, $field, $bindType)
+ protected function parseBetween(Query $query, string $key, string $exp, $value, $field, int $bindType): string
{
// BETWEEN 查询
$data = is_array($value) ? $value : explode(',', $value);
- $min = $query->bind($data[0], $bindType);
- $max = $query->bind($data[1], $bindType);
+ $min = $query->bindValue($data[0], $bindType);
+ $max = $query->bindValue($data[1], $bindType);
return $key . ' ' . $exp . ' :' . $min . ' AND :' . $max . ' ';
}
@@ -542,40 +645,40 @@ abstract class Builder
/**
* Exists查询
* @access protected
- * @param Query $query 查询对象
- * @param string $key
- * @param string $exp
- * @param mixed $value
- * @param string $field
- * @param integer $bindType
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
* @return string
*/
- protected function parseExists(Query $query, $key, $exp, $value, $field, $bindType)
+ protected function parseExists(Query $query, string $key, string $exp, $value, string $field, int $bindType): string
{
// EXISTS 查询
- if ($value instanceof \Closure) {
+ if ($value instanceof Closure) {
$value = $this->parseClosure($query, $value, false);
- } elseif ($value instanceof Expression) {
- $value = $value->getValue();
+ } elseif ($value instanceof Raw) {
+ $value = $this->parseRaw($query, $value);
} else {
throw new Exception('where express error:' . $value);
}
- return $exp . ' (' . $value . ')';
+ return $exp . ' ( ' . $value . ' )';
}
/**
* 时间比较查询
* @access protected
- * @param Query $query 查询对象
- * @param string $key
- * @param string $exp
- * @param mixed $value
- * @param string $field
- * @param integer $bindType
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
* @return string
*/
- protected function parseTime(Query $query, $key, $exp, $value, $field, $bindType)
+ protected function parseTime(Query $query, string $key, string $exp, $value, $field, int $bindType): string
{
return $key . ' ' . substr($exp, 0, 2) . ' ' . $this->parseDateTime($query, $value, $field, $bindType);
}
@@ -583,23 +686,29 @@ abstract class Builder
/**
* 大小比较查询
* @access protected
- * @param Query $query 查询对象
- * @param string $key
- * @param string $exp
- * @param mixed $value
- * @param string $field
- * @param integer $bindType
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
* @return string
*/
- protected function parseCompare(Query $query, $key, $exp, $value, $field, $bindType)
+ protected function parseCompare(Query $query, string $key, string $exp, $value, $field, int $bindType): string
{
if (is_array($value)) {
throw new Exception('where express error:' . $exp . var_export($value, true));
}
// 比较运算
- if ($value instanceof \Closure) {
+ if ($value instanceof Closure) {
$value = $this->parseClosure($query, $value);
+ } elseif ($value instanceof Raw) {
+ $value = $this->parseRaw($query, $value);
+ }
+
+ if ('=' == $exp && is_null($value)) {
+ return $key . ' IS NULL';
}
return $key . ' ' . $exp . ' ' . $value;
@@ -608,15 +717,15 @@ abstract class Builder
/**
* 时间范围查询
* @access protected
- * @param Query $query 查询对象
- * @param string $key
- * @param string $exp
- * @param mixed $value
- * @param string $field
- * @param integer $bindType
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
* @return string
*/
- protected function parseBetweenTime(Query $query, $key, $exp, $value, $field, $bindType)
+ protected function parseBetweenTime(Query $query, string $key, string $exp, $value, $field, int $bindType): string
{
if (is_string($value)) {
$value = explode(',', $value);
@@ -632,32 +741,38 @@ abstract class Builder
/**
* IN查询
* @access protected
- * @param Query $query 查询对象
- * @param string $key
- * @param string $exp
- * @param mixed $value
- * @param string $field
- * @param integer $bindType
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
* @return string
*/
- protected function parseIn(Query $query, $key, $exp, $value, $field, $bindType)
+ protected function parseIn(Query $query, string $key, string $exp, $value, $field, int $bindType): string
{
// IN 查询
- if ($value instanceof \Closure) {
+ if ($value instanceof Closure) {
$value = $this->parseClosure($query, $value, false);
+ } elseif ($value instanceof Raw) {
+ $value = $this->parseRaw($query, $value);
} else {
- $value = array_unique(is_array($value) ? $value : explode(',', $value));
-
+ $value = array_unique(is_array($value) ? $value : explode(',', (string) $value));
+ if (count($value) === 0) {
+ return 'IN' == $exp ? '0 = 1' : '1 = 1';
+ }
$array = [];
- foreach ($value as $k => $v) {
- $name = $query->bind($v, $bindType);
+ foreach ($value as $v) {
+ $name = $query->bindValue($v, $bindType);
$array[] = ':' . $name;
}
- $zone = implode(',', $array);
-
- $value = empty($zone) ? "''" : $zone;
+ if (count($array) == 1) {
+ return $key . ('IN' == $exp ? ' = ' : ' <> ') . $array[0];
+ } else {
+ $value = implode(',', $array);
+ }
}
return $key . ' ' . $exp . ' (' . $value . ')';
@@ -666,14 +781,14 @@ abstract class Builder
/**
* 闭包子查询
* @access protected
- * @param Query $query 查询对象
- * @param \Closure $call
- * @param bool $show
+ * @param Query $query 查询对象
+ * @param \Closure $call
+ * @param bool $show
* @return string
*/
- protected function parseClosure(Query $query, $call, $show = true)
+ protected function parseClosure(Query $query, Closure $call, bool $show = true): string
{
- $newQuery = $query->newQuery()->setConnection($this->connection);
+ $newQuery = $query->newQuery()->removeOption();
$call($newQuery);
return $newQuery->buildSql($show);
@@ -682,19 +797,19 @@ abstract class Builder
/**
* 日期时间条件解析
* @access protected
- * @param Query $query 查询对象
- * @param string $value
- * @param string $key
- * @param integer $bindType
+ * @param Query $query 查询对象
+ * @param mixed $value
+ * @param string $key
+ * @param integer $bindType
* @return string
*/
- protected function parseDateTime(Query $query, $value, $key, $bindType = null)
+ protected function parseDateTime(Query $query, $value, string $key, int $bindType): string
{
$options = $query->getOptions();
// 获取时间字段类型
if (strpos($key, '.')) {
- list($table, $key) = explode('.', $key);
+ [$table, $key] = explode('.', $key);
if (isset($options['alias']) && $pos = array_search($table, $options['alias'])) {
$table = $pos;
@@ -703,27 +818,25 @@ abstract class Builder
$table = $options['table'];
}
- $type = $this->connection->getTableInfo($table, 'type');
-
- if (isset($type[$key])) {
- $info = $type[$key];
- }
+ $type = $query->getFieldType($key);
- if (isset($info)) {
+ if ($type) {
if (is_string($value)) {
$value = strtotime($value) ?: $value;
}
- if (preg_match('/(datetime|timestamp)/is', $info)) {
- // 日期及时间戳类型
- $value = date('Y-m-d H:i:s', $value);
- } elseif (preg_match('/(date)/is', $info)) {
- // 日期及时间戳类型
- $value = date('Y-m-d', $value);
+ if (is_int($value)) {
+ if (preg_match('/(datetime|timestamp)/is', $type)) {
+ // 日期及时间戳类型
+ $value = date('Y-m-d H:i:s', $value);
+ } elseif (preg_match('/(date)/is', $type)) {
+ // 日期及时间戳类型
+ $value = date('Y-m-d', $value);
+ }
}
}
- $name = $query->bind($value, $bindType);
+ $name = $query->bindValue($value, $bindType);
return ':' . $name;
}
@@ -731,11 +844,11 @@ abstract class Builder
/**
* limit分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $limit
+ * @param Query $query 查询对象
+ * @param mixed $limit
* @return string
*/
- protected function parseLimit(Query $query, $limit)
+ protected function parseLimit(Query $query, string $limit): string
{
return (!empty($limit) && false === strpos($limit, '(')) ? ' LIMIT ' . $limit . ' ' : '';
}
@@ -743,35 +856,28 @@ abstract class Builder
/**
* join分析
* @access protected
- * @param Query $query 查询对象
- * @param array $join
+ * @param Query $query 查询对象
+ * @param array $join
* @return string
*/
- protected function parseJoin(Query $query, $join)
+ protected function parseJoin(Query $query, array $join): string
{
$joinStr = '';
- if (!empty($join)) {
- foreach ($join as $item) {
- list($table, $type, $on) = $item;
-
- $condition = [];
-
- foreach ((array) $on as $val) {
- if ($val instanceof Expression) {
- $condition[] = $val->getValue();
- } elseif (strpos($val, '=')) {
- list($val1, $val2) = explode('=', $val, 2);
- $condition[] = $this->parseKey($query, $val1) . '=' . $this->parseKey($query, $val2);
- } else {
- $condition[] = $val;
- }
- }
+ foreach ($join as $item) {
+ [$table, $type, $on] = $item;
- $table = $this->parseTable($query, $table);
+ if (strpos($on, '=')) {
+ [$val1, $val2] = explode('=', $on, 2);
- $joinStr .= ' ' . $type . ' JOIN ' . $table . ' ON ' . implode(' AND ', $condition);
+ $condition = $this->parseKey($query, $val1) . '=' . $this->parseKey($query, $val2);
+ } else {
+ $condition = $on;
}
+
+ $table = $this->parseTable($query, $table);
+
+ $joinStr .= ' ' . $type . ' JOIN ' . $table . ' ON ' . $condition;
}
return $joinStr;
@@ -780,22 +886,23 @@ abstract class Builder
/**
* order分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $order
+ * @param Query $query 查询对象
+ * @param array $order
* @return string
*/
- protected function parseOrder(Query $query, $order)
+ protected function parseOrder(Query $query, array $order): string
{
+ $array = [];
foreach ($order as $key => $val) {
- if ($val instanceof Expression) {
- $array[] = $val->getValue();
+ if ($val instanceof Raw) {
+ $array[] = $this->parseRaw($query, $val);
} elseif (is_array($val) && preg_match('/^[\w\.]+$/', $key)) {
$array[] = $this->parseOrderField($query, $key, $val);
} elseif ('[rand]' == $val) {
$array[] = $this->parseRand($query);
} elseif (is_string($val)) {
if (is_numeric($key)) {
- list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' ');
+ [$key, $sort] = explode(' ', strpos($val, ' ') ? $val : $val . ' ');
} else {
$sort = $val;
}
@@ -813,15 +920,45 @@ abstract class Builder
return empty($array) ? '' : ' ORDER BY ' . implode(',', $array);
}
+ /**
+ * 分析Raw对象
+ * @access protected
+ * @param Query $query 查询对象
+ * @param Raw $raw Raw对象
+ * @return string
+ */
+ protected function parseRaw(Query $query, Raw $raw): string
+ {
+ $sql = $raw->getValue();
+ $bind = $raw->getBind();
+
+ if ($bind) {
+ $query->bindParams($sql, $bind);
+ }
+
+ return $sql;
+ }
+
+ /**
+ * 随机排序
+ * @access protected
+ * @param Query $query 查询对象
+ * @return string
+ */
+ protected function parseRand(Query $query): string
+ {
+ return '';
+ }
+
/**
* orderField分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $key
- * @param array $val
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param array $val
* @return string
*/
- protected function parseOrderField($query, $key, $val)
+ protected function parseOrderField(Query $query, string $key, array $val): string
{
if (isset($val['sort'])) {
$sort = $val['sort'];
@@ -832,9 +969,7 @@ abstract class Builder
$sort = strtoupper($sort);
$sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : '';
-
- $options = $query->getOptions();
- $bind = $this->connection->getFieldsBind($options['table']);
+ $bind = $query->getFieldsBindType();
foreach ($val as $k => $item) {
$val[$k] = $this->parseDataBind($query, $key, $item, $bind);
@@ -846,23 +981,36 @@ abstract class Builder
/**
* group分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $group
+ * @param Query $query 查询对象
+ * @param mixed $group
* @return string
*/
- protected function parseGroup(Query $query, $group)
+ protected function parseGroup(Query $query, $group): string
{
- return !empty($group) ? ' GROUP BY ' . $this->parseKey($query, $group) : '';
+ if (empty($group)) {
+ return '';
+ }
+
+ if (is_string($group)) {
+ $group = explode(',', $group);
+ }
+
+ $val = [];
+ foreach ($group as $key) {
+ $val[] = $this->parseKey($query, $key);
+ }
+
+ return ' GROUP BY ' . implode(',', $val);
}
/**
* having分析
* @access protected
- * @param Query $query 查询对象
- * @param string $having
+ * @param Query $query 查询对象
+ * @param string $having
* @return string
*/
- protected function parseHaving(Query $query, $having)
+ protected function parseHaving(Query $query, string $having): string
{
return !empty($having) ? ' HAVING ' . $having : '';
}
@@ -870,11 +1018,11 @@ abstract class Builder
/**
* comment分析
* @access protected
- * @param Query $query 查询对象
+ * @param Query $query 查询对象
* @param string $comment
* @return string
*/
- protected function parseComment(Query $query, $comment)
+ protected function parseComment(Query $query, string $comment): string
{
if (false !== strpos($comment, '*/')) {
$comment = strstr($comment, '*/', true);
@@ -886,11 +1034,11 @@ abstract class Builder
/**
* distinct分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $distinct
+ * @param Query $query 查询对象
+ * @param mixed $distinct
* @return string
*/
- protected function parseDistinct(Query $query, $distinct)
+ protected function parseDistinct(Query $query, bool $distinct): string
{
return !empty($distinct) ? ' DISTINCT ' : '';
}
@@ -898,11 +1046,11 @@ abstract class Builder
/**
* union分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $union
+ * @param Query $query 查询对象
+ * @param array $union
* @return string
*/
- protected function parseUnion(Query $query, $union)
+ protected function parseUnion(Query $query, array $union): string
{
if (empty($union)) {
return '';
@@ -912,10 +1060,10 @@ abstract class Builder
unset($union['type']);
foreach ($union as $u) {
- if ($u instanceof \Closure) {
+ if ($u instanceof Closure) {
$sql[] = $type . ' ' . $this->parseClosure($query, $u);
} elseif (is_string($u)) {
- $sql[] = $type . ' ( ' . $this->connection->parseSqlTable($u) . ' )';
+ $sql[] = $type . ' ( ' . $u . ' )';
}
}
@@ -925,18 +1073,18 @@ abstract class Builder
/**
* index分析,可在操作链中指定需要强制使用的索引
* @access protected
- * @param Query $query 查询对象
- * @param mixed $index
+ * @param Query $query 查询对象
+ * @param mixed $index
* @return string
*/
- protected function parseForce(Query $query, $index)
+ protected function parseForce(Query $query, $index): string
{
if (empty($index)) {
return '';
}
if (is_array($index)) {
- $index = join(",", $index);
+ $index = join(',', $index);
}
return sprintf(" FORCE INDEX ( %s ) ", $index);
@@ -945,41 +1093,47 @@ abstract class Builder
/**
* 设置锁机制
* @access protected
- * @param Query $query 查询对象
- * @param bool|string $lock
+ * @param Query $query 查询对象
+ * @param bool|string $lock
* @return string
*/
- protected function parseLock(Query $query, $lock = false)
+ protected function parseLock(Query $query, $lock = false): string
{
if (is_bool($lock)) {
return $lock ? ' FOR UPDATE ' : '';
- } elseif (is_string($lock) && !empty($lock)) {
+ }
+
+ if (is_string($lock) && !empty($lock)) {
return ' ' . trim($lock) . ' ';
+ } else {
+ return '';
}
}
/**
* 生成查询SQL
* @access public
- * @param Query $query 查询对象
+ * @param Query $query 查询对象
+ * @param bool $one 是否仅获取一个记录
* @return string
*/
- public function select(Query $query)
+ public function select(Query $query, bool $one = false): string
{
$options = $query->getOptions();
return str_replace(
- ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
+ ['%TABLE%', '%DISTINCT%', '%EXTRA%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
[
$this->parseTable($query, $options['table']),
$this->parseDistinct($query, $options['distinct']),
- $this->parseField($query, $options['field']),
+ $this->parseExtra($query, $options['extra']),
+ $this->parseField($query, $options['field'] ?? '*'),
$this->parseJoin($query, $options['join']),
$this->parseWhere($query, $options['where']),
$this->parseGroup($query, $options['group']),
$this->parseHaving($query, $options['having']),
$this->parseOrder($query, $options['order']),
- $this->parseLimit($query, $options['limit']),
+ $this->parseLimit($query, $one ? '1' : $options['limit']),
$this->parseUnion($query, $options['union']),
$this->parseLock($query, $options['lock']),
$this->parseComment($query, $options['comment']),
@@ -991,11 +1145,10 @@ abstract class Builder
/**
* 生成Insert SQL
* @access public
- * @param Query $query 查询对象
- * @param bool $replace 是否replace
+ * @param Query $query 查询对象
* @return string
*/
- public function insert(Query $query, $replace = false)
+ public function insert(Query $query): string
{
$options = $query->getOptions();
@@ -1009,10 +1162,11 @@ abstract class Builder
$values = array_values($data);
return str_replace(
- ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
+ ['%INSERT%', '%TABLE%', '%EXTRA%', '%FIELD%', '%DATA%', '%COMMENT%'],
[
- $replace ? 'REPLACE' : 'INSERT',
+ !empty($options['replace']) ? 'REPLACE' : 'INSERT',
$this->parseTable($query, $options['table']),
+ $this->parseExtra($query, $options['extra']),
implode(' , ', $fields),
implode(' , ', $values),
$this->parseComment($query, $options['comment']),
@@ -1023,25 +1177,28 @@ abstract class Builder
/**
* 生成insertall SQL
* @access public
- * @param Query $query 查询对象
- * @param array $dataSet 数据集
- * @param bool $replace 是否replace
+ * @param Query $query 查询对象
+ * @param array $dataSet 数据集
* @return string
*/
- public function insertAll(Query $query, $dataSet, $replace = false)
+ public function insertAll(Query $query, array $dataSet): string
{
$options = $query->getOptions();
+ // 获取绑定信息
+ $bind = $query->getFieldsBindType();
+
// 获取合法的字段
- if ('*' == $options['field']) {
- $allowFields = $this->connection->getTableFields($options['table']);
+ if (empty($options['field']) || '*' == $options['field']) {
+ $allowFields = array_keys($bind);
} else {
$allowFields = $options['field'];
}
- // 获取绑定信息
- $bind = $this->connection->getFieldsBind($options['table']);
- foreach ($dataSet as $data) {
+ $fields = [];
+ $values = [];
+
+ foreach ($dataSet as $k => $data) {
$data = $this->parseData($query, $data, $allowFields, $bind);
$values[] = 'SELECT ' . implode(',', array_values($data));
@@ -1051,17 +1208,16 @@ abstract class Builder
}
}
- $fields = [];
-
foreach ($insertFields as $field) {
$fields[] = $this->parseKey($query, $field);
}
return str_replace(
- ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
+ ['%INSERT%', '%TABLE%', '%EXTRA%', '%FIELD%', '%DATA%', '%COMMENT%'],
[
- $replace ? 'REPLACE' : 'INSERT',
+ !empty($options['replace']) ? 'REPLACE' : 'INSERT',
$this->parseTable($query, $options['table']),
+ $this->parseExtra($query, $options['extra']),
implode(' , ', $fields),
implode(' UNION ALL ', $values),
$this->parseComment($query, $options['comment']),
@@ -1072,17 +1228,13 @@ abstract class Builder
/**
* 生成slect insert SQL
* @access public
- * @param Query $query 查询对象
- * @param array $fields 数据
- * @param string $table 数据表
+ * @param Query $query 查询对象
+ * @param array $fields 数据
+ * @param string $table 数据表
* @return string
*/
- public function selectInsert(Query $query, $fields, $table)
+ public function selectInsert(Query $query, array $fields, string $table): string
{
- if (is_string($fields)) {
- $fields = explode(',', $fields);
- }
-
foreach ($fields as &$field) {
$field = $this->parseKey($query, $field, true);
}
@@ -1093,28 +1245,29 @@ abstract class Builder
/**
* 生成update SQL
* @access public
- * @param Query $query 查询对象
+ * @param Query $query 查询对象
* @return string
*/
- public function update(Query $query)
+ public function update(Query $query): string
{
$options = $query->getOptions();
- $table = $this->parseTable($query, $options['table']);
- $data = $this->parseData($query, $options['data']);
+ $data = $this->parseData($query, $options['data']);
if (empty($data)) {
return '';
}
+ $set = [];
foreach ($data as $key => $val) {
$set[] = $key . ' = ' . $val;
}
return str_replace(
- ['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
+ ['%TABLE%', '%EXTRA%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
[
$this->parseTable($query, $options['table']),
+ $this->parseExtra($query, $options['extra']),
implode(' , ', $set),
$this->parseJoin($query, $options['join']),
$this->parseWhere($query, $options['where']),
@@ -1129,17 +1282,18 @@ abstract class Builder
/**
* 生成delete SQL
* @access public
- * @param Query $query 查询对象
+ * @param Query $query 查询对象
* @return string
*/
- public function delete(Query $query)
+ public function delete(Query $query): string
{
$options = $query->getOptions();
return str_replace(
- ['%TABLE%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
+ ['%TABLE%', '%EXTRA%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
[
$this->parseTable($query, $options['table']),
+ $this->parseExtra($query, $options['extra']),
!empty($options['using']) ? ' USING ' . $this->parseTable($query, $options['using']) . ' ' : '',
$this->parseJoin($query, $options['join']),
$this->parseWhere($query, $options['where']),
diff --git a/src/db/CacheItem.php b/src/db/CacheItem.php
new file mode 100644
index 0000000000000000000000000000000000000000..839f38409548f500803272b92129637cd842bebd
--- /dev/null
+++ b/src/db/CacheItem.php
@@ -0,0 +1,209 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use DateInterval;
+use DateTime;
+use DateTimeInterface;
+use think\db\exception\InvalidArgumentException;
+
+/**
+ * CacheItem实现类
+ */
+class CacheItem
+{
+ /**
+ * 缓存Key
+ * @var string
+ */
+ protected $key;
+
+ /**
+ * 缓存内容
+ * @var mixed
+ */
+ protected $value;
+
+ /**
+ * 过期时间
+ * @var int|DateTimeInterface
+ */
+ protected $expire;
+
+ /**
+ * 缓存tag
+ * @var string
+ */
+ protected $tag;
+
+ /**
+ * 缓存是否命中
+ * @var bool
+ */
+ protected $isHit = false;
+
+ public function __construct(string $key = null)
+ {
+ $this->key = $key;
+ }
+
+ /**
+ * 为此缓存项设置「键」
+ * @access public
+ * @param string $key
+ * @return $this
+ */
+ public function setKey(string $key)
+ {
+ $this->key = $key;
+ return $this;
+ }
+
+ /**
+ * 返回当前缓存项的「键」
+ * @access public
+ * @return string
+ */
+ public function getKey()
+ {
+ return $this->key;
+ }
+
+ /**
+ * 返回当前缓存项的有效期
+ * @access public
+ * @return DateTimeInterface|int|null
+ */
+ public function getExpire()
+ {
+ if ($this->expire instanceof DateTimeInterface) {
+ return $this->expire;
+ }
+
+ return $this->expire ? $this->expire - time() : null;
+ }
+
+ /**
+ * 获取缓存Tag
+ * @access public
+ * @return string|array
+ */
+ public function getTag()
+ {
+ return $this->tag;
+ }
+
+ /**
+ * 凭借此缓存项的「键」从缓存系统里面取出缓存项
+ * @access public
+ * @return mixed
+ */
+ public function get()
+ {
+ return $this->value;
+ }
+
+ /**
+ * 确认缓存项的检查是否命中
+ * @access public
+ * @return bool
+ */
+ public function isHit(): bool
+ {
+ return $this->isHit;
+ }
+
+ /**
+ * 为此缓存项设置「值」
+ * @access public
+ * @param mixed $value
+ * @return $this
+ */
+ public function set($value)
+ {
+ $this->value = $value;
+ $this->isHit = true;
+ return $this;
+ }
+
+ /**
+ * 为此缓存项设置所属标签
+ * @access public
+ * @param string|array $tag
+ * @return $this
+ */
+ public function tag($tag = null)
+ {
+ $this->tag = $tag;
+ return $this;
+ }
+
+ /**
+ * 设置缓存项的有效期
+ * @access public
+ * @param mixed $expire
+ * @return $this
+ */
+ public function expire($expire)
+ {
+ if (is_null($expire)) {
+ $this->expire = null;
+ } elseif (is_numeric($expire) || $expire instanceof DateInterval) {
+ $this->expiresAfter($expire);
+ } elseif ($expire instanceof DateTimeInterface) {
+ $this->expire = $expire;
+ } else {
+ throw new InvalidArgumentException('not support datetime');
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置缓存项的准确过期时间点
+ * @access public
+ * @param DateTimeInterface $expiration
+ * @return $this
+ */
+ public function expiresAt($expiration)
+ {
+ if ($expiration instanceof DateTimeInterface) {
+ $this->expire = $expiration;
+ } else {
+ throw new InvalidArgumentException('not support datetime');
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置缓存项的过期时间
+ * @access public
+ * @param int|DateInterval $timeInterval
+ * @return $this
+ * @throws InvalidArgumentException
+ */
+ public function expiresAfter($timeInterval)
+ {
+ if ($timeInterval instanceof DateInterval) {
+ $this->expire = (int) DateTime::createFromFormat('U', (string) time())->add($timeInterval)->format('U');
+ } elseif (is_numeric($timeInterval)) {
+ $this->expire = $timeInterval + time();
+ } else {
+ throw new InvalidArgumentException('not support datetime');
+ }
+
+ return $this;
+ }
+
+}
diff --git a/src/db/Connection.php b/src/db/Connection.php
index 05f0b0194fc15bd5cbfdbba95437aadce94f9c87..aa86ba8e591d06207d9b9b1f1313338aad60e7c9 100644
--- a/src/db/Connection.php
+++ b/src/db/Connection.php
@@ -2,2178 +2,346 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db;
-use Exception;
-use InvalidArgumentException;
-use PDO;
-use PDOStatement;
-use think\Db;
-use think\db\exception\BindParamException;
-use think\db\exception\PDOException;
+use Psr\SimpleCache\CacheInterface;
+use think\DbManager;
-abstract class Connection
+/**
+ * 数据库连接基础类
+ */
+abstract class Connection implements ConnectionInterface
{
- const PARAM_FLOAT = 21;
- protected static $instance = [];
- /** @var PDOStatement PDO操作实例 */
- protected $PDOStatement;
-
- /** @var string 当前SQL指令 */
- protected $queryStr = '';
- // 返回或者影响记录数
- protected $numRows = 0;
- // 事务指令数
- protected $transTimes = 0;
- // 错误信息
- protected $error = '';
-
- protected $queryStartTime;
- /** @var PDO[] 数据库连接ID 支持多个连接 */
- protected $links = [];
-
- /** @var PDO 当前连接ID */
- protected $linkID;
- protected $linkRead;
- protected $linkWrite;
- // 当前缓存对象
- protected $cache;
- // 查询结果类型
- protected $fetchType = PDO::FETCH_ASSOC;
- // 字段属性大小写
- protected $attrCase = PDO::CASE_LOWER;
- // 监听回调
- protected static $event = [];
-
- // 数据表信息
- protected static $info = [];
-
- // 数据库日志
- protected static $log = [];
-
- // 使用Builder类
- protected $builderClassName;
- // Builder对象
- protected $builder;
- // 数据库连接参数配置
- protected $config = [
- // 数据库类型
- 'type' => '',
- // 服务器地址
- 'hostname' => '',
- // 数据库名
- 'database' => '',
- // 用户名
- 'username' => '',
- // 密码
- 'password' => '',
- // 端口
- 'hostport' => '',
- // 连接dsn
- 'dsn' => '',
- // 数据库连接参数
- 'params' => [],
- // 数据库编码默认采用utf8
- 'charset' => 'utf8',
- // 数据库表前缀
- 'prefix' => '',
- // 数据库调试模式
- 'debug' => false,
- // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
- 'deploy' => 0,
- // 数据库读写是否分离 主从式有效
- 'rw_separate' => false,
- // 读写分离后 主服务器数量
- 'master_num' => 1,
- // 指定从服务器序号
- 'slave_no' => '',
- // 是否严格检查字段是否存在
- 'fields_strict' => true,
- // 数据集返回类型
- 'resultset_type' => '',
- // 自动写入时间戳字段
- 'auto_timestamp' => false,
- // 时间字段取出后的默认时间格式
- 'datetime_format' => 'Y-m-d H:i:s',
- // 是否需要进行SQL性能分析
- 'sql_explain' => false,
- // Builder类
- 'builder' => '',
- // Query类
- 'query' => '\\think\\db\\Query',
- // 是否需要断线重连
- 'break_reconnect' => false,
- // 数据字段缓存路径
- 'schema_path' => '',
- // 模型类后缀
- 'class_suffix' => false,
- ];
-
- // PDO连接参数
- protected $params = [
- PDO::ATTR_CASE => PDO::CASE_NATURAL,
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
- PDO::ATTR_STRINGIFY_FETCHES => false,
- PDO::ATTR_EMULATE_PREPARES => false,
- ];
-
- // 绑定参数
- protected $bind = [];
-
- /**
- * 架构函数 读取数据库配置信息
- * @access protected
- * @param array $config 数据库配置数组
- */
- protected function __construct(array $config = [])
- {
- if (!empty($config)) {
- $this->config = array_merge($this->config, $config);
- }
-
- // 创建Builder对象
- $class = $this->getBuilderClass();
-
- $this->builder = new $class($this);
- $this->cache = Db::getCacheHandler();
-
- // 执行初始化操作
- $this->initialize();
- }
-
- /**
- * 初始化
- * @access protected
- * @return void
- */
- protected function initialize()
- {}
-
- /**
- * 取得数据库连接类实例
- * @access public
- * @param mixed $config 连接配置
- * @param bool|string $name 连接标识 true 强制重新连接
- * @return Connection
- * @throws Exception
- */
- public static function instance($config = [], $name = false)
- {
- if (false === $name) {
- $name = md5(serialize($config));
- }
-
- if (true === $name || !isset(self::$instance[$name])) {
- // 解析连接参数 支持数组和字符串
- $options = self::parseConfig($config);
-
- if (empty($options['type'])) {
- throw new InvalidArgumentException('Undefined db type');
- }
-
- $class = false !== strpos($options['type'], '\\') ? $options['type'] : '\\think\\db\\connector\\' . ucwords($options['type']);
- // 记录初始化信息
- self::$log[] = '[ DB ] INIT ' . $options['type'];
-
- if (true === $name) {
- $name = md5(serialize($config));
- }
-
- self::$instance[$name] = new $class($options);
- }
-
- return self::$instance[$name];
- }
-
- /**
- * 获取当前连接器类对应的Builder类
- * @access public
- * @return string
- */
- public function getBuilderClass()
- {
- if (!empty($this->builderClassName)) {
- return $this->builderClassName;
- }
-
- return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type'));
- }
-
- /**
- * 设置当前的数据库Builder对象
- * @access protected
- * @param Builder $builder
- * @return void
- */
- protected function setBuilder(Builder $builder)
- {
- $this->builder = $builder;
-
- return $this;
- }
-
- /**
- * 获取当前的builder实例对象
- * @access public
- * @return Builder
- */
- public function getBuilder()
- {
- return $this->builder;
- }
-
- /**
- * 获取连接对象
- * @access public
- * @return object|null
- */
- public function getLinkID()
- {
- return $this->linkID ?: null;
- }
-
- /**
- * 解析pdo连接的dsn信息
- * @access protected
- * @param array $config 连接信息
- * @return string
- */
- abstract protected function parseDsn($config);
-
- /**
- * 取得数据表的字段信息
- * @access public
- * @param string $tableName
- * @return array
- */
- abstract public function getFields($tableName);
-
- /**
- * 取得数据库的表信息
- * @access public
- * @param string $dbName
- * @return array
- */
- abstract public function getTables($dbName);
-
- /**
- * SQL性能分析
- * @access protected
- * @param string $sql
- * @return array
- */
- abstract protected function getExplain($sql);
-
- /**
- * 对返数据表字段信息进行大小写转换出来
- * @access public
- * @param array $info 字段信息
- * @return array
- */
- public function fieldCase($info)
- {
- // 字段大小写转换
- 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:
- // 不做转换
- }
-
- return $info;
- }
-
- /**
- * 获取字段绑定类型
- * @access public
- * @param string $type 字段类型
- * @return integer
- */
- public function getFieldBindType($type)
- {
- if (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) {
- $bind = PDO::PARAM_STR;
- } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) {
- $bind = self::PARAM_FLOAT;
- } elseif (preg_match('/(int|serial|bit)/is', $type)) {
- $bind = PDO::PARAM_INT;
- } elseif (preg_match('/bool/is', $type)) {
- $bind = PDO::PARAM_BOOL;
- } else {
- $bind = PDO::PARAM_STR;
- }
-
- return $bind;
- }
-
- /**
- * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写)
- * @access public
- * @param string $sql sql语句
- * @return string
- */
- public function parseSqlTable($sql)
- {
- if (false !== strpos($sql, '__')) {
- $prefix = $this->getConfig('prefix');
- $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) use ($prefix) {
- return $prefix . strtolower($match[1]);
- }, $sql);
- }
-
- return $sql;
- }
-
- /**
- * 获取数据表信息
- * @access public
- * @param mixed $tableName 数据表名 留空自动获取
- * @param string $fetch 获取信息类型 包括 fields type bind pk
- * @return mixed
- */
- public function getTableInfo($tableName, $fetch = '')
- {
- if (is_array($tableName)) {
- $tableName = key($tableName) ?: current($tableName);
- }
-
- if (strpos($tableName, ',')) {
- // 多表不获取字段信息
- return false;
- } else {
- $tableName = $this->parseSqlTable($tableName);
- }
-
- // 修正子查询作为表名的问题
- if (strpos($tableName, ')')) {
- return [];
- }
-
- list($tableName) = explode(' ', $tableName);
-
- if (!strpos($tableName, '.')) {
- $schema = $this->getConfig('database') . '.' . $tableName;
- } else {
- $schema = $tableName;
- }
-
- if (!isset(self::$info[$schema])) {
- // 读取缓存
- $cacheFile = $this->config['schema_path'] . $schema . '.php';
- if (is_file($cacheFile)) {
- $info = include $cacheFile;
- } else {
- $info = $this->getFields($tableName);
- }
-
- $fields = array_keys($info);
- $bind = $type = [];
-
- foreach ($info as $key => $val) {
- // 记录字段类型
- $type[$key] = $val['type'];
- $bind[$key] = $this->getFieldBindType($val['type']);
- if (!empty($val['primary'])) {
- $pk[] = $key;
- }
- }
-
- if (isset($pk)) {
- // 设置主键
- $pk = count($pk) > 1 ? $pk : $pk[0];
- } else {
- $pk = null;
- }
-
- self::$info[$schema] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk];
- }
-
- return $fetch ? self::$info[$schema][$fetch] : self::$info[$schema];
- }
-
- /**
- * 获取数据表的主键
- * @access public
- * @param string $tableName 数据表名
- * @return string|array
- */
- public function getPk($tableName)
- {
- return $this->getTableInfo($tableName, 'pk');
- }
-
- // 获取当前数据表字段信息
- public function getTableFields($tableName)
- {
- return $this->getTableInfo($tableName, 'fields');
- }
-
- // 获取当前数据表字段类型
- public function getFieldsType($tableName)
- {
- return $this->getTableInfo($tableName, 'type');
- }
-
- // 获取当前数据表绑定信息
- public function getFieldsBind($tableName)
- {
- return $this->getTableInfo($tableName, 'bind');
- }
-
- /**
- * 获取数据库的配置参数
- * @access public
- * @param string $config 配置名称
- * @return mixed
- */
- public function getConfig($config = '')
- {
- return $config ? $this->config[$config] : $this->config;
- }
-
- /**
- * 设置数据库的配置参数
- * @access public
- * @param string|array $config 配置名称
- * @param mixed $value 配置值
- * @return void
- */
- public function setConfig($config, $value = '')
- {
- if (is_array($config)) {
- $this->config = array_merge($this->config, $config);
- } else {
- $this->config[$config] = $value;
- }
- }
-
- /**
- * 连接数据库方法
- * @access public
- * @param array $config 连接参数
- * @param integer $linkNum 连接序号
- * @param array|bool $autoConnection 是否自动连接主数据库(用于分布式)
- * @return PDO
- * @throws Exception
- */
- public function connect(array $config = [], $linkNum = 0, $autoConnection = false)
- {
- if (isset($this->links[$linkNum])) {
- return $this->links[$linkNum];
- }
-
- if (!$config) {
- $config = $this->config;
- } else {
- $config = array_merge($this->config, $config);
- }
-
- // 连接参数
- if (isset($config['params']) && is_array($config['params'])) {
- $params = $config['params'] + $this->params;
- } else {
- $params = $this->params;
- }
-
- // 记录当前字段属性大小写设置
- $this->attrCase = $params[PDO::ATTR_CASE];
-
- try {
- if (empty($config['dsn'])) {
- $config['dsn'] = $this->parseDsn($config);
- }
-
- if ($config['debug']) {
- $startTime = microtime(true);
- }
-
- $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params);
-
- if ($config['debug']) {
- // 记录数据库连接信息
- $this->log('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);
- }
-
- return $this->links[$linkNum];
- } catch (\PDOException $e) {
- if ($autoConnection) {
- $this->log('[ ERR ] ' . $e->getMessage());
- return $this->connect($autoConnection, $linkNum);
- } else {
- throw $e;
- }
- }
- }
-
- /**
- * 释放查询结果
- * @access public
- */
- public function free()
- {
- $this->PDOStatement = null;
- }
-
- /**
- * 获取PDO对象
- * @access public
- * @return \PDO|false
- */
- public function getPdo()
- {
- if (!$this->linkID) {
- return false;
- }
-
- return $this->linkID;
- }
-
- /**
- * 执行查询 使用生成器返回数据
- * @access public
- * @param string $sql sql指令
- * @param array $bind 参数绑定
- * @param bool $master 是否在主服务器读操作
- * @param Model $model 模型对象实例
- * @param array $condition 查询条件
- * @param mixed $relation 关联查询
- * @return \Generator
- */
- public function getCursor($sql, $bind = [], $master = false, $model = null, $condition = null, $relation = null)
- {
- $this->initConnect($master);
-
- // 记录SQL语句
- $this->queryStr = $sql;
-
- $this->bind = $bind;
-
- Db::$queryTimes++;
-
- // 调试开始
- $this->debug(true);
-
- // 预处理
- $this->PDOStatement = $this->linkID->prepare($sql);
-
- // 是否为存储过程调用
- $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
-
- // 参数绑定
- if ($procedure) {
- $this->bindParam($bind);
- } else {
- $this->bindValue($bind);
- }
-
- // 执行查询
- $this->PDOStatement->execute();
-
- // 调试结束
- $this->debug(false, '', $master);
-
- // 返回结果集
- while ($result = $this->PDOStatement->fetch($this->fetchType)) {
- if ($model) {
- $instance = $model->newInstance($result, $condition);
-
- if ($relation) {
- $instance->relationQuery($relation);
- }
-
- yield $instance;
- } else {
- yield $result;
- }
- }
- }
-
- /**
- * 执行查询 返回数据集
- * @access public
- * @param string $sql sql指令
- * @param array $bind 参数绑定
- * @param bool $master 是否在主服务器读操作
- * @param bool $pdo 是否返回PDO对象
- * @return array
- * @throws BindParamException
- * @throws \PDOException
- * @throws \Exception
- */
- public function query($sql, $bind = [], $master = false, $pdo = false)
- {
- $this->initConnect($master);
-
- if (!$this->linkID) {
- return false;
- }
-
- // 记录SQL语句
- $this->queryStr = $sql;
-
- $this->bind = $bind;
-
- Db::$queryTimes++;
-
- try {
- // 调试开始
- $this->debug(true);
-
- // 预处理
- $this->PDOStatement = $this->linkID->prepare($sql);
-
- // 是否为存储过程调用
- $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
-
- // 参数绑定
- if ($procedure) {
- $this->bindParam($bind);
- } else {
- $this->bindValue($bind);
- }
-
- // 执行查询
- $this->PDOStatement->execute();
-
- // 调试结束
- $this->debug(false, '', $master);
-
- // 返回结果集
- return $this->getResult($pdo, $procedure);
- } catch (\PDOException $e) {
- if ($this->isBreak($e)) {
- return $this->close()->query($sql, $bind, $master, $pdo);
- }
-
- throw new PDOException($e, $this->config, $this->getLastsql());
- } catch (\Throwable $e) {
- if ($this->isBreak($e)) {
- return $this->close()->query($sql, $bind, $master, $pdo);
- }
-
- throw $e;
- } catch (\Exception $e) {
- if ($this->isBreak($e)) {
- return $this->close()->query($sql, $bind, $master, $pdo);
- }
-
- throw $e;
- }
- }
-
- /**
- * 执行语句
- * @access public
- * @param string $sql sql指令
- * @param array $bind 参数绑定
- * @param Query $query 查询对象
- * @return int
- * @throws BindParamException
- * @throws \PDOException
- * @throws \Exception
- */
- public function execute($sql, $bind = [], Query $query = null)
- {
- $this->initConnect(true);
-
- if (!$this->linkID) {
- return false;
- }
-
- // 记录SQL语句
- $this->queryStr = $sql;
-
- $this->bind = $bind;
-
- Db::$executeTimes++;
- try {
- // 调试开始
- $this->debug(true);
-
- // 预处理
- $this->PDOStatement = $this->linkID->prepare($sql);
-
- // 是否为存储过程调用
- $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
-
- // 参数绑定
- if ($procedure) {
- $this->bindParam($bind);
- } else {
- $this->bindValue($bind);
- }
-
- // 执行语句
- $this->PDOStatement->execute();
-
- // 调试结束
- $this->debug(false, '', true);
-
- if ($query && !empty($this->config['deploy']) && !empty($this->config['read_master'])) {
- $query->readMaster();
- }
-
- $this->numRows = $this->PDOStatement->rowCount();
-
- return $this->numRows;
- } catch (\PDOException $e) {
- if ($this->isBreak($e)) {
- return $this->close()->execute($sql, $bind, $query);
- }
-
- throw new PDOException($e, $this->config, $this->getLastsql());
- } catch (\Throwable $e) {
- if ($this->isBreak($e)) {
- return $this->close()->execute($sql, $bind, $query);
- }
-
- throw $e;
- } catch (\Exception $e) {
- if ($this->isBreak($e)) {
- return $this->close()->execute($sql, $bind, $query);
- }
-
- throw $e;
- }
- }
-
- /**
- * 查找单条记录
- * @access public
- * @param Query $query 查询对象
- * @return array|null|\PDOStatement|string
- * @throws DbException
- * @throws ModelNotFoundException
- * @throws DataNotFoundException
- */
- public function find(Query $query)
- {
- // 分析查询表达式
- $options = $query->getOptions();
- $pk = $query->getPk($options);
-
- $data = $options['data'];
-
- $query->setOption('limit', 1);
-
- if ($this->cache && empty($options['fetch_sql']) && !empty($options['cache'])) {
- // 判断查询缓存
- $cache = $options['cache'];
-
- if (is_string($cache['key'])) {
- $key = $cache['key'];
- } elseif (!isset($key)) {
- $key = $this->getCacheKey($query, $data);
- }
-
- $result = $this->cache->get($key);
-
- if (false !== $result) {
- return $result;
- }
- }
-
- if (is_string($pk) && !is_array($data)) {
- if (isset($key) && strpos($key, '|')) {
- list($a, $val) = explode('|', $key);
- $item[$pk] = $val;
- } else {
- $item[$pk] = $data;
- }
- $data = $item;
- }
- $query->setOption('data', $data);
-
- // 生成查询SQL
- $sql = $this->builder->select($query);
-
- $query->removeOption('limit');
-
- $bind = $query->getBind();
-
- if (!empty($options['fetch_sql'])) {
- // 获取实际执行的SQL语句
- return $this->getRealSql($sql, $bind);
- }
-
- // 事件回调
- if ($result = $query->trigger('before_find')) {
- } else {
- // 执行查询
- $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']);
-
- if ($resultSet instanceof \PDOStatement) {
- // 返回PDOStatement对象
- return $resultSet;
- }
-
- $result = isset($resultSet[0]) ? $resultSet[0] : null;
- }
-
- if (isset($cache) && $result) {
- // 缓存数据
- $this->cacheData($key, $result, $cache);
- }
-
- return $result;
- }
-
- /**
- * 使用游标查询记录
- * @access public
- * @param Query $query 查询对象
- * @return \Generator
- */
- public function cursor(Query $query)
- {
- // 分析查询表达式
- $options = $query->getOptions();
-
- // 生成查询SQL
- $sql = $this->builder->select($query);
-
- $bind = $query->getBind();
-
- $condition = isset($options['where']['AND']) ? $options['where']['AND'] : null;
- $relation = isset($options['relaltion']) ? $options['relation'] : null;
-
- // 执行查询操作
- return $this->getCursor($sql, $bind, $options['master'], $query->getModel(), $condition, $relation);
- }
-
- /**
- * 获取缓存数据
- * @access protected
- * @param Query $query 查询对象
- * @param mixed $cache 缓存设置
- * @param array $options 缓存
- * @return mixed
- */
- protected function getCacheData(Query $query, $cache, $data, &$key = null)
- {
- // 判断查询缓存
- $key = is_string($cache['key']) ? $cache['key'] : $this->getCacheKey($query, $data);
-
- return $this->cache->get($key);
- }
-
- /**
- * 查找记录
- * @access public
- * @param Query $query 查询对象
- * @return array|\PDOStatement|string
- * @throws DbException
- * @throws ModelNotFoundException
- * @throws DataNotFoundException
- */
- public function select(Query $query)
- {
- // 分析查询表达式
- $options = $query->getOptions();
-
- if ($this->cache && empty($options['fetch_sql']) && !empty($options['cache'])) {
- // 判断查询缓存
- $resultSet = $this->getCacheData($query, $options['cache'], null, $key);
-
- if (false !== $resultSet) {
- return $resultSet;
- }
-
- }
-
- // 生成查询SQL
- $sql = $this->builder->select($query);
-
- $bind = $query->getBind();
-
- if (!empty($options['fetch_sql'])) {
- // 获取实际执行的SQL语句
- return $this->getRealSql($sql, $bind);
- }
-
- if ($resultSet = $query->trigger('before_select')) {
- } else {
- // 执行查询操作
- $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']);
-
- if ($resultSet instanceof \PDOStatement) {
- // 返回PDOStatement对象
- return $resultSet;
- }
- }
-
- if ($this->cache && !empty($options['cache']) && false !== $resultSet) {
- // 缓存数据集
- $this->cacheData($key, $resultSet, $options['cache']);
- }
-
- return $resultSet;
- }
-
- /**
- * 插入记录
- * @access public
- * @param Query $query 查询对象
- * @param boolean $replace 是否replace
- * @param boolean $getLastInsID 返回自增主键
- * @param string $sequence 自增序列名
- * @return integer|string
- */
- public function insert(Query $query, $replace = false, $getLastInsID = false, $sequence = null)
- {
- // 分析查询表达式
- $options = $query->getOptions();
-
- // 生成SQL语句
- $sql = $this->builder->insert($query, $replace);
-
- $bind = $query->getBind();
-
- if (!empty($options['fetch_sql'])) {
- // 获取实际执行的SQL语句
- return $this->getRealSql($sql, $bind);
- }
-
- // 执行操作
- $result = '' == $sql ? 0 : $this->execute($sql, $bind, $query);
-
- if ($result) {
- $sequence = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null);
- $lastInsId = $this->getLastInsID($sequence);
-
- $data = $options['data'];
-
- if ($lastInsId) {
- $pk = $query->getPk($options);
- if (is_string($pk)) {
- $data[$pk] = $lastInsId;
- }
- }
-
- $query->setOption('data', $data);
-
- $query->trigger('after_insert');
-
- if ($getLastInsID) {
- return $lastInsId;
- }
- }
-
- return $result;
- }
-
- /**
- * 批量插入记录
- * @access public
- * @param Query $query 查询对象
- * @param mixed $dataSet 数据集
- * @param bool $replace 是否replace
- * @param integer $limit 每次写入数据限制
- * @return integer|string
- * @throws \Exception
- * @throws \Throwable
- */
- public function insertAll(Query $query, $dataSet = [], $replace = false, $limit = null)
- {
- if (!is_array(reset($dataSet))) {
- return false;
- }
-
- $options = $query->getOptions();
-
- if ($limit) {
- // 分批写入 自动启动事务支持
- $this->startTrans();
-
- try {
- $array = array_chunk($dataSet, $limit, true);
- $count = 0;
-
- foreach ($array as $item) {
- $sql = $this->builder->insertAll($query, $item, $replace);
- $bind = $query->getBind();
- if (!empty($options['fetch_sql'])) {
- $fetchSql[] = $this->getRealSql($sql, $bind);
- } else {
- $count += $this->execute($sql, $bind, $query);
- }
- }
-
- // 提交事务
- $this->commit();
- } catch (\Exception $e) {
- $this->rollback();
- throw $e;
- } catch (\Throwable $e) {
- $this->rollback();
- throw $e;
- }
-
- return isset($fetchSql) ? implode(';', $fetchSql) : $count;
- }
-
- $sql = $this->builder->insertAll($query, $dataSet, $replace);
- $bind = $query->getBind();
-
- if (!empty($options['fetch_sql'])) {
- // 获取实际执行的SQL语句
- return $this->getRealSql($sql, $bind);
- }
- // 执行操作
- return $this->execute($sql, $bind, $query);
- }
-
- /**
- * 通过Select方式插入记录
- * @access public
- * @param Query $query 查询对象
- * @param string $fields 要插入的数据表字段名
- * @param string $table 要插入的数据表名
- * @return integer|string
- * @throws PDOException
- */
- public function selectInsert(Query $query, $fields, $table)
- {
- // 分析查询表达式
- $options = $query->getOptions();
-
- // 生成SQL语句
- $table = $this->parseSqlTable($table);
-
- $sql = $this->builder->selectInsert($query, $fields, $table);
-
- $bind = $query->getBind();
-
- if (!empty($options['fetch_sql'])) {
- // 获取实际执行的SQL语句
- return $this->getRealSql($sql, $bind);
- }
- // 执行操作
- return $this->execute($sql, $bind, $query);
- }
-
- /**
- * 更新记录
- * @access public
- * @param Query $query 查询对象
- * @return integer|string
- * @throws Exception
- * @throws PDOException
- */
- public function update(Query $query)
- {
- $options = $query->getOptions();
-
- if (isset($options['cache']) && is_string($options['cache']['key'])) {
- $key = $options['cache']['key'];
- }
-
- $pk = $query->getPk($options);
- $data = $options['data'];
-
- if (empty($options['where'])) {
- // 如果存在主键数据 则自动作为更新条件
- if (is_string($pk) && isset($data[$pk])) {
- $where[$pk] = [$pk, '=', $data[$pk]];
- if (!isset($key)) {
- $key = $this->getCacheKey($query, $data[$pk]);
- }
- unset($data[$pk]);
- } elseif (is_array($pk)) {
- // 增加复合主键支持
- foreach ($pk as $field) {
- if (isset($data[$field])) {
- $where[$field] = [$field, '=', $data[$field]];
- } else {
- // 如果缺少复合主键数据则不执行
- throw new Exception('miss complex primary data');
- }
- unset($data[$field]);
- }
- }
-
- if (!isset($where)) {
- // 如果没有任何更新条件则不执行
- throw new Exception('miss update condition');
- } else {
- $options['where']['AND'] = $where;
- $query->setOption('where', ['AND' => $where]);
- }
- } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'])) {
- foreach ($options['where']['AND'] as $val) {
- if (is_array($val) && $val[0] == $pk) {
- $key = $this->getCacheKey($query, $val);
- }
- }
- }
-
- // 更新数据
- $query->setOption('data', $data);
-
- // 生成UPDATE SQL语句
- $sql = $this->builder->update($query);
- $bind = $query->getBind();
-
- if (!empty($options['fetch_sql'])) {
- // 获取实际执行的SQL语句
- return $this->getRealSql($sql, $bind);
- }
-
- // 检测缓存
- if ($this->cache && isset($key) && $this->cache->get($key)) {
- // 删除缓存
- $this->cache->rm($key);
- }
-
- // 执行操作
- $result = '' == $sql ? 0 : $this->execute($sql, $bind, $query);
-
- if ($result) {
- if (is_string($pk) && isset($where[$pk])) {
- $data[$pk] = $where[$pk];
- } elseif (is_string($pk) && isset($key) && strpos($key, '|')) {
- list($a, $val) = explode('|', $key);
- $data[$pk] = $val;
- }
-
- $query->setOption('data', $data);
- $query->trigger('after_update');
- }
-
- return $result;
- }
-
- /**
- * 删除记录
- * @access public
- * @param Query $query 查询对象
- * @return int
- * @throws Exception
- * @throws PDOException
- */
- public function delete(Query $query)
- {
- // 分析查询表达式
- $options = $query->getOptions();
- $pk = $query->getPk($options);
- $data = $options['data'];
-
- if (isset($options['cache']) && is_string($options['cache']['key'])) {
- $key = $options['cache']['key'];
- } elseif (!is_null($data) && true !== $data && !is_array($data)) {
- $key = $this->getCacheKey($query, $data);
- } elseif (is_string($pk) && isset($options['where']['AND'])) {
- foreach ($options['where']['AND'] as $val) {
- if (is_array($val) && $val[0] == $pk) {
- $key = $this->getCacheKey($query, $val);
- }
- }
- }
-
- if (true !== $data && empty($options['where'])) {
- // 如果条件为空 不进行删除操作 除非设置 1=1
- throw new Exception('delete without condition');
- }
-
- // 生成删除SQL语句
- $sql = $this->builder->delete($query);
-
- $bind = $query->getBind();
-
- if (!empty($options['fetch_sql'])) {
- // 获取实际执行的SQL语句
- return $this->getRealSql($sql, $bind);
- }
-
- // 检测缓存
- if ($this->cache && isset($key) && $this->cache->get($key)) {
- // 删除缓存
- $this->cache->rm($key);
- }
-
- // 执行操作
- $result = $this->execute($sql, $bind, $query);
-
- if ($result) {
- if (!is_array($data) && is_string($pk) && isset($key) && strpos($key, '|')) {
- list($a, $val) = explode('|', $key);
- $item[$pk] = $val;
- $data = $item;
- }
-
- $options['data'] = $data;
-
- $query->trigger('after_delete');
- }
-
- return $result;
- }
-
- /**
- * 得到某个字段的值
- * @access public
- * @param Query $query 查询对象
- * @param string $field 字段名
- * @param bool $default 默认值
- * @return mixed
- */
- public function value(Query $query, $field, $default = null)
- {
- $options = $query->getOptions();
-
- if ($this->cache && empty($options['fetch_sql']) && !empty($options['cache'])) {
- // 判断查询缓存
- $cache = $options['cache'];
- $result = $this->getCacheData($query, $cache, null, $key);
-
- if (false !== $result) {
- return $result;
- }
-
- }
-
- if (isset($options['field'])) {
- $query->removeOption('field');
- }
-
- if (is_string($field)) {
- $field = array_map('trim', explode(',', $field));
- }
-
- $query->setOption('field', $field);
- $query->setOption('limit', 1);
-
- // 生成查询SQL
- $sql = $this->builder->select($query);
-
- if (isset($options['field'])) {
- $query->setOption('field', $options['field']);
- } else {
- $query->removeOption('field');
- }
-
- $query->removeOption('limit');
-
- $bind = $query->getBind();
-
- if (!empty($options['fetch_sql'])) {
- // 获取实际执行的SQL语句
- return $this->getRealSql($sql, $bind);
- }
-
- // 执行查询操作
- $pdo = $this->query($sql, $bind, $options['master'], true);
-
- $result = $pdo->fetchColumn();
-
- if (isset($cache) && false !== $result) {
- // 缓存数据
- $this->cacheData($key, $result, $cache);
- }
-
- return false !== $result ? $result : $default;
- }
-
- /**
- * 得到某个列的数组
- * @access public
- * @param Query $query 查询对象
- * @param string $field 字段名 多个字段用逗号分隔
- * @param string $key 索引
- * @return array
- */
- public function column(Query $query, $field, $key = '')
- {
- $options = $query->getOptions();
-
- if ($this->cache && empty($options['fetch_sql']) && !empty($options['cache'])) {
- // 判断查询缓存
- $cache = $options['cache'];
-
- $guid = is_string($cache['key']) ? $cache['key'] : $this->getCacheKey($query, $field);
- $result = $this->cache->get($guid);
-
- if (false !== $result) {
- return $result;
- }
- }
-
- if (isset($options['field'])) {
- $query->removeOption('field');
- }
-
- if (is_null($field)) {
- $field = ['*'];
- } elseif (is_string($field)) {
- $field = array_map('trim', explode(',', $field));
- }
-
- if ($key && ['*'] != $field) {
- array_unshift($field, $key);
- $field = array_unique($field);
- }
-
- $query->setOption('field', $field);
-
- // 生成查询SQL
- $sql = $this->builder->select($query);
-
- // 还原field参数
- if (isset($options['field'])) {
- $query->setOption('field', $options['field']);
- } else {
- $query->removeOption('field');
- }
-
- $bind = $query->getBind();
-
- if (!empty($options['fetch_sql'])) {
- // 获取实际执行的SQL语句
- return $this->getRealSql($sql, $bind);
- }
-
- // 执行查询操作
- $pdo = $this->query($sql, $bind, $options['master'], true);
-
- if (1 == $pdo->columnCount()) {
- $result = $pdo->fetchAll(PDO::FETCH_COLUMN);
- } else {
- $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC);
-
- if (['*'] == $field && $key) {
- $result = array_column($resultSet, null, $key);
- } elseif ($resultSet) {
- $fields = array_keys($resultSet[0]);
- $count = count($fields);
- $key1 = array_shift($fields);
- $key2 = $fields ? array_shift($fields) : '';
- $key = $key ?: $key1;
-
- if (strpos($key, '.')) {
- list($alias, $key) = explode('.', $key);
- }
-
- if (2 == $count) {
- $column = $key2;
- } elseif (1 == $count) {
- $column = $key1;
- } else {
- $column = null;
- }
-
- $result = array_column($resultSet, $column, $key);
- } else {
- $result = [];
- }
- }
-
- if (isset($cache) && isset($guid)) {
- // 缓存数据
- $this->cacheData($guid, $result, $cache);
- }
-
- return $result;
- }
-
- /**
- * 得到某个字段的值
- * @access public
- * @param Query $query 查询对象
- * @param string $aggregate 聚合方法
- * @param string $field 字段名
- * @return mixed
- */
- public function aggregate(Query $query, $aggregate, $field)
- {
- if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) {
- list($distinct, $field) = explode(' ', $field);
- }
-
- $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS tp_' . strtolower($aggregate);
-
- return $this->value($query, $field, 0);
- }
-
- /**
- * 执行查询但只返回PDOStatement对象
- * @access public
- * @return \PDOStatement|string
- */
- public function pdo(Query $query)
- {
- // 分析查询表达式
- $options = $query->getOptions();
-
- // 生成查询SQL
- $sql = $this->builder->select($query);
-
- $bind = $query->getBind();
-
- if (!empty($options['fetch_sql'])) {
- // 获取实际执行的SQL语句
- return $this->getRealSql($sql, $bind);
- }
-
- // 执行查询操作
- return $this->query($sql, $bind, $options['master'], true);
- }
-
- /**
- * 根据参数绑定组装最终的SQL语句 便于调试
- * @access public
- * @param string $sql 带参数绑定的sql语句
- * @param array $bind 参数绑定列表
- * @return string
- */
- public function getRealSql($sql, array $bind = [])
- {
- if (is_array($sql)) {
- $sql = implode(';', $sql);
- }
-
- foreach ($bind as $key => $val) {
- $value = is_array($val) ? $val[0] : $val;
- $type = is_array($val) ? $val[1] : PDO::PARAM_STR;
-
- if (PDO::PARAM_INT == $type || self::PARAM_FLOAT == $type) {
- $value = (float) $value;
- } elseif (PDO::PARAM_STR == $type) {
- $value = '\'' . addslashes($value) . '\'';
- }
-
- // 判断占位符
- $sql = is_numeric($key) ?
- substr_replace($sql, $value, strpos($sql, '?'), 1) :
- str_replace(':' . $key, $value, $sql);
- }
-
- return rtrim($sql);
- }
/**
- * 参数绑定
- * 支持 ['name'=>'value','id'=>123] 对应命名占位符
- * 或者 ['value',123] 对应问号占位符
- * @access public
- * @param array $bind 要绑定的参数列表
- * @return void
- * @throws BindParamException
+ * 当前SQL指令
+ * @var string
*/
- protected function bindValue(array $bind = [])
- {
- foreach ($bind as $key => $val) {
- // 占位符
- $param = is_numeric($key) ? $key + 1 : ':' . $key;
-
- if (is_array($val)) {
- if (PDO::PARAM_INT == $val[1] && '' === $val[0]) {
- $val[0] = 0;
- } elseif (self::PARAM_FLOAT == $val[1]) {
- $val[0] = (float) $val[0];
- $val[1] = PDO::PARAM_STR;
- }
-
- $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]);
- } else {
- $result = $this->PDOStatement->bindValue($param, $val);
- }
-
- if (!$result) {
- throw new BindParamException(
- "Error occurred when binding parameters '{$param}'",
- $this->config,
- $this->getLastsql(),
- $bind
- );
- }
- }
- }
+ protected $queryStr = '';
/**
- * 存储过程的输入输出参数绑定
- * @access public
- * @param array $bind 要绑定的参数列表
- * @return void
- * @throws BindParamException
+ * 返回或者影响记录数
+ * @var int
*/
- protected function bindParam($bind)
- {
- foreach ($bind as $key => $val) {
- $param = is_numeric($key) ? $key + 1 : ':' . $key;
-
- if (is_array($val)) {
- array_unshift($val, $param);
- $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val);
- } else {
- $result = $this->PDOStatement->bindValue($param, $val);
- }
-
- if (!$result) {
- $param = array_shift($val);
-
- throw new BindParamException(
- "Error occurred when binding parameters '{$param}'",
- $this->config,
- $this->getLastsql(),
- $bind
- );
- }
- }
- }
+ protected $numRows = 0;
/**
- * 获得数据集数组
- * @access protected
- * @param bool $pdo 是否返回PDOStatement
- * @param bool $procedure 是否存储过程
- * @return array
+ * 事务指令数
+ * @var int
*/
- protected function getResult($pdo = false, $procedure = false)
- {
- if ($pdo) {
- // 返回PDOStatement对象处理
- return $this->PDOStatement;
- }
-
- if ($procedure) {
- // 存储过程返回结果
- return $this->procedure();
- }
-
- $result = $this->PDOStatement->fetchAll($this->fetchType);
-
- $this->numRows = count($result);
-
- return $result;
- }
+ protected $transTimes = 0;
/**
- * 获得存储过程数据集
- * @access protected
- * @return array
+ * 错误信息
+ * @var string
*/
- protected function procedure()
- {
- $item = [];
-
- do {
- $result = $this->getResult();
- if ($result) {
- $item[] = $result;
- }
- } while ($this->PDOStatement->nextRowset());
-
- $this->numRows = count($item);
-
- return $item;
- }
+ protected $error = '';
/**
- * 执行数据库事务
- * @access public
- * @param callable $callback 数据操作方法回调
- * @return mixed
- * @throws PDOException
- * @throws \Exception
- * @throws \Throwable
+ * 数据库连接ID 支持多个连接
+ * @var array
*/
- public function transaction($callback)
- {
- $this->startTrans();
-
- try {
- $result = null;
- if (is_callable($callback)) {
- $result = call_user_func_array($callback, [$this]);
- }
-
- $this->commit();
- return $result;
- } catch (\Exception $e) {
- $this->rollback();
- throw $e;
- } catch (\Throwable $e) {
- $this->rollback();
- throw $e;
- }
- }
+ protected $links = [];
/**
- * 启动XA事务
- * @access public
- * @param string $xid XA事务id
- * @return void
+ * 当前连接ID
+ * @var object
*/
- public function startTransXa($xid)
- {}
+ protected $linkID;
/**
- * 预编译XA事务
- * @access public
- * @param string $xid XA事务id
- * @return void
+ * 当前读连接ID
+ * @var object
*/
- public function prepareXa($xid)
- {}
+ protected $linkRead;
/**
- * 提交XA事务
- * @access public
- * @param string $xid XA事务id
- * @return void
+ * 当前写连接ID
+ * @var object
*/
- public function commitXa($xid)
- {}
+ protected $linkWrite;
/**
- * 回滚XA事务
- * @access public
- * @param string $xid XA事务id
- * @return void
+ * 数据表信息
+ * @var array
*/
- public function rollbackXa($xid)
- {}
+ protected $info = [];
/**
- * 启动事务
- * @access public
- * @return void
- * @throws \PDOException
- * @throws \Exception
+ * 查询开始时间
+ * @var float
*/
- public function startTrans()
- {
- $this->initConnect(true);
- if (!$this->linkID) {
- return false;
- }
-
- ++$this->transTimes;
-
- try {
- if (1 == $this->transTimes) {
- $this->linkID->beginTransaction();
- } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
- $this->linkID->exec(
- $this->parseSavepoint('trans' . $this->transTimes)
- );
- }
- } catch (\Exception $e) {
- if ($this->isBreak($e)) {
- --$this->transTimes;
- return $this->close()->startTrans();
- }
- throw $e;
- }
- }
+ protected $queryStartTime;
/**
- * 用于非自动提交状态下面的查询提交
- * @access public
- * @return void
- * @throws PDOException
+ * Builder对象
+ * @var Builder
*/
- public function commit()
- {
- $this->initConnect(true);
-
- if (1 == $this->transTimes) {
- $this->linkID->commit();
- }
-
- --$this->transTimes;
- }
+ protected $builder;
/**
- * 事务回滚
- * @access public
- * @return void
- * @throws PDOException
+ * Db对象
+ * @var DbManager
*/
- public function rollback()
- {
- $this->initConnect(true);
-
- if (1 == $this->transTimes) {
- $this->linkID->rollBack();
- } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
- $this->linkID->exec(
- $this->parseSavepointRollBack('trans' . $this->transTimes)
- );
- }
-
- $this->transTimes = max(0, $this->transTimes - 1);
- }
+ protected $db;
/**
- * 是否支持事务嵌套
- * @return bool
+ * 是否读取主库
+ * @var bool
*/
- protected function supportSavepoint()
- {
- return false;
- }
+ protected $readMaster = false;
/**
- * 生成定义保存点的SQL
- * @param $name
- * @return string
+ * 数据库连接参数配置
+ * @var array
*/
- protected function parseSavepoint($name)
- {
- return 'SAVEPOINT ' . $name;
- }
+ protected $config = [];
/**
- * 生成回滚到保存点的SQL
- * @param $name
- * @return string
+ * 缓存对象
+ * @var CacheInterface
*/
- protected function parseSavepointRollBack($name)
- {
- return 'ROLLBACK TO SAVEPOINT ' . $name;
- }
+ protected $cache;
/**
- * 批处理执行SQL语句
- * 批处理的指令都认为是execute操作
+ * 架构函数 读取数据库配置信息
* @access public
- * @param array $sqlArray SQL批处理指令
- * @param array $bind 参数绑定
- * @return boolean
+ * @param array $config 数据库配置数组
*/
- public function batchQuery($sqlArray = [], $bind = [])
+ public function __construct(array $config = [])
{
- if (!is_array($sqlArray)) {
- return false;
- }
-
- // 自动启动事务支持
- $this->startTrans();
-
- try {
- foreach ($sqlArray as $sql) {
- $this->execute($sql, $bind);
- }
- // 提交事务
- $this->commit();
- } catch (\Exception $e) {
- $this->rollback();
- throw $e;
+ if (!empty($config)) {
+ $this->config = array_merge($this->config, $config);
}
- return true;
- }
+ // 创建Builder对象
+ $class = $this->getBuilderClass();
- /**
- * 获得查询次数
- * @access public
- * @param boolean $execute 是否包含所有查询
- * @return integer
- */
- public function getQueryTimes($execute = false)
- {
- return $execute ? Db::$queryTimes + Db::$executeTimes : Db::$queryTimes;
+ $this->builder = new $class($this);
}
/**
- * 获得执行次数
+ * 获取当前的builder实例对象
* @access public
- * @return integer
+ * @return Builder
*/
- public function getExecuteTimes()
+ public function getBuilder()
{
- return Db::$executeTimes;
+ return $this->builder;
}
/**
- * 关闭数据库(或者重新连接)
- * @access public
- * @return $this
+ * 创建查询对象
*/
- public function close()
+ public function newQuery()
{
- $this->linkID = null;
- $this->linkWrite = null;
- $this->linkRead = null;
- $this->links = [];
+ $class = $this->getQueryClass();
- return $this;
- }
+ /** @var BaseQuery $query */
+ $query = new $class($this);
- /**
- * 是否断线
- * @access protected
- * @param \PDOException|\Exception $e 异常对象
- * @return bool
- */
- protected function isBreak($e)
- {
- if (!$this->config['break_reconnect']) {
- return false;
+ $timeRule = $this->db->getConfig('time_query_rule');
+ if (!empty($timeRule)) {
+ $query->timeRule($timeRule);
}
- $info = [
- 'server has gone away',
- 'no connection to the server',
- 'Lost connection',
- 'is dead or not enabled',
- 'Error while sending',
- 'decryption failed or bad record mac',
- 'server closed the connection unexpectedly',
- 'SSL connection has been closed unexpectedly',
- 'Error writing data to the connection',
- 'Resource deadlock avoided',
- ];
-
- $error = $e->getMessage();
-
- foreach ($info as $msg) {
- if (false !== stripos($error, $msg)) {
- return true;
- }
- }
- return false;
+ return $query;
}
/**
- * 获取最近一次查询的sql语句
- * @access public
- * @return string
+ * 指定表名开始查询
+ * @param $table
+ * @return BaseQuery
*/
- public function getLastSql()
+ public function table($table)
{
- return $this->getRealSql($this->queryStr, $this->bind);
+ return $this->newQuery()->table($table);
}
/**
- * 获取最近插入的ID
- * @access public
- * @param string $sequence 自增序列名
- * @return string
+ * 指定表名开始查询(不带前缀)
+ * @param $name
+ * @return BaseQuery
*/
- public function getLastInsID($sequence = null)
+ public function name($name)
{
- return $this->linkID->lastInsertId($sequence);
+ return $this->newQuery()->name($name);
}
/**
- * 获取返回或者影响的记录数
+ * 设置当前的数据库Db对象
* @access public
- * @return integer
+ * @param DbManager $db
+ * @return void
*/
- public function getNumRows()
+ public function setDb(DbManager $db)
{
- return $this->numRows;
+ $this->db = $db;
}
/**
- * 获取最近的错误信息
+ * 设置当前的缓存对象
* @access public
- * @return string
- */
- public function getError()
- {
- if ($this->PDOStatement) {
- $error = $this->PDOStatement->errorInfo();
- $error = $error[1] . ':' . $error[2];
- } else {
- $error = '';
- }
-
- if ('' != $this->queryStr) {
- $error .= "\n [ SQL语句 ] : " . $this->getLastsql();
- }
-
- return $error;
- }
-
- /**
- * 数据库调试 记录当前SQL及分析性能
- * @access protected
- * @param boolean $start 调试开始标记 true 开始 false 结束
- * @param string $sql 执行的SQL语句 留空自动获取
- * @param bool $master 主从标记
+ * @param CacheInterface $cache
* @return void
*/
- protected function debug($start, $sql = '', $master = false)
+ public function setCache(CacheInterface $cache)
{
- if (!empty($this->config['debug'])) {
- // 开启数据库调试模式
- if ($start) {
- $this->queryStartTime = microtime(true);
- } else {
- $runtime = number_format((microtime(true) - $this->queryStartTime), 6);
- $sql = $sql ?: $this->getLastsql();
- $result = [];
-
- // SQL性能分析
- if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) {
- $result = $this->getExplain($sql);
- }
-
- // SQL监听
- $this->triggerSql($sql, $runtime, $result, $master);
- }
- }
+ $this->cache = $cache;
}
/**
- * 监听SQL执行
+ * 获取当前的缓存对象
* @access public
- * @param callable $callback 回调方法
- * @return void
+ * @return CacheInterface|null
*/
- public function listen($callback)
+ public function getCache()
{
- self::$event[] = $callback;
+ return $this->cache;
}
/**
- * 触发SQL事件
- * @access protected
- * @param string $sql SQL语句
- * @param float $runtime SQL运行时间
- * @param mixed $explain SQL分析
- * @param bool $master 主从标记
- * @return bool
+ * 获取数据库的配置参数
+ * @access public
+ * @param string $config 配置名称
+ * @return mixed
*/
- protected function triggerSql($sql, $runtime, $explain = [], $master = false)
+ public function getConfig(string $config = '')
{
- if (!empty(self::$event)) {
- foreach (self::$event as $callback) {
- if (is_callable($callback)) {
- call_user_func_array($callback, [$sql, $runtime, $explain, $master]);
- }
- }
- } else {
- if ($this->config['deploy']) {
- // 分布式记录当前操作的主从
- $master = $master ? 'master|' : 'slave|';
- } else {
- $master = '';
- }
-
- // 未注册监听则记录到日志中
- $this->log('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $runtime . 's ]');
-
- if (!empty($explain)) {
- $this->log('[ EXPLAIN : ' . var_export($explain, true) . ' ]');
- }
+ if ('' === $config) {
+ return $this->config;
}
- }
- public function log($log)
- {
- $this->config['debug'] && self::$log[] = $log;
- }
-
- public function getSqlLog()
- {
- return self::$log;
+ return $this->config[$config] ?? null;
}
/**
- * 初始化数据库连接
+ * 数据库SQL监控
* @access protected
- * @param boolean $master 是否主服务器
+ * @param string $sql 执行的SQL语句 留空自动获取
+ * @param bool $master 主从标记
* @return void
*/
- protected function initConnect($master = true)
+ protected function trigger(string $sql = '', bool $master = false): void
{
- if (!empty($this->config['deploy'])) {
- // 采用分布式数据库
- if ($master || $this->transTimes) {
- if (!$this->linkWrite) {
- $this->linkWrite = $this->multiConnect(true);
+ $listen = $this->db->getListen();
+ if (empty($listen)) {
+ $listen[] = function ($sql, $time, $master) {
+ if (0 === strpos($sql, 'CONNECT:')) {
+ $this->db->log($sql);
+ return;
}
- $this->linkID = $this->linkWrite;
- } else {
- if (!$this->linkRead) {
- $this->linkRead = $this->multiConnect(false);
+ // 记录SQL
+ if (is_bool($master)) {
+ // 分布式记录当前操作的主从
+ $master = $master ? 'master|' : 'slave|';
+ } else {
+ $master = '';
}
- $this->linkID = $this->linkRead;
- }
- } elseif (!$this->linkID) {
- // 默认单数据库
- $this->linkID = $this->connect();
- }
- }
-
- /**
- * 连接分布式服务器
- * @access protected
- * @param boolean $master 主服务器
- * @return PDO
- */
- protected function multiConnect($master = false)
- {
- $_config = [];
-
- // 分布式数据库配置解析
- foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
- $_config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
+ $this->db->log($sql . ' [ ' . $master . 'RunTime:' . $time . 's ]');
+ };
}
- // 主服务器序号
- $m = floor(mt_rand(0, $this->config['master_num'] - 1));
+ $runtime = number_format((microtime(true) - $this->queryStartTime), 6);
+ $sql = $sql ?: $this->getLastsql();
- if ($this->config['rw_separate']) {
- // 主从式采用读写分离
- if ($master) // 主服务器写入
- {
- $r = $m;
- } elseif (is_numeric($this->config['slave_no'])) {
- // 指定服务器读
- $r = $this->config['slave_no'];
- } else {
- // 读操作连接从服务器 每次随机连接的数据库
- $r = floor(mt_rand($this->config['master_num'], count($_config['hostname']) - 1));
- }
- } else {
- // 读写操作不区分服务器 每次随机连接的数据库
- $r = floor(mt_rand(0, count($_config['hostname']) - 1));
+ if (empty($this->config['deploy'])) {
+ $master = null;
}
- $dbMaster = false;
- if ($m != $r) {
- $dbMaster = [];
- foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
- $dbMaster[$name] = isset($_config[$name][$m]) ? $_config[$name][$m] : $_config[$name][0];
+ foreach ($listen as $callback) {
+ if (is_callable($callback)) {
+ $callback($sql, $runtime, $master);
}
}
-
- $dbConfig = [];
-
- foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
- $dbConfig[$name] = isset($_config[$name][$r]) ? $_config[$name][$r] : $_config[$name][0];
- }
-
- return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
}
/**
- * 析构方法
- * @access public
+ * 缓存数据
+ * @access protected
+ * @param CacheItem $cacheItem 缓存Item
*/
- public function __destruct()
+ protected function cacheData(CacheItem $cacheItem)
{
- // 释放查询
- $this->free();
-
- // 关闭连接
- $this->close();
+ if ($cacheItem->getTag() && method_exists($this->cache, 'tag')) {
+ $this->cache->tag($cacheItem->getTag())->set($cacheItem->getKey(), $cacheItem->get(), $cacheItem->getExpire());
+ } else {
+ $this->cache->set($cacheItem->getKey(), $cacheItem->get(), $cacheItem->getExpire());
+ }
}
/**
- * 缓存数据
- * @access public
- * @param string $key 缓存标识
- * @param mixed $data 缓存数据
- * @param array $config 缓存参数
+ * 分析缓存Key
+ * @access protected
+ * @param BaseQuery $query 查询对象
+ * @param string $method 查询方法
+ * @return string
*/
- protected function cacheData($key, $data, $config = [])
+ protected function getCacheKey(BaseQuery $query, string $method = ''): string
{
- $this->cache->set($key, $data, $config['expire']);
+ if (!empty($query->getOptions('key')) && empty($method)) {
+ $key = 'think_' . $this->getConfig('database') . '.' . $query->getTable() . '|' . $query->getOptions('key');
+ } else {
+ $key = $query->getQueryGuid();
+ }
+
+ return $key;
}
/**
- * 生成缓存标识
+ * 分析缓存
* @access protected
- * @param Query $query 查询对象
- * @param mixed $value 缓存数据
- * @return string
+ * @param BaseQuery $query 查询对象
+ * @param array $cache 缓存信息
+ * @param string $method 查询方法
+ * @return CacheItem
*/
- protected function getCacheKey(Query $query, $value)
+ protected function parseCache(BaseQuery $query, array $cache, string $method = ''): CacheItem
{
- if (is_scalar($value)) {
- $data = $value;
- } elseif (is_array($value) && isset($value[1], $value[2]) && in_array($value[1], ['=', 'eq'])) {
- $data = $value[2];
- }
+ [$key, $expire, $tag] = $cache;
- $prefix = 'think:' . $this->getConfig('database') . '.';
+ if ($key instanceof CacheItem) {
+ $cacheItem = $key;
+ } else {
+ if (true === $key) {
+ $key = $this->getCacheKey($query, $method);
+ }
- if (isset($data)) {
- return $prefix . $query->getTable() . '|' . $data;
+ $cacheItem = new CacheItem($key);
+ $cacheItem->expire($expire);
+ $cacheItem->tag($tag);
}
- try {
- return md5($prefix . serialize($query->getOptions()) . serialize($query->getBind(false)));
- } catch (\Exception $e) {
- return;
- }
+ return $cacheItem;
}
/**
- * 数据库连接参数解析
- * @access private
- * @param mixed $config
- * @return array
+ * 获取返回或者影响的记录数
+ * @access public
+ * @return integer
*/
- private static function parseConfig($config)
+ public function getNumRows(): int
{
- if (empty($config)) {
- $config = Db::getConfig();
- } elseif (is_string($config) && false === strpos($config, '/')) {
- // 支持读取配置参数
- $config = Db::getConfig($config);
- }
-
- if (is_string($config)) {
- return self::parseDsnConfig($config);
- } else {
- return $config;
- }
+ return $this->numRows;
}
/**
- * DSN解析
- * 格式: mysql://username:passwd@localhost:3306/DbName?param1=val1¶m2=val2#utf8
- * @access private
- * @param string $dsnStr
- * @return array
+ * 析构方法
+ * @access public
*/
- private static function parseDsnConfig($dsnStr)
+ public function __destruct()
{
- $info = parse_url($dsnStr);
-
- if (!$info) {
- return [];
- }
-
- $dsn = [
- 'type' => $info['scheme'],
- 'username' => isset($info['user']) ? $info['user'] : '',
- 'password' => isset($info['pass']) ? $info['pass'] : '',
- 'hostname' => isset($info['host']) ? $info['host'] : '',
- 'hostport' => isset($info['port']) ? $info['port'] : '',
- 'database' => !empty($info['path']) ? ltrim($info['path'], '/') : '',
- 'charset' => isset($info['fragment']) ? $info['fragment'] : 'utf8',
- ];
-
- if (isset($info['query'])) {
- parse_str($info['query'], $dsn['params']);
- } else {
- $dsn['params'] = [];
- }
-
- return $dsn;
+ // 关闭连接
+ $this->close();
}
-
}
diff --git a/src/db/ConnectionInterface.php b/src/db/ConnectionInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..18fe1316c6368b6295646f097675ec69d351615c
--- /dev/null
+++ b/src/db/ConnectionInterface.php
@@ -0,0 +1,190 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use Psr\SimpleCache\CacheInterface;
+use think\DbManager;
+
+/**
+ * Connection interface
+ */
+interface ConnectionInterface
+{
+ /**
+ * 获取当前连接器类对应的Query类
+ * @access public
+ * @return string
+ */
+ public function getQueryClass(): string;
+
+ /**
+ * 指定表名开始查询
+ * @param $table
+ * @return BaseQuery
+ */
+ public function table($table);
+
+ /**
+ * 指定表名开始查询(不带前缀)
+ * @param $name
+ * @return BaseQuery
+ */
+ public function name($name);
+
+ /**
+ * 连接数据库方法
+ * @access public
+ * @param array $config 接参数
+ * @param integer $linkNum 连接序号
+ * @return mixed
+ */
+ public function connect(array $config = [], $linkNum = 0);
+
+ /**
+ * 设置当前的数据库Db对象
+ * @access public
+ * @param DbManager $db
+ * @return void
+ */
+ public function setDb(DbManager $db);
+
+ /**
+ * 设置当前的缓存对象
+ * @access public
+ * @param CacheInterface $cache
+ * @return void
+ */
+ public function setCache(CacheInterface $cache);
+
+ /**
+ * 获取数据库的配置参数
+ * @access public
+ * @param string $config 配置名称
+ * @return mixed
+ */
+ public function getConfig(string $config = '');
+
+ /**
+ * 关闭数据库(或者重新连接)
+ * @access public
+ * @return $this
+ */
+ public function close();
+
+ /**
+ * 查找单条记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return array
+ */
+ public function find(BaseQuery $query): array;
+
+ /**
+ * 查找记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return array
+ */
+ public function select(BaseQuery $query): array;
+
+ /**
+ * 插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param boolean $getLastInsID 返回自增主键
+ * @return mixed
+ */
+ public function insert(BaseQuery $query, bool $getLastInsID = false);
+
+ /**
+ * 批量插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param mixed $dataSet 数据集
+ * @return integer
+ */
+ public function insertAll(BaseQuery $query, array $dataSet = []): int;
+
+ /**
+ * 更新记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return integer
+ */
+ public function update(BaseQuery $query): int;
+
+ /**
+ * 删除记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return int
+ */
+ public function delete(BaseQuery $query): int;
+
+ /**
+ * 得到某个字段的值
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $field 字段名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function value(BaseQuery $query, string $field, $default = null);
+
+ /**
+ * 得到某个列的数组
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string|array $column 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
+ */
+ public function column(BaseQuery $query, $column, string $key = ''): array;
+
+ /**
+ * 执行数据库事务
+ * @access public
+ * @param callable $callback 数据操作方法回调
+ * @return mixed
+ */
+ public function transaction(callable $callback);
+
+ /**
+ * 启动事务
+ * @access public
+ * @return void
+ */
+ public function startTrans();
+
+ /**
+ * 用于非自动提交状态下面的查询提交
+ * @access public
+ * @return void
+ */
+ public function commit();
+
+ /**
+ * 事务回滚
+ * @access public
+ * @return void
+ */
+ public function rollback();
+
+ /**
+ * 获取最近一次查询的sql语句
+ * @access public
+ * @return string
+ */
+ public function getLastSql(): string;
+
+}
diff --git a/src/db/Fetch.php b/src/db/Fetch.php
new file mode 100644
index 0000000000000000000000000000000000000000..a997a859f72f40f0a028b7128a8b550e0911861a
--- /dev/null
+++ b/src/db/Fetch.php
@@ -0,0 +1,492 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use think\db\exception\DbException as Exception;
+use think\helper\Str;
+
+/**
+ * SQL获取类
+ */
+class Fetch
+{
+ /**
+ * 查询对象
+ * @var Query
+ */
+ protected $query;
+
+ /**
+ * Connection对象
+ * @var Connection
+ */
+ protected $connection;
+
+ /**
+ * Builder对象
+ * @var Builder
+ */
+ protected $builder;
+
+ /**
+ * 创建一个查询SQL获取对象
+ *
+ * @param Query $query 查询对象
+ */
+ public function __construct(Query $query)
+ {
+ $this->query = $query;
+ $this->connection = $query->getConnection();
+ $this->builder = $this->connection->getBuilder();
+ }
+
+ /**
+ * 聚合查询
+ * @access protected
+ * @param string $aggregate 聚合方法
+ * @param string $field 字段名
+ * @return string
+ */
+ protected function aggregate(string $aggregate, string $field): string
+ {
+ $this->query->parseOptions();
+
+ $field = $aggregate . '(' . $this->builder->parseKey($this->query, $field) . ') AS think_' . strtolower($aggregate);
+
+ return $this->value($field, 0, false);
+ }
+
+ /**
+ * 得到某个字段的值
+ * @access public
+ * @param string $field 字段名
+ * @param mixed $default 默认值
+ * @param bool $one
+ * @return string
+ */
+ public function value(string $field, $default = null, bool $one = true): string
+ {
+ $options = $this->query->parseOptions();
+
+ if (isset($options['field'])) {
+ $this->query->removeOption('field');
+ }
+
+ $this->query->setOption('field', (array) $field);
+
+ // 生成查询SQL
+ $sql = $this->builder->select($this->query, $one);
+
+ if (isset($options['field'])) {
+ $this->query->setOption('field', $options['field']);
+ } else {
+ $this->query->removeOption('field');
+ }
+
+ return $this->fetch($sql);
+ }
+
+ /**
+ * 得到某个列的数组
+ * @access public
+ * @param string $field 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return string
+ */
+ public function column(string $field, string $key = ''): string
+ {
+ $options = $this->query->parseOptions();
+
+ if (isset($options['field'])) {
+ $this->query->removeOption('field');
+ }
+
+ if ($key && '*' != $field) {
+ $field = $key . ',' . $field;
+ }
+
+ $field = array_map('trim', explode(',', $field));
+
+ $this->query->setOption('field', $field);
+
+ // 生成查询SQL
+ $sql = $this->builder->select($this->query);
+
+ if (isset($options['field'])) {
+ $this->query->setOption('field', $options['field']);
+ } else {
+ $this->query->removeOption('field');
+ }
+
+ return $this->fetch($sql);
+ }
+
+ /**
+ * 插入记录
+ * @access public
+ * @param array $data 数据
+ * @return string
+ */
+ public function insert(array $data = []): string
+ {
+ $options = $this->query->parseOptions();
+
+ if (!empty($data)) {
+ $this->query->setOption('data', $data);
+ }
+
+ $sql = $this->builder->insert($this->query);
+
+ return $this->fetch($sql);
+ }
+
+ /**
+ * 插入记录并获取自增ID
+ * @access public
+ * @param array $data 数据
+ * @return string
+ */
+ public function insertGetId(array $data = []): string
+ {
+ return $this->insert($data);
+ }
+
+ /**
+ * 保存数据 自动判断insert或者update
+ * @access public
+ * @param array $data 数据
+ * @param bool $forceInsert 是否强制insert
+ * @return string
+ */
+ public function save(array $data = [], bool $forceInsert = false): string
+ {
+ if ($forceInsert) {
+ return $this->insert($data);
+ }
+
+ $data = array_merge($this->query->getOptions('data') ?: [], $data);
+
+ $this->query->setOption('data', $data);
+
+ if ($this->query->getOptions('where')) {
+ $isUpdate = true;
+ } else {
+ $isUpdate = $this->query->parseUpdateData($data);
+ }
+
+ return $isUpdate ? $this->update() : $this->insert();
+ }
+
+ /**
+ * 批量插入记录
+ * @access public
+ * @param array $dataSet 数据集
+ * @param integer $limit 每次写入数据限制
+ * @return string
+ */
+ public function insertAll(array $dataSet = [], int $limit = null): string
+ {
+ $options = $this->query->parseOptions();
+
+ if (empty($dataSet)) {
+ $dataSet = $options['data'];
+ }
+
+ if (empty($limit) && !empty($options['limit'])) {
+ $limit = $options['limit'];
+ }
+
+ if ($limit) {
+ $array = array_chunk($dataSet, $limit, true);
+ $fetchSql = [];
+ foreach ($array as $item) {
+ $sql = $this->builder->insertAll($this->query, $item);
+ $bind = $this->query->getBind();
+
+ $fetchSql[] = $this->connection->getRealSql($sql, $bind);
+ }
+
+ return implode(';', $fetchSql);
+ }
+
+ $sql = $this->builder->insertAll($this->query, $dataSet);
+
+ return $this->fetch($sql);
+ }
+
+ /**
+ * 通过Select方式插入记录
+ * @access public
+ * @param array $fields 要插入的数据表字段名
+ * @param string $table 要插入的数据表名
+ * @return string
+ */
+ public function selectInsert(array $fields, string $table): string
+ {
+ $this->query->parseOptions();
+
+ $sql = $this->builder->selectInsert($this->query, $fields, $table);
+
+ return $this->fetch($sql);
+ }
+
+ /**
+ * 更新记录
+ * @access public
+ * @param mixed $data 数据
+ * @return string
+ */
+ public function update(array $data = []): string
+ {
+ $options = $this->query->parseOptions();
+
+ $data = !empty($data) ? $data : $options['data'];
+
+ $pk = $this->query->getPk();
+
+ if (empty($options['where'])) {
+ // 如果存在主键数据 则自动作为更新条件
+ if (is_string($pk) && isset($data[$pk])) {
+ $this->query->where($pk, '=', $data[$pk]);
+ unset($data[$pk]);
+ } elseif (is_array($pk)) {
+ // 增加复合主键支持
+ foreach ($pk as $field) {
+ if (isset($data[$field])) {
+ $this->query->where($field, '=', $data[$field]);
+ } else {
+ // 如果缺少复合主键数据则不执行
+ throw new Exception('miss complex primary data');
+ }
+ unset($data[$field]);
+ }
+ }
+
+ if (empty($this->query->getOptions('where'))) {
+ // 如果没有任何更新条件则不执行
+ throw new Exception('miss update condition');
+ }
+ }
+
+ // 更新数据
+ $this->query->setOption('data', $data);
+
+ // 生成UPDATE SQL语句
+ $sql = $this->builder->update($this->query);
+
+ return $this->fetch($sql);
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param mixed $data 表达式 true 表示强制删除
+ * @return string
+ */
+ public function delete($data = null): string
+ {
+ $options = $this->query->parseOptions();
+
+ if (!is_null($data) && true !== $data) {
+ // AR模式分析主键条件
+ $this->query->parsePkWhere($data);
+ }
+
+ if (!empty($options['soft_delete'])) {
+ // 软删除
+ [$field, $condition] = $options['soft_delete'];
+ if ($condition) {
+ $this->query->setOption('soft_delete', null);
+ $this->query->setOption('data', [$field => $condition]);
+ // 生成删除SQL语句
+ $sql = $this->builder->delete($this->query);
+ return $this->fetch($sql);
+ }
+ }
+
+ // 生成删除SQL语句
+ $sql = $this->builder->delete($this->query);
+
+ return $this->fetch($sql);
+ }
+
+ /**
+ * 查找记录 返回SQL
+ * @access public
+ * @param mixed $data
+ * @return string
+ */
+ public function select($data = null): string
+ {
+ $this->query->parseOptions();
+
+ if (!is_null($data)) {
+ // 主键条件分析
+ $this->query->parsePkWhere($data);
+ }
+
+ // 生成查询SQL
+ $sql = $this->builder->select($this->query);
+
+ return $this->fetch($sql);
+ }
+
+ /**
+ * 查找单条记录 返回SQL语句
+ * @access public
+ * @param mixed $data
+ * @return string
+ */
+ public function find($data = null): string
+ {
+ $this->query->parseOptions();
+
+ if (!is_null($data)) {
+ // AR模式分析主键条件
+ $this->query->parsePkWhere($data);
+ }
+
+ // 生成查询SQL
+ $sql = $this->builder->select($this->query, true);
+
+ // 获取实际执行的SQL语句
+ return $this->fetch($sql);
+ }
+
+ /**
+ * 查找多条记录 如果不存在则抛出异常
+ * @access public
+ * @param mixed $data
+ * @return string
+ */
+ public function selectOrFail($data = null): string
+ {
+ return $this->select($data);
+ }
+
+ /**
+ * 查找单条记录 如果不存在则抛出异常
+ * @access public
+ * @param mixed $data
+ * @return string
+ */
+ public function findOrFail($data = null): string
+ {
+ return $this->find($data);
+ }
+
+ /**
+ * 查找单条记录 不存在返回空数据(或者空模型)
+ * @access public
+ * @param mixed $data 数据
+ * @return string
+ */
+ public function findOrEmpty($data = null)
+ {
+ return $this->find($data);
+ }
+
+ /**
+ * 获取实际的SQL语句
+ * @access public
+ * @param string $sql
+ * @return string
+ */
+ public function fetch(string $sql): string
+ {
+ $bind = $this->query->getBind();
+
+ return $this->connection->getRealSql($sql, $bind);
+ }
+
+ /**
+ * COUNT查询
+ * @access public
+ * @param string $field 字段名
+ * @return string
+ */
+ public function count(string $field = '*'): string
+ {
+ $options = $this->query->parseOptions();
+
+ if (!empty($options['group'])) {
+ // 支持GROUP
+ $subSql = $this->query->field('count(' . $field . ') AS think_count')->buildSql();
+ $query = $this->query->newQuery()->table([$subSql => '_group_count_']);
+
+ return $query->fetchsql()->aggregate('COUNT', '*');
+ } else {
+ return $this->aggregate('COUNT', $field);
+ }
+ }
+
+ /**
+ * SUM查询
+ * @access public
+ * @param string $field 字段名
+ * @return string
+ */
+ public function sum(string $field): string
+ {
+ return $this->aggregate('SUM', $field);
+ }
+
+ /**
+ * MIN查询
+ * @access public
+ * @param string $field 字段名
+ * @return string
+ */
+ public function min(string $field): string
+ {
+ return $this->aggregate('MIN', $field);
+ }
+
+ /**
+ * MAX查询
+ * @access public
+ * @param string $field 字段名
+ * @return string
+ */
+ public function max(string $field): string
+ {
+ return $this->aggregate('MAX', $field);
+ }
+
+ /**
+ * AVG查询
+ * @access public
+ * @param string $field 字段名
+ * @return string
+ */
+ public function avg(string $field): string
+ {
+ return $this->aggregate('AVG', $field);
+ }
+
+ public function __call($method, $args)
+ {
+ if (strtolower(substr($method, 0, 5)) == 'getby') {
+ // 根据某个字段获取记录
+ $field = Str::snake(substr($method, 5));
+ return $this->where($field, '=', $args[0])->find();
+ } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') {
+ // 根据某个字段获取记录的某个值
+ $name = Str::snake(substr($method, 10));
+ return $this->where($name, '=', $args[0])->value($args[1]);
+ }
+
+ $result = call_user_func_array([$this->query, $method], $args);
+ return $result === $this->query ? $this : $result;
+ }
+}
diff --git a/src/db/Mongo.php b/src/db/Mongo.php
index d705eaa4637f25c30d0773e8e988a22d73507b6c..cf6e9c4c45723e2faa91b804a62769ea1a4e3621 100644
--- a/src/db/Mongo.php
+++ b/src/db/Mongo.php
@@ -6,140 +6,67 @@
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
-
+declare (strict_types = 1);
namespace think\db;
-use Exception;
-use MongoDB\Driver\BulkWrite;
use MongoDB\Driver\Command;
use MongoDB\Driver\Cursor;
use MongoDB\Driver\Exception\AuthenticationException;
-use MongoDB\Driver\Exception\BulkWriteException;
use MongoDB\Driver\Exception\ConnectionException;
use MongoDB\Driver\Exception\InvalidArgumentException;
use MongoDB\Driver\Exception\RuntimeException;
-use MongoDB\Driver\Query as MongoQuery;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern;
-use think\db\connector\Mongo as MongoConnection;
-use think\db\Query;
+use think\db\exception\DbException as Exception;
+use think\Paginator;
-class Mongo extends Query
+class Mongo extends BaseQuery
{
-
/**
- * 架构函数
- * @access public
- */
- public function __construct(MongoConnection $connection = null)
- {
- if (is_null($connection)) {
- $this->connection = MongoConnection::instance();
- } else {
- $this->connection = $connection;
- }
-
- $this->prefix = $this->connection->getConfig('prefix');
- }
-
- /**
- * 去除某个查询条件
- * @access public
- * @param string $field 查询字段
- * @param string $logic 查询逻辑 and or xor
- * @return $this
+ * 当前数据库连接对象
+ * @var \think\db\connector\Mongo
*/
- public function removeWhereField($field, $logic = 'and')
- {
- $logic = '$' . strtoupper($logic);
-
- if (isset($this->options['where'][$logic])) {
- foreach ($this->options['where'][$logic] as $key => $val) {
- if (is_array($val) && $val[0] == $field) {
- unset($this->options['where'][$logic][$key]);
- }
- }
- }
-
- return $this;
- }
-
- /**
- * 执行查询 返回数据集
- * @access public
- * @param string $namespace
- * @param MongoQuery $query 查询对象
- * @param ReadPreference $readPreference readPreference
- * @param bool|string $class 指定返回的数据集对象
- * @param string|array $typeMap 指定返回的typeMap
- * @return mixed
- * @throws AuthenticationException
- * @throws InvalidArgumentException
- * @throws ConnectionException
- * @throws RuntimeException
- */
- public function mongoQuery($namespace, MongoQuery $query, ReadPreference $readPreference = null, $class = false, $typeMap = null)
- {
- return $this->connection->query($namespace, $query, $readPreference, $class, $typeMap);
- }
+ protected $connection;
/**
* 执行指令 返回数据集
* @access public
- * @param Command $command 指令
- * @param string $dbName
- * @param ReadPreference $readPreference readPreference
- * @param bool|string $class 指定返回的数据集对象
- * @param string|array $typeMap 指定返回的typeMap
+ * @param Command $command 指令
+ * @param string $dbName
+ * @param ReadPreference $readPreference readPreference
+ * @param string|array $typeMap 指定返回的typeMap
* @return mixed
* @throws AuthenticationException
* @throws InvalidArgumentException
* @throws ConnectionException
* @throws RuntimeException
*/
- public function command(Command $command, $dbName = '', ReadPreference $readPreference = null, $class = false, $typeMap = null)
+ public function command(Command $command, string $dbName = '', ReadPreference $readPreference = null, $typeMap = null)
{
- return $this->connection->command($command, $dbName, $readPreference, $class, $typeMap);
- }
-
- /**
- * 执行语句
- * @access public
- * @param string $namespace
- * @param BulkWrite $bulk
- * @param WriteConcern $writeConcern
- * @return int
- * @throws AuthenticationException
- * @throws InvalidArgumentException
- * @throws ConnectionException
- * @throws RuntimeException
- * @throws BulkWriteException
- */
- public function mongoExecute($namespace, BulkWrite $bulk, WriteConcern $writeConcern = null)
- {
- return $this->connection->execute($namespace, $bulk, $writeConcern);
+ return $this->connection->command($command, $dbName, $readPreference, $typeMap);
}
/**
* 执行command
* @access public
- * @param string|array|object $command 指令
- * @param mixed $extra 额外参数
- * @param string $db 数据库名
+ * @param string|array|object $command 指令
+ * @param mixed $extra 额外参数
+ * @param string $db 数据库名
* @return array
*/
- public function cmd($command, $extra = null, $db = null)
+ public function cmd($command, $extra = null, string $db = ''): array
{
+ $this->parseOptions();
return $this->connection->cmd($this, $command, $extra, $db);
}
/**
* 指定distinct查询
* @access public
- * @param string $field 字段名
+ * @param string $field 字段名
* @return array
*/
- public function distinct($field)
+ public function getDistinct(string $field)
{
$result = $this->cmd('distinct', $field);
return $result[0]['values'];
@@ -148,28 +75,28 @@ class Mongo extends Query
/**
* 获取数据库的所有collection
* @access public
- * @param string $db 数据库名称 留空为当前数据库
+ * @param string $db 数据库名称 留空为当前数据库
* @throws Exception
*/
- public function listCollections($db = '')
+ public function listCollections(string $db = '')
{
$cursor = $this->cmd('listCollections', null, $db);
$result = [];
foreach ($cursor as $collection) {
$result[] = $collection['name'];
}
+
return $result;
}
/**
* COUNT查询
* @access public
+ * @param string $field 字段名
* @return integer
*/
- public function count($field = null)
+ public function count(string $field = null): int
{
- $this->parseOptions();
-
$result = $this->cmd('count');
return $result[0]['n'];
@@ -178,17 +105,15 @@ class Mongo extends Query
/**
* 聚合查询
* @access public
- * @param string $aggregate 聚合指令
- * @param string $field 字段名
- * @param bool $force 强制转为数字类型
+ * @param string $aggregate 聚合指令
+ * @param string $field 字段名
+ * @param bool $force 强制转为数字类型
* @return mixed
*/
- public function aggregate($aggregate, $field, $force = false)
+ public function aggregate(string $aggregate, $field, bool $force = false)
{
- $this->parseOptions();
-
$result = $this->cmd('aggregate', [strtolower($aggregate), $field]);
- $value = isset($result[0]['aggregate']) ? $result[0]['aggregate'] : 0;
+ $value = $result[0]['aggregate'] ?? 0;
if ($force) {
$value += 0;
@@ -200,14 +125,12 @@ class Mongo extends Query
/**
* 多聚合操作
*
- * @param array $aggregate 聚合指令, 可以聚合多个参数, 如 ['sum' => 'field1', 'avg' => 'field2']
- * @param array $groupBy 类似mysql里面的group字段, 可以传入多个字段, 如 ['field_a', 'field_b', 'field_c']
+ * @param array $aggregate 聚合指令, 可以聚合多个参数, 如 ['sum' => 'field1', 'avg' => 'field2']
+ * @param array $groupBy 类似mysql里面的group字段, 可以传入多个字段, 如 ['field_a', 'field_b', 'field_c']
* @return array 查询结果
*/
- public function multiAggregate($aggregate, $groupBy)
+ public function multiAggregate(array $aggregate, array $groupBy): array
{
- $this->parseOptions();
-
$result = $this->cmd('multiAggregate', [$aggregate, $groupBy]);
foreach ($result as &$row) {
@@ -222,111 +145,54 @@ class Mongo extends Query
return $result;
}
- /**
- * 字段值(延迟)增长
- * @access public
- * @param string $field 字段名
- * @param integer $step 增长值
- * @param integer $lazyTime 延时时间(s)
- * @return integer|true
- * @throws 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->getTable() . '_' . $field . '_' . serialize($condition));
- $step = $this->lazyWrite($guid, $step, $lazyTime);
- if (empty($step)) {
- return true; // 等待下次写入
- }
- }
-
- return $this->setField($field, ['$inc', $step]);
- }
-
- /**
- * 字段值(延迟)减少
- * @access public
- * @param string $field 字段名
- * @param integer $step 减少值
- * @param integer $lazyTime 延时时间(s)
- * @return integer|true
- * @throws 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->getTable() . '_' . $field . '_' . serialize($condition));
- $step = $this->lazyWrite($guid, -$step, $lazyTime);
- if (empty($step)) {
- return true; // 等待下次写入
- }
- }
-
- return $this->setField($field, ['$inc', -1 * $step]);
- }
-
/**
* 字段值增长
* @access public
- * @param string|array $field 字段名
- * @param integer $step 增长值
+ * @param string $field 字段名
+ * @param float $step 增长值
* @return $this
*/
- public function inc($field, $step = 1, $op = 'inc')
+ public function inc(string $field, float $step = 1)
{
- return parent::inc($field, $step, strtolower('$' . $op));
+ $this->options['data'][$field] = ['$inc', $step];
+
+ return $this;
}
/**
* 字段值减少
* @access public
- * @param string|array $field 字段名
- * @param integer $step 减少值
+ * @param string $field 字段名
+ * @param float $step 减少值
* @return $this
*/
- public function dec($field, $step = 1)
+ public function dec(string $field, float $step = 1)
{
return $this->inc($field, -1 * $step);
}
/**
- * 指定当前操作的collection
+ * 指定当前操作的Collection
* @access public
- * @param string $collection
+ * @param string $table 表名
* @return $this
*/
- public function collection($collection)
+ public function table($table)
{
- return $this->table($collection);
+ $this->options['table'] = $table;
+
+ return $this;
}
/**
- * 不主动获取数据集
+ * table方法的别名
* @access public
- * @param bool $cursor 是否返回 Cursor 对象
+ * @param string $collection
* @return $this
*/
- public function fetchCursor($cursor = true)
+ public function collection(string $collection)
{
- $this->options['fetch_cursor'] = $cursor;
- return $this;
+ return $this->table($collection);
}
/**
@@ -347,7 +213,7 @@ class Mongo extends Query
* @param bool $awaitData
* @return $this
*/
- public function awaitData($awaitData)
+ public function awaitData(bool $awaitData)
{
$this->options['awaitData'] = $awaitData;
return $this;
@@ -359,7 +225,7 @@ class Mongo extends Query
* @param integer $batchSize
* @return $this
*/
- public function batchSize($batchSize)
+ public function batchSize(int $batchSize)
{
$this->options['batchSize'] = $batchSize;
return $this;
@@ -371,7 +237,7 @@ class Mongo extends Query
* @param bool $exhaust
* @return $this
*/
- public function exhaust($exhaust)
+ public function exhaust(bool $exhaust)
{
$this->options['exhaust'] = $exhaust;
return $this;
@@ -383,7 +249,7 @@ class Mongo extends Query
* @param array $modifiers
* @return $this
*/
- public function modifiers($modifiers)
+ public function modifiers(array $modifiers)
{
$this->options['modifiers'] = $modifiers;
return $this;
@@ -395,7 +261,7 @@ class Mongo extends Query
* @param bool $noCursorTimeout
* @return $this
*/
- public function noCursorTimeout($noCursorTimeout)
+ public function noCursorTimeout(bool $noCursorTimeout)
{
$this->options['noCursorTimeout'] = $noCursorTimeout;
return $this;
@@ -407,7 +273,7 @@ class Mongo extends Query
* @param bool $oplogReplay
* @return $this
*/
- public function oplogReplay($oplogReplay)
+ public function oplogReplay(bool $oplogReplay)
{
$this->options['oplogReplay'] = $oplogReplay;
return $this;
@@ -419,7 +285,7 @@ class Mongo extends Query
* @param bool $partial
* @return $this
*/
- public function partial($partial)
+ public function partial(bool $partial)
{
$this->options['partial'] = $partial;
return $this;
@@ -431,7 +297,7 @@ class Mongo extends Query
* @param string $maxTimeMS
* @return $this
*/
- public function maxTimeMS($maxTimeMS)
+ public function maxTimeMS(string $maxTimeMS)
{
$this->options['maxTimeMS'] = $maxTimeMS;
return $this;
@@ -443,47 +309,79 @@ class Mongo extends Query
* @param array $collation
* @return $this
*/
- public function collation($collation)
+ public function collation(array $collation)
{
$this->options['collation'] = $collation;
return $this;
}
+ /**
+ * 设置是否REPLACE
+ * @access public
+ * @param bool $replace 是否使用REPLACE写入数据
+ * @return $this
+ */
+ public function replace(bool $replace = true)
+ {
+ return $this;
+ }
+
/**
* 设置返回字段
* @access public
- * @param array $field
- * @param boolean $except 是否排除
+ * @param mixed $field 字段信息
* @return $this
*/
- public function field($field, $except = false, $tableName = '', $prefix = '', $alias = '')
+ public function field($field)
{
- if (empty($field)) {
- return $this;
- } elseif ($field instanceof Expression) {
- $this->options['field'][] = $field;
+ if (empty($field) || '*' == $field) {
return $this;
}
if (is_string($field)) {
- if (preg_match('/[\<\'\"\(]/', $field)) {
- return $this->fieldRaw($field);
+ $field = array_map('trim', explode(',', $field));
+ }
+
+ $projection = [];
+ foreach ($field as $key => $val) {
+ if (is_numeric($key)) {
+ $projection[$val] = 1;
+ } else {
+ $projection[$key] = $val;
}
+ }
+
+ $this->options['projection'] = $projection;
+
+ return $this;
+ }
+
+ /**
+ * 指定要排除的查询字段
+ * @access public
+ * @param array|string $field 要排除的字段
+ * @return $this
+ */
+ public function withoutField($field)
+ {
+ if (empty($field) || '*' == $field) {
+ return $this;
+ }
+ if (is_string($field)) {
$field = array_map('trim', explode(',', $field));
}
$projection = [];
foreach ($field as $key => $val) {
if (is_numeric($key)) {
- $projection[$val] = $except ? 0 : 1;
+ $projection[$val] = 0;
} else {
$projection[$key] = $val;
}
}
$this->options['projection'] = $projection;
-
return $this;
}
@@ -493,9 +391,9 @@ class Mongo extends Query
* @param integer $skip
* @return $this
*/
- public function skip($skip)
+ public function skip(int $skip)
{
- $this->options['skip'] = intval($skip);
+ $this->options['skip'] = $skip;
return $this;
}
@@ -505,7 +403,7 @@ class Mongo extends Query
* @param bool $slaveOk
* @return $this
*/
- public function slaveOk($slaveOk)
+ public function slaveOk(bool $slaveOk)
{
$this->options['slaveOk'] = $slaveOk;
return $this;
@@ -514,22 +412,19 @@ class Mongo extends Query
/**
* 指定查询数量
* @access public
- * @param mixed $offset 起始位置
- * @param mixed $length 查询数量
+ * @param int $offset 起始位置
+ * @param int $length 查询数量
* @return $this
*/
- public function limit($offset, $length = null)
+ public function limit(int $offset, int $length = null)
{
if (is_null($length)) {
- if (is_numeric($offset)) {
- $length = $offset;
- $offset = 0;
- } else {
- list($offset, $length) = explode(',', $offset);
- }
+ $length = $offset;
+ $offset = 0;
}
- $this->options['skip'] = intval($offset);
- $this->options['limit'] = intval($length);
+
+ $this->options['skip'] = $offset;
+ $this->options['limit'] = $length;
return $this;
}
@@ -537,11 +432,11 @@ class Mongo extends Query
/**
* 设置sort
* @access public
- * @param array|string|object $field
- * @param string $order
+ * @param array|string $field
+ * @param string $order
* @return $this
*/
- public function order($field, $order = '')
+ public function order($field, string $order = '')
{
if (is_array($field)) {
$this->options['sort'] = $field;
@@ -554,10 +449,10 @@ class Mongo extends Query
/**
* 设置tailable
* @access public
- * @param bool $tailable
+ * @param bool $tailable
* @return $this
*/
- public function tailable($tailable)
+ public function tailable(bool $tailable)
{
$this->options['tailable'] = $tailable;
return $this;
@@ -566,86 +461,167 @@ class Mongo extends Query
/**
* 设置writeConcern对象
* @access public
- * @param WriteConcern $writeConcern
+ * @param WriteConcern $writeConcern
* @return $this
*/
- public function writeConcern($writeConcern)
+ public function writeConcern(WriteConcern $writeConcern)
{
$this->options['writeConcern'] = $writeConcern;
return $this;
}
/**
- * 把主键值转换为查询条件 支持复合主键
+ * 获取当前数据表的主键
* @access public
- * @param array|string $data 主键数据
- * @param mixed $options 表达式参数
- * @return void
- * @throws Exception
+ * @return string|array
*/
- public function parsePkWhere($data)
+ public function getPk()
{
- $pk = $this->getPk();
-
- if (is_string($pk)) {
- // 根据主键查询
- if (is_array($data)) {
- $where[$pk] = isset($data[$pk]) ? [$pk, '=', $data[$pk]] : [$pk, 'in', $data];
- } else {
- $where[$pk] = strpos($data, ',') ? [$pk, 'IN', $data] : [$pk, '=', $data];
- }
- }
+ return $this->pk ?: $this->connection->getConfig('pk');
+ }
- if (!empty($where)) {
- if (isset($this->options['where']['$and'])) {
- $this->options['where']['$and'] = array_merge($this->options['where']['$and'], $where);
- } else {
- $this->options['where']['$and'] = $where;
- }
- }
+ /**
+ * 执行查询但只返回Cursor对象
+ * @access public
+ * @return Cursor
+ */
+ public function getCursor(): Cursor
+ {
+ $this->parseOptions();
- return;
+ return $this->connection->getCursor($this);
}
/**
- * 获取当前数据表的主键
+ * 获取当前的查询标识
* @access public
- * @param string|array $options 数据表名或者查询参数
- * @return string|array
+ * @param mixed $data 要序列化的数据
+ * @return string
*/
- public function getPk($options = '')
+ public function getQueryGuid($data = null): string
{
- return $this->pk ?: $this->connection->getConfig('pk');
+ return md5($this->getConfig('database') . serialize(var_export($data ?: $this->options, true)));
}
/**
- * 执行查询但只返回Cursor对象
+ * 分页查询
* @access public
- * @return Cursor
+ * @param int|array $listRows 每页数量 数组表示配置参数
+ * @param int|bool $simple 是否简洁模式或者总记录数
+ * @return Paginator
+ * @throws Exception
*/
- public function getCursor()
+ public function paginate($listRows = null, $simple = false): Paginator
{
- $this->parseOptions();
+ if (is_int($simple)) {
+ $total = $simple;
+ $simple = false;
+ }
- return $this->connection->getCursor($this);
+ $defaultConfig = [
+ 'query' => [], //url额外参数
+ 'fragment' => '', //url锚点
+ 'var_page' => 'page', //分页变量
+ 'list_rows' => 15, //每页数量
+ ];
+
+ if (is_array($listRows)) {
+ $config = array_merge($defaultConfig, $listRows);
+ $listRows = intval($config['list_rows']);
+ } else {
+ $config = $defaultConfig;
+ $listRows = intval($listRows ?: $config['list_rows']);
+ }
+
+ $page = isset($config['page']) ? (int) $config['page'] : Paginator::getCurrentPage($config['var_page']);
+
+ $page = $page < 1 ? 1 : $page;
+
+ $config['path'] = $config['path'] ?? Paginator::getCurrentPath();
+
+ if (!isset($total) && !$simple) {
+ $options = $this->getOptions();
+
+ unset($this->options['order'], $this->options['limit'], $this->options['page'], $this->options['field']);
+
+ $total = $this->count();
+ $results = $this->options($options)->page($page, $listRows)->select();
+ } elseif ($simple) {
+ $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select();
+ $total = null;
+ } else {
+ $results = $this->page($page, $listRows)->select();
+ }
+
+ $this->removeOption('limit');
+ $this->removeOption('page');
+
+ return Paginator::make($results, $listRows, $page, $total, $simple, $config);
}
/**
- * 获取模型的更新条件
- * @access protected
- * @param array $options 查询参数
+ * 分批数据返回处理
+ * @access public
+ * @param integer $count 每次处理的数据数量
+ * @param callable $callback 处理回调方法
+ * @param string|array $column 分批处理的字段名
+ * @param string $order 字段排序
+ * @return bool
+ * @throws Exception
*/
- protected function getModelUpdateCondition(array $options)
+ public function chunk(int $count, callable $callback, $column = null, string $order = 'asc'): bool
{
- return isset($options['where']['$and']) ? $options['where']['$and'] : null;
+ $options = $this->getOptions();
+ $column = $column ?: $this->getPk();
+
+ if (isset($options['order'])) {
+ unset($options['order']);
+ }
+
+ if (is_array($column)) {
+ $times = 1;
+ $query = $this->options($options)->page($times, $count);
+ } else {
+ $query = $this->options($options)->limit($count);
+
+ if (strpos($column, '.')) {
+ [$alias, $key] = explode('.', $column);
+ } else {
+ $key = $column;
+ }
+ }
+
+ $resultSet = $query->order($column, $order)->select();
+
+ while (count($resultSet) > 0) {
+ if (false === call_user_func($callback, $resultSet)) {
+ return false;
+ }
+
+ if (isset($times)) {
+ $times++;
+ $query = $this->options($options)->page($times, $count);
+ } else {
+ $end = $resultSet->pop();
+ $lastId = is_array($end) ? $end[$key] : $end->getData($key);
+
+ $query = $this->options($options)
+ ->limit($count)
+ ->where($column, 'asc' == strtolower($order) ? '>' : '<', $lastId);
+ }
+
+ $resultSet = $query->order($column, $order)->select();
+ }
+
+ return true;
}
/**
* 分析表达式(可用于查询或者写入操作)
- * @access protected
+ * @access public
* @return array
*/
- protected function parseOptions()
+ public function parseOptions(): array
{
$options = $this->options;
@@ -654,7 +630,7 @@ class Mongo extends Query
$options['table'] = $this->getTable();
}
- foreach (['where', 'data'] as $name) {
+ foreach (['where', 'data', 'projection', 'filter', 'json', 'with_attr', 'with_relation_attr'] as $name) {
if (!isset($options[$name])) {
$options[$name] = [];
}
@@ -673,10 +649,6 @@ class Mongo extends Query
$options['modifiers'] = $modifiers;
}
- if (!isset($options['projection']) || '*' == $options['projection']) {
- $options['projection'] = [];
- }
-
if (!isset($options['typeMap'])) {
$options['typeMap'] = $this->getConfig('type_map');
}
@@ -685,7 +657,7 @@ class Mongo extends Query
$options['limit'] = 0;
}
- foreach (['master', 'fetch_cursor'] as $name) {
+ foreach (['master', 'fetch_sql', 'fetch_cursor'] as $name) {
if (!isset($options[$name])) {
$options[$name] = false;
}
@@ -693,12 +665,13 @@ class Mongo extends Query
if (isset($options['page'])) {
// 根据页数计算limit
- list($page, $listRows) = $options['page'];
- $page = $page > 0 ? $page : 1;
- $listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20);
- $offset = $listRows * ($page - 1);
- $options['skip'] = intval($offset);
- $options['limit'] = intval($listRows);
+ [$page, $listRows] = $options['page'];
+
+ $page = $page > 0 ? $page : 1;
+ $listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20);
+ $offset = $listRows * ($page - 1);
+ $options['skip'] = intval($offset);
+ $options['limit'] = intval($listRows);
}
$this->options = $options;
@@ -706,4 +679,30 @@ class Mongo extends Query
return $options;
}
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @return array
+ */
+ public function getFieldsType(): array
+ {
+ if (!empty($this->options['field_type'])) {
+ return $this->options['field_type'];
+ }
+
+ return [];
+ }
+
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @param string $field 字段名
+ * @return string|null
+ */
+ public function getFieldType(string $field)
+ {
+ $fieldType = $this->getFieldsType();
+
+ return $fieldType[$field] ?? null;
+ }
}
diff --git a/src/db/PDOConnection.php b/src/db/PDOConnection.php
new file mode 100644
index 0000000000000000000000000000000000000000..1748d2085648dda4e6592b468834324d97954c46
--- /dev/null
+++ b/src/db/PDOConnection.php
@@ -0,0 +1,1847 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use Closure;
+use PDO;
+use PDOStatement;
+use think\db\exception\BindParamException;
+use think\db\exception\DbEventException;
+use think\db\exception\DbException;
+use think\db\exception\PDOException;
+use think\Model;
+
+/**
+ * 数据库连接基础类
+ * @property PDO[] $links
+ * @property PDO $linkID
+ * @property PDO $linkRead
+ * @property PDO $linkWrite
+ */
+abstract class PDOConnection extends Connection
+{
+ const PARAM_FLOAT = 21;
+
+ /**
+ * 数据库连接参数配置
+ * @var array
+ */
+ protected $config = [
+ // 数据库类型
+ 'type' => '',
+ // 服务器地址
+ 'hostname' => '',
+ // 数据库名
+ 'database' => '',
+ // 用户名
+ 'username' => '',
+ // 密码
+ 'password' => '',
+ // 端口
+ 'hostport' => '',
+ // 连接dsn
+ 'dsn' => '',
+ // 数据库连接参数
+ 'params' => [],
+ // 数据库编码默认采用utf8
+ 'charset' => 'utf8',
+ // 数据库表前缀
+ 'prefix' => '',
+ // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
+ 'deploy' => 0,
+ // 数据库读写是否分离 主从式有效
+ 'rw_separate' => false,
+ // 读写分离后 主服务器数量
+ 'master_num' => 1,
+ // 指定从服务器序号
+ 'slave_no' => '',
+ // 模型写入后自动读取主服务器
+ 'read_master' => false,
+ // 是否严格检查字段是否存在
+ 'fields_strict' => true,
+ // 开启字段缓存
+ 'fields_cache' => false,
+ // 监听SQL
+ 'trigger_sql' => true,
+ // Builder类
+ 'builder' => '',
+ // Query类
+ 'query' => '',
+ // 是否需要断线重连
+ 'break_reconnect' => false,
+ // 断线标识字符串
+ 'break_match_str' => [],
+ ];
+
+ /**
+ * PDO操作实例
+ * @var PDOStatement
+ */
+ protected $PDOStatement;
+
+ /**
+ * 当前SQL指令
+ * @var string
+ */
+ protected $queryStr = '';
+
+ /**
+ * 事务指令数
+ * @var int
+ */
+ protected $transTimes = 0;
+
+ /**
+ * 重连次数
+ * @var int
+ */
+ protected $reConnectTimes = 0;
+
+ /**
+ * 查询结果类型
+ * @var int
+ */
+ protected $fetchType = PDO::FETCH_ASSOC;
+
+ /**
+ * 字段属性大小写
+ * @var int
+ */
+ protected $attrCase = PDO::CASE_LOWER;
+
+ /**
+ * 数据表信息
+ * @var array
+ */
+ protected $info = [];
+
+ /**
+ * 查询开始时间
+ * @var float
+ */
+ protected $queryStartTime;
+
+ /**
+ * PDO连接参数
+ * @var array
+ */
+ protected $params = [
+ PDO::ATTR_CASE => PDO::CASE_NATURAL,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
+ PDO::ATTR_STRINGIFY_FETCHES => false,
+ PDO::ATTR_EMULATE_PREPARES => false,
+ ];
+
+ /**
+ * 参数绑定类型映射
+ * @var array
+ */
+ protected $bindType = [
+ 'string' => PDO::PARAM_STR,
+ 'str' => PDO::PARAM_STR,
+ 'integer' => PDO::PARAM_INT,
+ 'int' => PDO::PARAM_INT,
+ 'boolean' => PDO::PARAM_BOOL,
+ 'bool' => PDO::PARAM_BOOL,
+ 'float' => self::PARAM_FLOAT,
+ 'datetime' => PDO::PARAM_STR,
+ 'timestamp' => PDO::PARAM_STR,
+ ];
+
+ /**
+ * 服务器断线标识字符
+ * @var array
+ */
+ protected $breakMatchStr = [
+ 'server has gone away',
+ 'no connection to the server',
+ 'Lost connection',
+ 'is dead or not enabled',
+ 'Error while sending',
+ 'decryption failed or bad record mac',
+ 'server closed the connection unexpectedly',
+ 'SSL connection has been closed unexpectedly',
+ 'Error writing data to the connection',
+ 'Resource deadlock avoided',
+ 'failed with errno',
+ 'child connection forced to terminate due to client_idle_limit',
+ 'query_wait_timeout',
+ 'reset by peer',
+ 'Physical connection is not usable',
+ 'TCP Provider: Error code 0x68',
+ 'ORA-03114',
+ 'Packets out of order. Expected',
+ 'Adaptive Server connection failed',
+ 'Communication link failure',
+ 'connection is no longer usable',
+ 'Login timeout expired',
+ 'SQLSTATE[HY000] [2002] Connection refused',
+ 'running with the --read-only option so it cannot execute this statement',
+ 'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.',
+ 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again',
+ 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known',
+ 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected',
+ 'SQLSTATE[HY000] [2002] Connection timed out',
+ 'SSL: Connection timed out',
+ 'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.',
+ ];
+
+ /**
+ * 绑定参数
+ * @var array
+ */
+ protected $bind = [];
+
+ /**
+ * 获取当前连接器类对应的Query类
+ * @access public
+ * @return string
+ */
+ public function getQueryClass(): string
+ {
+ return $this->getConfig('query') ?: Query::class;
+ }
+
+ /**
+ * 获取当前连接器类对应的Builder类
+ * @access public
+ * @return string
+ */
+ public function getBuilderClass(): string
+ {
+ return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type'));
+ }
+
+ /**
+ * 解析pdo连接的dsn信息
+ * @access protected
+ * @param array $config 连接信息
+ * @return string
+ */
+ abstract protected function parseDsn(array $config): string;
+
+ /**
+ * 取得数据表的字段信息
+ * @access public
+ * @param string $tableName 数据表名称
+ * @return array
+ */
+ abstract public function getFields(string $tableName): array;
+
+ /**
+ * 取得数据库的表信息
+ * @access public
+ * @param string $dbName 数据库名称
+ * @return array
+ */
+ abstract public function getTables(string $dbName = ''): array;
+
+ /**
+ * 对返数据表字段信息进行大小写转换出来
+ * @access public
+ * @param array $info 字段信息
+ * @return array
+ */
+ public function fieldCase(array $info): array
+ {
+ // 字段大小写转换
+ 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:
+ // 不做转换
+ }
+
+ return $info;
+ }
+
+ /**
+ * 获取字段类型
+ * @access protected
+ * @param string $type 字段类型
+ * @return string
+ */
+ protected function getFieldType(string $type): string
+ {
+ if (0 === stripos($type, 'set') || 0 === stripos($type, 'enum')) {
+ $result = 'string';
+ } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) {
+ $result = 'float';
+ } elseif (preg_match('/(int|serial|bit)/is', $type)) {
+ $result = 'int';
+ } elseif (preg_match('/bool/is', $type)) {
+ $result = 'bool';
+ } elseif (0 === stripos($type, 'timestamp')) {
+ $result = 'timestamp';
+ } elseif (0 === stripos($type, 'datetime')) {
+ $result = 'datetime';
+ } elseif (0 === stripos($type, 'date')) {
+ $result = 'date';
+ } else {
+ $result = 'string';
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取字段绑定类型
+ * @access public
+ * @param string $type 字段类型
+ * @return integer
+ */
+ public function getFieldBindType(string $type): int
+ {
+ if (in_array($type, ['integer', 'string', 'float', 'boolean', 'bool', 'int', 'str'])) {
+ $bind = $this->bindType[$type];
+ } elseif (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) {
+ $bind = PDO::PARAM_STR;
+ } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) {
+ $bind = self::PARAM_FLOAT;
+ } elseif (preg_match('/(int|serial|bit)/is', $type)) {
+ $bind = PDO::PARAM_INT;
+ } elseif (preg_match('/bool/is', $type)) {
+ $bind = PDO::PARAM_BOOL;
+ } else {
+ $bind = PDO::PARAM_STR;
+ }
+
+ return $bind;
+ }
+
+ /**
+ * 获取数据表信息缓存key
+ * @access protected
+ * @param string $schema 数据表名称
+ * @return string
+ */
+ protected function getSchemaCacheKey(string $schema): string
+ {
+ return $this->getConfig('hostname') . ':' . $this->getConfig('hostport') . '@' . $schema;
+ }
+
+ /**
+ * @param string $tableName 数据表名称
+ * @param bool $force 强制从数据库获取
+ * @return array
+ */
+ public function getSchemaInfo(string $tableName, $force = false)
+ {
+ if (!strpos($tableName, '.')) {
+ $schema = $this->getConfig('database') . '.' . $tableName;
+ } else {
+ $schema = $tableName;
+ }
+
+ if (!isset($this->info[$schema]) || $force) {
+ // 读取字段缓存
+ $cacheKey = $this->getSchemaCacheKey($schema);
+ $cacheField = $this->config['fields_cache'] && !empty($this->cache);
+
+ if ($cacheField && !$force) {
+ $info = $this->cache->get($cacheKey);
+ }
+
+ if (empty($info)) {
+ $info = $this->getTableFieldsInfo($tableName);
+ if ($cacheField) {
+ $this->cache->set($cacheKey, $info);
+ }
+ }
+
+ $pk = $info['_pk'] ?? null;
+ $autoinc = $info['_autoinc'] ?? null;
+ unset($info['_pk'], $info['_autoinc']);
+
+ $bind = [];
+ foreach ($info as $name => $val) {
+ $bind[$name] = $this->getFieldBindType($val);
+ }
+
+ $this->info[$schema] = [
+ 'fields' => array_keys($info),
+ 'type' => $info,
+ 'bind' => $bind,
+ 'pk' => $pk,
+ 'autoinc' => $autoinc,
+ ];
+ }
+
+ return $this->info[$schema];
+ }
+
+ /**
+ * 获取数据表信息
+ * @access public
+ * @param mixed $tableName 数据表名 留空自动获取
+ * @param string $fetch 获取信息类型 包括 fields type bind pk
+ * @return mixed
+ */
+ public function getTableInfo($tableName, string $fetch = '')
+ {
+ if (is_array($tableName)) {
+ $tableName = key($tableName) ?: current($tableName);
+ }
+
+ if (strpos($tableName, ',') || strpos($tableName, ')')) {
+ // 多表不获取字段信息
+ return [];
+ }
+
+ [$tableName] = explode(' ', $tableName);
+
+ $info = $this->getSchemaInfo($tableName);
+
+ return $fetch ? $info[$fetch] : $info;
+ }
+
+ /**
+ * 获取数据表的字段信息
+ * @access public
+ * @param string $tableName 数据表名
+ * @return array
+ */
+ public function getTableFieldsInfo(string $tableName): array
+ {
+ $fields = $this->getFields($tableName);
+ $info = [];
+
+ foreach ($fields as $key => $val) {
+ // 记录字段类型
+ $info[$key] = $this->getFieldType($val['type']);
+
+ if (!empty($val['primary'])) {
+ $pk[] = $key;
+ }
+
+ if (!empty($val['autoinc'])) {
+ $autoinc = $key;
+ }
+ }
+
+ if (isset($pk)) {
+ // 设置主键
+ $pk = count($pk) > 1 ? $pk : $pk[0];
+ $info['_pk'] = $pk;
+ }
+
+ if (isset($autoinc)) {
+ $info['_autoinc'] = $autoinc;
+ }
+
+ return $info;
+ }
+
+ /**
+ * 获取数据表的主键
+ * @access public
+ * @param mixed $tableName 数据表名
+ * @return string|array
+ */
+ public function getPk($tableName)
+ {
+ return $this->getTableInfo($tableName, 'pk');
+ }
+
+ /**
+ * 获取数据表的自增主键
+ * @access public
+ * @param mixed $tableName 数据表名
+ * @return string
+ */
+ public function getAutoInc($tableName)
+ {
+ return $this->getTableInfo($tableName, 'autoinc');
+ }
+
+ /**
+ * 获取数据表字段信息
+ * @access public
+ * @param mixed $tableName 数据表名
+ * @return array
+ */
+ public function getTableFields($tableName): array
+ {
+ return $this->getTableInfo($tableName, 'fields');
+ }
+
+ /**
+ * 获取数据表字段类型
+ * @access public
+ * @param mixed $tableName 数据表名
+ * @param string $field 字段名
+ * @return array|string
+ */
+ public function getFieldsType($tableName, string $field = null)
+ {
+ $result = $this->getTableInfo($tableName, 'type');
+
+ if ($field && isset($result[$field])) {
+ return $result[$field];
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取数据表绑定信息
+ * @access public
+ * @param mixed $tableName 数据表名
+ * @return array
+ */
+ public function getFieldsBind($tableName): array
+ {
+ return $this->getTableInfo($tableName, 'bind');
+ }
+
+ /**
+ * 连接数据库方法
+ * @access public
+ * @param array $config 连接参数
+ * @param integer $linkNum 连接序号
+ * @param array|bool $autoConnection 是否自动连接主数据库(用于分布式)
+ * @return PDO
+ * @throws PDOException
+ */
+ public function connect(array $config = [], $linkNum = 0, $autoConnection = false): PDO
+ {
+ if (isset($this->links[$linkNum])) {
+ return $this->links[$linkNum];
+ }
+
+ if (empty($config)) {
+ $config = $this->config;
+ } else {
+ $config = array_merge($this->config, $config);
+ }
+
+ // 连接参数
+ if (isset($config['params']) && is_array($config['params'])) {
+ $params = $config['params'] + $this->params;
+ } else {
+ $params = $this->params;
+ }
+
+ // 记录当前字段属性大小写设置
+ $this->attrCase = $params[PDO::ATTR_CASE];
+
+ if (!empty($config['break_match_str'])) {
+ $this->breakMatchStr = array_merge($this->breakMatchStr, (array) $config['break_match_str']);
+ }
+
+ try {
+ if (empty($config['dsn'])) {
+ $config['dsn'] = $this->parseDsn($config);
+ }
+
+ $startTime = microtime(true);
+
+ $this->links[$linkNum] = $this->createPdo($config['dsn'], $config['username'], $config['password'], $params);
+
+ // SQL监控
+ if (!empty($config['trigger_sql'])) {
+ $this->trigger('CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);
+ }
+
+ return $this->links[$linkNum];
+ } catch (\PDOException $e) {
+ if ($autoConnection) {
+ $this->db->log($e->getMessage(), 'error');
+ return $this->connect($autoConnection, $linkNum);
+ } else {
+ throw $e;
+ }
+ }
+ }
+
+ /**
+ * 视图查询
+ * @access public
+ * @param array $args
+ * @return BaseQuery
+ */
+ public function view(...$args)
+ {
+ return $this->newQuery()->view(...$args);
+ }
+
+ /**
+ * 创建PDO实例
+ * @param $dsn
+ * @param $username
+ * @param $password
+ * @param $params
+ * @return PDO
+ */
+ protected function createPdo($dsn, $username, $password, $params)
+ {
+ return new PDO($dsn, $username, $password, $params);
+ }
+
+ /**
+ * 释放查询结果
+ * @access public
+ */
+ public function free(): void
+ {
+ $this->PDOStatement = null;
+ }
+
+ /**
+ * 获取PDO对象
+ * @access public
+ * @return PDO|false
+ */
+ public function getPdo()
+ {
+ if (!$this->linkID) {
+ return false;
+ }
+
+ return $this->linkID;
+ }
+
+ /**
+ * 执行查询 使用生成器返回数据
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $sql sql指令
+ * @param array $bind 参数绑定
+ * @param Model|null $model 模型对象实例
+ * @param null $condition 查询条件
+ * @return \Generator
+ * @throws DbException
+ */
+ public function getCursor(BaseQuery $query, string $sql, array $bind = [], $model = null, $condition = null)
+ {
+ $this->queryPDOStatement($query, $sql, $bind);
+
+ // 返回结果集
+ while ($result = $this->PDOStatement->fetch($this->fetchType)) {
+ if ($model) {
+ yield $model->newInstance($result, $condition);
+ } else {
+ yield $result;
+ }
+ }
+ }
+
+ /**
+ * 执行查询 返回数据集
+ * @access public
+ * @param string $sql sql指令
+ * @param array $bind 参数绑定
+ * @param bool $master 主库读取
+ * @return array
+ * @throws DbException
+ */
+ public function query(string $sql, array $bind = [], bool $master = false): array
+ {
+ return $this->pdoQuery($this->newQuery(), $sql, $bind, $master);
+ }
+
+ /**
+ * 执行语句
+ * @access public
+ * @param string $sql sql指令
+ * @param array $bind 参数绑定
+ * @return int
+ * @throws DbException
+ */
+ public function execute(string $sql, array $bind = []): int
+ {
+ return $this->pdoExecute($this->newQuery(), $sql, $bind, true);
+ }
+
+ /**
+ * 执行查询 返回数据集
+ * @access protected
+ * @param BaseQuery $query 查询对象
+ * @param mixed $sql sql指令
+ * @param array $bind 参数绑定
+ * @param bool $master 主库读取
+ * @return array
+ * @throws DbException
+ */
+ protected function pdoQuery(BaseQuery $query, $sql, array $bind = [], bool $master = null): array
+ {
+ // 分析查询表达式
+ $query->parseOptions();
+
+ if ($query->getOptions('cache')) {
+ // 检查查询缓存
+ $cacheItem = $this->parseCache($query, $query->getOptions('cache'));
+ $key = $cacheItem->getKey();
+
+ $data = $this->cache->get($key);
+
+ if (null !== $data) {
+ return $data;
+ }
+ }
+
+ if ($sql instanceof Closure) {
+ $sql = $sql($query);
+ $bind = $query->getBind();
+ }
+
+ if (!isset($master)) {
+ $master = $query->getOptions('master') ? true : false;
+ }
+
+ $procedure = $query->getOptions('procedure') ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
+
+ $this->getPDOStatement($sql, $bind, $master, $procedure);
+
+ $resultSet = $this->getResult($procedure);
+ $requireCache = $query->getOptions('cache_always') || !empty($resultSet);
+
+ if (isset($cacheItem) && $requireCache) {
+ // 缓存数据集
+ $cacheItem->set($resultSet);
+ $this->cacheData($cacheItem);
+ }
+
+ return $resultSet;
+ }
+
+ /**
+ * 执行查询但只返回PDOStatement对象
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return \PDOStatement
+ * @throws DbException
+ */
+ public function pdo(BaseQuery $query): PDOStatement
+ {
+ $bind = $query->getBind();
+ // 生成查询SQL
+ $sql = $this->builder->select($query);
+
+ return $this->queryPDOStatement($query, $sql, $bind);
+ }
+
+ /**
+ * 执行查询但只返回PDOStatement对象
+ * @access public
+ * @param string $sql sql指令
+ * @param array $bind 参数绑定
+ * @param bool $master 是否在主服务器读操作
+ * @param bool $procedure 是否为存储过程调用
+ * @return PDOStatement
+ * @throws DbException
+ */
+ public function getPDOStatement(string $sql, array $bind = [], bool $master = false, bool $procedure = false): PDOStatement
+ {
+ try {
+ $this->initConnect($this->readMaster ?: $master);
+ // 记录SQL语句
+ $this->queryStr = $sql;
+ $this->bind = $bind;
+
+ $this->db->updateQueryTimes();
+ $this->queryStartTime = microtime(true);
+
+ // 预处理
+ $this->PDOStatement = $this->linkID->prepare($sql);
+
+ // 参数绑定
+ if ($procedure) {
+ $this->bindParam($bind);
+ } else {
+ $this->bindValue($bind);
+ }
+
+ // 执行查询
+ $this->PDOStatement->execute();
+
+ // SQL监控
+ if (!empty($this->config['trigger_sql'])) {
+ $this->trigger('', $master);
+ }
+
+ $this->reConnectTimes = 0;
+
+ return $this->PDOStatement;
+ } catch (\Throwable | \Exception $e) {
+ if ($this->transTimes > 0) {
+ // 事务活动中时不应该进行重试,应直接中断执行,防止造成污染。
+ if ($this->isBreak($e)) {
+ // 尝试对事务计数进行重置
+ $this->transTimes = 0;
+ }
+ } else {
+ if ($this->reConnectTimes < 4 && $this->isBreak($e)) {
+ ++$this->reConnectTimes;
+ return $this->close()->getPDOStatement($sql, $bind, $master, $procedure);
+ }
+ }
+
+ if ($e instanceof \PDOException) {
+ throw new PDOException($e, $this->config, $this->getLastsql());
+ } else {
+ throw $e;
+ }
+ }
+ }
+
+ /**
+ * 执行语句
+ * @access protected
+ * @param BaseQuery $query 查询对象
+ * @param string $sql sql指令
+ * @param array $bind 参数绑定
+ * @param bool $origin 是否原生查询
+ * @return int
+ * @throws DbException
+ */
+ protected function pdoExecute(BaseQuery $query, string $sql, array $bind = [], bool $origin = false): int
+ {
+ if ($origin) {
+ $query->parseOptions();
+ }
+
+ $this->queryPDOStatement($query->master(true), $sql, $bind);
+
+ if (!$origin && !empty($this->config['deploy']) && !empty($this->config['read_master'])) {
+ $this->readMaster = true;
+ }
+
+ $this->numRows = $this->PDOStatement->rowCount();
+
+ if ($query->getOptions('cache')) {
+ // 清理缓存数据
+ $cacheItem = $this->parseCache($query, $query->getOptions('cache'));
+ $key = $cacheItem->getKey();
+ $tag = $cacheItem->getTag();
+
+ if (isset($key) && $this->cache->has($key)) {
+ $this->cache->delete($key);
+ } elseif (!empty($tag) && method_exists($this->cache, 'tag')) {
+ $this->cache->tag($tag)->clear();
+ }
+ }
+
+ return $this->numRows;
+ }
+
+ /**
+ * @param BaseQuery $query
+ * @param string $sql
+ * @param array $bind
+ * @return PDOStatement
+ * @throws DbException
+ */
+ protected function queryPDOStatement(BaseQuery $query, string $sql, array $bind = []): PDOStatement
+ {
+ $options = $query->getOptions();
+ $master = !empty($options['master']) ? true : false;
+ $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
+
+ return $this->getPDOStatement($sql, $bind, $master, $procedure);
+ }
+
+ /**
+ * 查找单条记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return array
+ * @throws DbException
+ */
+ public function find(BaseQuery $query): array
+ {
+ // 事件回调
+ try {
+ $this->db->trigger('before_find', $query);
+ } catch (DbEventException $e) {
+ return [];
+ }
+
+ // 执行查询
+ $resultSet = $this->pdoQuery($query, function ($query) {
+ return $this->builder->select($query, true);
+ });
+
+ return $resultSet[0] ?? [];
+ }
+
+ /**
+ * 使用游标查询记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return \Generator
+ */
+ public function cursor(BaseQuery $query)
+ {
+ // 分析查询表达式
+ $options = $query->parseOptions();
+
+ // 生成查询SQL
+ $sql = $this->builder->select($query);
+
+ $condition = $options['where']['AND'] ?? null;
+
+ // 执行查询操作
+ return $this->getCursor($query, $sql, $query->getBind(), $query->getModel(), $condition);
+ }
+
+ /**
+ * 查找记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return array
+ * @throws DbException
+ */
+ public function select(BaseQuery $query): array
+ {
+ try {
+ $this->db->trigger('before_select', $query);
+ } catch (DbEventException $e) {
+ return [];
+ }
+
+ // 执行查询操作
+ return $this->pdoQuery($query, function ($query) {
+ return $this->builder->select($query);
+ });
+ }
+
+ /**
+ * 插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param boolean $getLastInsID 返回自增主键
+ * @return mixed
+ */
+ public function insert(BaseQuery $query, bool $getLastInsID = false)
+ {
+ // 分析查询表达式
+ $options = $query->parseOptions();
+
+ // 生成SQL语句
+ $sql = $this->builder->insert($query);
+
+ // 执行操作
+ $result = '' == $sql ? 0 : $this->pdoExecute($query, $sql, $query->getBind());
+
+ if ($result) {
+ $sequence = $options['sequence'] ?? null;
+ $lastInsId = $this->getLastInsID($query, $sequence);
+
+ $data = $options['data'];
+
+ if ($lastInsId) {
+ $pk = $query->getAutoInc();
+ if ($pk) {
+ $data[$pk] = $lastInsId;
+ }
+ }
+
+ $query->setOption('data', $data);
+
+ $this->db->trigger('after_insert', $query);
+
+ if ($getLastInsID && $lastInsId) {
+ return $lastInsId;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 批量插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param mixed $dataSet 数据集
+ * @param integer $limit 每次写入数据限制
+ * @return integer
+ * @throws \Exception
+ * @throws \Throwable
+ */
+ public function insertAll(BaseQuery $query, array $dataSet = [], int $limit = 0): int
+ {
+ if (!is_array(reset($dataSet))) {
+ return 0;
+ }
+
+ $options = $query->parseOptions();
+ $replace = !empty($options['replace']);
+
+ if (0 === $limit && count($dataSet) >= 5000) {
+ $limit = 1000;
+ }
+
+ if ($limit) {
+ // 分批写入 自动启动事务支持
+ $this->startTrans();
+
+ try {
+ $array = array_chunk($dataSet, $limit, true);
+ $count = 0;
+
+ foreach ($array as $item) {
+ $sql = $this->builder->insertAll($query, $item, $replace);
+ $count += $this->pdoExecute($query, $sql, $query->getBind());
+ }
+
+ // 提交事务
+ $this->commit();
+ } catch (\Exception | \Throwable $e) {
+ $this->rollback();
+ throw $e;
+ }
+
+ return $count;
+ }
+
+ $sql = $this->builder->insertAll($query, $dataSet, $replace);
+
+ return $this->pdoExecute($query, $sql, $query->getBind());
+ }
+
+ /**
+ * 通过Select方式插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param array $fields 要插入的数据表字段名
+ * @param string $table 要插入的数据表名
+ * @return integer
+ * @throws PDOException
+ */
+ public function selectInsert(BaseQuery $query, array $fields, string $table): int
+ {
+ // 分析查询表达式
+ $query->parseOptions();
+
+ $sql = $this->builder->selectInsert($query, $fields, $table);
+
+ return $this->pdoExecute($query, $sql, $query->getBind());
+ }
+
+ /**
+ * 更新记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return integer
+ * @throws PDOException
+ */
+ public function update(BaseQuery $query): int
+ {
+ $query->parseOptions();
+
+ // 生成UPDATE SQL语句
+ $sql = $this->builder->update($query);
+
+ // 执行操作
+ $result = '' == $sql ? 0 : $this->pdoExecute($query, $sql, $query->getBind());
+
+ if ($result) {
+ $this->db->trigger('after_update', $query);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return int
+ * @throws PDOException
+ */
+ public function delete(BaseQuery $query): int
+ {
+ // 分析查询表达式
+ $query->parseOptions();
+
+ // 生成删除SQL语句
+ $sql = $this->builder->delete($query);
+
+ // 执行操作
+ $result = $this->pdoExecute($query, $sql, $query->getBind());
+
+ if ($result) {
+ $this->db->trigger('after_delete', $query);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 得到某个字段的值
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $field 字段名
+ * @param mixed $default 默认值
+ * @param bool $one 返回一个值
+ * @return mixed
+ */
+ public function value(BaseQuery $query, string $field, $default = null, bool $one = true)
+ {
+ $options = $query->parseOptions();
+
+ if (isset($options['field'])) {
+ $query->removeOption('field');
+ }
+
+ if (isset($options['group'])) {
+ $query->group('');
+ }
+
+ $query->setOption('field', (array) $field);
+
+ if (!empty($options['cache'])) {
+ $cacheItem = $this->parseCache($query, $options['cache'], 'value');
+ $key = $cacheItem->getKey();
+
+ if ($this->cache->has($key)) {
+ return $this->cache->get($key);
+ }
+ }
+
+ // 生成查询SQL
+ $sql = $this->builder->select($query, $one);
+
+ if (isset($options['field'])) {
+ $query->setOption('field', $options['field']);
+ } else {
+ $query->removeOption('field');
+ }
+
+ if (isset($options['group'])) {
+ $query->setOption('group', $options['group']);
+ }
+
+ // 执行查询操作
+ $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
+
+ $result = $pdo->fetchColumn();
+
+ if (isset($cacheItem)) {
+ // 缓存数据
+ $cacheItem->set($result);
+ $this->cacheData($cacheItem);
+ }
+
+ return false !== $result ? $result : $default;
+ }
+
+ /**
+ * 得到某个字段的值
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $aggregate 聚合方法
+ * @param mixed $field 字段名
+ * @param bool $force 强制转为数字类型
+ * @return mixed
+ */
+ public function aggregate(BaseQuery $query, string $aggregate, $field, bool $force = false)
+ {
+ if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) {
+ [$distinct, $field] = explode(' ', $field);
+ }
+
+ $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS think_' . strtolower($aggregate);
+
+ $result = $this->value($query, $field, 0);
+
+ return $force ? (float) $result : $result;
+ }
+
+ /**
+ * 得到某个列的数组
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string|array $column 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
+ */
+ public function column(BaseQuery $query, $column, string $key = ''): array
+ {
+ $options = $query->parseOptions();
+
+ if (isset($options['field'])) {
+ $query->removeOption('field');
+ }
+
+ if (empty($key) || trim($key) === '') {
+ $key = null;
+ }
+
+ if (\is_string($column)) {
+ $column = \trim($column);
+ if ('*' !== $column) {
+ $column = \array_map('\trim', \explode(',', $column));
+ }
+ } elseif (\is_array($column)) {
+ if (\in_array('*', $column)) {
+ $column = '*';
+ }
+ } else {
+ throw new DbException('not support type');
+ }
+
+ $field = $column;
+ if ('*' !== $column && $key && !\in_array($key, $column)) {
+ $field[] = $key;
+ }
+
+ $query->setOption('field', $field);
+
+ if (!empty($options['cache'])) {
+ // 判断查询缓存
+ $cacheItem = $this->parseCache($query, $options['cache'], 'column');
+ $name = $cacheItem->getKey();
+
+ if ($this->cache->has($name)) {
+ return $this->cache->get($name);
+ }
+ }
+
+ // 生成查询SQL
+ $sql = $this->builder->select($query);
+
+ if (isset($options['field'])) {
+ $query->setOption('field', $options['field']);
+ } else {
+ $query->removeOption('field');
+ }
+
+ // 执行查询操作
+ $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
+ $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC);
+
+ if (is_string($key) && strpos($key, '.')) {
+ [$alias, $key] = explode('.', $key);
+ }
+
+ if (empty($resultSet)) {
+ $result = [];
+ } elseif ('*' !== $column && \count($column) === 1) {
+ $column = \array_shift($column);
+ if (\strpos($column, ' ')) {
+ $column = \substr(\strrchr(\trim($column), ' '), 1);
+ }
+
+ if (\strpos($column, '.')) {
+ [$alias, $column] = \explode('.', $column);
+ }
+
+ $result = \array_column($resultSet, $column, $key);
+ } elseif ($key) {
+ $result = \array_column($resultSet, null, $key);
+ } else {
+ $result = $resultSet;
+ }
+
+ if (isset($cacheItem)) {
+ // 缓存数据
+ $cacheItem->set($result);
+ $this->cacheData($cacheItem);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 根据参数绑定组装最终的SQL语句 便于调试
+ * @access public
+ * @param string $sql 带参数绑定的sql语句
+ * @param array $bind 参数绑定列表
+ * @return string
+ */
+ public function getRealSql(string $sql, array $bind = []): string
+ {
+ foreach ($bind as $key => $val) {
+ $value = strval(is_array($val) ? $val[0] : $val);
+ $type = is_array($val) ? $val[1] : PDO::PARAM_STR;
+
+ if (self::PARAM_FLOAT == $type || PDO::PARAM_STR == $type) {
+ $value = '\'' . addslashes($value) . '\'';
+ } elseif (PDO::PARAM_INT == $type && '' === $value) {
+ $value = '0';
+ }
+
+ // 判断占位符
+ $sql = is_numeric($key) ?
+ substr_replace($sql, $value, strpos($sql, '?'), 1) :
+ substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key));
+ }
+
+ return rtrim($sql);
+ }
+
+ /**
+ * 参数绑定
+ * 支持 ['name'=>'value','id'=>123] 对应命名占位符
+ * 或者 ['value',123] 对应问号占位符
+ * @access public
+ * @param array $bind 要绑定的参数列表
+ * @return void
+ * @throws BindParamException
+ */
+ protected function bindValue(array $bind = []): void
+ {
+ foreach ($bind as $key => $val) {
+ // 占位符
+ $param = is_numeric($key) ? $key + 1 : ':' . $key;
+
+ if (is_array($val)) {
+ if (PDO::PARAM_INT == $val[1] && '' === $val[0]) {
+ $val[0] = 0;
+ } elseif (self::PARAM_FLOAT == $val[1]) {
+ $val[0] = is_string($val[0]) ? (float) $val[0] : $val[0];
+ $val[1] = PDO::PARAM_STR;
+ }
+
+ $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]);
+ } else {
+ $result = $this->PDOStatement->bindValue($param, $val);
+ }
+
+ if (!$result) {
+ throw new BindParamException(
+ "Error occurred when binding parameters '{$param}'",
+ $this->config,
+ $this->getLastsql(),
+ $bind
+ );
+ }
+ }
+ }
+
+ /**
+ * 存储过程的输入输出参数绑定
+ * @access public
+ * @param array $bind 要绑定的参数列表
+ * @return void
+ * @throws BindParamException
+ */
+ protected function bindParam(array $bind): void
+ {
+ foreach ($bind as $key => $val) {
+ $param = is_numeric($key) ? $key + 1 : ':' . $key;
+
+ if (is_array($val)) {
+ array_unshift($val, $param);
+ $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val);
+ } else {
+ $result = $this->PDOStatement->bindValue($param, $val);
+ }
+
+ if (!$result) {
+ $param = array_shift($val);
+
+ throw new BindParamException(
+ "Error occurred when binding parameters '{$param}'",
+ $this->config,
+ $this->getLastsql(),
+ $bind
+ );
+ }
+ }
+ }
+
+ /**
+ * 获得数据集数组
+ * @access protected
+ * @param bool $procedure 是否存储过程
+ * @return array
+ */
+ protected function getResult(bool $procedure = false): array
+ {
+ if ($procedure) {
+ // 存储过程返回结果
+ return $this->procedure();
+ }
+
+ $result = $this->PDOStatement->fetchAll($this->fetchType);
+
+ $this->numRows = count($result);
+
+ return $result;
+ }
+
+ /**
+ * 获得存储过程数据集
+ * @access protected
+ * @return array
+ */
+ protected function procedure(): array
+ {
+ $item = [];
+
+ do {
+ $result = $this->getResult();
+ if (!empty($result)) {
+ $item[] = $result;
+ }
+ } while ($this->PDOStatement->nextRowset());
+
+ $this->numRows = count($item);
+
+ return $item;
+ }
+
+ /**
+ * 执行数据库事务
+ * @access public
+ * @param callable $callback 数据操作方法回调
+ * @return mixed
+ * @throws PDOException
+ * @throws \Exception
+ * @throws \Throwable
+ */
+ public function transaction(callable $callback)
+ {
+ $this->startTrans();
+
+ try {
+ $result = null;
+ if (is_callable($callback)) {
+ $result = $callback($this);
+ }
+
+ $this->commit();
+ return $result;
+ } catch (\Exception | \Throwable $e) {
+ $this->rollback();
+ throw $e;
+ }
+ }
+
+ /**
+ * 启动事务
+ * @access public
+ * @return void
+ * @throws \PDOException
+ * @throws \Exception
+ */
+ public function startTrans(): void
+ {
+ try {
+ $this->initConnect(true);
+
+ ++$this->transTimes;
+
+ if (1 == $this->transTimes) {
+ $this->linkID->beginTransaction();
+ } elseif ($this->transTimes > 1 && $this->supportSavepoint() && $this->linkID->inTransaction()) {
+ $this->linkID->exec(
+ $this->parseSavepoint('trans' . $this->transTimes)
+ );
+ }
+ $this->reConnectTimes = 0;
+ } catch (\Throwable | \Exception $e) {
+ if (1 === $this->transTimes && $this->reConnectTimes < 4 && $this->isBreak($e)) {
+ --$this->transTimes;
+ ++$this->reConnectTimes;
+ $this->close()->startTrans();
+ } else {
+ if ($this->isBreak($e)) {
+ // 尝试对事务计数进行重置
+ $this->transTimes = 0;
+ }
+ throw $e;
+ }
+ }
+ }
+
+ /**
+ * 用于非自动提交状态下面的查询提交
+ * @access public
+ * @return void
+ * @throws \PDOException
+ */
+ public function commit(): void
+ {
+ $this->initConnect(true);
+
+ if (1 == $this->transTimes && $this->linkID->inTransaction()) {
+ $this->linkID->commit();
+ }
+
+ --$this->transTimes;
+ }
+
+ /**
+ * 事务回滚
+ * @access public
+ * @return void
+ * @throws \PDOException
+ */
+ public function rollback(): void
+ {
+ $this->initConnect(true);
+
+ if ($this->linkID->inTransaction()) {
+ if (1 == $this->transTimes) {
+ $this->linkID->rollBack();
+ } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
+ $this->linkID->exec(
+ $this->parseSavepointRollBack('trans' . $this->transTimes)
+ );
+ }
+ }
+
+ $this->transTimes = max(0, $this->transTimes - 1);
+ }
+
+ /**
+ * 是否支持事务嵌套
+ * @return bool
+ */
+ protected function supportSavepoint(): bool
+ {
+ return false;
+ }
+
+ /**
+ * 生成定义保存点的SQL
+ * @access protected
+ * @param string $name 标识
+ * @return string
+ */
+ protected function parseSavepoint(string $name): string
+ {
+ return 'SAVEPOINT ' . $name;
+ }
+
+ /**
+ * 生成回滚到保存点的SQL
+ * @access protected
+ * @param string $name 标识
+ * @return string
+ */
+ protected function parseSavepointRollBack(string $name): string
+ {
+ return 'ROLLBACK TO SAVEPOINT ' . $name;
+ }
+
+ /**
+ * 批处理执行SQL语句
+ * 批处理的指令都认为是execute操作
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param array $sqlArray SQL批处理指令
+ * @param array $bind 参数绑定
+ * @return bool
+ */
+ public function batchQuery(BaseQuery $query, array $sqlArray = [], array $bind = []): bool
+ {
+ // 自动启动事务支持
+ $this->startTrans();
+
+ try {
+ foreach ($sqlArray as $sql) {
+ $this->pdoExecute($query, $sql, $bind);
+ }
+ // 提交事务
+ $this->commit();
+ } catch (\Exception $e) {
+ $this->rollback();
+ throw $e;
+ }
+
+ return true;
+ }
+
+ /**
+ * 关闭数据库(或者重新连接)
+ * @access public
+ * @return $this
+ */
+ public function close()
+ {
+ $this->linkID = null;
+ $this->linkWrite = null;
+ $this->linkRead = null;
+ $this->links = [];
+ $this->transTimes = 0;
+
+ $this->free();
+
+ return $this;
+ }
+
+ /**
+ * 是否断线
+ * @access protected
+ * @param \PDOException|\Exception $e 异常对象
+ * @return bool
+ */
+ protected function isBreak($e): bool
+ {
+ if (!$this->config['break_reconnect']) {
+ return false;
+ }
+
+ $error = $e->getMessage();
+
+ foreach ($this->breakMatchStr as $msg) {
+ if (false !== stripos($error, $msg)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取最近一次查询的sql语句
+ * @access public
+ * @return string
+ */
+ public function getLastSql(): string
+ {
+ return $this->getRealSql($this->queryStr, $this->bind);
+ }
+
+ /**
+ * 获取最近插入的ID
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $sequence 自增序列名
+ * @return mixed
+ */
+ public function getLastInsID(BaseQuery $query, string $sequence = null)
+ {
+ try {
+ $insertId = $this->linkID->lastInsertId($sequence);
+ } catch (\Exception $e) {
+ $insertId = '';
+ }
+
+ return $this->autoInsIDType($query, $insertId);
+ }
+
+ /**
+ * 获取最近插入的ID
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $insertId 自增ID
+ * @return mixed
+ */
+ protected function autoInsIDType(BaseQuery $query, string $insertId)
+ {
+ $pk = $query->getAutoInc();
+
+ if ($pk) {
+ $type = $this->getFieldBindType($pk);
+
+ if (PDO::PARAM_INT == $type) {
+ $insertId = (int) $insertId;
+ } elseif (self::PARAM_FLOAT == $type) {
+ $insertId = (float) $insertId;
+ }
+ }
+
+ return $insertId;
+ }
+
+ /**
+ * 获取最近的错误信息
+ * @access public
+ * @return string
+ */
+ public function getError(): string
+ {
+ if ($this->PDOStatement) {
+ $error = $this->PDOStatement->errorInfo();
+ $error = $error[1] . ':' . $error[2];
+ } else {
+ $error = '';
+ }
+
+ if ('' != $this->queryStr) {
+ $error .= "\n [ SQL语句 ] : " . $this->getLastsql();
+ }
+
+ return $error;
+ }
+
+ /**
+ * 初始化数据库连接
+ * @access protected
+ * @param boolean $master 是否主服务器
+ * @return void
+ */
+ protected function initConnect(bool $master = true): void
+ {
+ if (!empty($this->config['deploy'])) {
+ // 采用分布式数据库
+ if ($master || $this->transTimes) {
+ if (!$this->linkWrite) {
+ $this->linkWrite = $this->multiConnect(true);
+ }
+
+ $this->linkID = $this->linkWrite;
+ } else {
+ if (!$this->linkRead) {
+ $this->linkRead = $this->multiConnect(false);
+ }
+
+ $this->linkID = $this->linkRead;
+ }
+ } elseif (!$this->linkID) {
+ // 默认单数据库
+ $this->linkID = $this->connect();
+ }
+ }
+
+ /**
+ * 连接分布式服务器
+ * @access protected
+ * @param boolean $master 主服务器
+ * @return PDO
+ */
+ protected function multiConnect(bool $master = false): PDO
+ {
+ $config = [];
+
+ // 分布式数据库配置解析
+ foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
+ $config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
+ }
+
+ // 主服务器序号
+ $m = floor(mt_rand(0, $this->config['master_num'] - 1));
+
+ if ($this->config['rw_separate']) {
+ // 主从式采用读写分离
+ if ($master) // 主服务器写入
+ {
+ $r = $m;
+ } elseif (is_numeric($this->config['slave_no'])) {
+ // 指定服务器读
+ $r = $this->config['slave_no'];
+ } else {
+ // 读操作连接从服务器 每次随机连接的数据库
+ $r = floor(mt_rand($this->config['master_num'], count($config['hostname']) - 1));
+ }
+ } else {
+ // 读写操作不区分服务器 每次随机连接的数据库
+ $r = floor(mt_rand(0, count($config['hostname']) - 1));
+ }
+ $dbMaster = false;
+
+ if ($m != $r) {
+ $dbMaster = [];
+ foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
+ $dbMaster[$name] = $config[$name][$m] ?? $config[$name][0];
+ }
+ }
+
+ $dbConfig = [];
+
+ foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
+ $dbConfig[$name] = $config[$name][$r] ?? $config[$name][0];
+ }
+
+ return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
+ }
+
+ /**
+ * 执行数据库Xa事务
+ * @access public
+ * @param callable $callback 数据操作方法回调
+ * @param array $dbs 多个查询对象或者连接对象
+ * @return mixed
+ * @throws PDOException
+ * @throws \Exception
+ * @throws \Throwable
+ */
+ public function transactionXa(callable $callback, array $dbs = [])
+ {
+ $xid = uniqid('xa');
+
+ if (empty($dbs)) {
+ $dbs[] = $this;
+ }
+
+ foreach ($dbs as $key => $db) {
+ if ($db instanceof BaseQuery) {
+ $db = $db->getConnection();
+
+ $dbs[$key] = $db;
+ }
+
+ $db->startTransXa($xid);
+ }
+
+ try {
+ $result = null;
+ if (is_callable($callback)) {
+ $result = $callback($this);
+ }
+
+ foreach ($dbs as $db) {
+ $db->prepareXa($xid);
+ }
+
+ foreach ($dbs as $db) {
+ $db->commitXa($xid);
+ }
+
+ return $result;
+ } catch (\Exception | \Throwable $e) {
+ foreach ($dbs as $db) {
+ $db->rollbackXa($xid);
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * 启动XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function startTransXa(string $xid): void
+ {}
+
+ /**
+ * 预编译XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function prepareXa(string $xid): void
+ {}
+
+ /**
+ * 提交XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function commitXa(string $xid): void
+ {}
+
+ /**
+ * 回滚XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function rollbackXa(string $xid): void
+ {}
+}
diff --git a/src/db/Query.php b/src/db/Query.php
index 1952e2fd11acd01d25edfb84c4f1177838c5c367..80e01cd9b258546d4df3acb96747396b56cae8f9 100644
--- a/src/db/Query.php
+++ b/src/db/Query.php
@@ -1,3597 +1,451 @@
-
-// +----------------------------------------------------------------------
-
-namespace think\db;
-
-use PDO;
-use think\Collection;
-use think\Db;
-use think\db\exception\BindParamException;
-use think\db\exception\DataNotFoundException;
-use think\db\exception\DbException;
-use think\db\exception\ModelNotFoundException;
-use think\db\exception\PDOException;
-use think\Exception;
-use think\Model;
-use think\model\Relation;
-use think\model\relation\OneToOne;
-use think\Paginator;
-
-class Query
-{
- // 数据库Connection对象
- protected static $connections = [];
- // 当前数据库Connection对象
- protected $connection;
- // 当前模型对象
- protected $model;
- // 当前数据表名称(不含前缀)
- protected $name = '';
- // 当前数据表主键
- protected $pk;
- // 当前数据表前缀
- protected $prefix = '';
- // 查询参数
- protected $options = [];
- // 参数绑定
- protected $bind = [];
-
- // 回调事件
- private static $event = [];
- // 扩展查询方法
- private static $extend = [];
-
- /**
- * 读取主库的表
- * @var array
- */
- private static $readMaster = [];
-
- // 日期查询快捷方式
- protected $timeExp = ['d' => 'today', 'w' => 'week', 'm' => 'month', 'y' => 'year'];
- // 日期查询表达式
- protected $timeRule = [
- 'today' => ['today', 'tomorrow'],
- 'yesterday' => ['yesterday', 'today'],
- 'week' => ['this week 00:00:00', 'next week 00:00:00'],
- 'last week' => ['last week 00:00:00', 'this week 00:00:00'],
- 'month' => ['first Day of this month 00:00:00', 'first Day of next month 00:00:00'],
- 'last month' => ['first Day of last month 00:00:00', 'first Day of this month 00:00:00'],
- 'year' => ['this year 1/1', 'next year 1/1'],
- 'last year' => ['last year 1/1', 'this year 1/1'],
- ];
-
- /**
- * 架构函数
- * @access public
- */
- public function __construct(Connection $connection = null)
- {
- if (is_null($connection)) {
- $this->connection = Connection::instance();
- } else {
- $this->connection = $connection;
- }
-
- $this->prefix = $this->connection->getConfig('prefix');
- }
-
- /**
- * 创建一个新的查询对象
- * @access public
- * @return Query
- */
- public function newQuery()
- {
- return new static($this->connection);
- }
-
- /**
- * 利用__call方法实现一些特殊的Model方法
- * @access public
- * @param string $method 方法名称
- * @param array $args 调用参数
- * @return mixed
- * @throws DbException
- * @throws Exception
- */
- public function __call($method, $args)
- {
- if (isset(self::$extend[strtolower($method)])) {
- // 调用扩展查询方法
- array_unshift($args, $this);
-
- return call_user_func_array(self::$extend[strtolower($method)], $args);
- } elseif (strtolower(substr($method, 0, 5)) == 'getby') {
- // 根据某个字段获取记录
- $field = Db::parseName(substr($method, 5));
- return $this->where($field, '=', $args[0])->find();
- } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') {
- // 根据某个字段获取记录的某个值
- $name = Db::parseName(substr($method, 10));
- return $this->where($name, '=', $args[0])->value($args[1]);
- } elseif (strtolower(substr($method, 0, 7)) == 'whereor') {
- $name = Db::parseName(substr($method, 7));
- array_unshift($args, $name);
- return call_user_func_array([$this, 'whereOr'], $args);
- } elseif (strtolower(substr($method, 0, 5)) == 'where') {
- $name = Db::parseName(substr($method, 5));
- array_unshift($args, $name);
- return call_user_func_array([$this, 'where'], $args);
- } elseif ($this->model && method_exists($this->model, 'scope' . $method)) {
- // 动态调用命名范围
- $method = 'scope' . $method;
- array_unshift($args, $this);
-
- call_user_func_array([$this->model, $method], $args);
- return $this;
- } else {
- throw new Exception('method not exist:' . ($this->model ? get_class($this->model) : static::class) . '->' . $method);
- }
- }
-
- /**
- * 扩展查询方法
- * @access public
- * @param string|array $method 查询方法名
- * @param callable $callback
- * @return void
- */
- public static function extend($method, $callback = null)
- {
- if (is_array($method)) {
- foreach ($method as $key => $val) {
- self::$extend[strtolower($key)] = $val;
- }
- } else {
- self::$extend[strtolower($method)] = $callback;
- }
- }
-
- /**
- * 设置当前的数据库Connection对象
- * @access public
- * @param Connection $connection
- * @return $this
- */
- public function setConnection(Connection $connection)
- {
- $this->connection = $connection;
- $this->prefix = $this->connection->getConfig('prefix');
-
- return $this;
- }
-
- /**
- * 获取当前的数据库Connection对象
- * @access public
- * @return Connection
- */
- public function getConnection()
- {
- return $this->connection;
- }
-
- /**
- * 指定模型
- * @access public
- * @param Model $model 模型对象实例
- * @return $this
- */
- public function model(Model $model)
- {
- $this->model = $model;
- return $this;
- }
-
- /**
- * 获取当前的模型对象
- * @access public
- * @return Model|null
- */
- public function getModel()
- {
- return $this->model ? $this->model->setQuery($this) : null;
- }
-
- /**
- * 设置从主库读取数据
- * @access public
- * @param bool $all 是否所有表有效
- * @return $this
- */
- public function readMaster($all = false)
- {
- $table = $all ? '*' : $this->getTable();
-
- static::$readMaster[$table] = true;
-
- return $this;
- }
-
- /**
- * 指定当前数据表名(不含前缀)
- * @access public
- * @param string $name
- * @return $this
- */
- public function name($name)
- {
- $this->name = $name;
- return $this;
- }
-
- /**
- * 得到当前或者指定名称的数据表
- * @access public
- * @param string $name
- * @return string
- */
- public function getTable($name = '')
- {
- if (empty($name) && isset($this->options['table'])) {
- return $this->options['table'];
- }
-
- $name = $name ?: $this->name;
-
- return $this->prefix . Db::parseName($name);
- }
-
- /**
- * 切换数据库连接
- * @access public
- * @param mixed $config 连接配置
- * @param bool|string $name 连接标识 true 强制重新连接
- * @return $this
- * @throws Exception
- */
- public function connect($config = [], $name = false)
- {
- $this->connection = Connection::instance($config, $name);
- $query = $this->connection->getConfig('query');
-
- if (__CLASS__ != trim($query, '\\')) {
- return new $query($this->connection);
- }
-
- $this->prefix = $this->connection->getConfig('prefix');
-
- return $this;
- }
-
- /**
- * 执行查询 返回数据集
- * @access public
- * @param string $sql sql指令
- * @param array $bind 参数绑定
- * @param boolean $master 是否在主服务器读操作
- * @param bool|string $class 指定返回的数据集对象
- * @return mixed
- * @throws BindParamException
- * @throws PDOException
- */
- public function query($sql, $bind = [], $master = false, $class = false)
- {
- return $this->connection->query($sql, $bind, $master, $class);
- }
-
- /**
- * 执行语句
- * @access public
- * @param string $sql sql指令
- * @param array $bind 参数绑定
- * @return int
- * @throws BindParamException
- * @throws PDOException
- */
- public function execute($sql, $bind = [])
- {
- return $this->connection->execute($sql, $bind);
- }
-
- /**
- * 监听SQL执行
- * @access public
- * @param callable $callback 回调方法
- * @return void
- */
- public function listen($callback)
- {
- $this->connection->listen($callback);
- }
-
- /**
- * 获取最近插入的ID
- * @access public
- * @param string $sequence 自增序列名
- * @return string
- */
- public function getLastInsID($sequence = null)
- {
- return $this->connection->getLastInsID($sequence);
- }
-
- /**
- * 获取返回或者影响的记录数
- * @access public
- * @return integer
- */
- public function getNumRows()
- {
- return $this->connection->getNumRows();
- }
-
- /**
- * 获取最近一次查询的sql语句
- * @access public
- * @return string
- */
- public function getLastSql()
- {
- return $this->connection->getLastSql();
- }
-
- /**
- * 获取sql记录
- * @access public
- * @return string
- */
- public function getSqlLog()
- {
- return $this->connection->getSqlLog();
- }
-
- /**
- * 执行数据库事务
- * @access public
- * @param callable $callback 数据操作方法回调
- * @return mixed
- */
- public function transaction($callback)
- {
- return $this->connection->transaction($callback);
- }
-
- /**
- * 执行数据库Xa事务
- * @access public
- * @param callable $callback 数据操作方法回调
- * @param array $dbs 多个查询对象或者连接对象
- * @return mixed
- * @throws PDOException
- * @throws \Exception
- * @throws \Throwable
- */
- public function transactionXa($callback, array $dbs = [])
- {
- $xid = uniqid('xa');
-
- if (empty($dbs)) {
- $dbs[] = $this->getConnection();
- }
-
- foreach ($dbs as $key => $db) {
- if ($db instanceof Query) {
- $db = $db->getConnection();
-
- $dbs[$key] = $db;
- }
-
- $db->startTransXa($xid);
- }
-
- try {
- $result = null;
- if (is_callable($callback)) {
- $result = call_user_func_array($callback, [$this]);
- }
-
- foreach ($dbs as $db) {
- $db->prepareXa($xid);
- }
-
- foreach ($dbs as $db) {
- $db->commitXa($xid);
- }
-
- return $result;
- } catch (\Exception $e) {
- foreach ($dbs as $db) {
- $db->rollbackXa($xid);
- }
- throw $e;
- } catch (\Throwable $e) {
- foreach ($dbs as $db) {
- $db->rollbackXa($xid);
- }
- throw $e;
- }
- }
-
- /**
- * 启动事务
- * @access public
- * @return void
- */
- public function startTrans()
- {
- $this->connection->startTrans();
- }
-
- /**
- * 用于非自动提交状态下面的查询提交
- * @access public
- * @return void
- * @throws PDOException
- */
- public function commit()
- {
- $this->connection->commit();
- }
-
- /**
- * 事务回滚
- * @access public
- * @return void
- * @throws PDOException
- */
- public function rollback()
- {
- $this->connection->rollback();
- }
-
- /**
- * 批处理执行SQL语句
- * 批处理的指令都认为是execute操作
- * @access public
- * @param array $sql SQL批处理指令
- * @return boolean
- */
- public function batchQuery($sql = [])
- {
- return $this->connection->batchQuery($sql);
- }
-
- /**
- * 获取数据库的配置参数
- * @access public
- * @param string $name 参数名称
- * @return boolean
- */
- public function getConfig($name = '')
- {
- return $this->connection->getConfig($name);
- }
-
- /**
- * 获取数据表字段信息
- * @access public
- * @param string $tableName 数据表名
- * @return array
- */
- public function getTableFields($tableName = '')
- {
- if ('' == $tableName) {
- $tableName = isset($this->options['table']) ? $this->options['table'] : $this->getTable();
- }
-
- return $this->connection->getTableFields($tableName);
- }
-
- /**
- * 获取数据表字段类型
- * @access public
- * @param string $tableName 数据表名
- * @param string $field 字段名
- * @return array|string
- */
- public function getFieldsType($tableName = '', $field = null)
- {
- if ('' == $tableName) {
- $tableName = isset($this->options['table']) ? $this->options['table'] : $this->getTable();
- }
-
- return $this->connection->getFieldsType($tableName, $field);
- }
-
- /**
- * 是否允许返回空数据(或空模型)
- * @access public
- * @param bool $allowEmpty 是否允许为空
- * @return $this
- */
- public function allowEmpty($allowEmpty = true)
- {
- $this->options['allow_empty'] = $allowEmpty;
- return $this;
- }
-
- /**
- * 得到分表的的数据表名
- * @access public
- * @param array $data 操作的数据
- * @param string $field 分表依据的字段
- * @param array $rule 分表规则
- * @return string
- */
- public function getPartitionTableName($data, $field, $rule = [])
- {
- // 对数据表进行分区
- if ($field && isset($data[$field])) {
- $value = $data[$field];
- $type = $rule['type'];
- switch ($type) {
- case 'id':
- // 按照id范围分表
- $step = $rule['expr'];
- $seq = floor($value / $step) + 1;
- break;
- case 'year':
- // 按照年份分表
- if (!is_numeric($value)) {
- $value = strtotime($value);
- }
- $seq = date('Y', $value) - $rule['expr'] + 1;
- break;
- case 'mod':
- // 按照id的模数分表
- $seq = ($value % $rule['num']) + 1;
- break;
- case 'md5':
- // 按照md5的序列分表
- $seq = (ord(substr(md5($value), 0, 1)) % $rule['num']) + 1;
- break;
- default:
- if (function_exists($type)) {
- // 支持指定函数哈希
- $seq = (ord(substr($type($value), 0, 1)) % $rule['num']) + 1;
- } else {
- // 按照字段的首字母的值分表
- $seq = (ord($value{0}) % $rule['num']) + 1;
- }
- }
- return $this->getTable() . '_' . $seq;
- }
-
- // 当设置的分表字段不在查询条件或者数据中
- // 进行联合查询,必须设定 partition['num']
- $tableName = [];
- for ($i = 0; $i < $rule['num']; $i++) {
- $tableName[] = 'SELECT * FROM ' . $this->getTable() . '_' . ($i + 1);
- }
-
- $tableName = '( ' . implode(" UNION ", $tableName) . ') AS ' . $this->name;
-
- return $tableName;
- }
-
- /**
- * 得到某个字段的值
- * @access public
- * @param string $field 字段名
- * @param mixed $default 默认值
- * @return mixed
- */
- public function value($field, $default = null)
- {
- $this->parseOptions();
-
- return $this->connection->value($this, $field, $default);
- }
-
- /**
- * 得到某个列的数组
- * @access public
- * @param string $field 字段名 多个字段用逗号分隔
- * @param string $key 索引
- * @return array
- */
- public function column($field, $key = '')
- {
- $this->parseOptions();
-
- return $this->connection->column($this, $field, $key);
- }
-
- /**
- * 聚合查询
- * @access public
- * @param string $aggregate 聚合方法
- * @param string $field 字段名
- * @param bool $force 强制转为数字类型
- * @return mixed
- */
- public function aggregate($aggregate, $field, $force = false)
- {
- $this->parseOptions();
-
- $result = $this->connection->aggregate($this, $aggregate, $field);
-
- if (!empty($this->options['fetch_sql'])) {
- return $result;
- } elseif ($force) {
- $result = (float) $result;
- }
-
- return $result;
- }
-
- /**
- * COUNT查询
- * @access public
- * @param string $field 字段名
- * @return integer|string
- */
- public function count($field = '*')
- {
- if (!empty($this->options['group'])) {
- // 支持GROUP
- $options = $this->getOptions();
- $subSql = $this->options($options)
- ->field('count(' . $field . ') AS think_count')
- ->bind($this->bind)
- ->buildSql();
-
- $query = $this->newQuery()->table([$subSql => '_group_count_']);
-
- if (!empty($options['fetch_sql'])) {
- $query->fetchSql(true);
- }
-
- $count = $query->aggregate('COUNT', '*');
- } else {
- $count = $this->aggregate('COUNT', $field);
- }
-
- return is_string($count) ? $count : (int) $count;
- }
-
- /**
- * SUM查询
- * @access public
- * @param string $field 字段名
- * @return float|int
- */
- public function sum($field)
- {
- return $this->aggregate('SUM', $field, true);
- }
-
- /**
- * MIN查询
- * @access public
- * @param string $field 字段名
- * @param bool $force 强制转为数字类型
- * @return mixed
- */
- public function min($field, $force = true)
- {
- return $this->aggregate('MIN', $field, $force);
- }
-
- /**
- * MAX查询
- * @access public
- * @param string $field 字段名
- * @param bool $force 强制转为数字类型
- * @return mixed
- */
- public function max($field, $force = true)
- {
- return $this->aggregate('MAX', $field, $force);
- }
-
- /**
- * AVG查询
- * @access public
- * @param string $field 字段名
- * @return float|int
- */
- public function avg($field)
- {
- return $this->aggregate('AVG', $field, true);
- }
-
- /**
- * 设置记录的某个字段值
- * 支持使用数据库字段和方法
- * @access public
- * @param string|array $field 字段名
- * @param mixed $value 字段值
- * @return integer
- */
- public function setField($field, $value = '')
- {
- $condition = !empty($this->options['where']) ? $this->options['where'] : [];
-
- if (empty($condition)) {
- // 没有条件不做任何更新
- throw new Exception('no data to update');
- }
-
- if (is_array($field)) {
- $data = $field;
- } else {
- $data[$field] = $value;
- }
-
- return $this->update($data);
- }
-
- /**
- * 字段值(延迟)增长
- * @access public
- * @param string $field 字段名
- * @param integer $step 增长值
- * @return integer|true
- * @throws Exception
- */
- public function setInc($field, $step = 1)
- {
- return $this->setField($field, ['inc', $step]);
- }
-
- /**
- * 字段值(延迟)减少
- * @access public
- * @param string $field 字段名
- * @param integer $step 减少值
- * @return integer|true
- * @throws Exception
- */
- public function setDec($field, $step = 1)
- {
- return $this->setField($field, ['dec', $step]);
- }
-
- /**
- * 查询SQL组装 join
- * @access public
- * @param mixed $join 关联的表名
- * @param mixed $condition 条件
- * @param string $type JOIN类型
- * @return $this
- */
- public function join($join, $condition = null, $type = 'INNER')
- {
- if (empty($condition)) {
- // 如果为组数,则循环调用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 {
- $table = $this->getJoinTable($join);
-
- $this->options['join'][] = [$table, strtoupper($type), $condition];
- }
-
- return $this;
- }
-
- /**
- * LEFT JOIN
- * @access public
- * @param mixed $join 关联的表名
- * @param mixed $condition 条件
- * @return $this
- */
- public function leftJoin($join, $condition = null)
- {
- return $this->join($join, $condition, 'LEFT');
- }
-
- /**
- * RIGHT JOIN
- * @access public
- * @param mixed $join 关联的表名
- * @param mixed $condition 条件
- * @return $this
- */
- public function rightJoin($join, $condition = null)
- {
- return $this->join($join, $condition, 'RIGHT');
- }
-
- /**
- * FULL JOIN
- * @access public
- * @param mixed $join 关联的表名
- * @param mixed $condition 条件
- * @return $this
- */
- public function fullJoin($join, $condition = null)
- {
- return $this->join($join, $condition, 'FULL');
- }
-
- /**
- * 获取Join表名及别名 支持
- * ['prefix_table或者子查询'=>'alias'] 'prefix_table alias' 'table alias'
- * @access public
- * @param array|string $join
- * @return array|string
- */
- protected function getJoinTable($join, &$alias = null)
- {
- // 传入的表名为数组
- if (is_array($join)) {
- $table = $join;
- } else {
- $join = trim($join);
-
- if (false !== strpos($join, '(')) {
- // 使用子查询
- $table = $join;
- } else {
- $prefix = $this->prefix;
- if (strpos($join, ' ')) {
- // 使用别名
- list($table, $alias) = explode(' ', $join);
- } else {
- $table = $join;
- if (false === strpos($join, '.') && 0 !== strpos($join, '__')) {
- $alias = $join;
- }
- }
-
- if ($prefix && false === strpos($table, '.') && 0 !== strpos($table, $prefix) && 0 !== strpos($table, '__')) {
- $table = $this->getTable($table);
- }
- }
-
- if (isset($alias) && $table != $alias) {
- $table = [$table => $alias];
- }
- }
-
- return $table;
- }
-
- /**
- * 查询SQL组装 union
- * @access public
- * @param mixed $union
- * @param boolean $all
- * @return $this
- */
- 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;
- }
-
- /**
- * 查询SQL组装 union all
- * @access public
- * @param mixed $union
- * @return $this
- */
- public function unionAll($union)
- {
- return $this->union($union, true);
- }
-
- /**
- * 指定查询字段 支持字段排除和指定数据表
- * @access public
- * @param mixed $field
- * @param boolean $except 是否排除
- * @param string $tableName 数据表名
- * @param string $prefix 字段前缀
- * @param string $alias 别名前缀
- * @return $this
- */
- public function field($field, $except = false, $tableName = '', $prefix = '', $alias = '')
- {
- if (empty($field)) {
- return $this;
- }
-
- if (is_string($field)) {
- if (preg_match('/[\<\'\"\(]/', $field)) {
- return $this->fieldRaw($field);
- }
- $field = array_map('trim', explode(',', $field));
- }
-
- if (true === $field) {
- // 获取全部字段
- $fields = $this->getTableFields($tableName);
- $field = $fields ?: ['*'];
- } elseif ($except) {
- // 字段排除
- $fields = $this->getTableFields($tableName);
- $field = $fields ? array_diff($fields, $field) : $field;
- }
-
- if ($tableName) {
- // 添加统一的前缀
- $prefix = $prefix ?: $tableName;
- foreach ($field as $key => &$val) {
- if (is_numeric($key) && $alias) {
- $field[$prefix . '.' . $val] = $alias . $val;
- unset($field[$key]);
- } elseif (is_numeric($key)) {
- $val = $prefix . '.' . $val;
- }
- }
- }
-
- if (isset($this->options['field'])) {
- $field = array_merge((array) $this->options['field'], $field);
- }
-
- $this->options['field'] = array_unique($field);
-
- return $this;
- }
-
- /**
- * 表达式方式指定查询字段
- * @access public
- * @param string $field 字段名
- * @return $this
- */
- public function fieldRaw($field)
- {
- $this->options['field'][] = $this->raw($field);
-
- return $this;
- }
-
- /**
- * 设置数据
- * @access public
- * @param mixed $field 字段名或者数据
- * @param mixed $value 字段值
- * @return $this
- */
- public function data($field, $value = null)
- {
- if (is_array($field)) {
- $this->options['data'] = isset($this->options['data']) ? array_merge($this->options['data'], $field) : $field;
- } else {
- $this->options['data'][$field] = $value;
- }
-
- return $this;
- }
-
- /**
- * 字段值增长
- * @access public
- * @param string|array $field 字段名
- * @param integer $step 增长值
- * @return $this
- */
- public function inc($field, $step = 1, $op = 'INC')
- {
- $fields = is_string($field) ? explode(',', $field) : $field;
-
- foreach ($fields as $field => $val) {
- if (is_numeric($field)) {
- $field = $val;
- } else {
- $step = $val;
- }
-
- $this->data($field, [$op, $step]);
- }
-
- return $this;
- }
-
- /**
- * 字段值减少
- * @access public
- * @param string|array $field 字段名
- * @param integer $step 增长值
- * @return $this
- */
- public function dec($field, $step = 1)
- {
- return $this->inc($field, $step, 'DEC');
- }
-
- /**
- * 使用表达式设置数据
- * @access public
- * @param string $field 字段名
- * @param string $value 字段值
- * @return $this
- */
- public function exp($field, $value)
- {
- $this->data($field, $this->raw($value));
- return $this;
- }
-
- /**
- * 使用表达式设置数据
- * @access public
- * @param mixed $value 表达式
- * @return Expression
- */
- public function raw($value)
- {
- return new Expression($value);
- }
-
- /**
- * 指定JOIN查询字段
- * @access public
- * @param string|array $table 数据表
- * @param string|array $field 查询字段
- * @param string|array $on JOIN条件
- * @param string $type JOIN类型
- * @return $this
- */
- public function view($join, $field = true, $on = null, $type = 'INNER')
- {
- $this->options['view'] = true;
-
- if (is_array($join) && key($join) !== 0) {
- foreach ($join as $key => $val) {
- $this->view($key, $val[0], isset($val[1]) ? $val[1] : null, isset($val[2]) ? $val[2] : 'INNER');
- }
- } else {
- $fields = [];
- $table = $this->getJoinTable($join, $alias);
-
- if (true === $field) {
- $fields = $alias . '.*';
- } else {
- if (is_string($field)) {
- $field = explode(',', $field);
- }
- foreach ($field as $key => $val) {
- if (is_numeric($key)) {
- $fields[] = $alias . '.' . $val;
- $this->options['map'][$val] = $alias . '.' . $val;
- } else {
- if (preg_match('/[,=\.\'\"\(\s]/', $key)) {
- $name = $key;
- } else {
- $name = $alias . '.' . $key;
- }
- $fields[] = $name . ' AS ' . $val;
- $this->options['map'][$val] = $name;
- }
- }
- }
-
- $this->field($fields);
-
- if ($on) {
- $this->join($table, $on, $type);
- } else {
- $this->table($table);
- }
- }
-
- return $this;
- }
-
- /**
- * 设置分表规则
- * @access public
- * @param array $data 操作的数据
- * @param string $field 分表依据的字段
- * @param array $rule 分表规则
- * @return $this
- */
- public function partition($data, $field, $rule = [])
- {
- $this->options['table'] = $this->getPartitionTableName($data, $field, $rule);
-
- return $this;
- }
-
- /**
- * 指定AND查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param mixed $op 查询表达式
- * @param mixed $condition 查询条件
- * @return $this
- */
- public function where($field, $op = null, $condition = null)
- {
- $param = func_get_args();
- array_shift($param);
- return $this->parseWhereExp('AND', $field, $op, $condition, $param);
- }
-
- /**
- * 指定表达式查询条件
- * @access public
- * @param string $where 查询条件
- * @param array $bind 参数绑定
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereRaw($where, $bind = [], $logic = 'AND')
- {
- if ($bind) {
- $this->bindParams($where, $bind);
- }
-
- $this->options['where'][$logic][] = $this->raw($where);
-
- return $this;
- }
-
- /**
- * 指定表达式查询条件 OR
- * @access public
- * @param string $where 查询条件
- * @param array $bind 参数绑定
- * @return $this
- */
- public function whereOrRaw($where, $bind = [])
- {
- return $this->whereRaw($where, $bind, 'OR');
- }
-
- /**
- * 指定OR查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param mixed $op 查询表达式
- * @param mixed $condition 查询条件
- * @return $this
- */
- public function whereOr($field, $op = null, $condition = null)
- {
- $param = func_get_args();
- array_shift($param);
- return $this->parseWhereExp('OR', $field, $op, $condition, $param);
- }
-
- /**
- * 指定XOR查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param mixed $op 查询表达式
- * @param mixed $condition 查询条件
- * @return $this
- */
- public function whereXor($field, $op = null, $condition = null)
- {
- $param = func_get_args();
- array_shift($param);
- return $this->parseWhereExp('XOR', $field, $op, $condition, $param);
- }
-
- /**
- * 指定Null查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereNull($field, $logic = 'AND')
- {
- return $this->parseWhereExp($logic, $field, 'NULL', null, [], true);
- }
-
- /**
- * 指定NotNull查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereNotNull($field, $logic = 'AND')
- {
- return $this->parseWhereExp($logic, $field, 'NOTNULL', null, [], true);
- }
-
- /**
- * 指定Exists查询条件
- * @access public
- * @param mixed $condition 查询条件
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereExists($condition, $logic = 'AND')
- {
- if (is_string($condition)) {
- $condition = $this->raw($condition);
- }
-
- $this->options['where'][strtoupper($logic)][] = ['', 'EXISTS', $condition];
- return $this;
- }
-
- /**
- * 指定NotExists查询条件
- * @access public
- * @param mixed $condition 查询条件
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereNotExists($condition, $logic = 'AND')
- {
- if (is_string($condition)) {
- $condition = $this->raw($condition);
- }
-
- $this->options['where'][strtoupper($logic)][] = ['', 'NOT EXISTS', $condition];
- return $this;
- }
-
- /**
- * 指定In查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param mixed $condition 查询条件
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereIn($field, $condition, $logic = 'AND')
- {
- return $this->parseWhereExp($logic, $field, 'IN', $condition, [], true);
- }
-
- /**
- * 指定NotIn查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param mixed $condition 查询条件
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereNotIn($field, $condition, $logic = 'AND')
- {
- return $this->parseWhereExp($logic, $field, 'NOT IN', $condition, [], true);
- }
-
- /**
- * 指定Like查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param mixed $condition 查询条件
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereLike($field, $condition, $logic = 'AND')
- {
- return $this->parseWhereExp($logic, $field, 'LIKE', $condition, [], true);
- }
-
- /**
- * 指定NotLike查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param mixed $condition 查询条件
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereNotLike($field, $condition, $logic = 'AND')
- {
- return $this->parseWhereExp($logic, $field, 'NOT LIKE', $condition, [], true);
- }
-
- /**
- * 指定Between查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param mixed $condition 查询条件
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereBetween($field, $condition, $logic = 'AND')
- {
- return $this->parseWhereExp($logic, $field, 'BETWEEN', $condition, [], true);
- }
-
- /**
- * 指定NotBetween查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param mixed $condition 查询条件
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereNotBetween($field, $condition, $logic = 'AND')
- {
- return $this->parseWhereExp($logic, $field, 'NOT BETWEEN', $condition, [], true);
- }
-
- /**
- * 比较两个字段
- * @access public
- * @param string|array $field1 查询字段
- * @param string $operator 比较操作符
- * @param string $field2 比较字段
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereColumn($field1, $operator, $field2 = null, $logic = 'AND')
- {
- if (is_array($field1)) {
- foreach ($field1 as $item) {
- $this->whereColumn($item[0], $item[1], isset($item[2]) ? $item[2] : null);
- }
- return $this;
- }
-
- if (is_null($field2)) {
- $field2 = $operator;
- $operator = '=';
- }
-
- return $this->parseWhereExp($logic, $field1, 'COLUMN', [$operator, $field2], [], true);
- }
-
- /**
- * 设置软删除字段及条件
- * @access public
- * @param false|string $field 查询字段
- * @param mixed $condition 查询条件
- * @return $this
- */
- public function useSoftDelete($field, $condition = null)
- {
- if ($field) {
- $this->options['soft_delete'] = [$field, $condition];
- }
-
- return $this;
- }
-
- /**
- * 指定Exp查询条件
- * @access public
- * @param mixed $field 查询字段
- * @param string $where 查询条件
- * @param array $bind 参数绑定
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function whereExp($field, $where, array $bind = [], $logic = 'AND')
- {
- if ($bind) {
- $this->bindParams($where, $bind);
- }
-
- $this->options['where'][$logic][] = [$field, 'EXP', $this->raw($where)];
-
- return $this;
- }
-
- /**
- * 分析查询表达式
- * @access public
- * @param string $logic 查询逻辑 and or xor
- * @param mixed $field 查询字段
- * @param mixed $op 查询表达式
- * @param mixed $condition 查询条件
- * @param array $param 查询参数
- * @param bool $strict 严格模式
- * @return $this
- */
- protected function parseWhereExp($logic, $field, $op, $condition, $param = [], $strict = false)
- {
- if ($field instanceof $this) {
- $this->options['where'] = $field->getOptions('where');
- return $this;
- }
-
- $logic = strtoupper($logic);
-
- if ($field instanceof Where) {
- $this->options['where'][$logic] = $field->parse();
- return $this;
- }
-
- if (is_string($field) && !empty($this->options['via']) && !strpos($field, '.')) {
- $field = $this->options['via'] . '.' . $field;
- }
-
- if ($field instanceof Expression) {
- return $this->whereRaw($field, is_array($op) ? $op : []);
- } elseif ($strict) {
- // 使用严格模式查询
- $where = [$field, $op, $condition];
- } elseif (is_array($field)) {
- // 解析数组批量查询
- return $this->parseArrayWhereItems($field, $logic);
- } elseif ($field instanceof \Closure) {
- $where = $field;
- $field = '';
- } elseif (is_string($field)) {
- if (preg_match('/[,=\<\'\"\(\s]/', $field)) {
- return $this->whereRaw($field, $op);
- } elseif (is_string($op) && strtolower($op) == 'exp') {
- $bind = isset($param[2]) && is_array($param[2]) ? $param[2] : null;
- return $this->whereExp($field, $condition, $bind, $logic);
- }
-
- $where = $this->parseWhereItem($logic, $field, $op, $condition, $param);
- }
-
- if (!empty($where)) {
- $this->options['where'][$logic][] = $where;
- }
-
- return $this;
- }
-
- /**
- * 分析查询表达式
- * @access protected
- * @param string $logic 查询逻辑 and or xor
- * @param mixed $field 查询字段
- * @param mixed $op 查询表达式
- * @param mixed $condition 查询条件
- * @param array $param 查询参数
- * @return mixed
- */
- protected function parseWhereItem($logic, $field, $op, $condition, $param = [])
- {
- if (is_array($op)) {
- // 同一字段多条件查询
- array_unshift($param, $field);
- $where = $param;
- } elseif ($field && is_null($condition)) {
- if (in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) {
- // null查询
- $where = [$field, $op, ''];
- } elseif (in_array($op, ['=', 'eq', 'EQ', null], true)) {
- $where = [$field, 'NULL', ''];
- } elseif (in_array($op, ['<>', 'neq', 'NEQ'], true)) {
- $where = [$field, 'NOTNULL', ''];
- } else {
- // 字段相等查询
- $where = [$field, '=', $op];
- }
- } elseif (in_array(strtoupper($op), ['REGEXP', 'NOT REGEXP', 'EXISTS', 'NOT EXISTS', 'NOTEXISTS'], true)) {
- $where = [$field, $op, is_string($condition) ? $this->raw($condition) : $condition];
- } else {
- $where = $field ? [$field, $op, $condition] : null;
- }
-
- return $where;
- }
-
- /**
- * 数组批量查询
- * @access protected
- * @param array $field 批量查询
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- protected function parseArrayWhereItems($field, $logic)
- {
- if (key($field) !== 0) {
- $where = [];
- foreach ($field as $key => $val) {
- if ($val instanceof Expression) {
- $where[] = [$key, 'exp', $val];
- } elseif (is_null($val)) {
- $where[] = [$key, 'NULL', ''];
- } else {
- $where[] = [$key, is_array($val) ? 'IN' : '=', $val];
- }
- }
- } else {
- // 数组批量查询
- $where = $field;
- }
-
- if (!empty($where)) {
- $this->options['where'][$logic] = isset($this->options['where'][$logic]) ? array_merge($this->options['where'][$logic], $where) : $where;
- }
-
- return $this;
- }
-
- /**
- * 去除某个查询条件
- * @access public
- * @param string $field 查询字段
- * @param string $logic 查询逻辑 and or xor
- * @return $this
- */
- public function removeWhereField($field, $logic = 'AND')
- {
- $logic = strtoupper($logic);
-
- if (isset($this->options['where'][$logic])) {
- foreach ($this->options['where'][$logic] as $key => $val) {
- if (is_array($val) && $val[0] == $field) {
- unset($this->options['where'][$logic][$key]);
- }
- }
- }
-
- return $this;
- }
-
- /**
- * 去除查询参数
- * @access public
- * @param string|bool $option 参数名 true 表示去除所有参数
- * @return $this
- */
- public function removeOption($option = true)
- {
- if (true === $option) {
- $this->options = [];
- } elseif (is_string($option) && isset($this->options[$option])) {
- unset($this->options[$option]);
- }
-
- return $this;
- }
-
- /**
- * 条件查询
- * @access public
- * @param mixed $condition 满足条件(支持闭包)
- * @param \Closure|array $query 满足条件后执行的查询表达式(闭包或数组)
- * @param \Closure|array $otherwise 不满足条件后执行
- * @return $this
- */
- public function when($condition, $query, $otherwise = null)
- {
- if ($condition instanceof \Closure) {
- $condition = $condition($this);
- }
-
- if ($condition) {
- if ($query instanceof \Closure) {
- $query($this, $condition);
- } elseif (is_array($query)) {
- $this->where($query);
- }
- } elseif ($otherwise) {
- if ($otherwise instanceof \Closure) {
- $otherwise($this, $condition);
- } elseif (is_array($otherwise)) {
- $this->where($otherwise);
- }
- }
-
- return $this;
- }
-
- /**
- * 指定查询数量
- * @access public
- * @param mixed $offset 起始位置
- * @param mixed $length 查询数量
- * @return $this
- */
- 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 $this
- */
- 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;
- }
-
- /**
- * 分页查询
- * @param int|array $listRows 每页数量 数组表示配置参数
- * @param int|bool $simple 是否简洁模式或者总记录数
- * @param array $config 配置参数
- * page:当前页,
- * path:url路径,
- * query:url额外参数,
- * fragment:url锚点,
- * var_page:分页变量,
- * list_rows:每页数量
- * type:分页类名
- * @return \think\Paginator
- * @throws DbException
- */
- public function paginate($listRows = null, $simple = false, $config = [])
- {
- if (is_int($simple)) {
- $total = $simple;
- $simple = false;
- }
-
- $paginate = Db::getConfig('paginate');
-
- if (is_array($listRows)) {
- $config = array_merge($paginate, $listRows);
- $listRows = $config['list_rows'];
- } else {
- $config = array_merge($paginate, $config);
- $listRows = $listRows ?: $config['list_rows'];
- }
-
- /** @var Paginator $class */
- $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\paginator\\driver\\' . ucwords($config['type']);
- $page = isset($config['page']) ? (int) $config['page'] : call_user_func([
- $class,
- 'getCurrentPage',
- ], $config['var_page']);
-
- $page = $page < 1 ? 1 : $page;
-
- $config['path'] = isset($config['path']) ? $config['path'] : call_user_func([$class, 'getCurrentPath']);
-
- if (!isset($total) && !$simple) {
- $options = $this->getOptions();
-
- unset($this->options['order'], $this->options['limit'], $this->options['page'], $this->options['field']);
-
- $bind = $this->bind;
- $total = $this->count();
- $results = $this->options($options)->bind($bind)->page($page, $listRows)->select();
- } elseif ($simple) {
- $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select();
- $total = null;
- } else {
- $results = $this->page($page, $listRows)->select();
- }
-
- $this->removeOption('limit');
- $this->removeOption('page');
-
- return $class::make($results, $listRows, $page, $total, $simple, $config);
- }
-
- /**
- * 指定当前操作的数据表
- * @access public
- * @param mixed $table 表名
- * @return $this
- */
- public function table($table)
- {
- if (is_string($table)) {
- if (strpos($table, ')')) {
- // 子查询
- } elseif (strpos($table, ',')) {
- $tables = explode(',', $table);
- $table = [];
-
- foreach ($tables as $item) {
- list($item, $alias) = explode(' ', trim($item));
- if ($alias) {
- $this->alias([$item => $alias]);
- $table[$item] = $alias;
- } else {
- $table[] = $item;
- }
- }
- } elseif (strpos($table, ' ')) {
- list($table, $alias) = explode(' ', $table);
-
- $table = [$table => $alias];
- $this->alias($table);
- }
- } else {
- $tables = $table;
- $table = [];
-
- foreach ($tables as $key => $val) {
- if (is_numeric($key)) {
- $table[] = $val;
- } else {
- $this->alias([$key => $val]);
- $table[$key] = $val;
- }
- }
- }
- $this->options['table'] = $table;
-
- return $this;
- }
-
- /**
- * USING支持 用于多表删除
- * @access public
- * @param mixed $using
- * @return $this
- */
- public function using($using)
- {
- $this->options['using'] = $using;
- return $this;
- }
-
- /**
- * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc'])
- * @access public
- * @param string|array $field 排序字段
- * @param string $order 排序
- * @return $this
- */
- public function order($field, $order = null)
- {
- if (empty($field)) {
- return $this;
- }
-
- if (is_string($field)) {
- if (!empty($this->options['via'])) {
- $field = $this->options['via'] . '.' . $field;
- }
-
- if (strpos($field, ',')) {
- $field = array_map('trim', explode(',', $field));
- } else {
- $field = empty($order) ? $field : [$field => $order];
- }
- } elseif (!empty($this->options['via'])) {
- foreach ($field as $key => $val) {
- if (is_numeric($key)) {
- $field[$key] = $this->options['via'] . '.' . $val;
- } else {
- $field[$this->options['via'] . '.' . $key] = $val;
- unset($field[$key]);
- }
- }
- }
-
- if (!isset($this->options['order'])) {
- $this->options['order'] = [];
- }
-
- if (is_array($field)) {
- $this->options['order'] = array_merge($this->options['order'], $field);
- } else {
- $this->options['order'][] = $field;
- }
-
- return $this;
- }
-
- /**
- * 表达式方式指定Field排序
- * @access public
- * @param string $field 排序字段
- * @param array $bind 参数绑定
- * @return $this
- */
- public function orderRaw($field, $bind = [])
- {
- if ($bind) {
- $this->bindParams($field, $bind);
- }
-
- $this->options['order'][] = $this->raw($field);
-
- return $this;
- }
-
- /**
- * 指定Field排序 order('id',[1,2,3],'desc')
- * @access public
- * @param string|array $field 排序字段
- * @param string $values 排序值
- * @param string $order
- * @return $this
- */
- public function orderField($field, array $values = [], $order = '')
- {
- if (!empty($values)) {
- $values['sort'] = $order;
-
- $this->options['order'][$field] = $values;
- }
-
- return $this;
- }
-
- /**
- * 随机排序
- * @access public
- * @return $this
- */
- public function orderRand()
- {
- $this->options['order'][] = '[rand]';
- return $this;
- }
-
- /**
- * 查询缓存
- * @access public
- * @param mixed $key 缓存key
- * @param integer|\DateTime $expire 缓存有效期
- * @param string $tag 缓存标签
- * @return $this
- */
- public function cache($key = true, $expire = null, $tag = null)
- {
- // 增加快捷调用方式 cache(10) 等同于 cache(true, 10)
- if ($key instanceof \DateTime || (is_numeric($key) && is_null($expire))) {
- $expire = $key;
- $key = true;
- }
-
- if (false !== $key) {
- $this->options['cache'] = ['key' => $key, 'expire' => $expire, 'tag' => $tag];
- }
-
- return $this;
- }
-
- /**
- * 指定group查询
- * @access public
- * @param string $group GROUP
- * @return $this
- */
- public function group($group)
- {
- $this->options['group'] = $group;
- return $this;
- }
-
- /**
- * 指定having查询
- * @access public
- * @param string $having having
- * @return $this
- */
- public function having($having)
- {
- $this->options['having'] = $having;
- return $this;
- }
-
- /**
- * 指定查询lock
- * @access public
- * @param bool|string $lock 是否lock
- * @return $this
- */
- public function lock($lock = false)
- {
- $this->options['lock'] = $lock;
- $this->options['master'] = true;
-
- return $this;
- }
-
- /**
- * 指定distinct查询
- * @access public
- * @param string $distinct 是否唯一
- * @return $this
- */
- public function distinct($distinct)
- {
- $this->options['distinct'] = $distinct;
- return $this;
- }
-
- /**
- * 指定数据表别名
- * @access public
- * @param mixed $alias 数据表别名
- * @return $this
- */
- public function alias($alias)
- {
- if (is_array($alias)) {
- foreach ($alias as $key => $val) {
- if (false !== strpos($key, '__')) {
- $table = $this->connection->parseSqlTable($key);
- } else {
- $table = $key;
- }
- $this->options['alias'][$table] = $val;
- }
- } else {
- if (isset($this->options['table'])) {
- $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table'];
- if (false !== strpos($table, '__')) {
- $table = $this->connection->parseSqlTable($table);
- }
- } else {
- $table = $this->getTable();
- }
-
- $this->options['alias'][$table] = $alias;
- }
-
- return $this;
- }
-
- /**
- * 指定强制索引
- * @access public
- * @param string $force 索引名称
- * @return $this
- */
- public function force($force)
- {
- $this->options['force'] = $force;
- return $this;
- }
-
- /**
- * 查询注释
- * @access public
- * @param string $comment 注释
- * @return $this
- */
- public function comment($comment)
- {
- $this->options['comment'] = $comment;
- return $this;
- }
-
- /**
- * 获取执行的SQL语句
- * @access public
- * @param boolean $fetch 是否返回sql
- * @return $this
- */
- public function fetchSql($fetch = true)
- {
- $this->options['fetch_sql'] = $fetch;
- return $this;
- }
-
- /**
- * 不主动获取数据集
- * @access public
- * @param bool $pdo 是否返回 PDOStatement 对象
- * @return $this
- */
- public function fetchPdo($pdo = true)
- {
- $this->options['fetch_pdo'] = $pdo;
- return $this;
- }
-
- /**
- * 设置是否返回数据集对象(支持设置数据集对象类名)
- * @access public
- * @param bool|string $collection 是否返回数据集对象
- * @return $this
- */
- public function fetchCollection($collection = true)
- {
- $this->options['collection'] = $collection;
- return $this;
- }
-
- /**
- * 设置从主服务器读取数据
- * @access public
- * @return $this
- */
- public function master()
- {
- $this->options['master'] = true;
- return $this;
- }
-
- /**
- * 设置是否严格检查字段名
- * @access public
- * @param bool $strict 是否严格检查字段
- * @return $this
- */
- public function strict($strict = true)
- {
- $this->options['strict'] = $strict;
- return $this;
- }
-
- /**
- * 设置查询数据不存在是否抛出异常
- * @access public
- * @param bool $fail 数据不存在是否抛出异常
- * @return $this
- */
- public function failException($fail = true)
- {
- $this->options['fail'] = $fail;
- return $this;
- }
-
- /**
- * 设置自增序列名
- * @access public
- * @param string $sequence 自增序列名
- * @return $this
- */
- public function sequence($sequence = null)
- {
- $this->options['sequence'] = $sequence;
- return $this;
- }
-
- /**
- * 设置需要隐藏的输出属性
- * @access public
- * @param mixed $hidden 需要隐藏的字段名
- * @return $this
- */
- public function hidden($hidden)
- {
- if ($this->model) {
- $this->options['hidden'] = $hidden;
- return $this;
- }
-
- return $this->field($hidden, true);
- }
-
- /**
- * 设置需要输出的属性
- * @access public
- * @param array $visible 需要输出的属性
- * @return $this
- */
- public function visible(array $visible)
- {
- $this->options['visible'] = $visible;
- return $this;
- }
-
- /**
- * 设置需要追加输出的属性
- * @access public
- * @param array $append 需要追加的属性
- * @return $this
- */
- public function append(array $append)
- {
- $this->options['append'] = $append;
- return $this;
- }
-
- /**
- * 设置数据字段获取器
- * @access public
- * @param string|array $name 字段名
- * @param callable $callback 闭包获取器
- * @return $this
- */
- public function withAttr($name, $callback = null)
- {
- if (is_array($name)) {
- $this->options['with_attr'] = $name;
- } else {
- $this->options['with_attr'][$name] = $callback;
- }
-
- return $this;
- }
-
- /**
- * 设置JSON字段信息
- * @access public
- * @param array $json JSON字段
- * @param bool $assoc 是否取出数组
- * @return $this
- */
- public function json(array $json = [], $assoc = false)
- {
- $this->options['json'] = $json;
- $this->options['json_assoc'] = $assoc;
- return $this;
- }
-
- /**
- * 设置字段类型信息
- * @access public
- * @param array $type 字段类型信息
- * @return $this
- */
- public function setJsonFieldType(array $type)
- {
- $this->options['field_type'] = $type;
- return $this;
- }
-
- /**
- * 获取字段类型信息
- * @access public
- * @param string $field 字段名
- * @return string|null
- */
- public function getJsonFieldType($field)
- {
- return isset($this->options['field_type'][$field]) ? $this->options['field_type'][$field] : null;
- }
-
- /**
- * 添加查询范围
- * @access public
- * @param array|string|\Closure $scope 查询范围定义
- * @param array $args 参数
- * @return $this
- */
- public function scope($scope, ...$args)
- {
- // 查询范围的第一个参数始终是当前查询对象
- array_unshift($args, $this);
-
- if ($scope instanceof \Closure) {
- call_user_func_array($scope, $args);
- return $this;
- }
-
- if (is_string($scope)) {
- $scope = explode(',', $scope);
- }
-
- if ($this->model) {
- // 检查模型类的查询范围方法
- foreach ($scope as $name) {
- $method = 'scope' . trim($name);
-
- if (method_exists($this->model, $method)) {
- call_user_func_array([$this->model, $method], $args);
- }
- }
- }
-
- return $this;
- }
-
- /**
- * 指定数据表主键
- * @access public
- * @param string $pk 主键
- * @return $this
- */
- public function pk($pk)
- {
- $this->pk = $pk;
- return $this;
- }
-
- /**
- * 查询日期或者时间
- * @access public
- * @param string $name 时间表达式
- * @param string|array $rule 时间范围
- * @return $this
- */
- public function timeRule($name, $rule)
- {
- $this->timeRule[$name] = $rule;
- return $this;
- }
-
- /**
- * 查询日期或者时间
- * @access public
- * @param string $field 日期字段名
- * @param string|array $op 比较运算符或者表达式
- * @param string|array $range 比较范围
- * @return $this
- */
- public function whereTime($field, $op, $range = null)
- {
- if (is_null($range)) {
- if (is_array($op)) {
- $range = $op;
- } else {
- if (isset($this->timeExp[strtolower($op)])) {
- $op = $this->timeExp[strtolower($op)];
- }
-
- if (isset($this->timeRule[strtolower($op)])) {
- $range = $this->timeRule[strtolower($op)];
- } else {
- $range = $op;
- }
- }
-
- $op = is_array($range) ? 'between' : '>=';
- }
-
- $this->where($field, strtolower($op) . ' time', $range);
-
- return $this;
- }
-
- /**
- * 查询日期或者时间范围
- * @access public
- * @param string $field 日期字段名
- * @param string $startTime 开始时间
- * @param string $endTime 结束时间
- * @return $this
- */
- public function whereBetweenTime($field, $startTime, $endTime = null)
- {
- if (is_null($endTime)) {
- $time = is_string($startTime) ? strtotime($startTime) : $startTime;
- $endTime = strtotime('+1 day', $time);
- }
-
- $this->where($field, 'between time', [$startTime, $endTime]);
-
- return $this;
- }
-
- /**
- * 获取当前数据表的主键
- * @access public
- * @param string|array $options 数据表名或者查询参数
- * @return string|array
- */
- public function getPk($options = '')
- {
- if (!empty($this->pk)) {
- $pk = $this->pk;
- } else {
- $pk = $this->connection->getPk(is_array($options) && isset($options['table']) ? $options['table'] : $this->getTable());
- }
-
- return $pk;
- }
-
- /**
- * 参数绑定
- * @access public
- * @param mixed $value 绑定变量值
- * @param integer $type 绑定类型
- * @param string $name 绑定标识
- * @return $this|string
- */
- public function bind($value, $type = PDO::PARAM_STR, $name = null)
- {
- if (is_array($value)) {
- $this->bind = array_merge($this->bind, $value);
- } else {
- $name = $name ?: 'ThinkBind_' . (count($this->bind) + 1) . '_';
-
- $this->bind[$name] = [$value, $type];
- return $name;
- }
-
- return $this;
- }
-
- /**
- * 参数绑定
- * @access public
- * @param string $sql 绑定的sql表达式
- * @param array $bind 参数绑定
- * @return void
- */
- protected function bindParams(&$sql, array $bind = [])
- {
- foreach ($bind as $key => $value) {
- if (is_array($value)) {
- $name = $this->bind($value[0], $value[1], isset($value[2]) ? $value[2] : null);
- } else {
- $name = $this->bind($value);
- }
-
- if (is_numeric($key)) {
- $sql = substr_replace($sql, ':' . $name, strpos($sql, '?'), 1);
- } else {
- $sql = str_replace(':' . $key, ':' . $name, $sql);
- }
- }
- }
-
- /**
- * 检测参数是否已经绑定
- * @access public
- * @param string $key 参数名
- * @return bool
- */
- public function isBind($key)
- {
- return isset($this->bind[$key]);
- }
-
- /**
- * 查询参数赋值
- * @access public
- * @param string $name 参数名
- * @param mixed $value 值
- * @return $this
- */
- public function option($name, $value)
- {
- $this->options[$name] = $value;
- return $this;
- }
-
- /**
- * 查询参数赋值
- * @access protected
- * @param array $options 表达式参数
- * @return $this
- */
- protected function options(array $options)
- {
- $this->options = $options;
- return $this;
- }
-
- /**
- * 获取当前的查询参数
- * @access public
- * @param string $name 参数名
- * @return mixed
- */
- public function getOptions($name = '')
- {
- if ('' === $name) {
- return $this->options;
- }
-
- return isset($this->options[$name]) ? $this->options[$name] : null;
- }
-
- /**
- * 设置当前的查询参数
- * @access public
- * @param string $option 参数名
- * @param mixed $value 参数值
- * @return $this
- */
- public function setOption($option, $value)
- {
- $this->options[$option] = $value;
- return $this;
- }
-
- /**
- * 设置关联查询JOIN预查询
- * @access public
- * @param string|array $with 关联方法名称
- * @return $this
- */
- public function with($with)
- {
- if (empty($with)) {
- return $this;
- }
-
- if (is_string($with)) {
- $with = explode(',', $with);
- }
-
- $first = true;
-
- /** @var Model $class */
- $class = $this->model;
- foreach ($with as $key => $relation) {
- $closure = null;
-
- if ($relation instanceof \Closure) {
- // 支持闭包查询过滤关联条件
- $closure = $relation;
- $relation = $key;
- } elseif (is_array($relation)) {
- $relation = $key;
- } elseif (is_string($relation) && strpos($relation, '.')) {
- list($relation, $subRelation) = explode('.', $relation, 2);
- }
-
- /** @var Relation $model */
- $relation = Db::parseName($relation, 1, false);
- $model = $class->$relation();
-
- if ($model instanceof OneToOne && 0 == $model->getEagerlyType()) {
- $table = $model->getTable();
- $model->removeOption()
- ->table($table)
- ->eagerly($this, $relation, true, '', $closure, $first);
- $first = false;
- }
- }
-
- $this->via();
-
- $this->options['with'] = $with;
-
- return $this;
- }
-
- /**
- * 关联预载入 JOIN方式(不支持嵌套)
- * @access protected
- * @param string|array $with 关联方法名
- * @param string $joinType JOIN方式
- * @return $this
- */
- public function withJoin($with, $joinType = '')
- {
- if (empty($with)) {
- return $this;
- }
-
- if (is_string($with)) {
- $with = explode(',', $with);
- }
-
- $first = true;
-
- /** @var Model $class */
- $class = $this->model;
- foreach ($with as $key => $relation) {
- $closure = null;
- $field = true;
-
- if ($relation instanceof \Closure) {
- // 支持闭包查询过滤关联条件
- $closure = $relation;
- $relation = $key;
- } elseif (is_array($relation)) {
- $field = $relation;
- $relation = $key;
- } elseif (is_string($relation) && strpos($relation, '.')) {
- list($relation, $subRelation) = explode('.', $relation, 2);
- }
-
- /** @var Relation $model */
- $relation = Db::parseName($relation, 1, false);
- $model = $class->$relation();
-
- if ($model instanceof OneToOne) {
- $model->eagerly($this, $relation, $field, $joinType, $closure, $first);
- $first = false;
- } else {
- // 不支持其它关联
- unset($with[$key]);
- }
- }
-
- $this->via();
-
- $this->options['with_join'] = $with;
-
- return $this;
- }
-
- /**
- * 使用搜索器条件搜索字段
- * @access public
- * @param array $fields 搜索字段
- * @param array $data 搜索数据
- * @param string $prefix 字段前缀标识
- * @return $this
- */
- public function withSearch(array $fields, array $data = [], $prefix = '')
- {
- foreach ($fields as $key => $field) {
- if ($field instanceof \Closure) {
- $field($this, isset($data[$key]) ? $data[$key] : null, $data, $prefix);
- } elseif ($this->model) {
- // 检测搜索器
- $fieldName = is_numeric($key) ? $field : $key;
- $method = 'search' . Db::parseName($fieldName, 1) . 'Attr';
-
- if (method_exists($this->model, $method)) {
- $this->model->$method($this, isset($data[$field]) ? $data[$field] : null, $data, $prefix);
- }
- }
- }
-
- return $this;
- }
-
- /**
- * 关联统计
- * @access protected
- * @param string|array $relation 关联方法名
- * @param string $aggregate 聚合查询方法
- * @param string $field 字段
- * @param bool $subQuery 是否使用子查询
- * @return $this
- */
- protected function withAggregate($relation, $aggregate = 'count', $field = '*', $subQuery = true)
- {
- $relations = is_string($relation) ? explode(',', $relation) : $relation;
-
- if (!$subQuery) {
- $this->options['with_count'][] = [$relations, $aggregate, $field];
- } else {
- if (!isset($this->options['field'])) {
- $this->field('*');
- }
-
- foreach ($relations as $key => $relation) {
- $closure = null;
- if ($relation instanceof \Closure) {
- $closure = $relation;
- $relation = $key;
- } elseif (!is_int($key)) {
- $aggregateField = $relation;
- $relation = $key;
- }
-
- if (!isset($aggregateField)) {
- $aggregateField = Db::parseName($relation) . '_' . $aggregate;
- }
-
- $relation = Db::parseName($relation, 1, false);
- $count = '(' . $this->model->$relation()->getRelationCountQuery($closure, $aggregate, $field) . ')';
-
- $this->field([$count => $aggregateField]);
- }
- }
-
- return $this;
- }
-
- /**
- * 关联统计
- * @access public
- * @param string|array $relation 关联方法名
- * @param bool $subQuery 是否使用子查询
- * @return $this
- */
- public function withCount($relation, $subQuery = true)
- {
- return $this->withAggregate($relation, 'count', '*', $subQuery);
- }
-
- /**
- * 关联统计Sum
- * @access public
- * @param string|array $relation 关联方法名
- * @param string $field 字段
- * @param bool $subQuery 是否使用子查询
- * @return $this
- */
- public function withSum($relation, $field, $subQuery = true)
- {
- return $this->withAggregate($relation, 'sum', $field, $subQuery);
- }
-
- /**
- * 关联统计Max
- * @access public
- * @param string|array $relation 关联方法名
- * @param string $field 字段
- * @param bool $subQuery 是否使用子查询
- * @return $this
- */
- public function withMax($relation, $field, $subQuery = true)
- {
- return $this->withAggregate($relation, 'max', $field, $subQuery);
- }
-
- /**
- * 关联统计Min
- * @access public
- * @param string|array $relation 关联方法名
- * @param string $field 字段
- * @param bool $subQuery 是否使用子查询
- * @return $this
- */
- public function withMin($relation, $field, $subQuery = true)
- {
- return $this->withAggregate($relation, 'min', $field, $subQuery);
- }
-
- /**
- * 关联统计Avg
- * @access public
- * @param string|array $relation 关联方法名
- * @param string $field 字段
- * @param bool $subQuery 是否使用子查询
- * @return $this
- */
- public function withAvg($relation, $field, $subQuery = true)
- {
- return $this->withAggregate($relation, 'avg', $field, $subQuery);
- }
-
- /**
- * 关联预加载中 获取关联指定字段值
- * example:
- * Model::with(['relation' => function($query){
- * $query->withField("id,name");
- * }])
- *
- * @param string | array $field 指定获取的字段
- * @return $this
- */
- public function withField($field)
- {
- $this->options['with_field'] = $field;
-
- return $this;
- }
-
- /**
- * 设置当前字段添加的表别名
- * @access public
- * @param string $via
- * @return $this
- */
- public function via($via = '')
- {
- $this->options['via'] = $via;
-
- return $this;
- }
-
- /**
- * 设置关联查询
- * @access public
- * @param string|array $relation 关联名称
- * @return $this
- */
- public function relation($relation)
- {
- if (empty($relation)) {
- return $this;
- }
-
- if (is_string($relation)) {
- $relation = explode(',', $relation);
- }
-
- if (isset($this->options['relation'])) {
- $this->options['relation'] = array_merge($this->options['relation'], $relation);
- } else {
- $this->options['relation'] = $relation;
- }
-
- return $this;
- }
-
- /**
- * 插入记录
- * @access public
- * @param array $data 数据
- * @param boolean $replace 是否replace
- * @param boolean $getLastInsID 返回自增主键
- * @param string $sequence 自增序列名
- * @return integer|string
- */
- public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null)
- {
- $this->parseOptions();
-
- $this->options['data'] = array_merge($this->options['data'], $data);
-
- return $this->connection->insert($this, $replace, $getLastInsID, $sequence);
- }
-
- /**
- * 插入记录并获取自增ID
- * @access public
- * @param array $data 数据
- * @param boolean $replace 是否replace
- * @param string $sequence 自增序列名
- * @return integer|string
- */
- public function insertGetId(array $data, $replace = false, $sequence = null)
- {
- return $this->insert($data, $replace, true, $sequence);
- }
-
- /**
- * 批量插入记录
- * @access public
- * @param array $dataSet 数据集
- * @param boolean $replace 是否replace
- * @param integer $limit 每次写入数据限制
- * @return integer|string
- */
- public function insertAll(array $dataSet, $replace = false, $limit = null)
- {
- $this->parseOptions();
-
- return $this->connection->insertAll($this, $dataSet, $replace, $limit);
- }
-
- /**
- * 通过Select方式插入记录
- * @access public
- * @param string $fields 要插入的数据表字段名
- * @param string $table 要插入的数据表名
- * @return integer|string
- * @throws PDOException
- */
- public function selectInsert($fields, $table)
- {
- $this->parseOptions();
-
- return $this->connection->selectInsert($this, $fields, $table);
- }
-
- /**
- * 更新记录
- * @access public
- * @param mixed $data 数据
- * @return integer|string
- * @throws Exception
- * @throws PDOException
- */
- public function update(array $data = [])
- {
- $this->parseOptions();
-
- $this->options['data'] = array_merge($this->options['data'], $data);
-
- return $this->connection->update($this);
- }
-
- /**
- * 删除记录
- * @access public
- * @param mixed $data 表达式 true 表示强制删除
- * @return int
- * @throws Exception
- * @throws PDOException
- */
- public function delete($data = null)
- {
- $this->parseOptions();
-
- if (!is_null($data) && true !== $data) {
- // AR模式分析主键条件
- $this->parsePkWhere($data);
- }
-
- if (!empty($this->options['soft_delete'])) {
- // 软删除
- list($field, $condition) = $this->options['soft_delete'];
- unset($this->options['soft_delete']);
- $this->options['data'] = [$field => $condition];
-
- return $this->connection->update($this);
- }
-
- $this->options['data'] = $data;
-
- return $this->connection->delete($this);
- }
-
- /**
- * 执行查询但只返回PDOStatement对象
- * @access public
- * @return \PDOStatement|string
- */
- public function getPdo()
- {
- $this->parseOptions();
-
- return $this->connection->pdo($this);
- }
-
- /**
- * 使用游标查找记录
- * @access public
- * @param array|string|Query|\Closure $data
- * @return \Generator
- */
- public function cursor($data = null)
- {
- if ($data instanceof \Closure) {
- $data($this);
- $data = null;
- }
-
- $this->parseOptions();
-
- if (!is_null($data)) {
- // 主键条件分析
- $this->parsePkWhere($data);
- }
-
- $this->options['data'] = $data;
-
- $connection = clone $this->connection;
-
- return $connection->cursor($this);
- }
-
- /**
- * 查找记录
- * @access public
- * @param array|string|Query|\Closure $data
- * @return Collection|array|\PDOStatement|string
- * @throws DbException
- * @throws ModelNotFoundException
- * @throws DataNotFoundException
- */
- public function select($data = null)
- {
- if ($data instanceof Query) {
- return $data->select();
- } elseif ($data instanceof \Closure) {
- $data($this);
- $data = null;
- }
-
- $this->parseOptions();
-
- if (false === $data) {
- // 用于子查询 不查询只返回SQL
- $this->options['fetch_sql'] = true;
- } elseif (!is_null($data)) {
- // 主键条件分析
- $this->parsePkWhere($data);
- }
-
- $this->options['data'] = $data;
-
- $resultSet = $this->connection->select($this);
-
- if (!empty($this->options['fetch_sql'])) {
- return $resultSet;
- }
-
- // 数据列表读取后的处理
- if (!empty($this->model)) {
- // 生成模型对象
- $resultSet = $this->resultSetToModelCollection($resultSet);
- } else {
- $this->resultSet($resultSet);
- }
-
- // 返回结果处理
- if (!empty($this->options['fail']) && count($resultSet) == 0) {
- $this->throwNotFound($this->options);
- }
-
- return $resultSet;
- }
-
- /**
- * 查询数据转换为模型数据集对象
- * @access protected
- * @param array $resultSet 数据集
- * @return ModelCollection
- */
- protected function resultSetToModelCollection(array $resultSet)
- {
- if (!empty($this->options['collection']) && is_string($this->options['collection'])) {
- $collection = $this->options['collection'];
- }
-
- if (empty($resultSet)) {
- return $this->model->toCollection([], isset($collection) ? $collection : null);
- }
-
- // 检查动态获取器
- if (!empty($this->options['with_attr'])) {
- foreach ($this->options['with_attr'] as $name => $val) {
- if (strpos($name, '.')) {
- list($relation, $field) = explode('.', $name);
-
- $withRelationAttr[$relation][$field] = $val;
- unset($this->options['with_attr'][$name]);
- }
- }
- }
-
- $withRelationAttr = isset($withRelationAttr) ? $withRelationAttr : [];
-
- foreach ($resultSet as $key => &$result) {
- // 数据转换为模型对象
- $this->resultToModel($result, $this->options, true, $withRelationAttr);
- }
-
- if (!empty($this->options['with'])) {
- // 预载入
- $result->eagerlyResultSet($resultSet, $this->options['with'], $withRelationAttr);
- }
-
- if (!empty($this->options['with_join'])) {
- // JOIN预载入
- $result->eagerlyResultSet($resultSet, $this->options['with_join'], $withRelationAttr, true);
- }
-
- // 模型数据集转换
- return $result->toCollection($resultSet, isset($collection) ? $collection : null);
- }
-
- /**
- * 处理数据集
- * @access public
- * @param array $resultSet
- * @return void
- */
- protected function resultSet(&$resultSet)
- {
- if (!empty($this->options['json'])) {
- foreach ($resultSet as &$result) {
- $this->jsonResult($result, $this->options['json'], true);
- }
- }
-
- if (!empty($this->options['with_attr'])) {
- foreach ($resultSet as &$result) {
- $this->getResultAttr($result, $this->options['with_attr']);
- }
- }
-
- if (!empty($this->options['collection']) || 'collection' == $this->connection->getConfig('resultset_type')) {
- // 返回Collection对象
- $resultSet = new Collection($resultSet);
- }
- }
-
- /**
- * 查找单条记录
- * @access public
- * @param array|string|Query|\Closure $data
- * @return array|null|\PDOStatement|string|Model
- * @throws DbException
- * @throws ModelNotFoundException
- * @throws DataNotFoundException
- */
- public function find($data = null)
- {
- if ($data instanceof Query) {
- return $data->find();
- } elseif ($data instanceof \Closure) {
- $data($this);
- $data = null;
- }
-
- $this->parseOptions();
-
- if (!is_null($data)) {
- // AR模式分析主键条件
- $this->parsePkWhere($data);
- }
-
- $this->options['data'] = $data;
-
- $result = $this->connection->find($this);
-
- if (!empty($this->options['fetch_sql'])) {
- return $result;
- }
-
- // 数据处理
- if (empty($result)) {
- return $this->resultToEmpty();
- }
-
- if (!empty($this->model)) {
- // 返回模型对象
- $this->resultToModel($result, $this->options);
- } else {
- $this->result($result);
- }
-
- return $result;
- }
-
- /**
- * 处理空数据
- * @access protected
- * @return array|Model|null
- * @throws DbException
- * @throws ModelNotFoundException
- * @throws DataNotFoundException
- */
- protected function resultToEmpty()
- {
- if (!empty($this->options['allow_empty'])) {
- return !empty($this->model) ? $this->model->newInstance([], $this->getModelUpdateCondition($this->options)) : [];
- } elseif (!empty($this->options['fail'])) {
- $this->throwNotFound($this->options);
- }
- }
-
- /**
- * 查找单条记录
- * @access public
- * @param mixed $data 主键值或者查询条件(闭包)
- * @param mixed $with 关联预查询
- * @param bool $cache 是否缓存
- * @param bool $failException 是否抛出异常
- * @return static|null
- * @throws exception\DbException
- */
- public function get($data, $with = [], $cache = false, $failException = false)
- {
- if (is_null($data)) {
- return;
- }
-
- if (true === $with || is_int($with)) {
- $cache = $with;
- $with = [];
- }
-
- return $this->parseQuery($data, $with, $cache)
- ->failException($failException)
- ->find($data);
- }
-
- /**
- * 查找单条记录 如果不存在直接抛出异常
- * @access public
- * @param mixed $data 主键值或者查询条件(闭包)
- * @param mixed $with 关联预查询
- * @param bool $cache 是否缓存
- * @return static|null
- * @throws exception\DbException
- */
- public function getOrFail($data, $with = [], $cache = false)
- {
- return $this->get($data, $with, $cache, true);
- }
-
- /**
- * 查找所有记录
- * @access public
- * @param mixed $data 主键列表或者查询条件(闭包)
- * @param array|string $with 关联预查询
- * @param bool $cache 是否缓存
- * @return static[]|false
- * @throws exception\DbException
- */
- public function all($data = null, $with = [], $cache = false)
- {
- if (true === $with || is_int($with)) {
- $cache = $with;
- $with = [];
- }
-
- return $this->parseQuery($data, $with, $cache)->select($data);
- }
-
- /**
- * 分析查询表达式
- * @access public
- * @param mixed $data 主键列表或者查询条件(闭包)
- * @param string $with 关联预查询
- * @param bool $cache 是否缓存
- * @return Query
- */
- protected function parseQuery(&$data, $with, $cache)
- {
- $result = $this->with($with)->cache($cache);
-
- if ((is_array($data) && key($data) !== 0) || $data instanceof Where) {
- $result = $result->where($data);
- $data = null;
- } elseif ($data instanceof \Closure) {
- $data($result);
- $data = null;
- } elseif ($data instanceof Query) {
- $result = $data->with($with)->cache($cache);
- $data = null;
- }
-
- return $result;
- }
-
- /**
- * 处理数据
- * @access protected
- * @param array $result 查询数据
- * @return void
- */
- protected function result(&$result)
- {
- if (!empty($this->options['json'])) {
- $this->jsonResult($result, $this->options['json'], true);
- }
-
- if (!empty($this->options['with_attr'])) {
- $this->getResultAttr($result, $this->options['with_attr']);
- }
- }
-
- /**
- * 使用获取器处理数据
- * @access protected
- * @param array $result 查询数据
- * @param array $withAttr 字段获取器
- * @return void
- */
- protected function getResultAttr(&$result, $withAttr = [])
- {
- foreach ($withAttr as $name => $closure) {
- $name = Db::parseName($name);
-
- if (strpos($name, '.')) {
- // 支持JSON字段 获取器定义
- list($key, $field) = explode('.', $name);
-
- if (isset($result[$key])) {
- $result[$key][$field] = $closure(isset($result[$key][$field]) ? $result[$key][$field] : null, $result[$key]);
- }
- } else {
- $result[$name] = $closure(isset($result[$name]) ? $result[$name] : null, $result);
- }
- }
- }
-
- /**
- * JSON字段数据转换
- * @access protected
- * @param array $result 查询数据
- * @param array $json JSON字段
- * @param bool $assoc 是否转换为数组
- * @param array $withRelationAttr 关联获取器
- * @return void
- */
- protected function jsonResult(&$result, $json = [], $assoc = false, $withRelationAttr = [])
- {
- foreach ($json as $name) {
- if (isset($result[$name])) {
- $result[$name] = json_decode($result[$name], $assoc);
-
- if (isset($withRelationAttr[$name])) {
- foreach ($withRelationAttr[$name] as $key => $closure) {
- $data = get_object_vars($result[$name]);
- $result[$name]->$key = $closure(isset($result[$name]->$key) ? $result[$name]->$key : null, $data);
- }
- }
- }
- }
- }
-
- /**
- * 查询数据转换为模型对象
- * @access public
- * @param array $result 查询数据
- * @param array $options 查询参数
- * @param bool $resultSet 是否为数据集查询
- * @param array $withRelationAttr 关联字段获取器
- * @return void
- */
- protected function resultToModel(&$result, $options = [], $resultSet = false, $withRelationAttr = [])
- {
- if (!empty($options['with_attr']) && empty($withRelationAttr)) {
- foreach ($options['with_attr'] as $name => $val) {
- if (strpos($name, '.')) {
- list($relation, $field) = explode('.', $name);
-
- $withRelationAttr[$relation][$field] = $val;
- unset($options['with_attr'][$name]);
- }
- }
- }
-
- if (!empty($options['json'])) {
- $this->jsonResult($result, $options['json'], $options['json_assoc'], $withRelationAttr);
- }
-
- $result = $this->model->newInstance($result, $resultSet ? null : $this->getModelUpdateCondition($options));
-
- // 动态获取器
- if (!empty($options['with_attr'])) {
- $result->withAttribute($options['with_attr']);
- }
-
- // 输出属性控制
- if (!empty($options['visible'])) {
- $result->visible($options['visible']);
- } elseif (!empty($options['hidden'])) {
- $result->hidden($options['hidden']);
- }
-
- if (!empty($options['append'])) {
- $result->append($options['append']);
- }
-
- // 关联查询
- if (!empty($options['relation'])) {
- $result->relationQuery($options['relation'], $withRelationAttr);
- }
-
- // 预载入查询
- if (!$resultSet && !empty($options['with'])) {
- $result->eagerlyResult($result, $options['with'], $withRelationAttr);
- }
-
- // JOIN预载入查询
- if (!$resultSet && !empty($options['with_join'])) {
- $result->eagerlyResult($result, $options['with_join'], $withRelationAttr, true);
- }
-
- // 关联统计
- if (!empty($options['with_count'])) {
- foreach ($options['with_count'] as $val) {
- $result->relationCount($result, $val[0], $val[1], $val[2]);
- }
- }
- }
-
- /**
- * 获取模型的更新条件
- * @access protected
- * @param array $options 查询参数
- */
- protected function getModelUpdateCondition(array $options)
- {
- return isset($options['where']['AND']) ? $options['where']['AND'] : null;
- }
-
- /**
- * 查询失败 抛出异常
- * @access public
- * @param array $options 查询参数
- * @throws ModelNotFoundException
- * @throws DataNotFoundException
- */
- protected function throwNotFound($options = [])
- {
- if (!empty($this->model)) {
- $class = get_class($this->model);
- throw new ModelNotFoundException('model data Not Found:' . $class, $class, $options);
- }
-
- $table = is_array($options['table']) ? key($options['table']) : $options['table'];
- throw new DataNotFoundException('table data not Found:' . $table, $table, $options);
- }
-
- /**
- * 查找多条记录 如果不存在则抛出异常
- * @access public
- * @param array|string|Query|\Closure $data
- * @return array|\PDOStatement|string|Model
- * @throws DbException
- * @throws ModelNotFoundException
- * @throws DataNotFoundException
- */
- public function selectOrFail($data = null)
- {
- return $this->failException(true)->select($data);
- }
-
- /**
- * 查找单条记录 如果不存在则抛出异常
- * @access public
- * @param array|string|Query|\Closure $data
- * @return array|\PDOStatement|string|Model
- * @throws DbException
- * @throws ModelNotFoundException
- * @throws DataNotFoundException
- */
- public function findOrFail($data = null)
- {
- return $this->failException(true)->find($data);
- }
-
- /**
- * 查找单条记录 如果不存在则抛出异常
- * @access public
- * @param array|string|Query|\Closure $data
- * @return array|\PDOStatement|string|Model
- * @throws DbException
- * @throws ModelNotFoundException
- * @throws DataNotFoundException
- */
- public function findOrEmpty($data = null)
- {
- return $this->allowEmpty(true)->find($data);
- }
-
- /**
- * 分批数据返回处理
- * @access public
- * @param integer $count 每次处理的数据数量
- * @param callable $callback 处理回调方法
- * @param string|array $column 分批处理的字段名
- * @param string $order 字段排序
- * @return boolean
- * @throws DbException
- */
- public function chunk($count, $callback, $column = null, $order = 'asc')
- {
- $options = $this->getOptions();
- $column = $column ?: $this->getPk($options);
-
- if (isset($options['order'])) {
- if ($this->config['debug']) {
- throw new DbException('chunk not support call order');
- }
- unset($options['order']);
- }
-
- $bind = $this->bind;
-
- if (is_array($column)) {
- $times = 1;
- $query = $this->options($options)->page($times, $count);
- } else {
- $query = $this->options($options)->limit($count);
-
- if (strpos($column, '.')) {
- list($alias, $key) = explode('.', $column);
- } else {
- $key = $column;
- }
- }
-
- $resultSet = $query->order($column, $order)->select();
-
- while (count($resultSet) > 0) {
- if ($resultSet instanceof Collection) {
- $resultSet = $resultSet->all();
- }
-
- if (false === call_user_func($callback, $resultSet)) {
- return false;
- }
-
- if (isset($times)) {
- $times++;
- $query = $this->options($options)->page($times, $count);
- } else {
- $end = end($resultSet);
- $lastId = is_array($end) ? $end[$key] : $end->getData($key);
-
- $query = $this->options($options)
- ->limit($count)
- ->where($column, 'asc' == strtolower($order) ? '>' : '<', $lastId);
- }
-
- $resultSet = $query->bind($bind)->order($column, $order)->select();
- }
-
- return true;
- }
-
- /**
- * 获取绑定的参数 并清空
- * @access public
- * @param bool $clear
- * @return array
- */
- public function getBind($clear = true)
- {
- $bind = $this->bind;
- if ($clear) {
- $this->bind = [];
- }
-
- return $bind;
- }
-
- /**
- * 创建子查询SQL
- * @access public
- * @param bool $sub
- * @return string
- * @throws DbException
- */
- public function buildSql($sub = true)
- {
- return $sub ? '( ' . $this->select(false) . ' )' : $this->select(false);
- }
-
- /**
- * 视图查询处理
- * @access public
- * @param array $options 查询参数
- * @return void
- */
- protected function parseView(&$options)
- {
- if (!isset($options['map'])) {
- return;
- }
-
- foreach (['AND', 'OR'] as $logic) {
- if (isset($options['where'][$logic])) {
- foreach ($options['where'][$logic] as $key => $val) {
- if (array_key_exists($key, $options['map'])) {
- array_shift($val);
- array_unshift($val, $options['map'][$key]);
- $options['where'][$logic][$options['map'][$key]] = $val;
- unset($options['where'][$logic][$key]);
- }
- }
- }
- }
-
- if (isset($options['order'])) {
- // 视图查询排序处理
- if (is_string($options['order'])) {
- $options['order'] = explode(',', $options['order']);
- }
- foreach ($options['order'] as $key => $val) {
- if (is_numeric($key)) {
- if (strpos($val, ' ')) {
- list($field, $sort) = explode(' ', $val);
- if (array_key_exists($field, $options['map'])) {
- $options['order'][$options['map'][$field]] = $sort;
- unset($options['order'][$key]);
- }
- } elseif (array_key_exists($val, $options['map'])) {
- $options['order'][$options['map'][$val]] = 'asc';
- unset($options['order'][$key]);
- }
- } elseif (array_key_exists($key, $options['map'])) {
- $options['order'][$options['map'][$key]] = $val;
- unset($options['order'][$key]);
- }
- }
- }
- }
-
- /**
- * 把主键值转换为查询条件 支持复合主键
- * @access public
- * @param array|string $data 主键数据
- * @return void
- * @throws Exception
- */
- public function parsePkWhere($data)
- {
- $pk = $this->getPk($this->options);
-
- // 获取当前数据表
- $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table'];
-
- if (!empty($this->options['alias'][$table])) {
- $alias = $this->options['alias'][$table];
- }
-
- if (is_string($pk)) {
- $key = isset($alias) ? $alias . '.' . $pk : $pk;
- // 根据主键查询
- if (is_array($data)) {
- $where[$pk] = isset($data[$pk]) ? [$key, '=', $data[$pk]] : [$key, 'in', $data];
- } else {
- $where[$pk] = strpos($data, ',') ? [$key, 'IN', $data] : [$key, '=', $data];
- }
- } elseif (is_array($pk) && is_array($data) && !empty($data)) {
- // 根据复合主键查询
- foreach ($pk as $key) {
- if (isset($data[$key])) {
- $attr = isset($alias) ? $alias . '.' . $key : $key;
- $where[$key] = [$attr, '=', $data[$key]];
- } else {
- throw new Exception('miss complex primary data');
- }
- }
- }
-
- if (!empty($where)) {
- if (isset($this->options['where']['AND'])) {
- $this->options['where']['AND'] = array_merge($this->options['where']['AND'], $where);
- } else {
- $this->options['where']['AND'] = $where;
- }
- }
-
- return;
- }
-
- /**
- * 分析表达式(可用于查询或者写入操作)
- * @access protected
- * @param Query $query 查询对象
- * @return array
- */
- protected function parseOptions()
- {
- $options = $this->getOptions();
-
- // 获取数据表
- if (empty($options['table'])) {
- $options['table'] = $this->getTable();
- }
-
- if (!isset($options['where'])) {
- $options['where'] = [];
- } elseif (isset($options['view'])) {
- // 视图查询条件处理
- $this->parseView($options);
- }
-
- if (!isset($options['field'])) {
- $options['field'] = '*';
- }
-
- foreach (['data', 'order'] as $name) {
- if (!isset($options[$name])) {
- $options[$name] = [];
- }
- }
-
- if (!isset($options['strict'])) {
- $options['strict'] = $this->getConfig('fields_strict');
- }
-
- foreach (['master', 'lock', 'fetch_pdo', 'distinct'] as $name) {
- if (!isset($options[$name])) {
- $options[$name] = false;
- }
- }
-
- if (isset(static::$readMaster['*']) || (is_string($options['table']) && isset(static::$readMaster[$options['table']]))) {
- $options['master'] = true;
- }
-
- foreach (['join', 'union', 'group', 'having', 'limit', 'force', 'comment'] as $name) {
- if (!isset($options[$name])) {
- $options[$name] = '';
- }
- }
-
- if (isset($options['page'])) {
- // 根据页数计算limit
- list($page, $listRows) = $options['page'];
- $page = $page > 0 ? $page : 1;
- $listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20);
- $offset = $listRows * ($page - 1);
- $options['limit'] = $offset . ',' . $listRows;
- }
-
- $this->options = $options;
-
- return $options;
- }
-
- /**
- * 注册回调方法
- * @access public
- * @param string $event 事件名
- * @param callable $callback 回调方法
- * @return void
- */
- public static function event($event, $callback)
- {
- self::$event[$event] = $callback;
- }
-
- /**
- * 触发事件
- * @access protected
- * @param string $event 事件名
- * @return bool
- */
- public function trigger($event)
- {
- $result = false;
- if (isset(self::$event[$event])) {
- $result = call_user_func_array(self::$event[$event], [$this]);
- }
-
- return $result;
- }
-
-}
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use PDOStatement;
+use think\helper\Str;
+
+/**
+ * PDO数据查询类
+ */
+class Query extends BaseQuery
+{
+ use concern\JoinAndViewQuery;
+ use concern\ParamsBind;
+ use concern\TableFieldInfo;
+
+ /**
+ * 表达式方式指定Field排序
+ * @access public
+ * @param string $field 排序字段
+ * @param array $bind 参数绑定
+ * @return $this
+ */
+ public function orderRaw(string $field, array $bind = [])
+ {
+ $this->options['order'][] = new Raw($field, $bind);
+
+ return $this;
+ }
+
+ /**
+ * 表达式方式指定查询字段
+ * @access public
+ * @param string $field 字段名
+ * @return $this
+ */
+ public function fieldRaw(string $field)
+ {
+ $this->options['field'][] = new Raw($field);
+
+ return $this;
+ }
+
+ /**
+ * 指定Field排序 orderField('id',[1,2,3],'desc')
+ * @access public
+ * @param string $field 排序字段
+ * @param array $values 排序值
+ * @param string $order 排序 desc/asc
+ * @return $this
+ */
+ public function orderField(string $field, array $values, string $order = '')
+ {
+ if (!empty($values)) {
+ $values['sort'] = $order;
+
+ $this->options['order'][$field] = $values;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 随机排序
+ * @access public
+ * @return $this
+ */
+ public function orderRand()
+ {
+ $this->options['order'][] = '[rand]';
+ return $this;
+ }
+
+ /**
+ * 使用表达式设置数据
+ * @access public
+ * @param string $field 字段名
+ * @param string $value 字段值
+ * @return $this
+ */
+ public function exp(string $field, string $value)
+ {
+ $this->options['data'][$field] = new Raw($value);
+ return $this;
+ }
+
+ /**
+ * 表达式方式指定当前操作的数据表
+ * @access public
+ * @param mixed $table 表名
+ * @return $this
+ */
+ public function tableRaw(string $table)
+ {
+ $this->options['table'] = new Raw($table);
+
+ return $this;
+ }
+
+ /**
+ * 获取执行的SQL语句而不进行实际的查询
+ * @access public
+ * @param bool $fetch 是否返回sql
+ * @return $this|Fetch
+ */
+ public function fetchSql(bool $fetch = true)
+ {
+ $this->options['fetch_sql'] = $fetch;
+
+ if ($fetch) {
+ return new Fetch($this);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 批处理执行SQL语句
+ * 批处理的指令都认为是execute操作
+ * @access public
+ * @param array $sql SQL批处理指令
+ * @return bool
+ */
+ public function batchQuery(array $sql = []): bool
+ {
+ return $this->connection->batchQuery($this, $sql);
+ }
+
+ /**
+ * USING支持 用于多表删除
+ * @access public
+ * @param mixed $using USING
+ * @return $this
+ */
+ public function using($using)
+ {
+ $this->options['using'] = $using;
+ return $this;
+ }
+
+ /**
+ * 存储过程调用
+ * @access public
+ * @param bool $procedure 是否为存储过程查询
+ * @return $this
+ */
+ public function procedure(bool $procedure = true)
+ {
+ $this->options['procedure'] = $procedure;
+ return $this;
+ }
+
+ /**
+ * 指定group查询
+ * @access public
+ * @param string|array $group GROUP
+ * @return $this
+ */
+ public function group($group)
+ {
+ $this->options['group'] = $group;
+ return $this;
+ }
+
+ /**
+ * 指定having查询
+ * @access public
+ * @param string $having having
+ * @return $this
+ */
+ public function having(string $having)
+ {
+ $this->options['having'] = $having;
+ return $this;
+ }
+
+ /**
+ * 指定distinct查询
+ * @access public
+ * @param bool $distinct 是否唯一
+ * @return $this
+ */
+ public function distinct(bool $distinct = true)
+ {
+ $this->options['distinct'] = $distinct;
+ return $this;
+ }
+
+ /**
+ * 指定强制索引
+ * @access public
+ * @param string $force 索引名称
+ * @return $this
+ */
+ public function force(string $force)
+ {
+ $this->options['force'] = $force;
+ return $this;
+ }
+
+ /**
+ * 查询注释
+ * @access public
+ * @param string $comment 注释
+ * @return $this
+ */
+ public function comment(string $comment)
+ {
+ $this->options['comment'] = $comment;
+ return $this;
+ }
+
+ /**
+ * 设置是否REPLACE
+ * @access public
+ * @param bool $replace 是否使用REPLACE写入数据
+ * @return $this
+ */
+ public function replace(bool $replace = true)
+ {
+ $this->options['replace'] = $replace;
+ return $this;
+ }
+
+ /**
+ * 设置当前查询所在的分区
+ * @access public
+ * @param string|array $partition 分区名称
+ * @return $this
+ */
+ public function partition($partition)
+ {
+ $this->options['partition'] = $partition;
+ return $this;
+ }
+
+ /**
+ * 设置DUPLICATE
+ * @access public
+ * @param array|string|Raw $duplicate DUPLICATE信息
+ * @return $this
+ */
+ public function duplicate($duplicate)
+ {
+ $this->options['duplicate'] = $duplicate;
+ return $this;
+ }
+
+ /**
+ * 设置查询的额外参数
+ * @access public
+ * @param string $extra 额外信息
+ * @return $this
+ */
+ public function extra(string $extra)
+ {
+ $this->options['extra'] = $extra;
+ return $this;
+ }
+
+ /**
+ * 创建子查询SQL
+ * @access public
+ * @param bool $sub 是否添加括号
+ * @return string
+ * @throws Exception
+ */
+ public function buildSql(bool $sub = true): string
+ {
+ return $sub ? '( ' . $this->fetchSql()->select() . ' )' : $this->fetchSql()->select();
+ }
+
+ /**
+ * 获取当前数据表的主键
+ * @access public
+ * @return string|array
+ */
+ public function getPk()
+ {
+ if (empty($this->pk)) {
+ $this->pk = $this->connection->getPk($this->getTable());
+ }
+
+ return $this->pk;
+ }
+
+ /**
+ * 指定数据表自增主键
+ * @access public
+ * @param string $autoinc 自增键
+ * @return $this
+ */
+ public function autoinc(string $autoinc)
+ {
+ $this->autoinc = $autoinc;
+ return $this;
+ }
+
+ /**
+ * 获取当前数据表的自增主键
+ * @access public
+ * @return string|null
+ */
+ public function getAutoInc()
+ {
+ $tableName = $this->getTable();
+
+ if (empty($this->autoinc) && $tableName) {
+ $this->autoinc = $this->connection->getAutoInc($tableName);
+ }
+
+ return $this->autoinc;
+ }
+
+ /**
+ * 字段值增长
+ * @access public
+ * @param string $field 字段名
+ * @param float $step 增长值
+ * @return $this
+ */
+ public function inc(string $field, float $step = 1)
+ {
+ $this->options['data'][$field] = ['INC', $step];
+
+ return $this;
+ }
+
+ /**
+ * 字段值减少
+ * @access public
+ * @param string $field 字段名
+ * @param float $step 增长值
+ * @return $this
+ */
+ public function dec(string $field, float $step = 1)
+ {
+ $this->options['data'][$field] = ['DEC', $step];
+ return $this;
+ }
+
+ /**
+ * 获取当前的查询标识
+ * @access public
+ * @param mixed $data 要序列化的数据
+ * @return string
+ */
+ public function getQueryGuid($data = null): string
+ {
+ return md5($this->getConfig('database') . serialize(var_export($data ?: $this->options, true)) . serialize($this->getBind(false)));
+ }
+
+ /**
+ * 执行查询但只返回PDOStatement对象
+ * @access public
+ * @return PDOStatement
+ */
+ public function getPdo(): PDOStatement
+ {
+ return $this->connection->pdo($this);
+ }
+
+ /**
+ * 使用游标查找记录
+ * @access public
+ * @param mixed $data 数据
+ * @return \Generator
+ */
+ public function cursor($data = null)
+ {
+ if (!is_null($data)) {
+ // 主键条件分析
+ $this->parsePkWhere($data);
+ }
+
+ $this->options['data'] = $data;
+
+ $connection = clone $this->connection;
+
+ return $connection->cursor($this);
+ }
+
+ /**
+ * 分批数据返回处理
+ * @access public
+ * @param integer $count 每次处理的数据数量
+ * @param callable $callback 处理回调方法
+ * @param string|array $column 分批处理的字段名
+ * @param string $order 字段排序
+ * @return bool
+ * @throws Exception
+ */
+ public function chunk(int $count, callable $callback, $column = null, string $order = 'asc'): bool
+ {
+ $options = $this->getOptions();
+ $column = $column ?: $this->getPk();
+
+ if (isset($options['order'])) {
+ unset($options['order']);
+ }
+
+ $bind = $this->bind;
+
+ if (is_array($column)) {
+ $times = 1;
+ $query = $this->options($options)->page($times, $count);
+ } else {
+ $query = $this->options($options)->limit($count);
+
+ if (strpos($column, '.')) {
+ [$alias, $key] = explode('.', $column);
+ } else {
+ $key = $column;
+ }
+ }
+
+ $resultSet = $query->order($column, $order)->select();
+
+ while (count($resultSet) > 0) {
+ if (false === call_user_func($callback, $resultSet)) {
+ return false;
+ }
+
+ if (isset($times)) {
+ $times++;
+ $query = $this->options($options)->page($times, $count);
+ } else {
+ $end = $resultSet->pop();
+ $lastId = is_array($end) ? $end[$key] : $end->getData($key);
+
+ $query = $this->options($options)
+ ->limit($count)
+ ->where($column, 'asc' == strtolower($order) ? '>' : '<', $lastId);
+ }
+
+ $resultSet = $query->bind($bind)->order($column, $order)->select();
+ }
+
+ return true;
+ }
+}
diff --git a/src/db/Expression.php b/src/db/Raw.php
similarity index 64%
rename from src/db/Expression.php
rename to src/db/Raw.php
index f1b92abd7b93116c0c9b681f45c3d8e15c0b8596..b956ff6905967e0bb4b1dde17d3bf566f7bbf4a4 100644
--- a/src/db/Expression.php
+++ b/src/db/Raw.php
@@ -2,16 +2,20 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db;
-class Expression
+/**
+ * SQL Raw
+ */
+class Raw
{
/**
* 查询表达式
@@ -20,15 +24,24 @@ class Expression
*/
protected $value;
+ /**
+ * 参数绑定
+ *
+ * @var array
+ */
+ protected $bind = [];
+
/**
* 创建一个查询表达式
*
* @param string $value
+ * @param array $bind
* @return void
*/
- public function __construct($value)
+ public function __construct(string $value, array $bind = [])
{
$this->value = $value;
+ $this->bind = $bind;
}
/**
@@ -36,13 +49,19 @@ class Expression
*
* @return string
*/
- public function getValue()
+ public function getValue(): string
{
return $this->value;
}
- public function __toString()
+ /**
+ * 获取参数绑定
+ *
+ * @return string
+ */
+ public function getBind(): array
{
- return (string) $this->value;
+ return $this->bind;
}
+
}
diff --git a/src/db/Where.php b/src/db/Where.php
index 1f4def6b79ca6e20ef7b58422e9a02d4b5d22e61..088046089331abc36d9912f52f188a6b13fce728 100644
--- a/src/db/Where.php
+++ b/src/db/Where.php
@@ -2,17 +2,21 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db;
use ArrayAccess;
+/**
+ * 数组查询对象
+ */
class Where implements ArrayAccess
{
/**
@@ -22,7 +26,7 @@ class Where implements ArrayAccess
protected $where = [];
/**
- * 是否需要增加括号
+ * 是否需要把查询条件两边增加括号
* @var bool
*/
protected $enclose = false;
@@ -33,7 +37,7 @@ class Where implements ArrayAccess
* @param array $where 查询条件数组
* @param bool $enclose 是否增加括号
*/
- public function __construct(array $where = [], $enclose = false)
+ public function __construct(array $where = [], bool $enclose = false)
{
$this->where = $where;
$this->enclose = $enclose;
@@ -45,7 +49,7 @@ class Where implements ArrayAccess
* @param bool $enclose
* @return $this
*/
- public function enclose($enclose = true)
+ public function enclose(bool $enclose = true)
{
$this->enclose = $enclose;
return $this;
@@ -56,12 +60,12 @@ class Where implements ArrayAccess
* @access public
* @return array
*/
- public function parse()
+ public function parse(): array
{
$where = [];
foreach ($this->where as $key => $val) {
- if ($val instanceof Expression) {
+ if ($val instanceof Raw) {
$where[] = [$key, 'exp', $val];
} elseif (is_null($val)) {
$where[] = [$key, 'NULL', ''];
@@ -79,24 +83,24 @@ class Where implements ArrayAccess
* 分析查询表达式
* @access protected
* @param string $field 查询字段
- * @param array $where 查询表达式
+ * @param array $where 查询条件
* @return array
*/
- protected function parseItem($field, $where = [])
+ protected function parseItem(string $field, array $where = []): array
{
$op = $where[0];
- $condition = isset($where[1]) ? $where[1] : null;
+ $condition = $where[1] ?? null;
if (is_array($op)) {
// 同一字段多条件查询
array_unshift($where, $field);
} elseif (is_null($condition)) {
- if (in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) {
+ if (is_string($op) && in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) {
// null查询
$where = [$field, $op, ''];
- } elseif (in_array($op, ['=', 'eq', 'EQ', null], true)) {
+ } elseif (is_null($op) || '=' == $op) {
$where = [$field, 'NULL', ''];
- } elseif (in_array($op, ['<>', 'neq', 'NEQ'], true)) {
+ } elseif ('<>' == $op) {
$where = [$field, 'NOTNULL', ''];
} else {
// 字段相等查询
@@ -129,14 +133,14 @@ class Where implements ArrayAccess
*/
public function __get($name)
{
- return isset($this->where[$name]) ? $this->where[$name] : null;
+ return $this->where[$name] ?? null;
}
/**
* 检测数据对象的值
* @access public
* @param string $name 名称
- * @return boolean
+ * @return bool
*/
public function __isset($name)
{
diff --git a/src/db/builder/Mongo.php b/src/db/builder/Mongo.php
index 0a0d9815c0eb89e7c40f548be89055233b3ff802..823156bc3b6b8b24e746d4eb54917b1b11c4d86c 100644
--- a/src/db/builder/Mongo.php
+++ b/src/db/builder/Mongo.php
@@ -6,7 +6,7 @@
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
-
+declare (strict_types = 1);
namespace think\db\builder;
use MongoDB\BSON\Javascript;
@@ -17,8 +17,8 @@ use MongoDB\Driver\Command;
use MongoDB\Driver\Exception\InvalidArgumentException;
use MongoDB\Driver\Query as MongoQuery;
use think\db\connector\Mongo as Connection;
+use think\db\exception\DbException as Exception;
use think\db\Mongo as Query;
-use think\Exception;
class Mongo
{
@@ -27,12 +27,12 @@ class Mongo
// 最后插入ID
protected $insertId = [];
// 查询表达式
- protected $exp = ['<>' => 'ne', 'neq' => 'ne', '=' => 'eq', '>' => 'gt', '>=' => 'gte', '<' => 'lt', '<=' => 'lte', 'in' => 'in', 'not in' => 'nin', 'nin' => 'nin', 'mod' => 'mod', 'exists' => 'exists', 'null' => 'null', 'notnull' => 'not null', 'not null' => 'not null', 'regex' => 'regex', 'type' => 'type', 'all' => 'all', '> time' => '> time', '< time' => '< time', 'between' => 'between', 'not between' => 'not between', 'between time' => 'between time', 'not between time' => 'not between time', 'notbetween time' => 'not between time', 'like' => 'like', 'near' => 'near', 'size' => 'size'];
+ protected $exp = ['<>' => 'ne', '=' => 'eq', '>' => 'gt', '>=' => 'gte', '<' => 'lt', '<=' => 'lte', 'in' => 'in', 'not in' => 'nin', 'nin' => 'nin', 'mod' => 'mod', 'exists' => 'exists', 'null' => 'null', 'notnull' => 'not null', 'not null' => 'not null', 'regex' => 'regex', 'type' => 'type', 'all' => 'all', '> time' => '> time', '< time' => '< time', 'between' => 'between', 'not between' => 'not between', 'between time' => 'between time', 'not between time' => 'not between time', 'notbetween time' => 'not between time', 'like' => 'like', 'near' => 'near', 'size' => 'size'];
/**
* 架构函数
* @access public
- * @param Connection $connection 数据库连接对象实例
+ * @param Connection $connection 数据库连接对象实例
*/
public function __construct(Connection $connection)
{
@@ -42,9 +42,9 @@ class Mongo
/**
* 获取当前的连接对象实例
* @access public
- * @return void
+ * @return Connection
*/
- public function getConnection()
+ public function getConnection(): Connection
{
return $this->connection;
}
@@ -55,10 +55,10 @@ class Mongo
* @param string $key
* @return string
*/
- protected function parseKey($key)
+ protected function parseKey(Query $query, string $key): string
{
if (0 === strpos($key, '__TABLE__.')) {
- list($collection, $key) = explode('.', $key, 2);
+ [$collection, $key] = explode('.', $key, 2);
}
if ('id' == $key && $this->connection->getConfig('pk_convert_id')) {
@@ -96,7 +96,7 @@ class Mongo
* @param array $data 数据
* @return array
*/
- protected function parseData(Query $query, $data)
+ protected function parseData(Query $query, array $data): array
{
if (empty($data)) {
return [];
@@ -105,12 +105,12 @@ class Mongo
$result = [];
foreach ($data as $key => $val) {
- $item = $this->parseKey($key);
+ $item = $this->parseKey($query, $key);
- if ($val instanceof Expression) {
- $result[$item] = $val->getValue();
- } elseif (is_object($val)) {
+ if (is_object($val)) {
$result[$item] = $val;
+ } elseif (isset($val[0]) && 'exp' == $val[0]) {
+ $result[$item] = $val[1];
} elseif (is_null($val)) {
$result[$item] = 'NULL';
} else {
@@ -128,7 +128,7 @@ class Mongo
* @param array $data 数据
* @return array
*/
- protected function parseSet(Query $query, $data)
+ protected function parseSet(Query $query, array $data): array
{
if (empty($data)) {
return [];
@@ -137,7 +137,7 @@ class Mongo
$result = [];
foreach ($data as $key => $val) {
- $item = $this->parseKey($key);
+ $item = $this->parseKey($query, $key);
if (is_array($val) && isset($val[0]) && is_string($val[0]) && 0 === strpos($val[0], '$')) {
$result[$val[0]][$item] = $this->parseValue($query, $val[1], $key);
@@ -156,21 +156,15 @@ class Mongo
* @param mixed $where
* @return array
*/
- public function parseWhere(Query $query, $where)
+ public function parseWhere(Query $query, array $where): array
{
if (empty($where)) {
$where = [];
}
$filter = [];
-
foreach ($where as $logic => $val) {
- $logic = strtolower($logic);
-
- if (0 !== strpos($logic, '$')) {
- $logic = '$' . $logic;
- }
-
+ $logic = '$' . strtolower($logic);
foreach ($val as $field => $value) {
if (is_array($value)) {
if (key($value) !== 0) {
@@ -211,22 +205,22 @@ class Mongo
$options = $query->getOptions();
if (!empty($options['soft_delete'])) {
// 附加软删除条件
- list($field, $condition) = $options['soft_delete'];
- $filter['$and'][] = $this->parseWhereItem($query, $field, $condition);
+ [$field, $condition] = $options['soft_delete'];
+ $filter['$and'][] = $this->parseWhereItem($query, $field, $condition);
}
return $filter;
}
// where子单元分析
- protected function parseWhereItem(Query $query, $field, $val)
+ protected function parseWhereItem(Query $query, $field, $val): array
{
- $key = $field ? $this->parseKey($field) : '';
+ $key = $field ? $this->parseKey($query, $field) : '';
// 查询规则和条件
if (!is_array($val)) {
$val = ['=', $val];
}
- list($exp, $value) = $val;
+ [$exp, $value] = $val;
// 对一个字段使用多个查询条件
if (is_array($exp)) {
@@ -243,8 +237,8 @@ class Mongo
$k = '$' . $exp;
$data[$k] = $value;
}
- $query[$key] = $data;
- return $query;
+ $result[$key] = $data;
+ return $result;
} elseif (!in_array($exp, $this->exp)) {
$exp = strtolower($exp);
if (isset($this->exp[$exp])) {
@@ -254,11 +248,6 @@ class Mongo
}
}
- if (is_object($value) && method_exists($value, '__toString')) {
- // 对象数据写入
- $value = $value->__toString();
- }
-
$result = [];
if ('=' == $exp) {
// 普通查询
@@ -341,17 +330,21 @@ class Mongo
protected function parseDateTime(Query $query, $value, $key)
{
// 获取时间字段类型
- $type = $this->connection->getFieldsType($query->getOptions('table') ?: $query->getTable());
+ $type = $query->getFieldType($key);
- if (isset($type[$key])) {
- $value = strtotime($value) ?: $value;
+ if ($type) {
+ if (is_string($value)) {
+ $value = strtotime($value) ?: $value;
+ }
- if (preg_match('/(datetime|timestamp)/is', $type[$key])) {
- // 日期及时间戳类型
- $value = date('Y-m-d H:i:s', $value);
- } elseif (preg_match('/(date)/is', $type[$key])) {
- // 日期及时间戳类型
- $value = date('Y-m-d', $value);
+ if (is_int($value)) {
+ if (preg_match('/(datetime|timestamp)/is', $type)) {
+ // 日期及时间戳类型
+ $value = date('Y-m-d H:i:s', $value);
+ } elseif (preg_match('/(date)/is', $type)) {
+ // 日期及时间戳类型
+ $value = date('Y-m-d', $value);
+ }
}
}
@@ -372,10 +365,9 @@ class Mongo
* 生成insert BulkWrite对象
* @access public
* @param Query $query 查询对象
- * @param array $data 数据
* @return BulkWrite
*/
- public function insert(Query $query, $replace = false)
+ public function insert(Query $query): BulkWrite
{
// 分析并处理数据
$options = $query->getOptions();
@@ -400,11 +392,12 @@ class Mongo
* @param array $dataSet 数据集
* @return BulkWrite
*/
- public function insertAll(Query $query, $dataSet)
+ public function insertAll(Query $query, array $dataSet): BulkWrite
{
$bulk = new BulkWrite;
$options = $query->getOptions();
+ $this->insertId = [];
foreach ($dataSet as $data) {
// 分析并处理数据
$data = $this->parseData($query, $data);
@@ -424,7 +417,7 @@ class Mongo
* @param Query $query 查询对象
* @return BulkWrite
*/
- public function update(Query $query)
+ public function update(Query $query): BulkWrite
{
$options = $query->getOptions();
@@ -452,7 +445,7 @@ class Mongo
* @param Query $query 查询对象
* @return BulkWrite
*/
- public function delete(Query $query)
+ public function delete(Query $query): BulkWrite
{
$options = $query->getOptions();
$where = $this->parseWhere($query, $options['where']);
@@ -475,14 +468,20 @@ class Mongo
/**
* 生成Mongo查询对象
* @access public
- * @param Query $query 查询对象
+ * @param Query $query 查询对象
+ * @param bool $one 是否仅获取一个记录
* @return MongoQuery
*/
- public function select(Query $query)
+ public function select(Query $query, bool $one = false): MongoQuery
{
$options = $query->getOptions();
$where = $this->parseWhere($query, $options['where']);
+
+ if ($one) {
+ $options['limit'] = 1;
+ }
+
$query = new MongoQuery($where, $options);
$this->log('find', $where, $options);
@@ -496,12 +495,12 @@ class Mongo
* @param Query $query 查询对象
* @return Command
*/
- public function count(Query $query)
+ public function count(Query $query): Command
{
$options = $query->getOptions();
$cmd['count'] = $options['table'];
- $cmd['query'] = $this->parseWhere($query, $options['where']);
+ $cmd['query'] = (object) $this->parseWhere($query, $options['where']);
foreach (['hint', 'limit', 'maxTimeMS', 'skip'] as $option) {
if (isset($options[$option])) {
@@ -522,10 +521,10 @@ class Mongo
* @param array $extra 指令和字段
* @return Command
*/
- public function aggregate(Query $query, $extra)
+ public function aggregate(Query $query, array $extra): Command
{
- $options = $query->getOptions();
- list($fun, $field) = $extra;
+ $options = $query->getOptions();
+ [$fun, $field] = $extra;
if ('id' == $field && $this->connection->getConfig('pk_convert_id')) {
$field = '_id';
@@ -561,16 +560,18 @@ class Mongo
/**
* 多聚合查询命令, 可以对多个字段进行 group by 操作
*
- * @param array $options 参数
- * @param array $extra 指令和字段
+ * @param Query $query 查询对象
+ * @param array $extra 指令和字段
* @return Command
*/
- public function multiAggregate(Query $query, $extra)
+ public function multiAggregate(Query $query, $extra): Command
{
$options = $query->getOptions();
- list($aggregate, $groupBy) = $extra;
- $groups = ['_id' => []];
+ [$aggregate, $groupBy] = $extra;
+
+ $groups = ['_id' => []];
+
foreach ($groupBy as $field) {
$groups['_id'][$field] = '$' . $field;
}
@@ -578,10 +579,12 @@ class Mongo
foreach ($aggregate as $fun => $field) {
$groups[$field . '_' . $fun] = ['$' . $fun => '$' . $field];
}
+
$pipeline = [
['$match' => (object) $this->parseWhere($query, $options['where'])],
['$group' => $groups],
];
+
$cmd = [
'aggregate' => $options['table'],
'allowDiskUse' => true,
@@ -594,8 +597,10 @@ class Mongo
$cmd[$option] = $options[$option];
}
}
+
$command = new Command($cmd);
$this->log('group', $cmd);
+
return $command;
}
@@ -606,7 +611,7 @@ class Mongo
* @param string $field 字段名
* @return Command
*/
- public function distinct(Query $query, $field)
+ public function distinct(Query $query, $field): Command
{
$options = $query->getOptions();
@@ -616,7 +621,7 @@ class Mongo
];
if (!empty($options['where'])) {
- $cmd['query'] = $this->parseWhere($query, $options['where']);
+ $cmd['query'] = (object) $this->parseWhere($query, $options['where']);
}
if (isset($options['maxTimeMS'])) {
@@ -635,7 +640,7 @@ class Mongo
* @access public
* @return Command
*/
- public function listcollections()
+ public function listcollections(): Command
{
$cmd = ['listCollections' => 1];
$command = new Command($cmd);
@@ -651,7 +656,7 @@ class Mongo
* @param Query $query 查询对象
* @return Command
*/
- public function collStats(Query $query)
+ public function collStats(Query $query): Command
{
$options = $query->getOptions();
@@ -665,8 +670,6 @@ class Mongo
protected function log($type, $data, $options = [])
{
- if ($this->connection->getConfig('debug')) {
- $this->connection->log($type, $data, $options);
- }
+ $this->connection->mongoLog($type, $data, $options);
}
}
diff --git a/src/db/builder/Mysql.php b/src/db/builder/Mysql.php
index 85bab3e02f8e3c18f3e4c49ef903e7f791b6ef88..136b0dee042a2ea2c47e2ec237f2c05492e1125d 100644
--- a/src/db/builder/Mysql.php
+++ b/src/db/builder/Mysql.php
@@ -2,26 +2,30 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db\builder;
use think\db\Builder;
-use think\db\Expression;
+use think\db\exception\DbException as Exception;
use think\db\Query;
-use think\Exception;
+use think\db\Raw;
/**
* mysql数据库驱动
*/
class Mysql extends Builder
{
- // 查询表达式解析
+ /**
+ * 查询表达式解析
+ * @var array
+ */
protected $parser = [
'parseCompare' => ['=', '<>', '>', '>=', '<', '<='],
'parseLike' => ['LIKE', 'NOT LIKE'],
@@ -34,10 +38,106 @@ class Mysql extends Builder
'parseTime' => ['< TIME', '> TIME', '<= TIME', '>= TIME'],
'parseExists' => ['NOT EXISTS', 'EXISTS'],
'parseColumn' => ['COLUMN'],
+ 'parseFindInSet' => ['FIND IN SET'],
];
- protected $insertAllSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES %DATA% %COMMENT%';
- protected $updateSql = 'UPDATE %TABLE% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%';
+ /**
+ * SELECT SQL表达式
+ * @var string
+ */
+ protected $selectSql = 'SELECT%DISTINCT%%EXTRA% %FIELD% FROM %TABLE%%PARTITION%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * INSERT SQL表达式
+ * @var string
+ */
+ protected $insertSql = '%INSERT%%EXTRA% INTO %TABLE%%PARTITION% SET %SET% %DUPLICATE%%COMMENT%';
+
+ /**
+ * INSERT ALL SQL表达式
+ * @var string
+ */
+ protected $insertAllSql = '%INSERT%%EXTRA% INTO %TABLE%%PARTITION% (%FIELD%) VALUES %DATA% %DUPLICATE%%COMMENT%';
+
+ /**
+ * UPDATE SQL表达式
+ * @var string
+ */
+ protected $updateSql = 'UPDATE%EXTRA% %TABLE%%PARTITION% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * DELETE SQL表达式
+ * @var string
+ */
+ protected $deleteSql = 'DELETE%EXTRA% FROM %TABLE%%PARTITION%%USING%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * 生成查询SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @param bool $one 是否仅获取一个记录
+ * @return string
+ */
+ public function select(Query $query, bool $one = false): string
+ {
+ $options = $query->getOptions();
+
+ return str_replace(
+ ['%TABLE%', '%PARTITION%', '%DISTINCT%', '%EXTRA%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
+ [
+ $this->parseTable($query, $options['table']),
+ $this->parsePartition($query, $options['partition']),
+ $this->parseDistinct($query, $options['distinct']),
+ $this->parseExtra($query, $options['extra']),
+ $this->parseField($query, $options['field'] ?? '*'),
+ $this->parseJoin($query, $options['join']),
+ $this->parseWhere($query, $options['where']),
+ $this->parseGroup($query, $options['group']),
+ $this->parseHaving($query, $options['having']),
+ $this->parseOrder($query, $options['order']),
+ $this->parseLimit($query, $one ? '1' : $options['limit']),
+ $this->parseUnion($query, $options['union']),
+ $this->parseLock($query, $options['lock']),
+ $this->parseComment($query, $options['comment']),
+ $this->parseForce($query, $options['force']),
+ ],
+ $this->selectSql);
+ }
+
+ /**
+ * 生成Insert SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @return string
+ */
+ public function insert(Query $query): string
+ {
+ $options = $query->getOptions();
+
+ // 分析并处理数据
+ $data = $this->parseData($query, $options['data']);
+ if (empty($data)) {
+ return '';
+ }
+
+ $set = [];
+ foreach ($data as $key => $val) {
+ $set[] = $key . ' = ' . $val;
+ }
+
+ return str_replace(
+ ['%INSERT%', '%EXTRA%', '%TABLE%', '%PARTITION%', '%SET%', '%DUPLICATE%', '%COMMENT%'],
+ [
+ !empty($options['replace']) ? 'REPLACE' : 'INSERT',
+ $this->parseExtra($query, $options['extra']),
+ $this->parseTable($query, $options['table']),
+ $this->parsePartition($query, $options['partition']),
+ implode(' , ', $set),
+ $this->parseDuplicate($query, $options['duplicate']),
+ $this->parseComment($query, $options['comment']),
+ ],
+ $this->insertSql);
+ }
/**
* 生成insertall SQL
@@ -47,19 +147,22 @@ class Mysql extends Builder
* @param bool $replace 是否replace
* @return string
*/
- public function insertAll(Query $query, $dataSet, $replace = false)
+ public function insertAll(Query $query, array $dataSet, bool $replace = false): string
{
$options = $query->getOptions();
+ // 获取绑定信息
+ $bind = $query->getFieldsBindType();
+
// 获取合法的字段
- if ('*' == $options['field']) {
- $allowFields = $this->connection->getTableFields($options['table']);
+ if (empty($options['field']) || '*' == $options['field']) {
+ $allowFields = array_keys($bind);
} else {
$allowFields = $options['field'];
}
- // 获取绑定信息
- $bind = $this->connection->getFieldsBind($options['table']);
+ $fields = [];
+ $values = [];
foreach ($dataSet as $data) {
$data = $this->parseData($query, $data, $allowFields, $bind);
@@ -71,63 +174,156 @@ class Mysql extends Builder
}
}
- $fields = [];
foreach ($insertFields as $field) {
$fields[] = $this->parseKey($query, $field);
}
return str_replace(
- ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
+ ['%INSERT%', '%EXTRA%', '%TABLE%', '%PARTITION%', '%FIELD%', '%DATA%', '%DUPLICATE%', '%COMMENT%'],
[
$replace ? 'REPLACE' : 'INSERT',
+ $this->parseExtra($query, $options['extra']),
$this->parseTable($query, $options['table']),
+ $this->parsePartition($query, $options['partition']),
implode(' , ', $fields),
implode(' , ', $values),
+ $this->parseDuplicate($query, $options['duplicate']),
$this->parseComment($query, $options['comment']),
],
$this->insertAllSql);
}
+ /**
+ * 生成update SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @return string
+ */
+ public function update(Query $query): string
+ {
+ $options = $query->getOptions();
+
+ $data = $this->parseData($query, $options['data']);
+
+ if (empty($data)) {
+ return '';
+ }
+ $set = [];
+ foreach ($data as $key => $val) {
+ $set[] = $key . ' = ' . $val;
+ }
+
+ return str_replace(
+ ['%TABLE%', '%PARTITION%', '%EXTRA%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
+ [
+ $this->parseTable($query, $options['table']),
+ $this->parsePartition($query, $options['partition']),
+ $this->parseExtra($query, $options['extra']),
+ implode(' , ', $set),
+ $this->parseJoin($query, $options['join']),
+ $this->parseWhere($query, $options['where']),
+ $this->parseOrder($query, $options['order']),
+ $this->parseLimit($query, $options['limit']),
+ $this->parseLock($query, $options['lock']),
+ $this->parseComment($query, $options['comment']),
+ ],
+ $this->updateSql);
+ }
+
+ /**
+ * 生成delete SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @return string
+ */
+ public function delete(Query $query): string
+ {
+ $options = $query->getOptions();
+
+ return str_replace(
+ ['%TABLE%', '%PARTITION%', '%EXTRA%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
+ [
+ $this->parseTable($query, $options['table']),
+ $this->parsePartition($query, $options['partition']),
+ $this->parseExtra($query, $options['extra']),
+ !empty($options['using']) ? ' USING ' . $this->parseTable($query, $options['using']) . ' ' : '',
+ $this->parseJoin($query, $options['join']),
+ $this->parseWhere($query, $options['where']),
+ $this->parseOrder($query, $options['order']),
+ $this->parseLimit($query, $options['limit']),
+ $this->parseLock($query, $options['lock']),
+ $this->parseComment($query, $options['comment']),
+ ],
+ $this->deleteSql);
+ }
+
/**
* 正则查询
* @access protected
* @param Query $query 查询对象
* @param string $key
* @param string $exp
- * @param Expression $value
+ * @param mixed $value
+ * @param string $field
+ * @return string
+ */
+ protected function parseRegexp(Query $query, string $key, string $exp, $value, string $field): string
+ {
+ if ($value instanceof Raw) {
+ $value = $this->parseRaw($query, $value);
+ }
+
+ return $key . ' ' . $exp . ' ' . $value;
+ }
+
+ /**
+ * FIND_IN_SET 查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
* @param string $field
* @return string
*/
- protected function parseRegexp(Query $query, $key, $exp, Expression $value, $field)
+ protected function parseFindInSet(Query $query, string $key, string $exp, $value, string $field): string
{
- return $key . ' ' . $exp . ' ' . $value->getValue();
+ if ($value instanceof Raw) {
+ $value = $this->parseRaw($query, $value);
+ }
+
+ return 'FIND_IN_SET(' . $value . ', ' . $key . ')';
}
/**
* 字段和表名处理
* @access public
- * @param Query $query 查询对象
- * @param string $key
+ * @param Query $query 查询对象
+ * @param mixed $key 字段名
* @param bool $strict 严格检测
* @return string
*/
- public function parseKey(Query $query, $key, $strict = false)
+ public function parseKey(Query $query, $key, bool $strict = false): string
{
- if (is_numeric($key)) {
- return $key;
- } elseif ($key instanceof Expression) {
- return $key->getValue();
+ if (is_int($key)) {
+ return (string) $key;
+ } elseif ($key instanceof Raw) {
+ return $this->parseRaw($query, $key);
}
$key = trim($key);
- if (strpos($key, '->') && false === strpos($key, '(')) {
+ if (strpos($key, '->>') && false === strpos($key, '(')) {
// JSON字段支持
- list($field, $name) = explode('->', $key, 2);
+ [$field, $name] = explode('->>', $key, 2);
- return 'json_extract(' . $field . ', \'$.' . str_replace('->', '.', $name) . '\')';
+ return $this->parseKey($query, $field, true) . '->>\'$' . (strpos($name, '[') === 0 ? '' : '.') . str_replace('->>', '.', $name) . '\'';
+ } elseif (strpos($key, '->') && false === strpos($key, '(')) {
+ // JSON字段支持
+ [$field, $name] = explode('->', $key, 2);
+ return 'json_extract(' . $this->parseKey($query, $field, true) . ', \'$' . (strpos($name, '[') === 0 ? '' : '.') . str_replace('->', '.', $name) . '\')';
} elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {
- list($table, $key) = explode('.', $key, 2);
+ [$table, $key] = explode('.', $key, 2);
$alias = $query->getOptions('alias');
@@ -145,7 +341,7 @@ class Mysql extends Builder
throw new Exception('not support data:' . $key);
}
- if ('*' != $key && ($strict || !preg_match('/[,\'\"\*\(\)`.\s]/', $key))) {
+ if ('*' != $key && !preg_match('/[,\'\"\*\(\)`.\s]/', $key)) {
$key = '`' . $key . '`';
}
@@ -163,12 +359,68 @@ class Mysql extends Builder
/**
* 随机排序
* @access protected
- * @param Query $query 查询对象
+ * @param Query $query 查询对象
* @return string
*/
- protected function parseRand(Query $query)
+ protected function parseRand(Query $query): string
{
return 'rand()';
}
+ /**
+ * Partition 分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string|array $partition 分区
+ * @return string
+ */
+ protected function parsePartition(Query $query, $partition): string
+ {
+ if ('' == $partition) {
+ return '';
+ }
+
+ if (is_string($partition)) {
+ $partition = explode(',', $partition);
+ }
+
+ return ' PARTITION (' . implode(' , ', $partition) . ') ';
+ }
+
+ /**
+ * ON DUPLICATE KEY UPDATE 分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $duplicate
+ * @return string
+ */
+ protected function parseDuplicate(Query $query, $duplicate): string
+ {
+ if ('' == $duplicate) {
+ return '';
+ }
+
+ if ($duplicate instanceof Raw) {
+ return ' ON DUPLICATE KEY UPDATE ' . $this->parseRaw($query, $duplicate) . ' ';
+ }
+
+ if (is_string($duplicate)) {
+ $duplicate = explode(',', $duplicate);
+ }
+
+ $updates = [];
+ foreach ($duplicate as $key => $val) {
+ if (is_numeric($key)) {
+ $val = $this->parseKey($query, $val);
+ $updates[] = $val . ' = VALUES(' . $val . ')';
+ } elseif ($val instanceof Raw) {
+ $updates[] = $this->parseKey($query, $key) . " = " . $this->parseRaw($query, $val);
+ } else {
+ $name = $query->bindValue($val, $query->getConnection()->getFieldBindType($key));
+ $updates[] = $this->parseKey($query, $key) . " = :" . $name;
+ }
+ }
+
+ return ' ON DUPLICATE KEY UPDATE ' . implode(' , ', $updates) . ' ';
+ }
}
diff --git a/src/db/builder/Oracle.php b/src/db/builder/Oracle.php
index 2c2bec1944e1d89e9928d035e6872508fbb1006b..38feb5dc1b55787cac4cd0b1c9e9ddc6388efaf1 100644
--- a/src/db/builder/Oracle.php
+++ b/src/db/builder/Oracle.php
@@ -6,32 +6,36 @@
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db\builder;
use think\db\Builder;
use think\db\Query;
+use think\db\exception\DbException as Exception;
+use think\db\Raw;
/**
* Oracle数据库驱动
*/
class Oracle extends Builder
{
-
protected $selectSql = 'SELECT * FROM (SELECT thinkphp.*, rownum AS numrow FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%) thinkphp ) %LIMIT%%COMMENT%';
/**
* limit分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $limit
+ * @param Query $query 查询对象
+ * @param mixed $limit
* @return string
*/
- protected function parseLimit(Query $query, $limit)
+ protected function parseLimit(Query $query, string $limit): string
{
$limitStr = '';
+
if (!empty($limit)) {
$limit = explode(',', $limit);
+
if (count($limit) > 1) {
$limitStr = "(numrow>" . $limit[0] . ") AND (numrow<=" . ($limit[0] + $limit[1]) . ")";
} else {
@@ -46,11 +50,11 @@ class Oracle extends Builder
/**
* 设置锁机制
* @access protected
- * @param Query $query 查询对象
+ * @param Query $query 查询对象
* @param bool|false $lock
* @return string
*/
- protected function parseLock(Query $query, $lock = false)
+ protected function parseLock(Query $query, $lock = false): string
{
if (!$lock) {
return '';
@@ -62,25 +66,51 @@ class Oracle extends Builder
/**
* 字段和表名处理
* @access public
- * @param Query $query 查询对象
- * @param mixed $key 字段名
- * @param bool $strict 严格检测
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param bool $strict
* @return string
+ * @throws Exception
*/
- public function parseKey(Query $query, $key, $strict = false)
+ public function parseKey(Query $query, $key, bool $strict = false): string
{
- if (is_numeric($key)) {
- return $key;
- } elseif ($key instanceof Expression) {
- return $key->getValue();
+ if (is_int($key)) {
+ return (string) $key;
+ } elseif ($key instanceof Raw) {
+ return $this->parseRaw($query, $key);
}
$key = trim($key);
if (strpos($key, '->') && false === strpos($key, '(')) {
// JSON字段支持
- list($field, $name) = explode($key, '->');
- $key = $field . '."' . $name . '"';
+ [$field, $name] = explode($key, '->');
+ $key = $field . '."' . $name . '"';
+ } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)\[\s]/', $key)) {
+ [$table, $key] = explode('.', $key, 2);
+
+ $alias = $query->getOptions('alias');
+
+ if ('__TABLE__' == $table) {
+ $table = $query->getOptions('table');
+ $table = is_array($table) ? array_shift($table) : $table;
+ }
+
+ if (isset($alias[$table])) {
+ $table = $alias[$table];
+ }
+ }
+
+ if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) {
+ throw new Exception('not support data:' . $key);
+ }
+
+ if ('*' != $key && !preg_match('/[,\'\"\*\(\)\[.\s]/', $key)) {
+ $key = '"' . $key . '"';
+ }
+
+ if (isset($table)) {
+ $key = '"' . $table . '".' . $key;
}
return $key;
@@ -89,12 +119,11 @@ class Oracle extends Builder
/**
* 随机排序
* @access protected
- * @param Query $query 查询对象
+ * @param Query $query 查询对象
* @return string
*/
- protected function parseRand(Query $query)
+ protected function parseRand(Query $query): string
{
return 'DBMS_RANDOM.value';
}
-
}
diff --git a/src/db/builder/Pgsql.php b/src/db/builder/Pgsql.php
index 65fb231c7f61d7d8271835b95a23ade3b6b73dca..4eace0ac9de86a183a14fe4a8e3a21205873c78c 100644
--- a/src/db/builder/Pgsql.php
+++ b/src/db/builder/Pgsql.php
@@ -2,35 +2,45 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db\builder;
use think\db\Builder;
use think\db\Query;
+use think\db\Raw;
/**
* Pgsql数据库驱动
*/
class Pgsql extends Builder
{
+ /**
+ * INSERT SQL表达式
+ * @var string
+ */
+ protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
- protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
+ /**
+ * INSERT ALL SQL表达式
+ * @var string
+ */
protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
/**
* limit分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $limit
+ * @param Query $query 查询对象
+ * @param mixed $limit
* @return string
*/
- public function parseLimit(Query $query, $limit)
+ public function parseLimit(Query $query, string $limit): string
{
$limitStr = '';
@@ -54,22 +64,22 @@ class Pgsql extends Builder
* @param bool $strict 严格检测
* @return string
*/
- public function parseKey(Query $query, $key, $strict = false)
+ public function parseKey(Query $query, $key, bool $strict = false): string
{
- if (is_numeric($key)) {
- return $key;
- } elseif ($key instanceof Expression) {
- return $key->getValue();
+ if (is_int($key)) {
+ return (string) $key;
+ } elseif ($key instanceof Raw) {
+ return $this->parseRaw($query, $key);
}
$key = trim($key);
- if (strpos($key, '$.') && false === strpos($key, '(')) {
+ if (strpos($key, '->') && false === strpos($key, '(')) {
// JSON字段支持
- list($field, $name) = explode('$.', $key);
- $key = $field . '->>\'' . $name . '\'';
+ [$field, $name] = explode('->', $key);
+ $key = '"' . $field . '"' . '->>\'' . $name . '\'';
} elseif (strpos($key, '.')) {
- list($table, $key) = explode('.', $key, 2);
+ [$table, $key] = explode('.', $key, 2);
$alias = $query->getOptions('alias');
@@ -81,6 +91,10 @@ class Pgsql extends Builder
if (isset($alias[$table])) {
$table = $alias[$table];
}
+
+ if ('*' != $key && !preg_match('/[,\"\*\(\).\s]/', $key)) {
+ $key = '"' . $key . '"';
+ }
}
if (isset($table)) {
@@ -93,10 +107,10 @@ class Pgsql extends Builder
/**
* 随机排序
* @access protected
- * @param Query $query 查询对象
+ * @param Query $query 查询对象
* @return string
*/
- protected function parseRand(Query $query)
+ protected function parseRand(Query $query): string
{
return 'RANDOM()';
}
diff --git a/src/db/builder/Sqlite.php b/src/db/builder/Sqlite.php
index bdbb31fc007ebc7d97418884e81af5e3dbf82ade..ff17c5d6cd636e55c39557d0a4cd975fee8d1442 100644
--- a/src/db/builder/Sqlite.php
+++ b/src/db/builder/Sqlite.php
@@ -2,32 +2,33 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db\builder;
use think\db\Builder;
use think\db\Query;
+use think\db\Raw;
/**
* Sqlite数据库驱动
*/
class Sqlite extends Builder
{
-
/**
* limit
* @access public
- * @param Query $query 查询对象
- * @param mixed $limit
+ * @param Query $query 查询对象
+ * @param mixed $limit
* @return string
*/
- public function parseLimit(Query $query, $limit)
+ public function parseLimit(Query $query, string $limit): string
{
$limitStr = '';
@@ -46,10 +47,10 @@ class Sqlite extends Builder
/**
* 随机排序
* @access protected
- * @param Query $query 查询对象
+ * @param Query $query 查询对象
* @return string
*/
- protected function parseRand(Query $query)
+ protected function parseRand(Query $query): string
{
return 'RANDOM()';
}
@@ -57,22 +58,23 @@ class Sqlite extends Builder
/**
* 字段和表名处理
* @access public
- * @param Query $query 查询对象
- * @param mixed $key 字段名
- * @param bool $strict 严格检测
+ * @param Query $query 查询对象
+ * @param mixed $key 字段名
+ * @param bool $strict 严格检测
* @return string
*/
- public function parseKey(Query $query, $key, $strict = false)
+ public function parseKey(Query $query, $key, bool $strict = false): string
{
- if (is_numeric($key)) {
- return $key;
- } elseif ($key instanceof Expression) {
- return $key->getValue();
+ if (is_int($key)) {
+ return (string) $key;
+ } elseif ($key instanceof Raw) {
+ return $this->parseRaw($query, $key);
}
$key = trim($key);
- if (strpos($key, '.')) {
- list($table, $key) = explode('.', $key, 2);
+
+ if (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {
+ [$table, $key] = explode('.', $key, 2);
$alias = $query->getOptions('alias');
@@ -86,10 +88,26 @@ class Sqlite extends Builder
}
}
+ if ('*' != $key && !preg_match('/[,\'\"\*\(\)`.\s]/', $key)) {
+ $key = '`' . $key . '`';
+ }
+
if (isset($table)) {
- $key = $table . '.' . $key;
+ $key = '`' . $table . '`.' . $key;
}
return $key;
}
+
+ /**
+ * 设置锁机制
+ * @access protected
+ * @param Query $query 查询对象
+ * @param bool|string $lock
+ * @return string
+ */
+ protected function parseLock(Query $query, $lock = false): string
+ {
+ return '';
+ }
}
diff --git a/src/db/builder/Sqlsrv.php b/src/db/builder/Sqlsrv.php
index 58da40a0f5d3f023d446efa04ffc4932939301b1..779b5e3514f8c04ec47ecd172af0d676cdeb95d9 100644
--- a/src/db/builder/Sqlsrv.php
+++ b/src/db/builder/Sqlsrv.php
@@ -12,29 +12,58 @@
namespace think\db\builder;
use think\db\Builder;
+use think\db\exception\DbException as Exception;
use think\db\Query;
-use think\Exception;
+use think\db\Raw;
/**
* Sqlsrv数据库驱动
*/
class Sqlsrv extends Builder
{
- protected $selectSql = 'SELECT T1.* FROM (SELECT thinkphp.*, ROW_NUMBER() OVER (%ORDER%) AS ROW_NUMBER FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%) AS thinkphp) AS T1 %LIMIT%%COMMENT%';
+ /**
+ * SELECT SQL表达式
+ * @var string
+ */
+ protected $selectSql = 'SELECT T1.* FROM (SELECT thinkphp.*, ROW_NUMBER() OVER (%ORDER%) AS ROW_NUMBER FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%) AS thinkphp) AS T1 %LIMIT%%COMMENT%';
+ /**
+ * SELECT INSERT SQL表达式
+ * @var string
+ */
protected $selectInsertSql = 'SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%';
- protected $updateSql = 'UPDATE %TABLE% SET %SET% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%';
- protected $deleteSql = 'DELETE FROM %TABLE% %USING% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%';
- protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
- protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
+
+ /**
+ * UPDATE SQL表达式
+ * @var string
+ */
+ protected $updateSql = 'UPDATE %TABLE% SET %SET% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * DELETE SQL表达式
+ * @var string
+ */
+ protected $deleteSql = 'DELETE FROM %TABLE% %USING% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * INSERT SQL表达式
+ * @var string
+ */
+ protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
+
+ /**
+ * INSERT ALL SQL表达式
+ * @var string
+ */
+ protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
/**
* order分析
* @access protected
- * @param Query $query 查询对象
- * @param mixed $order
+ * @param Query $query 查询对象
+ * @param mixed $order
* @return string
*/
- protected function parseOrder(Query $query, $order)
+ protected function parseOrder(Query $query, array $order): string
{
if (empty($order)) {
return ' ORDER BY rand()';
@@ -43,13 +72,13 @@ class Sqlsrv extends Builder
$array = [];
foreach ($order as $key => $val) {
- if ($val instanceof Expression) {
- $array[] = $val->getValue();
+ if ($val instanceof Raw) {
+ $array[] = $this->parseRaw($query, $val);
} elseif ('[rand]' == $val) {
$array[] = $this->parseRand($query);
} else {
if (is_numeric($key)) {
- list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' ');
+ [$key, $sort] = explode(' ', strpos($val, ' ') ? $val : $val . ' ');
} else {
$sort = $val;
}
@@ -65,10 +94,10 @@ class Sqlsrv extends Builder
/**
* 随机排序
* @access protected
- * @param Query $query 查询对象
+ * @param Query $query 查询对象
* @return string
*/
- protected function parseRand(Query $query)
+ protected function parseRand(Query $query): string
{
return 'rand()';
}
@@ -81,18 +110,18 @@ class Sqlsrv extends Builder
* @param bool $strict 严格检测
* @return string
*/
- public function parseKey(Query $query, $key, $strict = false)
+ public function parseKey(Query $query, $key, bool $strict = false): string
{
- if (is_numeric($key)) {
- return $key;
- } elseif ($key instanceof Expression) {
- return $key->getValue();
+ if (is_int($key)) {
+ return (string) $key;
+ } elseif ($key instanceof Raw) {
+ return $this->parseRaw($query, $key);
}
$key = trim($key);
if (strpos($key, '.') && !preg_match('/[,\'\"\(\)\[\s]/', $key)) {
- list($table, $key) = explode('.', $key, 2);
+ [$table, $key] = explode('.', $key, 2);
$alias = $query->getOptions('alias');
@@ -110,7 +139,7 @@ class Sqlsrv extends Builder
throw new Exception('not support data:' . $key);
}
- if ('*' != $key && ($strict || !preg_match('/[,\'\"\*\(\)\[.\s]/', $key))) {
+ if ('*' != $key && !preg_match('/[,\'\"\*\(\)\[.\s]/', $key)) {
$key = '[' . $key . ']';
}
@@ -124,11 +153,11 @@ class Sqlsrv extends Builder
/**
* limit
* @access protected
- * @param Query $query 查询对象
- * @param mixed $limit
+ * @param Query $query 查询对象
+ * @param mixed $limit
* @return string
*/
- protected function parseLimit(Query $query, $limit)
+ protected function parseLimit(Query $query, string $limit): string
{
if (empty($limit)) {
return '';
@@ -145,7 +174,7 @@ class Sqlsrv extends Builder
return 'WHERE ' . $limitStr;
}
- public function selectInsert(Query $query, $fields, $table)
+ public function selectInsert(Query $query, array $fields, string $table): string
{
$this->selectSql = $this->selectInsertSql;
diff --git a/src/db/concern/AggregateQuery.php b/src/db/concern/AggregateQuery.php
new file mode 100644
index 0000000000000000000000000000000000000000..0eab36355123bee3846463185112f6f6c6d4d979
--- /dev/null
+++ b/src/db/concern/AggregateQuery.php
@@ -0,0 +1,113 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+use think\db\exception\DbException;
+use think\db\Raw;
+
+/**
+ * 聚合查询
+ */
+trait AggregateQuery
+{
+ /**
+ * 聚合查询
+ * @access protected
+ * @param string $aggregate 聚合方法
+ * @param string|Raw $field 字段名
+ * @param bool $force 强制转为数字类型
+ * @return mixed
+ */
+ protected function aggregate(string $aggregate, $field, bool $force = false)
+ {
+ return $this->connection->aggregate($this, $aggregate, $field, $force);
+ }
+
+ /**
+ * COUNT查询
+ * @access public
+ * @param string|Raw $field 字段名
+ * @return int
+ */
+ public function count(string $field = '*'): int
+ {
+ if (!empty($this->options['group'])) {
+ // 支持GROUP
+
+ if (!preg_match('/^[\w\.\*]+$/', $field)) {
+ throw new DbException('not support data:' . $field);
+ }
+
+ $options = $this->getOptions();
+ $subSql = $this->options($options)
+ ->field('count(' . $field . ') AS think_count')
+ ->bind($this->bind)
+ ->buildSql();
+
+ $query = $this->newQuery()->table([$subSql => '_group_count_']);
+
+ $count = $query->aggregate('COUNT', '*');
+ } else {
+ $count = $this->aggregate('COUNT', $field);
+ }
+
+ return (int) $count;
+ }
+
+ /**
+ * SUM查询
+ * @access public
+ * @param string|Raw $field 字段名
+ * @return float
+ */
+ public function sum($field): float
+ {
+ return $this->aggregate('SUM', $field, true);
+ }
+
+ /**
+ * MIN查询
+ * @access public
+ * @param string|Raw $field 字段名
+ * @param bool $force 强制转为数字类型
+ * @return mixed
+ */
+ public function min($field, bool $force = true)
+ {
+ return $this->aggregate('MIN', $field, $force);
+ }
+
+ /**
+ * MAX查询
+ * @access public
+ * @param string|Raw $field 字段名
+ * @param bool $force 强制转为数字类型
+ * @return mixed
+ */
+ public function max($field, bool $force = true)
+ {
+ return $this->aggregate('MAX', $field, $force);
+ }
+
+ /**
+ * AVG查询
+ * @access public
+ * @param string|Raw $field 字段名
+ * @return float
+ */
+ public function avg($field): float
+ {
+ return $this->aggregate('AVG', $field, true);
+ }
+
+}
diff --git a/src/db/concern/JoinAndViewQuery.php b/src/db/concern/JoinAndViewQuery.php
new file mode 100644
index 0000000000000000000000000000000000000000..c33d1ed2fbe7cbb2cff6965d3a5a584fbb417b5c
--- /dev/null
+++ b/src/db/concern/JoinAndViewQuery.php
@@ -0,0 +1,229 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+use think\db\Raw;
+use think\helper\Str;
+
+/**
+ * JOIN和VIEW查询
+ */
+trait JoinAndViewQuery
+{
+
+ /**
+ * 查询SQL组装 join
+ * @access public
+ * @param mixed $join 关联的表名
+ * @param mixed $condition 条件
+ * @param string $type JOIN类型
+ * @param array $bind 参数绑定
+ * @return $this
+ */
+ public function join($join, string $condition = null, string $type = 'INNER', array $bind = [])
+ {
+ $table = $this->getJoinTable($join);
+
+ if (!empty($bind) && $condition) {
+ $this->bindParams($condition, $bind);
+ }
+
+ $this->options['join'][] = [$table, strtoupper($type), $condition];
+
+ return $this;
+ }
+
+ /**
+ * LEFT JOIN
+ * @access public
+ * @param mixed $join 关联的表名
+ * @param mixed $condition 条件
+ * @param array $bind 参数绑定
+ * @return $this
+ */
+ public function leftJoin($join, string $condition = null, array $bind = [])
+ {
+ return $this->join($join, $condition, 'LEFT', $bind);
+ }
+
+ /**
+ * RIGHT JOIN
+ * @access public
+ * @param mixed $join 关联的表名
+ * @param mixed $condition 条件
+ * @param array $bind 参数绑定
+ * @return $this
+ */
+ public function rightJoin($join, string $condition = null, array $bind = [])
+ {
+ return $this->join($join, $condition, 'RIGHT', $bind);
+ }
+
+ /**
+ * FULL JOIN
+ * @access public
+ * @param mixed $join 关联的表名
+ * @param mixed $condition 条件
+ * @param array $bind 参数绑定
+ * @return $this
+ */
+ public function fullJoin($join, string $condition = null, array $bind = [])
+ {
+ return $this->join($join, $condition, 'FULL');
+ }
+
+ /**
+ * 获取Join表名及别名 支持
+ * ['prefix_table或者子查询'=>'alias'] 'table alias'
+ * @access protected
+ * @param array|string|Raw $join JION表名
+ * @param string $alias 别名
+ * @return string|array
+ */
+ protected function getJoinTable($join, &$alias = null)
+ {
+ if (is_array($join)) {
+ $table = $join;
+ $alias = array_shift($join);
+ return $table;
+ } elseif ($join instanceof Raw) {
+ return $join;
+ }
+
+ $join = trim($join);
+
+ if (false !== strpos($join, '(')) {
+ // 使用子查询
+ $table = $join;
+ } else {
+ // 使用别名
+ if (strpos($join, ' ')) {
+ // 使用别名
+ [$table, $alias] = explode(' ', $join);
+ } else {
+ $table = $join;
+ if (false === strpos($join, '.')) {
+ $alias = $join;
+ }
+ }
+
+ if ($this->prefix && false === strpos($table, '.') && 0 !== strpos($table, $this->prefix)) {
+ $table = $this->getTable($table);
+ }
+ }
+
+ if (!empty($alias) && $table != $alias) {
+ $table = [$table => $alias];
+ }
+
+ return $table;
+ }
+
+ /**
+ * 指定JOIN查询字段
+ * @access public
+ * @param string|array $join 数据表
+ * @param string|array $field 查询字段
+ * @param string $on JOIN条件
+ * @param string $type JOIN类型
+ * @param array $bind 参数绑定
+ * @return $this
+ */
+ public function view($join, $field = true, $on = null, string $type = 'INNER', array $bind = [])
+ {
+ $this->options['view'] = true;
+
+ $fields = [];
+ $table = $this->getJoinTable($join, $alias);
+
+ if (true === $field) {
+ $fields = $alias . '.*';
+ } else {
+ if (is_string($field)) {
+ $field = explode(',', $field);
+ }
+
+ foreach ($field as $key => $val) {
+ if (is_numeric($key)) {
+ $fields[] = $alias . '.' . $val;
+
+ $this->options['map'][$val] = $alias . '.' . $val;
+ } else {
+ if (preg_match('/[,=\.\'\"\(\s]/', $key)) {
+ $name = $key;
+ } else {
+ $name = $alias . '.' . $key;
+ }
+
+ $fields[] = $name . ' AS ' . $val;
+
+ $this->options['map'][$val] = $name;
+ }
+ }
+ }
+
+ $this->field($fields);
+
+ if ($on) {
+ $this->join($table, $on, $type, $bind);
+ } else {
+ $this->table($table);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 视图查询处理
+ * @access protected
+ * @param array $options 查询参数
+ * @return void
+ */
+ protected function parseView(array &$options): void
+ {
+ foreach (['AND', 'OR'] as $logic) {
+ if (isset($options['where'][$logic])) {
+ foreach ($options['where'][$logic] as $key => $val) {
+ if (array_key_exists($key, $options['map'])) {
+ array_shift($val);
+ array_unshift($val, $options['map'][$key]);
+ $options['where'][$logic][$options['map'][$key]] = $val;
+ unset($options['where'][$logic][$key]);
+ }
+ }
+ }
+ }
+
+ if (isset($options['order'])) {
+ // 视图查询排序处理
+ foreach ($options['order'] as $key => $val) {
+ if (is_numeric($key) && is_string($val)) {
+ if (strpos($val, ' ')) {
+ [$field, $sort] = explode(' ', $val);
+ if (array_key_exists($field, $options['map'])) {
+ $options['order'][$options['map'][$field]] = $sort;
+ unset($options['order'][$key]);
+ }
+ } elseif (array_key_exists($val, $options['map'])) {
+ $options['order'][$options['map'][$val]] = 'asc';
+ unset($options['order'][$key]);
+ }
+ } elseif (array_key_exists($key, $options['map'])) {
+ $options['order'][$options['map'][$key]] = $val;
+ unset($options['order'][$key]);
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/db/concern/ModelRelationQuery.php b/src/db/concern/ModelRelationQuery.php
new file mode 100644
index 0000000000000000000000000000000000000000..74f6e6aad998db2946ca2007d114df13d2df2bd7
--- /dev/null
+++ b/src/db/concern/ModelRelationQuery.php
@@ -0,0 +1,574 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+use Closure;
+use think\helper\Str;
+use think\Model;
+use think\model\Collection as ModelCollection;
+
+/**
+ * 模型及关联查询
+ */
+trait ModelRelationQuery
+{
+
+ /**
+ * 当前模型对象
+ * @var Model
+ */
+ protected $model;
+
+ /**
+ * 指定模型
+ * @access public
+ * @param Model $model 模型对象实例
+ * @return $this
+ */
+ public function model(Model $model)
+ {
+ $this->model = $model;
+ return $this;
+ }
+
+ /**
+ * 获取当前的模型对象
+ * @access public
+ * @return Model|null
+ */
+ public function getModel()
+ {
+ return $this->model;
+ }
+
+ /**
+ * 设置需要隐藏的输出属性
+ * @access public
+ * @param array $hidden 属性列表
+ * @return $this
+ */
+ public function hidden(array $hidden = [])
+ {
+ $this->options['hidden'] = $hidden;
+
+ return $this;
+ }
+
+ /**
+ * 设置需要输出的属性
+ * @access public
+ * @param array $visible
+ * @return $this
+ */
+ public function visible(array $visible = [])
+ {
+ $this->options['visible'] = $visible;
+
+ return $this;
+ }
+
+ /**
+ * 设置需要附加的输出属性
+ * @access public
+ * @param array $append 属性列表
+ * @return $this
+ */
+ public function append(array $append = [])
+ {
+ $this->options['append'] = $append;
+
+ return $this;
+ }
+
+ /**
+ * 添加查询范围
+ * @access public
+ * @param array|string|Closure $scope 查询范围定义
+ * @param array $args 参数
+ * @return $this
+ */
+ public function scope($scope, ...$args)
+ {
+ // 查询范围的第一个参数始终是当前查询对象
+ array_unshift($args, $this);
+
+ if ($scope instanceof Closure) {
+ call_user_func_array($scope, $args);
+ return $this;
+ }
+
+ if (is_string($scope)) {
+ $scope = explode(',', $scope);
+ }
+
+ if ($this->model) {
+ // 检查模型类的查询范围方法
+ foreach ($scope as $name) {
+ $method = 'scope' . trim($name);
+
+ if (method_exists($this->model, $method)) {
+ call_user_func_array([$this->model, $method], $args);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置关联查询
+ * @access public
+ * @param array $relation 关联名称
+ * @return $this
+ */
+ public function relation(array $relation)
+ {
+ if (empty($this->model) || empty($relation)) {
+ return $this;
+ }
+
+ $this->options['relation'] = $relation;
+ return $this;
+ }
+
+ /**
+ * 使用搜索器条件搜索字段
+ * @access public
+ * @param string|array $fields 搜索字段
+ * @param mixed $data 搜索数据
+ * @param string $prefix 字段前缀标识
+ * @return $this
+ */
+ public function withSearch($fields, $data = [], string $prefix = '')
+ {
+ if (is_string($fields)) {
+ $fields = explode(',', $fields);
+ }
+
+ $likeFields = $this->getConfig('match_like_fields') ?: [];
+
+ foreach ($fields as $key => $field) {
+ if ($field instanceof Closure) {
+ $field($this, $data[$key] ?? null, $data, $prefix);
+ } elseif ($this->model) {
+ // 检测搜索器
+ $fieldName = is_numeric($key) ? $field : $key;
+ $method = 'search' . Str::studly($fieldName) . 'Attr';
+
+ if (method_exists($this->model, $method)) {
+ $this->model->$method($this, $data[$field] ?? null, $data, $prefix);
+ } elseif (isset($data[$field])) {
+ $this->where($fieldName, in_array($fieldName, $likeFields) ? 'like' : '=', in_array($fieldName, $likeFields) ? '%' . $data[$field] . '%' : $data[$field]);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 限制关联数据的数量
+ * @access public
+ * @param int $limit 关联数量限制
+ * @return $this
+ */
+ public function withLimit(int $limit)
+ {
+ $this->options['with_limit'] = $limit;
+ return $this;
+ }
+
+ /**
+ * 设置数据字段获取器
+ * @access public
+ * @param string|array $name 字段名
+ * @param callable $callback 闭包获取器
+ * @return $this
+ */
+ public function withAttr($name, callable $callback = null)
+ {
+ if (is_array($name)) {
+ foreach ($name as $key => $val) {
+ $this->withAttr($key, $val);
+ }
+ return $this;
+ }
+
+ $this->options['with_attr'][$name] = $callback;
+
+ if (strpos($name, '.')) {
+ [$relation, $field] = explode('.', $name);
+
+ if (!empty($this->options['json']) && in_array($relation, $this->options['json'])) {
+
+ } else {
+ $this->options['with_relation_attr'][$relation][$field] = $callback;
+ unset($this->options['with_attr'][$name]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 关联预载入 In方式
+ * @access public
+ * @param array|string $with 关联方法名称
+ * @return $this
+ */
+ public function with($with)
+ {
+ if (empty($this->model) || empty($with)) {
+ return $this;
+ }
+
+ $this->options['with'] = (array) $with;
+ return $this;
+ }
+
+ /**
+ * 关联预载入 JOIN方式
+ * @access protected
+ * @param array|string $with 关联方法名
+ * @param string $joinType JOIN方式
+ * @return $this
+ */
+ public function withJoin($with, string $joinType = '')
+ {
+ if (empty($this->model) || empty($with)) {
+ return $this;
+ }
+
+ $with = (array) $with;
+ $first = true;
+
+ foreach ($with as $key => $relation) {
+ $closure = null;
+ $field = true;
+
+ if ($relation instanceof Closure) {
+ // 支持闭包查询过滤关联条件
+ $closure = $relation;
+ $relation = $key;
+ } elseif (is_array($relation)) {
+ $field = $relation;
+ $relation = $key;
+ } elseif (is_string($relation) && strpos($relation, '.')) {
+ $relation = strstr($relation, '.', true);
+ }
+
+ $result = $this->model->eagerly($this, $relation, $field, $joinType, $closure, $first);
+
+ if (!$result) {
+ unset($with[$key]);
+ } else {
+ $first = false;
+ }
+ }
+
+ $this->via();
+ $this->options['with_join'] = $with;
+
+ return $this;
+ }
+
+ /**
+ * 关联统计
+ * @access protected
+ * @param array|string $relations 关联方法名
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param bool $subQuery 是否使用子查询
+ * @return $this
+ */
+ protected function withAggregate($relations, string $aggregate = 'count', $field = '*', bool $subQuery = true)
+ {
+ if (empty($this->model)) {
+ return $this;
+ }
+
+ if (!$subQuery) {
+ $this->options['with_aggregate'][] = [(array) $relations, $aggregate, $field];
+ return $this;
+ }
+
+ if (!isset($this->options['field'])) {
+ $this->field('*');
+ }
+
+ $this->model->relationCount($this, (array) $relations, $aggregate, $field, true);
+ return $this;
+ }
+
+ /**
+ * 关联缓存
+ * @access public
+ * @param string|array|bool $relation 关联方法名
+ * @param mixed $key 缓存key
+ * @param integer|\DateTime $expire 缓存有效期
+ * @param string $tag 缓存标签
+ * @return $this
+ */
+ public function withCache($relation = true, $key = true, $expire = null, string $tag = null)
+ {
+ if (empty($this->model)) {
+ return $this;
+ }
+
+ if (false === $relation || false === $key || !$this->getConnection()->getCache()) {
+ return $this;
+ }
+
+ if ($key instanceof \DateTimeInterface || $key instanceof \DateInterval || (is_int($key) && is_null($expire))) {
+ $expire = $key;
+ $key = true;
+ }
+
+ if (true === $relation || is_numeric($relation)) {
+ $this->options['with_cache'] = $relation;
+ return $this;
+ }
+
+ $relations = (array) $relation;
+ foreach ($relations as $name => $relation) {
+ if (!is_numeric($name)) {
+ $this->options['with_cache'][$name] = is_array($relation) ? $relation : [$key, $relation, $tag];
+ } else {
+ $this->options['with_cache'][$relation] = [$key, $expire, $tag];
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 关联统计
+ * @access public
+ * @param string|array $relation 关联方法名
+ * @param bool $subQuery 是否使用子查询
+ * @return $this
+ */
+ public function withCount($relation, bool $subQuery = true)
+ {
+ return $this->withAggregate($relation, 'count', '*', $subQuery);
+ }
+
+ /**
+ * 关联统计Sum
+ * @access public
+ * @param string|array $relation 关联方法名
+ * @param string $field 字段
+ * @param bool $subQuery 是否使用子查询
+ * @return $this
+ */
+ public function withSum($relation, string $field, bool $subQuery = true)
+ {
+ return $this->withAggregate($relation, 'sum', $field, $subQuery);
+ }
+
+ /**
+ * 关联统计Max
+ * @access public
+ * @param string|array $relation 关联方法名
+ * @param string $field 字段
+ * @param bool $subQuery 是否使用子查询
+ * @return $this
+ */
+ public function withMax($relation, string $field, bool $subQuery = true)
+ {
+ return $this->withAggregate($relation, 'max', $field, $subQuery);
+ }
+
+ /**
+ * 关联统计Min
+ * @access public
+ * @param string|array $relation 关联方法名
+ * @param string $field 字段
+ * @param bool $subQuery 是否使用子查询
+ * @return $this
+ */
+ public function withMin($relation, string $field, bool $subQuery = true)
+ {
+ return $this->withAggregate($relation, 'min', $field, $subQuery);
+ }
+
+ /**
+ * 关联统计Avg
+ * @access public
+ * @param string|array $relation 关联方法名
+ * @param string $field 字段
+ * @param bool $subQuery 是否使用子查询
+ * @return $this
+ */
+ public function withAvg($relation, string $field, bool $subQuery = true)
+ {
+ return $this->withAggregate($relation, 'avg', $field, $subQuery);
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $relation 关联方法名
+ * @param mixed $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return $this
+ */
+ public function has(string $relation, string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '')
+ {
+ return $this->model->has($relation, $operator, $count, $id, $joinType, $this);
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $relation 关联方法名
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @return $this
+ */
+ public function hasWhere(string $relation, $where = [], string $fields = '*', string $joinType = '')
+ {
+ return $this->model->hasWhere($relation, $where, $fields, $joinType, $this);
+ }
+
+ /**
+ * JSON字段数据转换
+ * @access protected
+ * @param array $result 查询数据
+ * @return void
+ */
+ protected function jsonModelResult(array &$result): void
+ {
+ $withAttr = $this->options['with_attr'];
+ foreach ($this->options['json'] as $name) {
+ if (!isset($result[$name])) {
+ continue;
+ }
+
+ $jsonData = json_decode($result[$name], true);
+
+ if (isset($withAttr[$name])) {
+ foreach ($withAttr[$name] as $key => $closure) {
+ $jsonData[$key] = $closure($jsonData[$key] ?? null, $jsonData);
+ }
+ }
+
+ $result[$name] = !$this->options['json_assoc'] ? (object) $jsonData : $jsonData;
+ }
+ }
+
+ /**
+ * 查询数据转换为模型数据集对象
+ * @access protected
+ * @param array $resultSet 数据集
+ * @return ModelCollection
+ */
+ protected function resultSetToModelCollection(array $resultSet): ModelCollection
+ {
+ if (empty($resultSet)) {
+ return $this->model->toCollection();
+ }
+
+ $this->options['is_resultSet'] = true;
+
+ foreach ($resultSet as $key => &$result) {
+ // 数据转换为模型对象
+ $this->resultToModel($result);
+ }
+
+ foreach (['with', 'with_join'] as $with) {
+ // 关联预载入
+ if (!empty($this->options[$with])) {
+ $result->eagerlyResultSet(
+ $resultSet,
+ $this->options[$with],
+ $this->options['with_relation_attr'],
+ 'with_join' == $with ? true : false,
+ $this->options['with_cache'] ?? false
+ );
+ }
+ }
+
+ // 模型数据集转换
+ return $this->model->toCollection($resultSet);
+ }
+
+ /**
+ * 查询数据转换为模型对象
+ * @access protected
+ * @param array $result 查询数据
+ * @return void
+ */
+ protected function resultToModel(array &$result): void
+ {
+ // JSON数据处理
+ if (!empty($this->options['json'])) {
+ $this->jsonModelResult($result);
+ }
+
+ $result = $this->model->newInstance(
+ $result,
+ !empty($this->options['is_resultSet']) ? null : $this->getModelUpdateCondition($this->options),
+ $this->options
+ );
+
+ // 模型数据处理
+ foreach ($this->options['filter'] as $filter) {
+ call_user_func_array($filter, [$result, $this->options]);
+ }
+
+ // 关联查询
+ if (!empty($this->options['relation'])) {
+ $result->relationQuery($this->options['relation'], $this->options['with_relation_attr']);
+ }
+
+ // 关联预载入查询
+ if (empty($this->options['is_resultSet'])) {
+ foreach (['with', 'with_join'] as $with) {
+ if (!empty($this->options[$with])) {
+ $result->eagerlyResult(
+ $this->options[$with],
+ $this->options['with_relation_attr'],
+ 'with_join' == $with ? true : false,
+ $this->options['with_cache'] ?? false
+ );
+ }
+ }
+ }
+
+ // 关联统计查询
+ if (!empty($this->options['with_aggregate'])) {
+ foreach ($this->options['with_aggregate'] as $val) {
+ $result->relationCount($this, $val[0], $val[1], $val[2], false);
+ }
+ }
+
+ // 动态获取器
+ if (!empty($this->options['with_attr'])) {
+ $result->withAttr($this->options['with_attr']);
+ }
+
+ foreach (['hidden', 'visible', 'append'] as $name) {
+ if (!empty($this->options[$name])) {
+ $result->$name($this->options[$name]);
+ }
+ }
+
+ // 刷新原始数据
+ $result->refreshOrigin();
+ }
+
+}
diff --git a/src/db/concern/ParamsBind.php b/src/db/concern/ParamsBind.php
new file mode 100644
index 0000000000000000000000000000000000000000..296e2212dd1134fd8ea39d2b1e525afb99900771
--- /dev/null
+++ b/src/db/concern/ParamsBind.php
@@ -0,0 +1,106 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+use PDO;
+
+/**
+ * 参数绑定支持
+ */
+trait ParamsBind
+{
+ /**
+ * 当前参数绑定
+ * @var array
+ */
+ protected $bind = [];
+
+ /**
+ * 批量参数绑定
+ * @access public
+ * @param array $value 绑定变量值
+ * @return $this
+ */
+ public function bind(array $value)
+ {
+ $this->bind = array_merge($this->bind, $value);
+ return $this;
+ }
+
+ /**
+ * 单个参数绑定
+ * @access public
+ * @param mixed $value 绑定变量值
+ * @param integer $type 绑定类型
+ * @param string $name 绑定标识
+ * @return string
+ */
+ public function bindValue($value, int $type = null, string $name = null)
+ {
+ $name = $name ?: 'ThinkBind_' . (count($this->bind) + 1) . '_' . mt_rand() . '_';
+
+ $this->bind[$name] = [$value, $type ?: PDO::PARAM_STR];
+ return $name;
+ }
+
+ /**
+ * 检测参数是否已经绑定
+ * @access public
+ * @param string $key 参数名
+ * @return bool
+ */
+ public function isBind($key)
+ {
+ return isset($this->bind[$key]);
+ }
+
+ /**
+ * 参数绑定
+ * @access public
+ * @param string $sql 绑定的sql表达式
+ * @param array $bind 参数绑定
+ * @return void
+ */
+ public function bindParams(string &$sql, array $bind = []): void
+ {
+ foreach ($bind as $key => $value) {
+ if (is_array($value)) {
+ $name = $this->bindValue($value[0], $value[1], $value[2] ?? null);
+ } else {
+ $name = $this->bindValue($value);
+ }
+
+ if (is_numeric($key)) {
+ $sql = substr_replace($sql, ':' . $name, strpos($sql, '?'), 1);
+ } else {
+ $sql = str_replace(':' . $key, ':' . $name, $sql);
+ }
+ }
+ }
+
+ /**
+ * 获取绑定的参数 并清空
+ * @access public
+ * @param bool $clear 是否清空绑定数据
+ * @return array
+ */
+ public function getBind(bool $clear = true): array
+ {
+ $bind = $this->bind;
+ if ($clear) {
+ $this->bind = [];
+ }
+
+ return $bind;
+ }
+}
diff --git a/src/db/concern/ResultOperation.php b/src/db/concern/ResultOperation.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea2691632019369ee9fd8aef34381e13f7504e52
--- /dev/null
+++ b/src/db/concern/ResultOperation.php
@@ -0,0 +1,227 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+use Closure;
+use think\Collection;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\db\Query;
+use think\helper\Str;
+use think\Model;
+
+/**
+ * 查询数据处理
+ */
+trait ResultOperation
+{
+ /**
+ * 设置数据处理(支持模型)
+ * @access public
+ * @param callable $filter 数据处理Callable
+ * @param string $index 索引(唯一)
+ * @return $this
+ */
+ public function filter(callable $filter, string $index = null)
+ {
+ if ($index) {
+ $this->options['filter'][$index] = $filter;
+ } else {
+ $this->options['filter'][] = $filter;
+ }
+ return $this;
+ }
+
+ /**
+ * 是否允许返回空数据(或空模型)
+ * @access public
+ * @param bool $allowEmpty 是否允许为空
+ * @return $this
+ */
+ public function allowEmpty(bool $allowEmpty = true)
+ {
+ $this->options['allow_empty'] = $allowEmpty;
+ return $this;
+ }
+
+ /**
+ * 设置查询数据不存在是否抛出异常
+ * @access public
+ * @param bool $fail 数据不存在是否抛出异常
+ * @return $this
+ */
+ public function failException(bool $fail = true)
+ {
+ $this->options['fail'] = $fail;
+ return $this;
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param array $result 查询数据
+ * @return void
+ */
+ protected function result(array &$result): void
+ {
+ // JSON数据处理
+ if (!empty($this->options['json'])) {
+ $this->jsonResult($result);
+ }
+
+ // 查询数据处理
+ foreach ($this->options['filter'] as $filter) {
+ $result = call_user_func_array($filter, [$result, $this->options]);
+ }
+
+ // 获取器
+ if (!empty($this->options['with_attr'])) {
+ $this->getResultAttr($result, $this->options['with_attr']);
+ }
+ }
+
+ /**
+ * 处理数据集
+ * @access public
+ * @param array $resultSet 数据集
+ * @param bool $toCollection 是否转为对象
+ * @return void
+ */
+ protected function resultSet(array &$resultSet, bool $toCollection = true): void
+ {
+ foreach ($resultSet as &$result) {
+ $this->result($result);
+ }
+
+ // 返回Collection对象
+ if ($toCollection) {
+ $resultSet = new Collection($resultSet);
+ }
+ }
+
+ /**
+ * 使用获取器处理数据
+ * @access protected
+ * @param array $result 查询数据
+ * @param array $withAttr 字段获取器
+ * @return void
+ */
+ protected function getResultAttr(array &$result, array $withAttr = []): void
+ {
+ foreach ($withAttr as $name => $closure) {
+ $name = Str::snake($name);
+
+ if (strpos($name, '.')) {
+ // 支持JSON字段 获取器定义
+ [$key, $field] = explode('.', $name);
+
+ if (isset($result[$key])) {
+ $result[$key][$field] = $closure($result[$key][$field] ?? null, $result[$key]);
+ }
+ } else {
+ $result[$name] = $closure($result[$name] ?? null, $result);
+ }
+ }
+ }
+
+ /**
+ * 处理空数据
+ * @access protected
+ * @return array|Model|null|static
+ * @throws DbException
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ protected function resultToEmpty()
+ {
+ if (!empty($this->options['fail'])) {
+ $this->throwNotFound();
+ } elseif (!empty($this->options['allow_empty'])) {
+ return !empty($this->model) ? $this->model->newInstance() : [];
+ }
+ }
+
+ /**
+ * 查找单条记录 不存在返回空数据(或者空模型)
+ * @access public
+ * @param mixed $data 数据
+ * @return array|Model|static|mixed
+ */
+ public function findOrEmpty($data = null)
+ {
+ return $this->allowEmpty(true)->find($data);
+ }
+
+ /**
+ * JSON字段数据转换
+ * @access protected
+ * @param array $result 查询数据
+ * @return void
+ */
+ protected function jsonResult(array &$result): void
+ {
+ foreach ($this->options['json'] as $name) {
+ if (!isset($result[$name])) {
+ continue;
+ }
+
+ $result[$name] = json_decode($result[$name], true);
+ }
+ }
+
+ /**
+ * 查询失败 抛出异常
+ * @access protected
+ * @return void
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ protected function throwNotFound(): void
+ {
+ if (!empty($this->model)) {
+ $class = get_class($this->model);
+ throw new ModelNotFoundException('model data Not Found:' . $class, $class, $this->options);
+ }
+
+ $table = $this->getTable();
+ throw new DataNotFoundException('table data not Found:' . $table, $table, $this->options);
+ }
+
+ /**
+ * 查找多条记录 如果不存在则抛出异常
+ * @access public
+ * @param array|string|Query|Closure $data 数据
+ * @return array|Collection|static[]
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ public function selectOrFail($data = null)
+ {
+ return $this->failException(true)->select($data);
+ }
+
+ /**
+ * 查找单条记录 如果不存在则抛出异常
+ * @access public
+ * @param array|string|Query|Closure $data 数据
+ * @return array|Model|static|mixed
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ public function findOrFail($data = null)
+ {
+ return $this->failException(true)->find($data);
+ }
+
+}
diff --git a/src/db/concern/TableFieldInfo.php b/src/db/concern/TableFieldInfo.php
new file mode 100644
index 0000000000000000000000000000000000000000..9070befeabb9ca0832f5c0f2f7c93702315417cf
--- /dev/null
+++ b/src/db/concern/TableFieldInfo.php
@@ -0,0 +1,99 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+/**
+ * 数据字段信息
+ */
+trait TableFieldInfo
+{
+
+ /**
+ * 获取数据表字段信息
+ * @access public
+ * @param string $tableName 数据表名
+ * @return array
+ */
+ public function getTableFields($tableName = ''): array
+ {
+ if ('' == $tableName) {
+ $tableName = $this->getTable();
+ }
+
+ return $this->connection->getTableFields($tableName);
+ }
+
+ /**
+ * 获取详细字段类型信息
+ * @access public
+ * @param string $tableName 数据表名称
+ * @return array
+ */
+ public function getFields(string $tableName = ''): array
+ {
+ return $this->connection->getFields($tableName ?: $this->getTable());
+ }
+
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @return array
+ */
+ public function getFieldsType(): array
+ {
+ if (!empty($this->options['field_type'])) {
+ return $this->options['field_type'];
+ }
+
+ return $this->connection->getFieldsType($this->getTable());
+ }
+
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @param string $field 字段名
+ * @return string|null
+ */
+ public function getFieldType(string $field)
+ {
+ $fieldType = $this->getFieldsType();
+
+ return $fieldType[$field] ?? null;
+ }
+
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @return array
+ */
+ public function getFieldsBindType(): array
+ {
+ $fieldType = $this->getFieldsType();
+
+ return array_map([$this->connection, 'getFieldBindType'], $fieldType);
+ }
+
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @param string $field 字段名
+ * @return int
+ */
+ public function getFieldBindType(string $field): int
+ {
+ $fieldType = $this->getFieldType($field);
+
+ return $this->connection->getFieldBindType($fieldType ?: '');
+ }
+
+}
diff --git a/src/db/concern/TimeFieldQuery.php b/src/db/concern/TimeFieldQuery.php
new file mode 100644
index 0000000000000000000000000000000000000000..69b7eae4b7267230cb371def9b60d36bd24c8f1c
--- /dev/null
+++ b/src/db/concern/TimeFieldQuery.php
@@ -0,0 +1,214 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+/**
+ * 时间查询支持
+ */
+trait TimeFieldQuery
+{
+ /**
+ * 日期查询表达式
+ * @var array
+ */
+ protected $timeRule = [
+ 'today' => ['today', 'tomorrow -1second'],
+ 'yesterday' => ['yesterday', 'today -1second'],
+ 'week' => ['this week 00:00:00', 'next week 00:00:00 -1second'],
+ 'last week' => ['last week 00:00:00', 'this week 00:00:00 -1second'],
+ 'month' => ['first Day of this month 00:00:00', 'first Day of next month 00:00:00 -1second'],
+ 'last month' => ['first Day of last month 00:00:00', 'first Day of this month 00:00:00 -1second'],
+ 'year' => ['this year 1/1', 'next year 1/1 -1second'],
+ 'last year' => ['last year 1/1', 'this year 1/1 -1second'],
+ ];
+
+ /**
+ * 添加日期或者时间查询规则
+ * @access public
+ * @param array $rule 时间表达式
+ * @return $this
+ */
+ public function timeRule(array $rule)
+ {
+ $this->timeRule = array_merge($this->timeRule, $rule);
+ return $this;
+ }
+
+ /**
+ * 查询日期或者时间
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $op 比较运算符或者表达式
+ * @param string|array $range 比较范围
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereTime(string $field, string $op, $range = null, string $logic = 'AND')
+ {
+ if (is_null($range)) {
+ if (isset($this->timeRule[$op])) {
+ $range = $this->timeRule[$op];
+ } else {
+ $range = $op;
+ }
+ $op = is_array($range) ? 'between' : '>=';
+ }
+
+ return $this->parseWhereExp($logic, $field, strtolower($op) . ' time', $range, [], true);
+ }
+
+ /**
+ * 查询某个时间间隔数据
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $start 开始时间
+ * @param string $interval 时间间隔单位 day/month/year/week/hour/minute/second
+ * @param int $step 间隔
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereTimeInterval(string $field, string $start, string $interval = 'day', int $step = 1, string $logic = 'AND')
+ {
+ $startTime = strtotime($start);
+ $endTime = strtotime(($step > 0 ? '+' : '-') . abs($step) . ' ' . $interval . (abs($step) > 1 ? 's' : ''), $startTime);
+
+ return $this->whereTime($field, 'between', $step > 0 ? [$startTime, $endTime - 1] : [$endTime, $startTime - 1], $logic);
+ }
+
+ /**
+ * 查询月数据 whereMonth('time_field', '2018-1')
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $month 月份信息
+ * @param int $step 间隔
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereMonth(string $field, string $month = 'this month', int $step = 1, string $logic = 'AND')
+ {
+ if (in_array($month, ['this month', 'last month'])) {
+ $month = date('Y-m', strtotime($month));
+ }
+
+ return $this->whereTimeInterval($field, $month, 'month', $step, $logic);
+ }
+
+ /**
+ * 查询周数据 whereWeek('time_field', '2018-1-1') 从2018-1-1开始的一周数据
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $week 周信息
+ * @param int $step 间隔
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereWeek(string $field, string $week = 'this week', int $step = 1, string $logic = 'AND')
+ {
+ if (in_array($week, ['this week', 'last week'])) {
+ $week = date('Y-m-d', strtotime($week));
+ }
+
+ return $this->whereTimeInterval($field, $week, 'week', $step, $logic);
+ }
+
+ /**
+ * 查询年数据 whereYear('time_field', '2018')
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $year 年份信息
+ * @param int $step 间隔
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereYear(string $field, string $year = 'this year', int $step = 1, string $logic = 'AND')
+ {
+ if (in_array($year, ['this year', 'last year'])) {
+ $year = date('Y', strtotime($year));
+ }
+
+ return $this->whereTimeInterval($field, $year . '-1-1', 'year', $step, $logic);
+ }
+
+ /**
+ * 查询日数据 whereDay('time_field', '2018-1-1')
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $day 日期信息
+ * @param int $step 间隔
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereDay(string $field, string $day = 'today', int $step = 1, string $logic = 'AND')
+ {
+ if (in_array($day, ['today', 'yesterday'])) {
+ $day = date('Y-m-d', strtotime($day));
+ }
+
+ return $this->whereTimeInterval($field, $day, 'day', $step, $logic);
+ }
+
+ /**
+ * 查询日期或者时间范围 whereBetweenTime('time_field', '2018-1-1','2018-1-15')
+ * @access public
+ * @param string $field 日期字段名
+ * @param string|int $startTime 开始时间
+ * @param string|int $endTime 结束时间
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereBetweenTime(string $field, $startTime, $endTime, string $logic = 'AND')
+ {
+ return $this->whereTime($field, 'between', [$startTime, $endTime], $logic);
+ }
+
+ /**
+ * 查询日期或者时间范围 whereNotBetweenTime('time_field', '2018-1-1','2018-1-15')
+ * @access public
+ * @param string $field 日期字段名
+ * @param string|int $startTime 开始时间
+ * @param string|int $endTime 结束时间
+ * @return $this
+ */
+ public function whereNotBetweenTime(string $field, $startTime, $endTime)
+ {
+ return $this->whereTime($field, '<', $startTime)
+ ->whereTime($field, '>', $endTime, 'OR');
+ }
+
+ /**
+ * 查询当前时间在两个时间字段范围 whereBetweenTimeField('start_time', 'end_time')
+ * @access public
+ * @param string $startField 开始时间字段
+ * @param string $endField 结束时间字段
+ * @return $this
+ */
+ public function whereBetweenTimeField(string $startField, string $endField)
+ {
+ return $this->whereTime($startField, '<=', time())
+ ->whereTime($endField, '>=', time());
+ }
+
+ /**
+ * 查询当前时间不在两个时间字段范围 whereNotBetweenTimeField('start_time', 'end_time')
+ * @access public
+ * @param string $startField 开始时间字段
+ * @param string $endField 结束时间字段
+ * @return $this
+ */
+ public function whereNotBetweenTimeField(string $startField, string $endField)
+ {
+ return $this->whereTime($startField, '>', time())
+ ->whereTime($endField, '<', time(), 'OR');
+ }
+
+}
diff --git a/src/db/concern/Transaction.php b/src/db/concern/Transaction.php
new file mode 100644
index 0000000000000000000000000000000000000000..b586132c43d3f0462d97ddf0ff74799543289bbc
--- /dev/null
+++ b/src/db/concern/Transaction.php
@@ -0,0 +1,122 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+/**
+ * 事务支持
+ */
+trait Transaction
+{
+
+ /**
+ * 执行数据库Xa事务
+ * @access public
+ * @param callable $callback 数据操作方法回调
+ * @param array $dbs 多个查询对象或者连接对象
+ * @return mixed
+ * @throws PDOException
+ * @throws \Exception
+ * @throws \Throwable
+ */
+ public function transactionXa(callable $callback, array $dbs = [])
+ {
+ return $this->connection->transactionXa($callback, $dbs);
+ }
+
+ /**
+ * 执行数据库事务
+ * @access public
+ * @param callable $callback 数据操作方法回调
+ * @return mixed
+ */
+ public function transaction(callable $callback)
+ {
+ return $this->connection->transaction($callback);
+ }
+
+ /**
+ * 启动事务
+ * @access public
+ * @return void
+ */
+ public function startTrans(): void
+ {
+ $this->connection->startTrans();
+ }
+
+ /**
+ * 用于非自动提交状态下面的查询提交
+ * @access public
+ * @return void
+ * @throws PDOException
+ */
+ public function commit(): void
+ {
+ $this->connection->commit();
+ }
+
+ /**
+ * 事务回滚
+ * @access public
+ * @return void
+ * @throws PDOException
+ */
+ public function rollback(): void
+ {
+ $this->connection->rollback();
+ }
+
+ /**
+ * 启动XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function startTransXa(string $xid): void
+ {
+ $this->connection->startTransXa($xid);
+ }
+
+ /**
+ * 预编译XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function prepareXa(string $xid): void
+ {
+ $this->connection->prepareXa($xid);
+ }
+
+ /**
+ * 提交XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function commitXa(string $xid): void
+ {
+ $this->connection->commitXa($xid);
+ }
+
+ /**
+ * 回滚XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function rollbackXa(string $xid): void
+ {
+ $this->connection->rollbackXa($xid);
+ }
+}
diff --git a/src/db/concern/WhereQuery.php b/src/db/concern/WhereQuery.php
new file mode 100644
index 0000000000000000000000000000000000000000..5f2ed47dce99baaa208ea3ded03d5f8fe8613597
--- /dev/null
+++ b/src/db/concern/WhereQuery.php
@@ -0,0 +1,532 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+use Closure;
+use think\db\BaseQuery;
+use think\db\Raw;
+
+trait WhereQuery
+{
+ /**
+ * 指定AND查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $op 查询表达式
+ * @param mixed $condition 查询条件
+ * @return $this
+ */
+ public function where($field, $op = null, $condition = null)
+ {
+ if ($field instanceof $this) {
+ $this->parseQueryWhere($field);
+ return $this;
+ } elseif (true === $field || 1 === $field) {
+ $this->options['where']['AND'][] = true;
+ return $this;
+ }
+
+ $param = func_get_args();
+ array_shift($param);
+ return $this->parseWhereExp('AND', $field, $op, $condition, $param);
+ }
+
+ /**
+ * 解析Query对象查询条件
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return void
+ */
+ protected function parseQueryWhere(BaseQuery $query): void
+ {
+ $this->options['where'] = $query->getOptions('where') ?? [];
+
+ if ($query->getOptions('via')) {
+ $via = $query->getOptions('via');
+ foreach ($this->options['where'] as $logic => &$where) {
+ foreach ($where as $key => &$val) {
+ if (is_array($val) && !strpos($val[0], '.')) {
+ $val[0] = $via . '.' . $val[0];
+ }
+ }
+ }
+ }
+
+ $this->bind($query->getBind(false));
+ }
+
+ /**
+ * 指定OR查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $op 查询表达式
+ * @param mixed $condition 查询条件
+ * @return $this
+ */
+ public function whereOr($field, $op = null, $condition = null)
+ {
+ $param = func_get_args();
+ array_shift($param);
+ return $this->parseWhereExp('OR', $field, $op, $condition, $param);
+ }
+
+ /**
+ * 指定XOR查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $op 查询表达式
+ * @param mixed $condition 查询条件
+ * @return $this
+ */
+ public function whereXor($field, $op = null, $condition = null)
+ {
+ $param = func_get_args();
+ array_shift($param);
+ return $this->parseWhereExp('XOR', $field, $op, $condition, $param);
+ }
+
+ /**
+ * 指定Null查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNull(string $field, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'NULL', null, [], true);
+ }
+
+ /**
+ * 指定NotNull查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNotNull(string $field, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'NOTNULL', null, [], true);
+ }
+
+ /**
+ * 指定Exists查询条件
+ * @access public
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereExists($condition, string $logic = 'AND')
+ {
+ if (is_string($condition)) {
+ $condition = new Raw($condition);
+ }
+
+ $this->options['where'][strtoupper($logic)][] = ['', 'EXISTS', $condition];
+ return $this;
+ }
+
+ /**
+ * 指定NotExists查询条件
+ * @access public
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNotExists($condition, string $logic = 'AND')
+ {
+ if (is_string($condition)) {
+ $condition = new Raw($condition);
+ }
+
+ $this->options['where'][strtoupper($logic)][] = ['', 'NOT EXISTS', $condition];
+ return $this;
+ }
+
+ /**
+ * 指定In查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereIn(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'IN', $condition, [], true);
+ }
+
+ /**
+ * 指定NotIn查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNotIn(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'NOT IN', $condition, [], true);
+ }
+
+ /**
+ * 指定Like查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereLike(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'LIKE', $condition, [], true);
+ }
+
+ /**
+ * 指定NotLike查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNotLike(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'NOT LIKE', $condition, [], true);
+ }
+
+ /**
+ * 指定Between查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereBetween(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'BETWEEN', $condition, [], true);
+ }
+
+ /**
+ * 指定NotBetween查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNotBetween(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'NOT BETWEEN', $condition, [], true);
+ }
+
+ /**
+ * 指定FIND_IN_SET查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereFindInSet(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'FIND IN SET', $condition, [], true);
+ }
+
+ /**
+ * 比较两个字段
+ * @access public
+ * @param string $field1 查询字段
+ * @param string $operator 比较操作符
+ * @param string $field2 比较字段
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereColumn(string $field1, string $operator, string $field2 = null, string $logic = 'AND')
+ {
+ if (is_null($field2)) {
+ $field2 = $operator;
+ $operator = '=';
+ }
+
+ return $this->parseWhereExp($logic, $field1, 'COLUMN', [$operator, $field2], [], true);
+ }
+
+ /**
+ * 设置软删除字段及条件
+ * @access public
+ * @param string $field 查询字段
+ * @param mixed $condition 查询条件
+ * @return $this
+ */
+ public function useSoftDelete(string $field, $condition = null)
+ {
+ if ($field) {
+ $this->options['soft_delete'] = [$field, $condition];
+ }
+
+ return $this;
+ }
+
+ /**
+ * 指定Exp查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param string $where 查询条件
+ * @param array $bind 参数绑定
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereExp(string $field, string $where, array $bind = [], string $logic = 'AND')
+ {
+ $this->options['where'][$logic][] = [$field, 'EXP', new Raw($where, $bind)];
+
+ return $this;
+ }
+
+ /**
+ * 指定字段Raw查询
+ * @access public
+ * @param string $field 查询字段表达式
+ * @param mixed $op 查询表达式
+ * @param string $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereFieldRaw(string $field, $op, $condition = null, string $logic = 'AND')
+ {
+ if (is_null($condition)) {
+ $condition = $op;
+ $op = '=';
+ }
+
+ $this->options['where'][$logic][] = [new Raw($field), $op, $condition];
+ return $this;
+ }
+
+ /**
+ * 指定表达式查询条件
+ * @access public
+ * @param string $where 查询条件
+ * @param array $bind 参数绑定
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereRaw(string $where, array $bind = [], string $logic = 'AND')
+ {
+ $this->options['where'][$logic][] = new Raw($where, $bind);
+
+ return $this;
+ }
+
+ /**
+ * 指定表达式查询条件 OR
+ * @access public
+ * @param string $where 查询条件
+ * @param array $bind 参数绑定
+ * @return $this
+ */
+ public function whereOrRaw(string $where, array $bind = [])
+ {
+ return $this->whereRaw($where, $bind, 'OR');
+ }
+
+ /**
+ * 分析查询表达式
+ * @access protected
+ * @param string $logic 查询逻辑 and or xor
+ * @param mixed $field 查询字段
+ * @param mixed $op 查询表达式
+ * @param mixed $condition 查询条件
+ * @param array $param 查询参数
+ * @param bool $strict 严格模式
+ * @return $this
+ */
+ protected function parseWhereExp(string $logic, $field, $op, $condition, array $param = [], bool $strict = false)
+ {
+ $logic = strtoupper($logic);
+
+ if (is_string($field) && !empty($this->options['via']) && false === strpos($field, '.')) {
+ $field = $this->options['via'] . '.' . $field;
+ }
+
+ if ($strict) {
+ // 使用严格模式查询
+ if ('=' == $op) {
+ $where = $this->whereEq($field, $condition);
+ } else {
+ $where = [$field, $op, $condition, $logic];
+ }
+ } elseif (is_array($field)) {
+ // 解析数组批量查询
+ return $this->parseArrayWhereItems($field, $logic);
+ } elseif ($field instanceof Closure) {
+ $where = $field;
+ } elseif (is_string($field)) {
+ if ($condition instanceof Raw) {
+
+ } elseif (preg_match('/[,=\<\'\"\(\s]/', $field)) {
+ return $this->whereRaw($field, is_array($op) ? $op : [], $logic);
+ } elseif (is_string($op) && strtolower($op) == 'exp' && !is_null($condition)) {
+ $bind = isset($param[2]) && is_array($param[2]) ? $param[2] : [];
+ return $this->whereExp($field, $condition, $bind, $logic);
+ }
+
+ $where = $this->parseWhereItem($logic, $field, $op, $condition, $param);
+ }
+
+ if (!empty($where)) {
+ $this->options['where'][$logic][] = $where;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 分析查询表达式
+ * @access protected
+ * @param string $logic 查询逻辑 and or xor
+ * @param mixed $field 查询字段
+ * @param mixed $op 查询表达式
+ * @param mixed $condition 查询条件
+ * @param array $param 查询参数
+ * @return array
+ */
+ protected function parseWhereItem(string $logic, $field, $op, $condition, array $param = []): array
+ {
+ if (is_array($op)) {
+ // 同一字段多条件查询
+ array_unshift($param, $field);
+ $where = $param;
+ } elseif ($field && is_null($condition)) {
+ if (is_string($op) && in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) {
+ // null查询
+ $where = [$field, $op, ''];
+ } elseif ('=' === $op || is_null($op)) {
+ $where = [$field, 'NULL', ''];
+ } elseif ('<>' === $op) {
+ $where = [$field, 'NOTNULL', ''];
+ } else {
+ // 字段相等查询
+ $where = $this->whereEq($field, $op);
+ }
+ } elseif (is_string($op) && in_array(strtoupper($op), ['EXISTS', 'NOT EXISTS', 'NOTEXISTS'], true)) {
+ $where = [$field, $op, is_string($condition) ? new Raw($condition) : $condition];
+ } else {
+ $where = $field ? [$field, $op, $condition, $param[2] ?? null] : [];
+ }
+
+ return $where;
+ }
+
+ /**
+ * 相等查询的主键处理
+ * @access protected
+ * @param string $field 字段名
+ * @param mixed $value 字段值
+ * @return array
+ */
+ protected function whereEq(string $field, $value): array
+ {
+ if ($this->getPk() == $field) {
+ $this->options['key'] = $value;
+ }
+
+ return [$field, '=', $value];
+ }
+
+ /**
+ * 数组批量查询
+ * @access protected
+ * @param array $field 批量查询
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ protected function parseArrayWhereItems(array $field, string $logic)
+ {
+ if (key($field) !== 0) {
+ $where = [];
+ foreach ($field as $key => $val) {
+ if ($val instanceof Raw) {
+ $where[] = [$key, 'exp', $val];
+ } else {
+ $where[] = is_null($val) ? [$key, 'NULL', ''] : [$key, is_array($val) ? 'IN' : '=', $val];
+ }
+ }
+ } else {
+ // 数组批量查询
+ $where = $field;
+ }
+
+ if (!empty($where)) {
+ $this->options['where'][$logic] = isset($this->options['where'][$logic]) ?
+ array_merge($this->options['where'][$logic], $where) : $where;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 去除某个查询条件
+ * @access public
+ * @param string $field 查询字段
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function removeWhereField(string $field, string $logic = 'AND')
+ {
+ $logic = strtoupper($logic);
+
+ if (isset($this->options['where'][$logic])) {
+ foreach ($this->options['where'][$logic] as $key => $val) {
+ if (is_array($val) && $val[0] == $field) {
+ unset($this->options['where'][$logic][$key]);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 条件查询
+ * @access public
+ * @param mixed $condition 满足条件(支持闭包)
+ * @param Closure|array $query 满足条件后执行的查询表达式(闭包或数组)
+ * @param Closure|array $otherwise 不满足条件后执行
+ * @return $this
+ */
+ public function when($condition, $query, $otherwise = null)
+ {
+ if ($condition instanceof Closure) {
+ $condition = $condition($this);
+ }
+
+ if ($condition) {
+ if ($query instanceof Closure) {
+ $query($this, $condition);
+ } elseif (is_array($query)) {
+ $this->where($query);
+ }
+ } elseif ($otherwise) {
+ if ($otherwise instanceof Closure) {
+ $otherwise($this, $condition);
+ } elseif (is_array($otherwise)) {
+ $this->where($otherwise);
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/src/db/connector/Mongo.php b/src/db/connector/Mongo.php
index a6d3c339ce31c6beec42fc0b65b5afe55ca1781f..418dc505236dc787a0b4534ff9ac1436dafa68ab 100644
--- a/src/db/connector/Mongo.php
+++ b/src/db/connector/Mongo.php
@@ -6,9 +6,11 @@
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db\connector;
+use Closure;
use MongoDB\BSON\ObjectID;
use MongoDB\Driver\BulkWrite;
use MongoDB\Driver\Command;
@@ -22,48 +24,33 @@ use MongoDB\Driver\Manager;
use MongoDB\Driver\Query as MongoQuery;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern;
-use think\Collection;
-use think\Db;
+use think\db\BaseQuery;
use think\db\builder\Mongo as Builder;
+use think\db\Connection;
+use think\db\exception\DbEventException;
+use think\db\exception\DbException as Exception;
use think\db\Mongo as Query;
-use think\Exception;
/**
* Mongo数据库驱动
+ * @property Manager[] $links
+ * @property Manager $linkRead
+ * @property Manager $linkWrite
*/
-class Mongo
+class Mongo extends Connection
{
- protected static $instance = [];
- protected $dbName = ''; // dbName
- /** @var string 当前SQL指令 */
- protected $queryStr = '';
+
// 查询数据类型
+ protected $dbName = '';
protected $typeMap = 'array';
protected $mongo; // MongoDb Object
protected $cursor; // MongoCursor Object
+ protected $session_uuid; // sessions会话列表当前会话数组key 随机生成
+ protected $sessions = []; // 会话列表
- // 监听回调
- protected static $event = [];
- /** @var PDO[] 数据库连接ID 支持多个连接 */
- protected $links = [];
- /** @var PDO 当前连接ID */
- protected $linkID;
- protected $linkRead;
- protected $linkWrite;
- // Builder对象
+ /** @var Builder */
protected $builder;
- // 缓存对象
- protected $cache;
- // 返回或者影响记录数
- protected $numRows = 0;
- // 错误信息
- protected $error = '';
- // 查询参数
- protected $options = [];
- // 数据表信息
- protected static $info = [];
- // 数据库日志
- protected static $log = [];
+
// 数据库连接参数配置
protected $config = [
// 数据库类型
@@ -92,8 +79,6 @@ class Mongo
'pk_type' => 'ObjectID',
// 数据库表前缀
'prefix' => '',
- // 数据库调试模式
- 'debug' => false,
// 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
'deploy' => 0,
// 数据库读写是否分离 主从式有效
@@ -104,73 +89,55 @@ class Mongo
'slave_no' => '',
// 是否严格检查字段是否存在
'fields_strict' => true,
- // 数据集返回类型
- 'resultset_type' => 'array',
+ // 开启字段缓存
+ 'fields_cache' => false,
+ // 监听SQL
+ 'trigger_sql' => true,
// 自动写入时间戳字段
'auto_timestamp' => false,
// 时间字段取出后的默认时间格式
'datetime_format' => 'Y-m-d H:i:s',
- // 是否需要进行SQL性能分析
- 'sql_explain' => false,
// 是否_id转换为id
'pk_convert_id' => false,
// typeMap
'type_map' => ['root' => 'array', 'document' => 'array'],
- // Query对象
- 'query' => '\\think\\mongo\\Query',
];
/**
- * 架构函数 读取数据库配置信息
+ * 获取当前连接器类对应的Query类
* @access public
- * @param array $config 数据库配置数组
+ * @return string
*/
- public function __construct(array $config = [])
+ public function getQueryClass(): string
{
- if (!class_exists('\MongoDB\Driver\Manager')) {
- throw new Exception('require mongodb > 1.0');
- }
-
- if (!empty($config)) {
- $this->config = array_merge($this->config, $config);
- }
-
- $this->builder = new Builder($this);
- $this->cache = Db::getCacheHandler();
+ return Query::class;
}
/**
- * 取得数据库连接类实例
+ * 获取当前的builder实例对象
* @access public
- * @param mixed $config 连接配置
- * @param bool|string $name 连接标识 true 强制重新连接
- * @return Connection
- * @throws Exception
+ * @return Builder
*/
- public static function instance($config = [], $name = false)
+ public function getBuilder()
{
- if (false === $name) {
- $name = md5(serialize($config));
- }
-
- if (true === $name || !isset(self::$instance[$name])) {
- // 解析连接参数 支持数组和字符串
- $options = self::parseConfig($config);
-
- if (true === $name) {
- $name = md5(serialize($config));
- }
- self::$instance[$name] = new static($options);
- }
+ return $this->builder;
+ }
- return self::$instance[$name];
+ /**
+ * 获取当前连接器类对应的Builder类
+ * @access public
+ * @return string
+ */
+ public function getBuilderClass(): string
+ {
+ return Builder::class;
}
/**
* 连接数据库方法
* @access public
- * @param array $config 连接参数
- * @param integer $linkNum 连接序号
+ * @param array $config 连接参数
+ * @param integer $linkNum 连接序号
* @return Manager
* @throws InvalidArgumentException
* @throws RuntimeException
@@ -191,46 +158,24 @@ class Mongo
$this->config['pk'] = 'id';
}
- $host = 'mongodb://' . ($config['username'] ? "{$config['username']}" : '') . ($config['password'] ? ":{$config['password']}@" : '') . $config['hostname'] . ($config['hostport'] ? ":{$config['hostport']}" : '') . '/' . ($config['database'] ? "{$config['database']}" : '');
-
- if ($config['debug']) {
- $startTime = microtime(true);
+ if (empty($config['dsn'])) {
+ $config['dsn'] = 'mongodb://' . ($config['username'] ? "{$config['username']}" : '') . ($config['password'] ? ":{$config['password']}@" : '') . $config['hostname'] . ($config['hostport'] ? ":{$config['hostport']}" : '');
}
- $this->links[$linkNum] = new Manager($host, $this->config['params']);
+ $startTime = microtime(true);
+
+ $this->links[$linkNum] = new Manager($config['dsn'], $config['params']);
- if ($config['debug']) {
+ if (!empty($config['trigger_sql'])) {
// 记录数据库连接信息
- $this->logger('[ MongoDb ] CONNECT :[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);
+ $this->trigger('CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);
}
+
}
return $this->links[$linkNum];
}
- /**
- * 获取数据库的配置参数
- * @access public
- * @param string $config 配置名称
- * @return mixed
- */
- public function getConfig($config = '')
- {
- return $config ? $this->config[$config] : $this->config;
- }
-
- /**
- * 设置数据库的配置参数
- * @access public
- * @param string $config 配置名称
- * @param mixed $value 配置值
- * @return void
- */
- public function setConfig($config, $value)
- {
- $this->config[$config] = $value;
- }
-
/**
* 获取Mongo Manager对象
* @access public
@@ -238,20 +183,16 @@ class Mongo
*/
public function getMongo()
{
- if (!$this->mongo) {
- return;
- } else {
- return $this->mongo;
- }
+ return $this->mongo ?: null;
}
/**
* 设置/获取当前操作的database
* @access public
- * @param string $db db
- * @throws Exception
+ * @param string $db db
+ * @return string
*/
- public function db($db = null)
+ public function db(string $db = null)
{
if (is_null($db)) {
return $this->dbName;
@@ -261,137 +202,269 @@ class Mongo
}
/**
- * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写)
+ * 执行查询但只返回Cursor对象
* @access public
- * @param string $sql sql语句
- * @return string
+ * @param Query $query 查询对象
+ * @return Cursor
*/
- public function parseSqlTable($sql)
+ public function cursor($query)
{
- if (false !== strpos($sql, '__')) {
- $prefix = $this->getConfig('prefix');
+ // 分析查询表达式
+ $options = $query->parseOptions();
- $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) use ($prefix) {
- return $prefix . strtolower($match[1]);
- }, $sql);
- }
+ // 生成MongoQuery对象
+ $mongoQuery = $this->builder->select($query);
- return $sql;
+ $master = $query->getOptions('master') ? true : false;
+
+ // 执行查询操作
+ return $this->getCursor($query, $mongoQuery, $master);
}
/**
- * 启动事务
+ * 执行查询并返回Cursor对象
* @access public
- * @return void
- * @throws \PDOException
- * @throws \Exception
+ * @param BaseQuery $query 查询对象
+ * @param MongoQuery|Closure $mongoQuery Mongo查询对象
+ * @param bool $master 是否主库操作
+ * @return Cursor
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
*/
- public function startTrans()
- {}
+ public function getCursor(BaseQuery $query, $mongoQuery, bool $master = false): Cursor
+ {
+ $this->initConnect($master);
+ $this->db->updateQueryTimes();
+
+ $options = $query->getOptions();
+ $namespace = $options['table'];
+
+ if (false === strpos($namespace, '.')) {
+ $namespace = $this->dbName . '.' . $namespace;
+ }
+
+ if (!empty($this->queryStr)) {
+ // 记录执行指令
+ $this->queryStr = 'db' . strstr($namespace, '.') . '.' . $this->queryStr;
+ }
+
+ if ($mongoQuery instanceof Closure) {
+ $mongoQuery = $mongoQuery($query);
+ }
+
+ $readPreference = $options['readPreference'] ?? null;
+ $this->queryStartTime = microtime(true);
+
+ if ($session = $this->getSession()) {
+ $this->cursor = $this->mongo->executeQuery($namespace, $query, [
+ 'readPreference' => is_null($readPreference) ? new ReadPreference(ReadPreference::RP_PRIMARY) : $readPreference,
+ 'session' => $session,
+ ]);
+ } else {
+ $this->cursor = $this->mongo->executeQuery($namespace, $mongoQuery, $readPreference);
+ }
+
+ // SQL监控
+ if (!empty($this->config['trigger_sql'])) {
+ $this->trigger('', $master);
+ }
+
+ return $this->cursor;
+ }
/**
- * 用于非自动提交状态下面的查询提交
+ * 执行查询 返回数据集
* @access public
- * @return void
- * @throws PDOException
+ * @param MongoQuery $query 查询对象
+ * @return mixed
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
*/
- public function commit()
- {}
+ public function query(MongoQuery $query)
+ {
+ return $this->mongoQuery($this->newQuery(), $query);
+ }
/**
- * 事务回滚
+ * 执行语句
* @access public
- * @return void
- * @throws PDOException
+ * @param BulkWrite $bulk
+ * @return int
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ * @throws BulkWriteException
*/
- public function rollback()
- {}
+ public function execute(BulkWrite $bulk)
+ {
+ return $this->mongoExecute($this->newQuery(), $bulk);
+ }
/**
* 执行查询
- * @access public
- * @param string $namespace 当前查询的collection
- * @param MongoQuery $query 查询对象
- * @param ReadPreference $readPreference readPreference
- * @param string|bool $class 返回的数据集类型
- * @param string|array $typeMap 指定返回的typeMap
- * @return mixed
+ * @access protected
+ * @param BaseQuery $query 查询对象
+ * @param MongoQuery|Closure $mongoQuery Mongo查询对象
+ * @return array
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ */
+ protected function mongoQuery(BaseQuery $query, $mongoQuery): array
+ {
+ $options = $query->parseOptions();
+
+ if ($query->getOptions('cache')) {
+ // 检查查询缓存
+ $cacheItem = $this->parseCache($query, $query->getOptions('cache'));
+ $key = $cacheItem->getKey();
+
+ if ($this->cache->has($key)) {
+ return $this->cache->get($key);
+ }
+ }
+
+ if ($mongoQuery instanceof Closure) {
+ $mongoQuery = $mongoQuery($query);
+ }
+
+ $master = $query->getOptions('master') ? true : false;
+ $this->getCursor($query, $mongoQuery, $master);
+
+ $resultSet = $this->getResult($options['typeMap']);
+
+ if (isset($cacheItem) && $resultSet) {
+ // 缓存数据集
+ $cacheItem->set($resultSet);
+ $this->cacheData($cacheItem);
+ }
+
+ return $resultSet;
+ }
+
+ /**
+ * 执行写操作
+ * @access protected
+ * @param BaseQuery $query
+ * @param BulkWrite $bulk
+ *
+ * @return WriteResult
* @throws AuthenticationException
* @throws InvalidArgumentException
* @throws ConnectionException
* @throws RuntimeException
+ * @throws BulkWriteException
*/
- public function query($namespace, MongoQuery $query, ReadPreference $readPreference = null, $class = false, $typeMap = null)
+ protected function mongoExecute(BaseQuery $query, BulkWrite $bulk)
{
- $this->initConnect(false);
- Db::$queryTimes++;
+ $this->initConnect(true);
+ $this->db->updateQueryTimes();
+
+ $options = $query->getOptions();
+ $namespace = $options['table'];
if (false === strpos($namespace, '.')) {
$namespace = $this->dbName . '.' . $namespace;
}
- if ($this->config['debug'] && !empty($this->queryStr)) {
+ if (!empty($this->queryStr)) {
// 记录执行指令
$this->queryStr = 'db' . strstr($namespace, '.') . '.' . $this->queryStr;
}
- $this->debug(true);
+ $writeConcern = $options['writeConcern'] ?? null;
+ $this->queryStartTime = microtime(true);
- $this->cursor = $this->mongo->executeQuery($namespace, $query, $readPreference);
+ if ($session = $this->getSession()) {
+ $writeResult = $this->mongo->executeBulkWrite($namespace, $bulk, [
+ 'session' => $session,
+ 'writeConcern' => is_null($writeConcern) ? new WriteConcern(1) : $writeConcern,
+ ]);
+ } else {
+ $writeResult = $this->mongo->executeBulkWrite($namespace, $bulk, $writeConcern);
+ }
+
+ // SQL监控
+ if (!empty($this->config['trigger_sql'])) {
+ $this->trigger();
+ }
+
+ $this->numRows = $writeResult->getMatchedCount();
+
+ if ($query->getOptions('cache')) {
+ // 清理缓存数据
+ $cacheItem = $this->parseCache($query, $query->getOptions('cache'));
+ $key = $cacheItem->getKey();
+ $tag = $cacheItem->getTag();
- $this->debug(false);
+ if (isset($key) && $this->cache->has($key)) {
+ $this->cache->delete($key);
+ } elseif (!empty($tag) && method_exists($this->cache, 'tag')) {
+ $this->cache->tag($tag)->clear();
+ }
+ }
- return $this->getResult($class, $typeMap);
+ return $writeResult;
}
/**
* 执行指令
* @access public
- * @param Command $command 指令
- * @param string $dbName 当前数据库名
- * @param ReadPreference $readPreference readPreference
- * @param string|bool $class 返回的数据集类型
- * @param string|array $typeMap 指定返回的typeMap
- * @return mixed
+ * @param Command $command 指令
+ * @param string $dbName 当前数据库名
+ * @param ReadPreference $readPreference readPreference
+ * @param string|array $typeMap 指定返回的typeMap
+ * @param bool $master 是否主库操作
+ * @return array
* @throws AuthenticationException
* @throws InvalidArgumentException
* @throws ConnectionException
* @throws RuntimeException
*/
- public function command(Command $command, $dbName = '', ReadPreference $readPreference = null, $class = false, $typeMap = null)
+ public function command(Command $command, string $dbName = '', ReadPreference $readPreference = null, $typeMap = null, bool $master = false): array
{
- $this->initConnect(false);
- Db::$queryTimes++;
+ $this->initConnect($master);
+ $this->db->updateQueryTimes();
- $this->debug(true);
+ $this->queryStartTime = microtime(true);
$dbName = $dbName ?: $this->dbName;
- if ($this->config['debug'] && !empty($this->queryStr)) {
+ if (!empty($this->queryStr)) {
$this->queryStr = 'db.' . $this->queryStr;
}
- $this->cursor = $this->mongo->executeCommand($dbName, $command, $readPreference);
-
- $this->debug(false);
+ if ($session = $this->getSession()) {
+ $this->cursor = $this->mongo->executeCommand($dbName, $command, [
+ 'readPreference' => is_null($readPreference) ? new ReadPreference(ReadPreference::RP_PRIMARY) : $readPreference,
+ 'session' => $session,
+ ]);
+ } else {
+ $this->cursor = $this->mongo->executeCommand($dbName, $command, $readPreference);
+ }
- return $this->getResult($class, $typeMap);
+ // SQL监控
+ if (!empty($this->config['trigger_sql'])) {
+ $this->trigger('', $master);
+ }
+ return $this->getResult($typeMap);
}
/**
* 获得数据集
* @access protected
- * @param bool|string $class true 返回Mongo cursor对象 字符串用于指定返回的类名
- * @param string|array $typeMap 指定返回的typeMap
+ * @param string|array $typeMap 指定返回的typeMap
* @return mixed
*/
- protected function getResult($class = '', $typeMap = null)
+ protected function getResult($typeMap = null): array
{
- if (true === $class) {
- return $this->cursor;
- }
-
// 设置结果数据类型
if (is_null($typeMap)) {
$typeMap = $this->typeMap;
@@ -418,74 +491,35 @@ class Mongo
/**
* ObjectID处理
- * @access public
- * @param array $data
+ * @access protected
+ * @param array $data 数据
* @return void
*/
- private function convertObjectID(&$data)
+ protected function convertObjectID(array &$data): void
{
- if (isset($data['_id'])) {
+ if (isset($data['_id']) && is_object($data['_id'])) {
$data['id'] = $data['_id']->__toString();
unset($data['_id']);
}
}
- /**
- * 执行写操作
- * @access public
- * @param string $namespace
- * @param BulkWrite $bulk
- * @param WriteConcern $writeConcern
- *
- * @return WriteResult
- * @throws AuthenticationException
- * @throws InvalidArgumentException
- * @throws ConnectionException
- * @throws RuntimeException
- * @throws BulkWriteException
- */
- public function execute($namespace, BulkWrite $bulk, WriteConcern $writeConcern = null)
- {
- $this->initConnect(true);
- Db::$executeTimes++;
-
- if (false === strpos($namespace, '.')) {
- $namespace = $this->dbName . '.' . $namespace;
- }
-
- if ($this->config['debug'] && !empty($this->queryStr)) {
- // 记录执行指令
- $this->queryStr = 'db' . strstr($namespace, '.') . '.' . $this->queryStr;
- }
-
- $this->debug(true);
-
- $writeResult = $this->mongo->executeBulkWrite($namespace, $bulk, $writeConcern);
-
- $this->debug(false);
-
- $this->numRows = $writeResult->getMatchedCount();
-
- return $writeResult;
- }
-
/**
* 数据库日志记录(仅供参考)
* @access public
- * @param string $type 类型
- * @param mixed $data 数据
- * @param array $options 参数
+ * @param string $type 类型
+ * @param mixed $data 数据
+ * @param array $options 参数
* @return void
*/
- public function log($type, $data, $options = [])
+ public function mongoLog(string $type, $data, array $options = [])
{
- if (!$this->config['debug']) {
+ if (!$this->config['trigger_sql']) {
return;
}
if (is_array($data)) {
array_walk_recursive($data, function (&$value) {
- if ($value instanceof ObjectID || $value instanceof \MongoDB\BSON\ObjectId) {
+ if ($value instanceof ObjectID) {
$value = $value->__toString();
}
});
@@ -502,6 +536,10 @@ class Mongo
$this->queryStr .= '.sort(' . json_encode($options['sort']) . ')';
}
+ if (isset($options['skip'])) {
+ $this->queryStr .= '.skip(' . $options['skip'] . ')';
+ }
+
if (isset($options['limit'])) {
$this->queryStr .= '.limit(' . $options['limit'] . ')';
}
@@ -528,87 +566,11 @@ class Mongo
* @access public
* @return string
*/
- public function getLastSql()
+ public function getLastSql(): string
{
return $this->queryStr;
}
- /**
- * 监听SQL执行
- * @access public
- * @param callable $callback 回调方法
- * @return void
- */
- public function listen($callback)
- {
- self::$event[] = $callback;
- }
-
- /**
- * 触发SQL事件
- * @access protected
- * @param string $sql SQL语句
- * @param float $runtime SQL运行时间
- * @param mixed $options 参数
- * @return bool
- */
- protected function triggerSql($sql, $runtime, $options = [])
- {
- if (!empty(self::$event)) {
- foreach (self::$event as $callback) {
- if (is_callable($callback)) {
- call_user_func_array($callback, [$sql, $runtime, $options]);
- }
- }
- } else {
- // 未注册监听则记录到日志中
- $this->logger('[ SQL ] ' . $sql . ' [ RunTime:' . $runtime . 's ]');
- }
- }
-
- public function logger($log, $type = 'sql')
- {
- $this->config['debug'] && self::$log[] = $log;
- }
-
- public function getSqlLog()
- {
- return self::$log;
- }
-
- /**
- * 数据库调试 记录当前SQL及分析性能
- * @access protected
- * @param boolean $start 调试开始标记 true 开始 false 结束
- * @param string $sql 执行的SQL语句 留空自动获取
- * @return void
- */
- protected function debug($start, $sql = '')
- {
- if (!empty($this->config['debug'])) {
- // 开启数据库调试模式
- if ($start) {
- $this->queryStartTime = microtime(true);
- } else {
- $runtime = number_format((microtime(true) - $this->queryStartTime), 6);
-
- $sql = $sql ?: $this->queryStr;
-
- // SQL监听
- $this->triggerSql($sql, $runtime, $this->options);
- }
- }
- }
-
- /**
- * 释放查询结果
- * @access public
- */
- public function free()
- {
- $this->cursor = null;
- }
-
/**
* 关闭数据库
* @access public
@@ -628,7 +590,7 @@ class Mongo
* @param boolean $master 是否主服务器
* @return void
*/
- protected function initConnect($master = true)
+ protected function initConnect(bool $master = true): void
{
if (!empty($this->config['deploy'])) {
// 采用分布式数据库
@@ -654,15 +616,15 @@ class Mongo
/**
* 连接分布式服务器
* @access protected
- * @param boolean $master 主服务器
+ * @param boolean $master 主服务器
* @return Manager
*/
- protected function multiConnect($master = false)
+ protected function multiConnect(bool $master = false): Manager
{
$config = [];
// 分布式数据库配置解析
foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn'] as $name) {
- $config[$name] = explode(',', $this->config[$name]);
+ $config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
}
// 主服务器序号
@@ -692,7 +654,7 @@ class Mongo
$dbConfig = [];
foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn'] as $name) {
- $dbConfig[$name] = isset($config[$name][$r]) ? $config[$name][$r] : $config[$name][0];
+ $dbConfig[$name] = $config[$name][$r] ?? $config[$name][0];
}
return $this->connect($dbConfig, $r);
@@ -702,22 +664,20 @@ class Mongo
* 创建基于复制集的连接
* @return Manager
*/
- public function replicaSetConnect()
+ public function replicaSetConnect(): Manager
{
$this->dbName = $this->config['database'];
$this->typeMap = $this->config['type_map'];
- if ($this->config['debug']) {
- $startTime = microtime(true);
- }
+ $startTime = microtime(true);
$this->config['params']['replicaSet'] = $this->config['database'];
$manager = new Manager($this->buildUrl(), $this->config['params']);
- if ($this->config['debug']) {
- // 记录数据库连接信息
- $this->logger('[ MongoDB ] ReplicaSet CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $this->config['dsn']);
+ // 记录数据库连接信息
+ if (!empty($config['trigger_sql'])) {
+ $this->trigger('CONNECT:ReplicaSet[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $this->config['dsn']);
}
return $manager;
@@ -727,12 +687,12 @@ class Mongo
* 根据配置信息 生成适用于连接复制集的 URL
* @return string
*/
- private function buildUrl()
+ private function buildUrl(): string
{
$url = 'mongodb://' . ($this->config['username'] ? "{$this->config['username']}" : '') . ($this->config['password'] ? ":{$this->config['password']}@" : '');
- $hostList = explode(',', $this->config['hostname']);
- $portList = explode(',', $this->config['hostport']);
+ $hostList = is_string($this->config['hostname']) ? explode(',', $this->config['hostname']) : $this->config['hostname'];
+ $portList = is_string($this->config['hostport']) ? explode(',', $this->config['hostport']) : $this->config['hostport'];
for ($i = 0; $i < count($hostList); $i++) {
$url = $url . $hostList[$i] . ':' . $portList[0] . ',';
@@ -744,67 +704,68 @@ class Mongo
/**
* 插入记录
* @access public
- * @param Query $query 查询对象
- * @param boolean $replace 是否replace(目前无效)
- * @param boolean $getLastInsID 返回自增主键
- * @return WriteResult
+ * @param BaseQuery $query 查询对象
+ * @param boolean $getLastInsID 返回自增主键
+ * @return mixed
* @throws AuthenticationException
* @throws InvalidArgumentException
* @throws ConnectionException
* @throws RuntimeException
* @throws BulkWriteException
*/
- public function insert(Query $query, $replace = null, $getLastInsID = false)
+ public function insert(BaseQuery $query, bool $getLastInsID = false)
{
// 分析查询表达式
- $options = $query->getOptions();
+ $options = $query->parseOptions();
if (empty($options['data'])) {
throw new Exception('miss data to insert');
}
// 生成bulk对象
- $bulk = $this->builder->insert($query, $replace);
- $writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : null;
- $writeResult = $this->execute($options['table'], $bulk, $writeConcern);
- $result = $writeResult->getInsertedCount();
+ $bulk = $this->builder->insert($query);
+
+ $writeResult = $this->mongoExecute($query, $bulk);
+ $result = $writeResult->getInsertedCount();
if ($result) {
$data = $options['data'];
- $lastInsId = $this->getLastInsID();
+ $lastInsId = $this->getLastInsID($query);
if ($lastInsId) {
- $pk = $query->getPk($options);
+ $pk = $query->getPk();
$data[$pk] = $lastInsId;
}
$query->setOption('data', $data);
- $query->trigger('after_insert');
+ $this->db->trigger('after_insert', $query);
if ($getLastInsID) {
return $lastInsId;
}
}
+
return $result;
}
/**
* 获取最近插入的ID
* @access public
+ * @param BaseQuery $query 查询对象
* @return mixed
*/
- public function getLastInsID($sequence = null)
+ public function getLastInsID(BaseQuery $query)
{
$id = $this->builder->getLastInsID();
if (is_array($id)) {
array_walk($id, function (&$item, $key) {
- if ($item instanceof ObjectID || $item instanceof \MongoDB\BSON\ObjectId) {
+ if ($item instanceof ObjectID) {
$item = $item->__toString();
}
});
- } elseif ($id instanceof ObjectID || $id instanceof \MongoDB\BSON\ObjectId) {
+ } elseif ($id instanceof ObjectID) {
$id = $id->__toString();
}
@@ -814,8 +775,8 @@ class Mongo
/**
* 批量插入记录
* @access public
- * @param Query $query 查询对象
- * @param mixed $dataSet 数据集
+ * @param BaseQuery $query 查询对象
+ * @param array $dataSet 数据集
* @return integer
* @throws AuthenticationException
* @throws InvalidArgumentException
@@ -823,19 +784,19 @@ class Mongo
* @throws RuntimeException
* @throws BulkWriteException
*/
- public function insertAll(Query $query, array $dataSet)
+ public function insertAll(BaseQuery $query, array $dataSet = []): int
{
// 分析查询表达式
- $options = $query->getOptions();
+ $query->parseOptions();
if (!is_array(reset($dataSet))) {
- return false;
+ return 0;
}
// 生成bulkWrite对象
- $bulk = $this->builder->insertAll($query, $dataSet);
- $writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : null;
- $writeResult = $this->execute($options['table'], $bulk, $writeConcern);
+ $bulk = $this->builder->insertAll($query, $dataSet);
+
+ $writeResult = $this->mongoExecute($query, $bulk);
return $writeResult->getInsertedCount();
}
@@ -843,7 +804,7 @@ class Mongo
/**
* 更新记录
* @access public
- * @param Query $query 查询对象
+ * @param BaseQuery $query 查询对象
* @return int
* @throws Exception
* @throws AuthenticationException
@@ -852,70 +813,19 @@ class Mongo
* @throws RuntimeException
* @throws BulkWriteException
*/
- public function update(Query $query)
+ public function update(BaseQuery $query): int
{
- $options = $query->getOptions();
- $data = $options['data'];
-
- if (isset($options['cache']) && is_string($options['cache']['key'])) {
- $key = $options['cache']['key'];
- }
-
- $pk = $query->getPk($options);
-
- if (empty($options['where'])) {
- // 如果存在主键数据 则自动作为更新条件
- if (is_string($pk) && isset($data[$pk])) {
- $where[$pk] = $data[$pk];
- $key = 'mongo:' . $options['table'] . '|' . $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('miss update condition');
- } else {
- $options['where']['$and'] = $where;
- }
- } elseif (!isset($key) && is_string($pk) && isset($options['where']['$and'][$pk])) {
- $key = $this->getCacheKey($options['where']['$and'][$pk], $options);
- }
+ $query->parseOptions();
// 生成bulkWrite对象
- $bulk = $this->builder->update($query);
- $writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : null;
- $writeResult = $this->execute($options['table'], $bulk, $writeConcern);
-
- // 检测缓存
- if ($this->cache && isset($key) && $this->cache->get($key)) {
- // 删除缓存
- $this->cache->rm($key);
- }
+ $bulk = $this->builder->update($query);
+
+ $writeResult = $this->mongoExecute($query, $bulk);
$result = $writeResult->getModifiedCount();
if ($result) {
- if (isset($where[$pk])) {
- $data[$pk] = $where[$pk];
- } elseif (is_string($pk) && isset($key) && strpos($key, '|')) {
- list($a, $val) = explode('|', $key);
- $data[$pk] = $val;
- }
-
- $query->setOption('data', $data);
-
- $query->trigger('after_update');
+ $this->db->trigger('after_update', $query);
}
return $result;
@@ -924,7 +834,7 @@ class Mongo
/**
* 删除记录
* @access public
- * @param Query $query 查询对象
+ * @param BaseQuery $query 查询对象
* @return int
* @throws Exception
* @throws AuthenticationException
@@ -933,85 +843,31 @@ class Mongo
* @throws RuntimeException
* @throws BulkWriteException
*/
- public function delete(Query $query)
+ public function delete(BaseQuery $query): int
{
// 分析查询表达式
- $options = $query->getOptions();
- $pk = $query->getPk($options);
- $data = $options['data'];
-
- if (!is_null($data) && true !== $data) {
- if (!is_array($data)) {
- // 缓存标识
- $key = 'mongo:' . $options['table'] . '|' . $data;
- }
-
- // AR模式分析主键条件
- $query->parsePkWhere($data);
- } elseif (!isset($key) && is_string($pk) && isset($options['where']['$and'][$pk])) {
- $key = $this->getCacheKey($options['where']['$and'][$pk], $options);
- }
-
- if (true !== $data && empty($options['where'])) {
- // 如果不是强制删除且条件为空 不进行删除操作
- throw new Exception('delete without condition');
- }
+ $query->parseOptions();
// 生成bulkWrite对象
$bulk = $this->builder->delete($query);
- $writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : null;
-
// 执行操作
- $writeResult = $this->execute($options['table'], $bulk, $writeConcern);
-
- // 检测缓存
- if ($this->cache && isset($key) && $this->cache->get($key)) {
- // 删除缓存
- $this->cache->rm($key);
- }
+ $writeResult = $this->mongoExecute($query, $bulk);
$result = $writeResult->getDeletedCount();
if ($result) {
- if (!is_array($data) && is_string($pk) && isset($key) && strpos($key, '|')) {
- list($a, $val) = explode('|', $key);
-
- $item[$pk] = $val;
- $data = $item;
- }
-
- $query->setOption('data', $data);
- $query->trigger('after_delete');
+ $this->db->trigger('after_delete', $query);
}
- return $result;
- }
-
- /**
- * 执行查询但只返回Cursor对象
- * @access public
- * @param Query $query 查询对象
- * @return Cursor
- */
- public function getCursor(Query $query)
- {
- // 分析查询表达式
- $options = $query->getOptions();
-
- // 生成MongoQuery对象
- $mongoQuery = $this->builder->select($query);
-
- // 执行查询操作
- $readPreference = isset($options['readPreference']) ? $options['readPreference'] : null;
- return $this->query($options['table'], $mongoQuery, $readPreference, true, $options['typeMap']);
+ return $result;
}
/**
* 查找记录
* @access public
- * @param Query $query 查询对象
- * @return Collection|false|Cursor|string
+ * @param BaseQuery $query 查询对象
+ * @return array
* @throws ModelNotFoundException
* @throws DataNotFoundException
* @throws AuthenticationException
@@ -1019,48 +875,24 @@ class Mongo
* @throws ConnectionException
* @throws RuntimeException
*/
- public function select(Query $query)
+ public function select(BaseQuery $query): array
{
- $options = $query->getOptions();
- $resultSet = false;
- if ($this->cache && !empty($options['cache'])) {
- // 判断查询缓存
- $cache = $options['cache'];
- $key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options));
- $resultSet = $this->cache->get($key);
- }
-
- if (!$resultSet) {
- // 生成MongoQuery对象
- $mongoQuery = $this->builder->select($query);
-
- if ($resultSet = $query->trigger('before_select')) {
- } else {
- // 执行查询操作
- $readPreference = isset($options['readPreference']) ? $options['readPreference'] : null;
-
- $resultSet = $this->query($options['table'], $mongoQuery, $readPreference, $options['fetch_cursor'], $options['typeMap']);
-
- if ($resultSet instanceof Cursor) {
- // 返回MongoDB\Driver\Cursor对象
- return $resultSet;
- }
- }
-
- if (isset($cache)) {
- // 缓存数据集
- $this->cacheData($key, $resultSet, $cache);
- }
+ try {
+ $this->db->trigger('before_select', $query);
+ } catch (DbEventException $e) {
+ return [];
}
- return $resultSet;
+ return $this->mongoQuery($query, function ($query) {
+ return $this->builder->select($query);
+ });
}
/**
* 查找单条记录
* @access public
- * @param Query $query 查询对象
- * @return array|null|Cursor|string|Model
+ * @param BaseQuery $query 查询对象
+ * @return array
* @throws ModelNotFoundException
* @throws DataNotFoundException
* @throws AuthenticationException
@@ -1068,280 +900,136 @@ class Mongo
* @throws ConnectionException
* @throws RuntimeException
*/
- public function find(Query $query)
+ public function find(BaseQuery $query): array
{
- // 分析查询表达式
- $options = $query->getOptions();
- $pk = $query->getPk($options);
- $data = $options['data'];
- if ($this->cache && !empty($options['cache']) && true === $options['cache']['key'] && is_string($pk) && isset($options['where']['$and'][$pk])) {
- $key = $this->getCacheKey($options['where']['$and'][$pk], $options);
- }
-
- $result = false;
- if ($this->cache && !empty($options['cache'])) {
- // 判断查询缓存
- $cache = $options['cache'];
- if (true === $cache['key'] && !is_null($data) && !is_array($data)) {
- $key = 'mongo:' . $options['table'] . '|' . $data;
- } elseif (!isset($key)) {
- $key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options));
- }
- $result = $this->cache->get($key);
+ // 事件回调
+ try {
+ $this->db->trigger('before_find', $query);
+ } catch (DbEventException $e) {
+ return [];
}
- if (false === $result) {
-
- if (is_string($pk)) {
- if (!is_array($data)) {
- if (isset($key) && strpos($key, '|')) {
- list($a, $val) = explode('|', $key);
- $item[$pk] = $val;
- } else {
- $item[$pk] = $data;
- }
- $data = $item;
- }
- }
-
- $query->setOption('data', $data);
- $query->setOption('limit', 1);
-
- // 生成查询对象
- $mongoQuery = $this->builder->select($query);
+ // 执行查询
+ $resultSet = $this->mongoQuery($query, function ($query) {
+ return $this->builder->select($query, true);
+ });
- // 事件回调
- if ($result = $query->trigger('before_find')) {
- } else {
- // 执行查询
- $readPreference = isset($options['readPreference']) ? $options['readPreference'] : null;
- $resultSet = $this->query($options['table'], $mongoQuery, $readPreference, $options['fetch_cursor'], $options['typeMap']);
-
- if ($resultSet instanceof Cursor) {
- // 返回MongoDB\Driver\Cursor对象
- return $resultSet;
- }
-
- $result = isset($resultSet[0]) ? $resultSet[0] : null;
- }
-
- if (isset($cache)) {
- // 缓存数据
- $this->cacheData($key, $result, $cache);
- }
- }
-
- return $result;
+ return $resultSet[0] ?? [];
}
/**
- * 缓存数据
+ * 得到某个字段的值
* @access public
- * @param string $key 缓存标识
- * @param mixed $data 缓存数据
- * @param array $config 缓存参数
+ * @param string $field 字段名
+ * @param mixed $default 默认值
+ * @return mixed
*/
- protected function cacheData($key, $data, $config = [])
+ public function value(BaseQuery $query, string $field, $default = null)
{
- $this->cache->set($key, $data, $config['expire']);
- }
+ $options = $query->parseOptions();
- /**
- * 生成缓存标识
- * @access public
- * @param mixed $value 缓存数据
- * @param array $options 缓存参数
- */
- protected function getCacheKey($value, $options)
- {
- if (is_scalar($value)) {
- $data = $value;
- } elseif (is_array($value) && 'eq' == strtolower($value[0])) {
- $data = $value[1];
+ if (isset($options['projection'])) {
+ $query->removeOption('projection');
}
- if (isset($data)) {
- return 'mongo:' . $options['table'] . '|' . $data;
- } else {
- return md5(serialize($options));
- }
- }
+ $query->setOption('projection', (array) $field);
- /**
- * 获取数据表信息
- * @access public
- * @param string $tableName 数据表名 留空自动获取
- * @param string $fetch 获取信息类型 包括 fields type pk
- * @return mixed
- */
- public function getTableInfo($tableName, $fetch = '')
- {
- if (is_array($tableName)) {
- $tableName = key($tableName) ?: current($tableName);
- }
+ if (!empty($options['cache'])) {
+ $cacheItem = $this->parseCache($query, $options['cache']);
+ $key = $cacheItem->getKey();
- if (strpos($tableName, ',')) {
- // 多表不获取字段信息
- return false;
- } else {
- $tableName = $this->parseSqlTable($tableName);
+ if ($this->cache->has($key)) {
+ return $this->cache->get($key);
+ }
}
- $guid = md5($tableName);
- if (!isset(self::$info[$guid])) {
- $mongoQuery = new MongoQuery([], ['limit' => 1]);
-
- $cursor = $this->query($tableName, $mongoQuery, null, true, ['root' => 'array', 'document' => 'array']);
-
- $resultSet = $cursor->toArray();
- $result = isset($resultSet[0]) ? (array) $resultSet[0] : [];
- $fields = array_keys($result);
- $type = [];
+ $mongoQuery = $this->builder->select($query, true);
- foreach ($result as $key => $val) {
- // 记录字段类型
- $type[$key] = getType($val);
- if ('_id' == $key) {
- $pk = $key;
- }
- }
+ if (isset($options['projection'])) {
+ $query->setOption('projection', $options['projection']);
+ } else {
+ $query->removeOption('projection');
+ }
- if (!isset($pk)) {
- // 设置主键
- $pk = null;
- }
+ // 执行查询操作
+ $resultSet = $this->mongoQuery($query, $mongoQuery);
- $result = ['fields' => $fields, 'type' => $type, 'pk' => $pk];
+ if (!empty($resultSet)) {
+ $data = array_shift($resultSet);
+ $result = $data[$field];
+ } else {
+ $result = false;
+ }
- self::$info[$guid] = $result;
+ if (isset($cacheItem) && false !== $result) {
+ // 缓存数据
+ $cacheItem->set($result);
+ $this->cacheData($cacheItem);
}
- return $fetch ? self::$info[$guid][$fetch] : self::$info[$guid];
+ return false !== $result ? $result : $default;
}
/**
- * 得到某个字段的值
+ * 得到某个列的数组
* @access public
- * @param string $field 字段名
- * @param mixed $default 默认值
- * @return mixed
+ * @param BaseQuery $query
+ * @param string|array $field 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
*/
- public function value(Query $query, $field, $default = null)
+ public function column(BaseQuery $query, $field, string $key = ''): array
{
- $options = $query->getOptions();
- $result = null;
- if ($this->cache && !empty($options['cache'])) {
- // 判断查询缓存
- $cache = $options['cache'];
- $key = is_string($cache['key']) ? $cache['key'] : md5($field . serialize($options));
- $result = $this->cache->get($key);
- }
+ $options = $query->parseOptions();
- if (!$result) {
- if (isset($options['field'])) {
- $query->removeOption('field');
- }
+ if (isset($options['projection'])) {
+ $query->removeOption('projection');
+ }
- $query->setOption('field', $field);
- $query->setOption('limit', 1);
+ if (is_array($field)) {
+ $field = implode(',', $field);
+ }
+ if ($key && '*' != $field) {
+ $projection = $key . ',' . $field;
+ } else {
+ $projection = $field;
+ }
- $mongoQuery = $this->builder->select($query);
+ $query->field($projection);
- // 执行查询操作
- $readPreference = isset($options['readPreference']) ? $options['readPreference'] : null;
- $cursor = $this->query($options['table'], $mongoQuery, $readPreference, true, ['root' => 'array']);
- $resultSet = $cursor->toArray();
- if (!empty($resultSet)) {
- $data = (array) array_shift($resultSet);
- if ($this->getConfig('pk_convert_id')) {
- // 转换ObjectID 字段
- $data['id'] = $data['_id']->__toString();
- }
- $result = $data[$field];
- } else {
- $result = null;
- }
+ if (!empty($options['cache'])) {
+ // 判断查询缓存
+ $cacheItem = $this->parseCache($query, $options['cache']);
+ $key = $cacheItem->getKey();
- if (isset($cache)) {
- // 缓存数据
- $this->cacheData($key, $result, $cache);
+ if ($this->cache->has($key)) {
+ return $this->cache->get($key);
}
}
- return !is_null($result) ? $result : $default;
- }
+ $mongoQuery = $this->builder->select($query);
- /**
- * 得到某个列的数组
- * @access public
- * @param string $field 字段名 多个字段用逗号分隔
- * @param string $key 索引
- * @return array
- */
- public function column(Query $query, $field, $key = '')
- {
- $options = $query->getOptions();
- $result = false;
- if ($this->cache && !empty($options['cache'])) {
- // 判断查询缓存
- $cache = $options['cache'];
- $guid = is_string($cache['key']) ? $cache['key'] : md5($field . serialize($options));
- $result = $this->cache->get($guid);
+ if (isset($options['projection'])) {
+ $query->setOption('projection', $options['projection']);
+ } else {
+ $query->removeOption('projection');
}
- if (!$result) {
- if (isset($options['projection'])) {
- $query->removeOption('projection');
- }
-
- if ($key && '*' != $field) {
- $field = $key . ',' . $field;
- }
-
- if (is_string($field)) {
- $field = array_map('trim', explode(',', $field));
- }
+ // 执行查询操作
+ $resultSet = $this->mongoQuery($query, $mongoQuery);
- $query->field($field);
-
- $mongoQuery = $this->builder->select($query);
- // 执行查询操作
- $readPreference = isset($options['readPreference']) ? $options['readPreference'] : null;
- $cursor = $this->query($options['table'], $mongoQuery, $readPreference, true, ['root' => 'array']);
- $resultSet = $cursor->toArray();
-
- if ($resultSet) {
- $fields = array_keys(get_object_vars($resultSet[0]));
- $count = count($fields);
- $key1 = array_shift($fields);
- $key2 = $fields ? array_shift($fields) : '';
- $key = $key ?: $key1;
-
- foreach ($resultSet as $val) {
- $val = (array) $val;
- if ($this->getConfig('pk_convert_id')) {
- // 转换ObjectID 字段
- $val['id'] = $val['_id']->__toString();
- unset($val['_id']);
- }
- $name = $val[$key];
-
- if (2 == $count) {
- $result[$name] = $val[$key2];
- } elseif (1 == $count) {
- $result[$name] = $val[$key1];
- } else {
- $result[$name] = $val;
- }
- }
- } else {
- $result = [];
- }
+ if (('*' == $field || strpos($field, ',')) && $key) {
+ $result = array_column($resultSet, null, $key);
+ } elseif (!empty($resultSet)) {
+ $result = array_column($resultSet, $field, $key);
+ } else {
+ $result = [];
+ }
- if (isset($cache) && isset($guid)) {
- // 缓存数据
- $this->cacheData($guid, $result, $cache);
- }
+ if (isset($cacheItem)) {
+ // 缓存数据
+ $cacheItem->set($result);
+ $this->cacheData($cacheItem);
}
return $result;
@@ -1350,18 +1038,17 @@ class Mongo
/**
* 执行command
* @access public
- * @param Query $query 查询对象
- * @param string|array|object $command 指令
- * @param mixed $extra 额外参数
- * @param string $db 数据库名
+ * @param BaseQuery $query 查询对象
+ * @param string|array|object $command 指令
+ * @param mixed $extra 额外参数
+ * @param string $db 数据库名
* @return array
*/
- public function cmd(Query $query, $command, $extra = null, $db = null)
+ public function cmd(BaseQuery $query, $command, $extra = null, string $db = ''): array
{
if (is_array($command) || is_object($command)) {
- if ($this->getConfig('debug')) {
- $this->log('cmd', 'cmd', $command);
- }
+
+ $this->mongoLog('cmd', 'cmd', $command);
// 直接创建Command对象
$command = new Command($command);
@@ -1374,94 +1061,115 @@ class Mongo
}
/**
- * 数据库连接参数解析
- * @access private
- * @param mixed $config
+ * 获取数据库字段
+ * @access public
+ * @param mixed $tableName 数据表名
* @return array
*/
- private static function parseConfig($config)
+ public function getTableFields($tableName): array
{
- if (empty($config)) {
- $config = Db::getConfig();
- } elseif (is_string($config) && false === strpos($config, '/')) {
- // 支持读取配置参数
- $config = Db::getConfig($config);
- }
-
- if (is_string($config)) {
- return self::parseDsnConfig($config);
- } else {
- return $config;
- }
+ return [];
}
/**
- * DSN解析
- * 格式: mysql://username:passwd@localhost:3306/DbName?param1=val1¶m2=val2#utf8
- * @access private
- * @param string $dsnStr
- * @return array
+ * 执行数据库事务
+ * @access public
+ * @param callable $callback 数据操作方法回调
+ * @return mixed
+ * @throws PDOException
+ * @throws \Exception
+ * @throws \Throwable
*/
- private static function parseDsnConfig($dsnStr)
+ public function transaction(callable $callback)
{
- $info = parse_url($dsnStr);
-
- if (!$info) {
- return [];
+ $this->startTrans();
+ try {
+ $result = null;
+ if (is_callable($callback)) {
+ $result = call_user_func_array($callback, [$this]);
+ }
+ $this->commit();
+ return $result;
+ } catch (\Exception $e) {
+ $this->rollback();
+ throw $e;
+ } catch (\Throwable $e) {
+ $this->rollback();
+ throw $e;
}
+ }
- $dsn = [
- 'type' => $info['scheme'],
- 'username' => isset($info['user']) ? $info['user'] : '',
- 'password' => isset($info['pass']) ? $info['pass'] : '',
- 'hostname' => isset($info['host']) ? $info['host'] : '',
- 'hostport' => isset($info['port']) ? $info['port'] : '',
- 'database' => !empty($info['path']) ? ltrim($info['path'], '/') : '',
- 'charset' => isset($info['fragment']) ? $info['fragment'] : 'utf8',
- ];
-
- if (isset($info['query'])) {
- parse_str($info['query'], $dsn['params']);
- } else {
- $dsn['params'] = [];
- }
+ /**
+ * 启动事务
+ * @access public
+ * @return void
+ * @throws \PDOException
+ * @throws \Exception
+ */
+ public function startTrans()
+ {
+ $this->initConnect(true);
+ $this->session_uuid = uniqid();
+ $this->sessions[$this->session_uuid] = $this->getMongo()->startSession();
- return $dsn;
+ $this->sessions[$this->session_uuid]->startTransaction([]);
}
/**
- * 获取数据表的主键
+ * 用于非自动提交状态下面的查询提交
* @access public
- * @param string $tableName 数据表名
- * @return string|array
+ * @return void
+ * @throws PDOException
*/
- public function getPk($tableName)
+ public function commit()
{
- return $this->getTableInfo($tableName, 'pk');
+ if ($session = $this->getSession()) {
+ $session->commitTransaction();
+ $this->setLastSession();
+ }
}
- // 获取当前数据表字段信息
- public function getTableFields($tableName)
+ /**
+ * 事务回滚
+ * @access public
+ * @return void
+ * @throws PDOException
+ */
+ public function rollback()
{
- return $this->getTableInfo($tableName, 'fields');
+ if ($session = $this->getSession()) {
+ $session->abortTransaction();
+ $this->setLastSession();
+ }
}
- // 获取当前数据表字段类型
- public function getFieldsType($tableName)
+ /**
+ * 结束当前会话,设置上一个会话为当前会话
+ * @author klinson
+ */
+ protected function setLastSession()
{
- return $this->getTableInfo($tableName, 'type');
+ if ($session = $this->getSession()) {
+ $session->endSession();
+ unset($this->sessions[$this->session_uuid]);
+ if (empty($this->sessions)) {
+ $this->session_uuid = null;
+ } else {
+ end($this->sessions);
+ $this->session_uuid = key($this->sessions);
+ }
+ }
}
/**
- * 析构方法
- * @access public
+ * 获取当前会话
+ * @return \MongoDB\Driver\Session|null
+ * @author klinson
*/
- public function __destruct()
+ public function getSession()
{
- // 释放查询
- $this->free();
-
- // 关闭连接
- $this->close();
+ return ($this->session_uuid && isset($this->sessions[$this->session_uuid]))
+ ? $this->sessions[$this->session_uuid]
+ : null;
}
}
diff --git a/src/db/connector/Mysql.php b/src/db/connector/Mysql.php
index 4d1cf347fc805a3414a55559297b121c87aebe0e..fd9e63a4c7dd591779a5b95aea7fde04d9c2d6ec 100644
--- a/src/db/connector/Mysql.php
+++ b/src/db/connector/Mysql.php
@@ -2,56 +2,32 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db\connector;
use PDO;
-use think\db\Connection;
-use think\db\Query;
+use think\db\PDOConnection;
/**
* mysql数据库驱动
*/
-class Mysql extends Connection
+class Mysql extends PDOConnection
{
- protected $builder = '\\think\\db\\builder\\Mysql';
-
- /**
- * 初始化
- * @access protected
- * @return void
- */
- protected function initialize()
- {
- // Point类型支持
- Query::extend('point', function ($query, $field, $value = null, $fun = 'GeomFromText', $type = 'POINT') {
- if (!is_null($value)) {
- $query->data($field, ['point', $value, $fun, $type]);
- } else {
- if (is_string($field)) {
- $field = explode(',', $field);
- }
- $query->setOption('point', $field);
- }
-
- return $query;
- });
- }
-
/**
* 解析pdo连接的dsn信息
* @access protected
- * @param array $config 连接信息
+ * @param array $config 连接信息
* @return string
*/
- protected function parseDsn($config)
+ protected function parseDsn(array $config): string
{
if (!empty($config['socket'])) {
$dsn = 'mysql:unix_socket=' . $config['socket'];
@@ -72,12 +48,12 @@ class Mysql extends Connection
/**
* 取得数据表的字段信息
* @access public
- * @param string $tableName
+ * @param string $tableName
* @return array
*/
- public function getFields($tableName)
+ public function getFields(string $tableName): array
{
- list($tableName) = explode(' ', $tableName);
+ [$tableName] = explode(' ', $tableName);
if (false === strpos($tableName, '`')) {
if (strpos($tableName, '.')) {
@@ -86,21 +62,23 @@ class Mysql extends Connection
$tableName = '`' . $tableName . '`';
}
- $sql = 'SHOW COLUMNS FROM ' . $tableName;
- $pdo = $this->query($sql, [], false, true);
+ $sql = 'SHOW FULL COLUMNS FROM ' . $tableName;
+ $pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
- if ($result) {
+ if (!empty($result)) {
foreach ($result as $key => $val) {
- $val = array_change_key_case($val);
+ $val = array_change_key_case($val);
+
$info[$val['field']] = [
'name' => $val['field'],
'type' => $val['type'],
- 'notnull' => (bool) ('' === $val['null']), // not null is empty, null is yes
+ 'notnull' => 'NO' == $val['null'],
'default' => $val['default'],
- 'primary' => (strtolower($val['key']) == 'pri'),
- 'autoinc' => (strtolower($val['extra']) == 'auto_increment'),
+ 'primary' => strtolower($val['key']) == 'pri',
+ 'autoinc' => strtolower($val['extra']) == 'auto_increment',
+ 'comment' => $val['comment'],
];
}
}
@@ -111,13 +89,13 @@ class Mysql extends Connection
/**
* 取得数据库的表信息
* @access public
- * @param string $dbName
+ * @param string $dbName
* @return array
*/
- public function getTables($dbName = '')
+ public function getTables(string $dbName = ''): array
{
$sql = !empty($dbName) ? 'SHOW TABLES FROM ' . $dbName : 'SHOW TABLES ';
- $pdo = $this->query($sql, [], false, true);
+ $pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
@@ -128,28 +106,7 @@ class Mysql extends Connection
return $info;
}
- /**
- * SQL性能分析
- * @access protected
- * @param string $sql
- * @return array
- */
- protected function getExplain($sql)
- {
- $pdo = $this->linkID->query("EXPLAIN " . $sql);
- $result = $pdo->fetch(PDO::FETCH_ASSOC);
- $result = array_change_key_case($result);
-
- if (isset($result['extra'])) {
- if (strpos($result['extra'], 'filesort') || strpos($result['extra'], 'temporary')) {
- $this->log('SQL:' . $this->queryStr . '[' . $result['extra'] . ']', 'warn');
- }
- }
-
- return $result;
- }
-
- protected function supportSavepoint()
+ protected function supportSavepoint(): bool
{
return true;
}
@@ -160,14 +117,10 @@ class Mysql extends Connection
* @param string $xid XA事务id
* @return void
*/
- public function startTransXa($xid)
+ public function startTransXa(string $xid): void
{
$this->initConnect(true);
- if (!$this->linkID) {
- return false;
- }
-
- $this->execute("XA START '$xid'");
+ $this->linkID->exec("XA START '$xid'");
}
/**
@@ -176,11 +129,11 @@ class Mysql extends Connection
* @param string $xid XA事务id
* @return void
*/
- public function prepareXa($xid)
+ public function prepareXa(string $xid): void
{
$this->initConnect(true);
- $this->execute("XA END '$xid'");
- $this->execute("XA PREPARE '$xid'");
+ $this->linkID->exec("XA END '$xid'");
+ $this->linkID->exec("XA PREPARE '$xid'");
}
/**
@@ -189,10 +142,10 @@ class Mysql extends Connection
* @param string $xid XA事务id
* @return void
*/
- public function commitXa($xid)
+ public function commitXa(string $xid): void
{
$this->initConnect(true);
- $this->execute("XA COMMIT '$xid'");
+ $this->linkID->exec("XA COMMIT '$xid'");
}
/**
@@ -201,9 +154,9 @@ class Mysql extends Connection
* @param string $xid XA事务id
* @return void
*/
- public function rollbackXa($xid)
+ public function rollbackXa(string $xid): void
{
$this->initConnect(true);
- $this->execute("XA ROLLBACK '$xid'");
+ $this->linkID->exec("XA ROLLBACK '$xid'");
}
}
diff --git a/src/db/connector/Oracle.php b/src/db/connector/Oracle.php
index fb971864f86c63f6a5877cb8370214f37c55c1ad..c8e957a01489beba326651eeb266c3c9b5180c02 100644
--- a/src/db/connector/Oracle.php
+++ b/src/db/connector/Oracle.php
@@ -10,12 +10,13 @@
namespace think\db\connector;
use PDO;
-use think\db\Connection;
+use think\db\BaseQuery;
+use think\db\PDOConnection;
/**
* Oracle数据库驱动
*/
-class Oracle extends Connection
+class Oracle extends PDOConnection
{
/**
* 解析pdo连接的dsn信息
@@ -23,17 +24,21 @@ class Oracle extends Connection
* @param array $config 连接信息
* @return string
*/
- protected function parseDsn($config)
+ protected function parseDsn(array $config): string
{
$dsn = 'oci:dbname=';
+
if (!empty($config['hostname'])) {
// Oracle Instant Client
$dsn .= '//' . $config['hostname'] . ($config['hostport'] ? ':' . $config['hostport'] : '') . '/';
}
+
$dsn .= $config['database'];
+
if (!empty($config['charset'])) {
$dsn .= ';charset=' . $config['charset'];
}
+
return $dsn;
}
@@ -43,17 +48,19 @@ class Oracle extends Connection
* @param string $tableName
* @return array
*/
- public function getFields($tableName)
+ public function getFields(string $tableName): array
{
- $this->initConnect(true);
- list($tableName) = explode(' ', $tableName);
- $sql = "select a.column_name,data_type,DECODE (nullable, 'Y', 0, 1) notnull,data_default, DECODE (A .column_name,b.column_name,1,0) pk from all_tab_columns a,(select column_name from all_constraints c, all_cons_columns col where c.constraint_name = col.constraint_name and c.constraint_type = 'P' and c.table_name = '" . strtoupper($tableName) . "' ) b where table_name = '" . strtoupper($tableName) . "' and a.column_name = b.column_name (+)";
- $pdo = $this->linkID->query($sql);
- $result = $pdo->fetchAll(PDO::FETCH_ASSOC);
- $info = [];
+ [$tableName] = explode(' ', $tableName);
+ $sql = "select a.column_name,data_type,DECODE (nullable, 'Y', 0, 1) notnull,data_default, DECODE (A .column_name,b.column_name,1,0) pk from all_tab_columns a,(select column_name from all_constraints c, all_cons_columns col where c.constraint_name = col.constraint_name and c.constraint_type = 'P' and c.table_name = '" . $tableName . "' ) b where table_name = '" . $tableName . "' and a.column_name = b.column_name (+)";
+
+ $pdo = $this->getPDOStatement($sql);
+ $result = $pdo->fetchAll(PDO::FETCH_ASSOC);
+ $info = [];
+
if ($result) {
foreach ($result as $key => $val) {
- $val = array_change_key_case($val);
+ $val = array_change_key_case($val);
+
$info[$val['column_name']] = [
'name' => $val['column_name'],
'type' => $val['data_type'],
@@ -64,6 +71,7 @@ class Oracle extends Connection
];
}
}
+
return $this->fieldCase($info);
}
@@ -73,45 +81,38 @@ class Oracle extends Connection
* @param string $dbName
* @return array
*/
- public function getTables($dbName = '')
+ public function getTables(string $dbName = ''): array
{
- $pdo = $this->linkID->query("select table_name from all_tables");
+ $sql = 'select table_name from all_tables';
+ $pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
+
foreach ($result as $key => $val) {
$info[$key] = current($val);
}
+
return $info;
}
/**
* 获取最近插入的ID
* @access public
- * @param string $sequence 自增序列名
- * @return string
+ * @param BaseQuery $query 查询对象
+ * @param string|null $sequence 自增序列名
+ * @return mixed
*/
- public function getLastInsID($sequence = null)
+ public function getLastInsID(BaseQuery $query, string $sequence = null)
{
- if ($sequence === null) {
- return '';
+ if(!is_null($sequence)) {
+ $pdo = $this->linkID->query("select {$sequence}.currval as id from dual");
+ $result = $pdo->fetchColumn();
}
- $pdo = $this->linkID->query("select {$sequence}.currval as id from dual");
- $result = $pdo->fetchColumn();
- return $result;
- }
- /**
- * SQL性能分析
- * @access protected
- * @param string $sql
- * @return array
- */
- protected function getExplain($sql)
- {
- return [];
+ return $result ?? null;
}
- protected function supportSavepoint()
+ protected function supportSavepoint(): bool
{
return true;
}
diff --git a/src/db/connector/Pgsql.php b/src/db/connector/Pgsql.php
index ab1912294be3e9be565b5868084286947b23f069..fec8f8401314a5520bad38f280eb6100cae0eee4 100644
--- a/src/db/connector/Pgsql.php
+++ b/src/db/connector/Pgsql.php
@@ -2,7 +2,7 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
@@ -12,16 +12,18 @@
namespace think\db\connector;
use PDO;
-use think\db\Connection;
+use think\db\PDOConnection;
/**
* Pgsql数据库驱动
*/
-class Pgsql extends Connection
+class Pgsql extends PDOConnection
{
- protected $builder = '\\think\\db\\builder\\Pgsql';
- // PDO连接参数
+ /**
+ * 默认PDO连接参数
+ * @var array
+ */
protected $params = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
@@ -32,10 +34,10 @@ class Pgsql extends Connection
/**
* 解析pdo连接的dsn信息
* @access protected
- * @param array $config 连接信息
+ * @param array $config 连接信息
* @return string
*/
- protected function parseDsn($config)
+ protected function parseDsn(array $config): string
{
$dsn = 'pgsql:dbname=' . $config['database'] . ';host=' . $config['hostname'];
@@ -49,21 +51,22 @@ class Pgsql extends Connection
/**
* 取得数据表的字段信息
* @access public
- * @param string $tableName
+ * @param string $tableName
* @return array
*/
- public function getFields($tableName)
+ public function getFields(string $tableName): array
{
- list($tableName) = explode(' ', $tableName);
- $sql = 'select fields_name as "field",fields_type as "type",fields_not_null as "null",fields_key_name as "key",fields_default as "default",fields_default as "extra" from table_msg(\'' . $tableName . '\');';
+ [$tableName] = explode(' ', $tableName);
+ $sql = 'select fields_name as "field",fields_type as "type",fields_not_null as "null",fields_key_name as "key",fields_default as "default",fields_default as "extra" from table_msg(\'' . $tableName . '\');';
- $pdo = $this->query($sql, [], false, true);
+ $pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
- if ($result) {
+ if (!empty($result)) {
foreach ($result as $key => $val) {
- $val = array_change_key_case($val);
+ $val = array_change_key_case($val);
+
$info[$val['field']] = [
'name' => $val['field'],
'type' => $val['type'],
@@ -81,13 +84,13 @@ class Pgsql extends Connection
/**
* 取得数据库的表信息
* @access public
- * @param string $dbName
+ * @param string $dbName
* @return array
*/
- public function getTables($dbName = '')
+ public function getTables(string $dbName = ''): array
{
$sql = "select tablename as Tables_in_test from pg_tables where schemaname ='public'";
- $pdo = $this->query($sql, [], false, true);
+ $pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
@@ -98,18 +101,7 @@ class Pgsql extends Connection
return $info;
}
- /**
- * SQL性能分析
- * @access protected
- * @param string $sql
- * @return array
- */
- protected function getExplain($sql)
- {
- return [];
- }
-
- protected function supportSavepoint()
+ protected function supportSavepoint(): bool
{
return true;
}
diff --git a/src/db/connector/Sqlite.php b/src/db/connector/Sqlite.php
index f6779d9567f09bd3d9ffe6dc7decb36788299252..3e42a9092e8f246c1f0477ac260a9eee2d32322e 100644
--- a/src/db/connector/Sqlite.php
+++ b/src/db/connector/Sqlite.php
@@ -2,7 +2,7 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
@@ -12,23 +12,21 @@
namespace think\db\connector;
use PDO;
-use think\db\Connection;
+use think\db\PDOConnection;
/**
* Sqlite数据库驱动
*/
-class Sqlite extends Connection
+class Sqlite extends PDOConnection
{
- protected $builder = '\\think\\db\\builder\\Sqlite';
-
/**
* 解析pdo连接的dsn信息
* @access protected
- * @param array $config 连接信息
+ * @param array $config 连接信息
* @return string
*/
- protected function parseDsn($config)
+ protected function parseDsn(array $config): string
{
$dsn = 'sqlite:' . $config['database'];
@@ -38,21 +36,22 @@ class Sqlite extends Connection
/**
* 取得数据表的字段信息
* @access public
- * @param string $tableName
+ * @param string $tableName
* @return array
*/
- public function getFields($tableName)
+ public function getFields(string $tableName): array
{
- list($tableName) = explode(' ', $tableName);
- $sql = 'PRAGMA table_info( ' . $tableName . ' )';
+ [$tableName] = explode(' ', $tableName);
+ $sql = 'PRAGMA table_info( \'' . $tableName . '\' )';
- $pdo = $this->query($sql, [], false, true);
+ $pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
- if ($result) {
+ if (!empty($result)) {
foreach ($result as $key => $val) {
- $val = array_change_key_case($val);
+ $val = array_change_key_case($val);
+
$info[$val['name']] = [
'name' => $val['name'],
'type' => $val['type'],
@@ -70,16 +69,16 @@ class Sqlite extends Connection
/**
* 取得数据库的表信息
* @access public
- * @param string $dbName
+ * @param string $dbName
* @return array
*/
- public function getTables($dbName = '')
+ public function getTables(string $dbName = ''): array
{
$sql = "SELECT name FROM sqlite_master WHERE type='table' "
. "UNION ALL SELECT name FROM sqlite_temp_master "
. "WHERE type='table' ORDER BY name";
- $pdo = $this->query($sql, [], false, true);
+ $pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
@@ -90,18 +89,7 @@ class Sqlite extends Connection
return $info;
}
- /**
- * SQL性能分析
- * @access protected
- * @param string $sql
- * @return array
- */
- protected function getExplain($sql)
- {
- return [];
- }
-
- protected function supportSavepoint()
+ protected function supportSavepoint(): bool
{
return true;
}
diff --git a/src/db/connector/Sqlsrv.php b/src/db/connector/Sqlsrv.php
index b56654ab80288380df01ba95eae70b45a140ca24..aee3343c93614db26623f340e4a1fd6bfd032a5f 100644
--- a/src/db/connector/Sqlsrv.php
+++ b/src/db/connector/Sqlsrv.php
@@ -12,14 +12,17 @@
namespace think\db\connector;
use PDO;
-use think\db\Connection;
+use think\db\PDOConnection;
/**
* Sqlsrv数据库驱动
*/
-class Sqlsrv extends Connection
+class Sqlsrv extends PDOConnection
{
- // PDO连接参数
+ /**
+ * 默认PDO连接参数
+ * @var array
+ */
protected $params = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
@@ -27,15 +30,13 @@ class Sqlsrv extends Connection
PDO::ATTR_STRINGIFY_FETCHES => false,
];
- protected $builder = '\\think\\db\\builder\\Sqlsrv';
-
/**
* 解析pdo连接的dsn信息
* @access protected
- * @param array $config 连接信息
+ * @param array $config 连接信息
* @return string
*/
- protected function parseDsn($config)
+ protected function parseDsn(array $config): string
{
$dsn = 'sqlsrv:Database=' . $config['database'] . ';Server=' . $config['hostname'];
@@ -43,21 +44,23 @@ class Sqlsrv extends Connection
$dsn .= ',' . $config['hostport'];
}
+ if (!empty($config['trust_server_certificate'])) {
+ $dsn .= ';TrustServerCertificate=' . $config['trust_server_certificate'];
+ }
+
return $dsn;
}
/**
* 取得数据表的字段信息
* @access public
- * @param string $tableName
+ * @param string $tableName
* @return array
*/
- public function getFields($tableName)
+ public function getFields(string $tableName): array
{
- list($tableName) = explode(' ', $tableName);
- $tableNames = explode('.', $tableName);
- $tableName = isset($tableNames[1]) ? $tableNames[1] : $tableNames[0];
-
+ [$tableName] = explode(' ', $tableName);
+ strpos($tableName,'.') && $tableName = substr($tableName,strpos($tableName,'.') + 1);
$sql = "SELECT column_name, data_type, column_default, is_nullable
FROM information_schema.tables AS t
JOIN information_schema.columns AS c
@@ -66,13 +69,14 @@ class Sqlsrv extends Connection
AND t.table_name = c.table_name
WHERE t.table_name = '$tableName'";
- $pdo = $this->query($sql, [], false, true);
+ $pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
- if ($result) {
+ if (!empty($result)) {
foreach ($result as $key => $val) {
- $val = array_change_key_case($val);
+ $val = array_change_key_case($val);
+
$info[$val['column_name']] = [
'name' => $val['column_name'],
'type' => $val['data_type'],
@@ -84,16 +88,8 @@ class Sqlsrv extends Connection
}
}
- $sql = "SELECT column_name FROM information_schema.key_column_usage WHERE table_name='$tableName'";
-
- // 调试开始
- $this->debug(true);
-
- $pdo = $this->linkID->query($sql);
-
- // 调试结束
- $this->debug(false, $sql);
-
+ $sql = "SELECT column_name FROM information_schema.key_column_usage WHERE table_name='$tableName'";
+ $pdo = $this->linkID->query($sql);
$result = $pdo->fetch(PDO::FETCH_ASSOC);
if ($result) {
@@ -106,17 +102,17 @@ class Sqlsrv extends Connection
/**
* 取得数据表的字段信息
* @access public
- * @param string $dbName
+ * @param string $dbName
* @return array
*/
- public function getTables($dbName = '')
+ public function getTables(string $dbName = ''): array
{
$sql = "SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
";
- $pdo = $this->query($sql, [], false, true);
+ $pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
@@ -127,14 +123,4 @@ class Sqlsrv extends Connection
return $info;
}
- /**
- * SQL性能分析
- * @access protected
- * @param string $sql
- * @return array
- */
- protected function getExplain($sql)
- {
- return [];
- }
}
diff --git a/src/db/exception/BindParamException.php b/src/db/exception/BindParamException.php
index 274fe0afea1e9f969ff143d643801808f62b194e..08bb38804dfa6f3ee21eebf74a8912c310642ee7 100644
--- a/src/db/exception/BindParamException.php
+++ b/src/db/exception/BindParamException.php
@@ -2,12 +2,13 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db\exception;
@@ -19,13 +20,14 @@ class BindParamException extends DbException
/**
* BindParamException constructor.
- * @param string $message
- * @param array $config
- * @param string $sql
- * @param array $bind
- * @param int $code
+ * @access public
+ * @param string $message
+ * @param array $config
+ * @param string $sql
+ * @param array $bind
+ * @param int $code
*/
- public function __construct($message, $config, $sql, $bind, $code = 10502)
+ public function __construct(string $message, array $config, string $sql, array $bind, int $code = 10502)
{
$this->setData('Bind Param', $bind);
parent::__construct($message, $config, $sql, $code);
diff --git a/src/db/exception/DataNotFoundException.php b/src/db/exception/DataNotFoundException.php
index f987aabcd058e31df16f86971f4f75270f128b40..d10dd4330ab900c9588c8de33d4280a9b785ff3f 100644
--- a/src/db/exception/DataNotFoundException.php
+++ b/src/db/exception/DataNotFoundException.php
@@ -2,12 +2,13 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db\exception;
@@ -17,11 +18,12 @@ class DataNotFoundException extends DbException
/**
* DbException constructor.
- * @param string $message
- * @param string $table
- * @param array $config
+ * @access public
+ * @param string $message
+ * @param string $table
+ * @param array $config
*/
- public function __construct($message, $table = '', array $config = [])
+ public function __construct(string $message, string $table = '', array $config = [])
{
$this->message = $message;
$this->table = $table;
diff --git a/src/db/exception/ClassNotFoundException.php b/src/db/exception/DbEventException.php
similarity index 63%
rename from src/db/exception/ClassNotFoundException.php
rename to src/db/exception/DbEventException.php
index cd70dae1146787d8454d2c4f76b09835b0fae603..394a1e82fb7c5ba6ff3cb748329fabc9b89a72e0 100644
--- a/src/db/exception/ClassNotFoundException.php
+++ b/src/db/exception/DbEventException.php
@@ -11,22 +11,9 @@
namespace think\db\exception;
-class ClassNotFoundException extends \RuntimeException
+/**
+ * Db事件异常
+ */
+class DbEventException extends DbException
{
- protected $class;
- public function __construct($message, $class = '')
- {
- $this->message = $message;
- $this->class = $class;
- }
-
- /**
- * 获取类名
- * @access public
- * @return string
- */
- public function getClass()
- {
- return $this->class;
- }
}
diff --git a/src/db/exception/DbException.php b/src/db/exception/DbException.php
index f2e99cbcc5c21a64337fd318f9ce851ba4a3d1eb..f68b21c025395531769aadc09c7294194476c3de 100644
--- a/src/db/exception/DbException.php
+++ b/src/db/exception/DbException.php
@@ -2,12 +2,13 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db\exception;
@@ -20,12 +21,13 @@ class DbException extends Exception
{
/**
* DbException constructor.
- * @param string $message
- * @param array $config
- * @param string $sql
- * @param int $code
+ * @access public
+ * @param string $message
+ * @param array $config
+ * @param string $sql
+ * @param int $code
*/
- public function __construct($message, array $config, $sql, $code = 10500)
+ public function __construct(string $message, array $config = [], string $sql = '', int $code = 10500)
{
$this->message = $message;
$this->code = $code;
@@ -39,5 +41,4 @@ class DbException extends Exception
unset($config['username'], $config['password']);
$this->setData('Database Config', $config);
}
-
}
diff --git a/src/db/exception/InvalidArgumentException.php b/src/db/exception/InvalidArgumentException.php
new file mode 100644
index 0000000000000000000000000000000000000000..047e45e9b89748982776d37ac53b8ed9f465cba1
--- /dev/null
+++ b/src/db/exception/InvalidArgumentException.php
@@ -0,0 +1,21 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+namespace think\db\exception;
+
+use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInvalidArgumentInterface;
+
+/**
+ * 非法数据异常
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements SimpleCacheInvalidArgumentInterface
+{
+}
diff --git a/src/db/exception/ModelEventException.php b/src/db/exception/ModelEventException.php
new file mode 100644
index 0000000000000000000000000000000000000000..767bc1a9f899700853725d7c81e0de85811048b4
--- /dev/null
+++ b/src/db/exception/ModelEventException.php
@@ -0,0 +1,19 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\db\exception;
+
+/**
+ * 模型事件异常
+ */
+class ModelEventException extends DbException
+{
+}
diff --git a/src/db/exception/ModelNotFoundException.php b/src/db/exception/ModelNotFoundException.php
index 43505a3b97079a46fca3f3c323e253b4be4ce0d7..84a152579b7804304090a8753810aa62a2b24453 100644
--- a/src/db/exception/ModelNotFoundException.php
+++ b/src/db/exception/ModelNotFoundException.php
@@ -2,12 +2,13 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db\exception;
@@ -17,10 +18,12 @@ class ModelNotFoundException extends DbException
/**
* 构造方法
- * @param string $message
- * @param string $model
+ * @access public
+ * @param string $message
+ * @param string $model
+ * @param array $config
*/
- public function __construct($message, $model = '', array $config = [])
+ public function __construct(string $message, string $model = '', array $config = [])
{
$this->message = $message;
$this->model = $model;
diff --git a/src/db/exception/PDOException.php b/src/db/exception/PDOException.php
index f23d67e0ed8308255edf098c6b7b358b2c5a406f..efe78b9615be12a7938dcacf992ed6a3e24f36e1 100644
--- a/src/db/exception/PDOException.php
+++ b/src/db/exception/PDOException.php
@@ -2,12 +2,13 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\db\exception;
@@ -19,21 +20,25 @@ class PDOException extends DbException
{
/**
* PDOException constructor.
- * @param \PDOException $exception
- * @param array $config
- * @param string $sql
- * @param int $code
+ * @access public
+ * @param \PDOException $exception
+ * @param array $config
+ * @param string $sql
+ * @param int $code
*/
- public function __construct(\PDOException $exception, array $config, $sql, $code = 10501)
+ public function __construct(\PDOException $exception, array $config = [], string $sql = '', int $code = 10501)
{
- $error = $exception->errorInfo;
+ $error = $exception->errorInfo;
+ $message = $exception->getMessage();
- $this->setData('PDO Error Info', [
- 'SQLSTATE' => $error[0],
- 'Driver Error Code' => isset($error[1]) ? $error[1] : 0,
- 'Driver Error Message' => isset($error[2]) ? $error[2] : '',
- ]);
+ if (!empty($error)) {
+ $this->setData('PDO Error Info', [
+ 'SQLSTATE' => $error[0],
+ 'Driver Error Code' => isset($error[1]) ? $error[1] : 0,
+ 'Driver Error Message' => isset($error[2]) ? $error[2] : '',
+ ]);
+ }
- parent::__construct($exception->getMessage(), $config, $sql, $code);
+ parent::__construct($message, $config, $sql, $code);
}
}
diff --git a/src/facade/Db.php b/src/facade/Db.php
new file mode 100644
index 0000000000000000000000000000000000000000..b0296c69bf74b6a14357dd30d2017ebab4200fbd
--- /dev/null
+++ b/src/facade/Db.php
@@ -0,0 +1,31 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\DbManager
+ * @mixin \think\DbManager
+ */
+class Db extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'think\DbManager';
+ }
+}
diff --git a/src/model/Collection.php b/src/model/Collection.php
index 2054acc85314f8f7b535dc7913699c219543e27c..079e8565505285aedb4a9617d9775879054df451 100644
--- a/src/model/Collection.php
+++ b/src/model/Collection.php
@@ -2,46 +2,92 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: zhangyajun <448901948@qq.com>
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model;
use think\Collection as BaseCollection;
use think\Model;
+use think\Paginator;
+/**
+ * 模型数据集类
+ *
+ * @template TKey of array-key
+ * @template TModel of \think\Model
+ *
+ * @extends BaseCollection
+ */
class Collection extends BaseCollection
{
/**
* 延迟预载入关联查询
* @access public
- * @param mixed $relation 关联
+ * @param array|string $relation 关联
+ * @param mixed $cache 关联缓存
* @return $this
*/
- public function load($relation)
+ public function load($relation, $cache = false)
{
- $item = current($this->items);
- $item->eagerlyResultSet($this->items, $relation);
+ if (!$this->isEmpty()) {
+ $item = current($this->items);
+ $item->eagerlyResultSet($this->items, (array) $relation, [], false, $cache);
+ }
return $this;
}
+ /**
+ * 删除数据集的数据
+ * @access public
+ * @return bool
+ */
+ public function delete(): bool
+ {
+ $this->each(function (Model $model) {
+ $model->delete();
+ });
+
+ return true;
+ }
+
+ /**
+ * 更新数据
+ * @access public
+ * @param array $data 数据数组
+ * @param array $allowField 允许字段
+ * @return bool
+ */
+ public function update(array $data, array $allowField = []): bool
+ {
+ $this->each(function (Model $model) use ($data, $allowField) {
+ if (!empty($allowField)) {
+ $model->allowField($allowField);
+ }
+
+ $model->save($data);
+ });
+
+ return true;
+ }
+
/**
* 设置需要隐藏的输出属性
* @access public
- * @param array $hidden 属性列表
- * @param bool $override 是否覆盖
+ * @param array $hidden 属性列表
+ * @param bool $merge 是否合并
* @return $this
*/
- public function hidden($hidden = [], $override = false)
+ public function hidden(array $hidden, bool $merge = false)
{
- $this->each(function ($model) use ($hidden, $override) {
- /** @var Model $model */
- $model->hidden($hidden, $override);
+ $this->each(function (Model $model) use ($hidden, $merge) {
+ $model->hidden($hidden, $merge);
});
return $this;
@@ -49,15 +95,15 @@ class Collection extends BaseCollection
/**
* 设置需要输出的属性
- * @param array $visible
- * @param bool $override 是否覆盖
+ * @access public
+ * @param array $visible
+ * @param bool $merge 是否合并
* @return $this
*/
- public function visible($visible = [], $override = false)
+ public function visible(array $visible, bool $merge = false)
{
- $this->each(function ($model) use ($visible, $override) {
- /** @var Model $model */
- $model->visible($visible, $override);
+ $this->each(function (Model $model) use ($visible, $merge) {
+ $model->visible($visible, $merge);
});
return $this;
@@ -66,15 +112,44 @@ class Collection extends BaseCollection
/**
* 设置需要追加的输出属性
* @access public
- * @param array $append 属性列表
- * @param bool $override 是否覆盖
+ * @param array $append 属性列表
+ * @param bool $merge 是否合并
+ * @return $this
+ */
+ public function append(array $append, bool $merge = false)
+ {
+ $this->each(function (Model $model) use ($append, $merge) {
+ $model->append($append, $merge);
+ });
+
+ return $this;
+ }
+
+ /**
+ * 设置模型输出场景
+ * @access public
+ * @param string $scene 场景名称
+ * @return $this
+ */
+ public function scene(string $scene)
+ {
+ $this->each(function (Model $model) use ($scene) {
+ $model->scene($scene);
+ });
+
+ return $this;
+ }
+
+ /**
+ * 设置父模型
+ * @access public
+ * @param Model $parent 父模型
* @return $this
*/
- public function append($append = [], $override = false)
+ public function setParent(Model $parent)
{
- $this->each(function ($model) use ($append, $override) {
- /** @var Model $model */
- $model && $model->append($append, $override);
+ $this->each(function (Model $model) use ($parent) {
+ $model->setParent($parent);
});
return $this;
@@ -89,12 +164,110 @@ class Collection extends BaseCollection
*/
public function withAttr($name, $callback = null)
{
- $this->each(function ($model) use ($name, $callback) {
- /** @var Model $model */
- $model && $model->withAttribute($name, $callback);
+ $this->each(function (Model $model) use ($name, $callback) {
+ $model->withAttr($name, $callback);
+ });
+
+ return $this;
+ }
+
+ /**
+ * 绑定(一对一)关联属性到当前模型
+ * @access protected
+ * @param string $relation 关联名称
+ * @param array $attrs 绑定属性
+ * @return $this
+ * @throws Exception
+ */
+ public function bindAttr(string $relation, array $attrs = [])
+ {
+ $this->each(function (Model $model) use ($relation, $attrs) {
+ $model->bindAttr($relation, $attrs);
});
return $this;
}
+ /**
+ * 按指定键整理数据
+ *
+ * @access public
+ * @param mixed $items 数据
+ * @param string|null $indexKey 键名
+ * @return array
+ */
+ public function dictionary($items = null, string &$indexKey = null)
+ {
+ if ($items instanceof self || $items instanceof Paginator) {
+ $items = $items->all();
+ }
+
+ $items = is_null($items) ? $this->items : $items;
+
+ if ($items && empty($indexKey)) {
+ $indexKey = $items[0]->getPk();
+ }
+
+ if (isset($indexKey) && is_string($indexKey)) {
+ return array_column($items, null, $indexKey);
+ }
+
+ return $items;
+ }
+
+ /**
+ * 比较数据集,返回差集
+ *
+ * @access public
+ * @param mixed $items 数据
+ * @param string|null $indexKey 指定比较的键名
+ * @return static
+ */
+ public function diff($items, string $indexKey = null)
+ {
+ if ($this->isEmpty()) {
+ return new static($items);
+ }
+
+ $diff = [];
+ $dictionary = $this->dictionary($items, $indexKey);
+
+ if (is_string($indexKey)) {
+ foreach ($this->items as $item) {
+ if (!isset($dictionary[$item[$indexKey]])) {
+ $diff[] = $item;
+ }
+ }
+ }
+
+ return new static($diff);
+ }
+
+ /**
+ * 比较数据集,返回交集
+ *
+ * @access public
+ * @param mixed $items 数据
+ * @param string|null $indexKey 指定比较的键名
+ * @return static
+ */
+ public function intersect($items, string $indexKey = null)
+ {
+ if ($this->isEmpty()) {
+ return new static([]);
+ }
+
+ $intersect = [];
+ $dictionary = $this->dictionary($items, $indexKey);
+
+ if (is_string($indexKey)) {
+ foreach ($this->items as $item) {
+ if (isset($dictionary[$item[$indexKey]])) {
+ $intersect[] = $item;
+ }
+ }
+ }
+
+ return new static($intersect);
+ }
}
diff --git a/src/model/Pivot.php b/src/model/Pivot.php
index 3efb718583b99e65263ec5c32cac0f599d2f352b..ac16d9e0bc368e775991006f5a7cc75612b85ec4 100644
--- a/src/model/Pivot.php
+++ b/src/model/Pivot.php
@@ -2,33 +2,44 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model;
use think\Model;
+/**
+ * 多对多中间表模型类
+ */
class Pivot extends Model
{
- /** @var Model */
+ /**
+ * 父模型
+ * @var Model
+ */
public $parent;
+ /**
+ * 是否时间自动写入
+ * @var bool
+ */
protected $autoWriteTimestamp = false;
/**
* 架构函数
* @access public
- * @param Model $parent 上级模型
- * @param array|object $data 数据
- * @param string $table 中间数据表名
+ * @param array $data 数据
+ * @param Model|null $parent 上级模型
+ * @param string $table 中间数据表名
*/
- public function __construct(Model $parent = null, $data = [], $table = '')
+ public function __construct(array $data = [], Model $parent = null, string $table = '')
{
$this->parent = $parent;
@@ -37,9 +48,23 @@ class Pivot extends Model
}
parent::__construct($data);
-
- // 当前类名
- $this->class = $this->name;
}
+ /**
+ * 创建新的模型实例
+ * @access public
+ * @param array $data 数据
+ * @param mixed $where 更新条件
+ * @param array $options 参数
+ * @return Model
+ */
+ public function newInstance(array $data = [], $where = null, array $options = []): Model
+ {
+ $model = parent::newInstance($data, $where, $options);
+
+ $model->parent = $this->parent;
+ $model->name = $this->name;
+
+ return $model;
+ }
}
diff --git a/src/model/Relation.php b/src/model/Relation.php
index af1fe276ce21fba4479d1d30c2e5928bcba2c57b..3aa22be0a82c37f3d246131c14b1bba2ae986f3f 100644
--- a/src/model/Relation.php
+++ b/src/model/Relation.php
@@ -2,72 +2,143 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model;
-use think\db\Query;
-use think\Exception;
+use Closure;
+use ReflectionFunction;
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
use think\Model;
/**
- * Class Relation
+ * 模型关联基础类
* @package think\model
- *
* @mixin Query
*/
abstract class Relation
{
- // 父模型对象
+ /**
+ * 父模型对象
+ * @var Model
+ */
protected $parent;
- /** @var Model 当前关联的模型类 */
+
+ /**
+ * 当前关联的模型类名
+ * @var string
+ */
protected $model;
- /** @var Query 关联模型查询对象 */
+
+ /**
+ * 关联模型查询对象
+ * @var Query
+ */
protected $query;
- // 关联表外键
+
+ /**
+ * 关联表外键
+ * @var string
+ */
protected $foreignKey;
- // 关联表主键
+
+ /**
+ * 关联表主键
+ * @var string
+ */
protected $localKey;
- // 基础查询
+
+ /**
+ * 是否执行关联基础查询
+ * @var bool
+ */
protected $baseQuery;
- // 是否为自关联
- protected $selfRelation;
+
+ /**
+ * 是否为自关联
+ * @var bool
+ */
+ protected $selfRelation = false;
+
+ /**
+ * 关联数据数量限制
+ * @var int
+ */
+ protected $withLimit;
+
+ /**
+ * 关联数据字段限制
+ * @var array
+ */
+ protected $withField;
+
+ /**
+ * 排除关联数据字段
+ * @var array
+ */
+ protected $withoutField;
+
+ /**
+ * 默认数据
+ * @var mixed
+ */
+ protected $default;
/**
* 获取关联的所属模型
* @access public
* @return Model
*/
- public function getParent()
+ public function getParent(): Model
{
return $this->parent;
}
/**
- * 获取当前的关联模型类的实例
+ * 获取当前的关联模型类的Query实例
* @access public
- * @return Model
+ * @return Query
*/
- public function getModel()
+ public function getQuery()
{
- return $this->query->getModel();
+ return $this->query;
}
/**
- * 设置当前关联为自关联
+ * 获取关联表外键
* @access public
- * @param bool $self 是否自关联
- * @return $this
+ * @return string
*/
- public function selfRelation($self = true)
+ public function getForeignKey()
{
- $this->selfRelation = $self;
- return $this;
+ return $this->foreignKey;
+ }
+
+ /**
+ * 获取关联表主键
+ * @access public
+ * @return string
+ */
+ public function getLocalKey()
+ {
+ return $this->localKey;
+ }
+
+ /**
+ * 获取当前的关联模型类的实例
+ * @access public
+ * @return Model
+ */
+ public function getModel(): Model
+ {
+ return $this->query->getModel();
}
/**
@@ -75,7 +146,7 @@ abstract class Relation
* @access public
* @return bool
*/
- public function isSelfRelation()
+ public function isSelfRelation(): bool
{
return $this->selfRelation;
}
@@ -83,41 +154,41 @@ abstract class Relation
/**
* 封装关联数据集
* @access public
- * @param array $resultSet 数据集
+ * @param array $resultSet 数据集
+ * @param Model $parent 父模型
* @return mixed
*/
- protected function resultSetBuild($resultSet)
+ protected function resultSetBuild(array $resultSet, Model $parent = null)
{
- return (new $this->model)->toCollection($resultSet);
+ return (new $this->model)->toCollection($resultSet)->setParent($parent);
}
- protected function getQueryFields($model)
+ protected function getQueryFields(string $model)
{
$fields = $this->query->getOptions('field');
return $this->getRelationQueryFields($fields, $model);
}
- protected function getRelationQueryFields($fields, $model)
+ protected function getRelationQueryFields($fields, string $model)
{
- if ($fields) {
+ if (empty($fields) || '*' == $fields) {
+ return $model . '.*';
+ }
- if (is_string($fields)) {
- $fields = explode(',', $fields);
- }
+ if (is_string($fields)) {
+ $fields = explode(',', $fields);
+ }
- foreach ($fields as &$field) {
- if (false === strpos($field, '.')) {
- $field = $model . '.' . $field;
- }
+ foreach ($fields as &$field) {
+ if (false === strpos($field, '.')) {
+ $field = $model . '.' . $field;
}
- } else {
- $fields = $model . '.*';
}
return $fields;
}
- protected function getQueryWhere(&$where, $relation)
+ protected function getQueryWhere(array &$where, string $relation): void
{
foreach ($where as $key => &$val) {
if (is_string($key)) {
@@ -130,16 +201,97 @@ abstract class Relation
}
/**
- * 删除记录
+ * 限制关联数据的数量
+ * @access public
+ * @param int $limit 关联数量限制
+ * @return $this
+ */
+ public function withLimit(int $limit)
+ {
+ $this->withLimit = $limit;
+ return $this;
+ }
+
+ /**
+ * 限制关联数据的字段
+ * @access public
+ * @param array|string $field 关联字段限制
+ * @return $this
+ */
+ public function withField($field)
+ {
+ if (is_string($field)) {
+ $field = array_map('trim', explode(',', $field));
+ }
+
+ $this->withField = $field;
+ return $this;
+ }
+
+ /**
+ * 排除关联数据的字段
+ * @access public
+ * @param array|string $field 关联字段限制
+ * @return $this
+ */
+ public function withoutField($field)
+ {
+ if (is_string($field)) {
+ $field = array_map('trim', explode(',', $field));
+ }
+
+ $this->withoutField = $field;
+ return $this;
+ }
+
+ /**
+ * 设置关联数据不存在的时候默认值
* @access public
- * @param mixed $data 表达式 true 表示强制删除
- * @return int
- * @throws Exception
- * @throws PDOException
+ * @param mixed $data 默认值
+ * @return $this
+ */
+ public function withDefault($data = null)
+ {
+ $this->default = $data;
+ return $this;
+ }
+
+ /**
+ * 获取关联数据默认值
+ * @access protected
+ * @return mixed
*/
- public function delete($data = null)
+ protected function getDefaultModel()
{
- return $this->query->delete($data);
+ if (is_array($this->default)) {
+ $model = (new $this->model)->data($this->default);
+ } elseif ($this->default instanceof Closure) {
+ $closure = $this->default;
+ $model = new $this->model;
+ $closure($model);
+ } else {
+ $model = $this->default;
+ }
+
+ return $model;
+ }
+
+ /**
+ * 判断闭包的参数类型
+ * @access protected
+ * @return mixed
+ */
+ protected function getClosureType(Closure $closure)
+ {
+ $reflect = new ReflectionFunction($closure);
+ $params = $reflect->getParameters();
+
+ if (!empty($params)) {
+ $type = $params[0]->getType();
+ return is_null($type) || Relation::class == $type->getName() ? $this : $this->query;
+ }
+
+ return $this;
}
/**
@@ -147,7 +299,7 @@ abstract class Relation
* @access protected
* @return void
*/
- protected function baseQuery()
+ protected function baseQuery(): void
{}
public function __call($method, $args)
@@ -156,11 +308,11 @@ abstract class Relation
// 执行基础查询
$this->baseQuery();
- $result = call_user_func_array([$this->query->getModel(), $method], $args);
+ $result = call_user_func_array([$this->query, $method], $args);
return $result === $this->query ? $this : $result;
- } else {
- throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
}
+
+ throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
}
}
diff --git a/src/model/concern/Attribute.php b/src/model/concern/Attribute.php
index 9d9d0988ed649cda9dbe376e5efd790550930921..561aef866d8dc4390b67f20f18f65bb318162f8d 100644
--- a/src/model/concern/Attribute.php
+++ b/src/model/concern/Attribute.php
@@ -2,19 +2,25 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model\concern;
use InvalidArgumentException;
-use think\Db;
+use think\db\Raw;
+use think\helper\Str;
+use think\Model;
use think\model\Relation;
+/**
+ * 模型数据处理
+ */
trait Attribute
{
/**
@@ -27,25 +33,19 @@ trait Attribute
* 数据表字段信息 留空则自动获取
* @var array
*/
- protected $field = [];
+ protected $schema = [];
/**
- * JSON数据表字段
+ * 当前允许写入的字段
* @var array
*/
- protected $json = [];
+ protected $field = [];
/**
- * JSON数据表字段类型
+ * 字段自动类型转换
* @var array
*/
- protected $jsonType = [];
-
- /**
- * JSON数据取出是否需要转换为数组
- * @var bool
- */
- protected $jsonAssoc = false;
+ protected $type = [];
/**
* 数据表废弃字段
@@ -59,12 +59,6 @@ trait Attribute
*/
protected $readonly = [];
- /**
- * 数据表字段类型
- * @var array
- */
- protected $type = [];
-
/**
* 当前模型数据
* @var array
@@ -78,10 +72,34 @@ trait Attribute
private $origin = [];
/**
- * 修改器执行记录
+ * JSON数据表字段
+ * @var array
+ */
+ protected $json = [];
+
+ /**
+ * JSON数据表字段类型
* @var array
*/
- private $set = [];
+ protected $jsonType = [];
+
+ /**
+ * JSON数据取出是否需要转换为数组
+ * @var bool
+ */
+ protected $jsonAssoc = false;
+
+ /**
+ * 是否严格字段大小写
+ * @var bool
+ */
+ protected $strict = true;
+
+ /**
+ * 获取器数据
+ * @var array
+ */
+ private $get = [];
/**
* 动态获取器
@@ -102,12 +120,13 @@ trait Attribute
/**
* 判断一个字段名是否为主键字段
* @access public
- * @param string $key 名称
+ * @param string $key 名称
* @return bool
*/
- protected function isPk($key)
+ protected function isPk(string $key): bool
{
$pk = $this->getPk();
+
if (is_string($pk) && $pk == $key) {
return true;
} elseif (is_array($pk) && in_array($key, $pk)) {
@@ -120,11 +139,12 @@ trait Attribute
/**
* 获取模型对象的主键值
* @access public
- * @return integer
+ * @return mixed
*/
public function getKey()
{
$pk = $this->getPk();
+
if (is_string($pk) && array_key_exists($pk, $this->data)) {
return $this->data[$pk];
}
@@ -135,15 +155,11 @@ trait Attribute
/**
* 设置允许写入的字段
* @access public
- * @param mixed $field 允许写入的字段 如果为true只允许写入数据表字段
+ * @param array $field 允许写入的字段
* @return $this
*/
- public function allowField($field)
+ public function allowField(array $field)
{
- if (is_string($field)) {
- $field = explode(',', $field);
- }
-
$this->field = $field;
return $this;
@@ -152,110 +168,139 @@ trait Attribute
/**
* 设置只读字段
* @access public
- * @param mixed $field 只读字段
+ * @param array $field 只读字段
* @return $this
*/
- public function readonly($field)
+ public function readOnly(array $field)
{
- if (is_string($field)) {
- $field = explode(',', $field);
- }
-
$this->readonly = $field;
return $this;
}
+ /**
+ * 获取实际的字段名
+ * @access protected
+ * @param string $name 字段名
+ * @return string
+ */
+ protected function getRealFieldName(string $name): string
+ {
+ if ($this->convertNameToCamel || !$this->strict) {
+ return Str::snake($name);
+ }
+
+ return $name;
+ }
+
/**
* 设置数据对象值
* @access public
- * @param mixed $data 数据或者属性名
- * @param mixed $value 值
+ * @param array $data 数据
+ * @param bool $set 是否调用修改器
+ * @param array $allow 允许的字段名
* @return $this
*/
- public function data($data, $value = null)
+ public function data(array $data, bool $set = false, array $allow = [])
{
- if (is_string($data)) {
- $this->data[$data] = $value;
- } else {
- // 清空数据
- $this->data = [];
+ // 清空数据
+ $this->data = [];
- if (is_object($data)) {
- $data = get_object_vars($data);
+ // 废弃字段
+ foreach ($this->disuse as $key) {
+ if (array_key_exists($key, $data)) {
+ unset($data[$key]);
}
+ }
- if (true === $value) {
- // 数据对象赋值
- foreach ($data as $key => $value) {
- $this->setAttr($key, $value, $data);
+ if (!empty($allow)) {
+ $result = [];
+ foreach ($allow as $name) {
+ if (isset($data[$name])) {
+ $result[$name] = $data[$name];
}
- } else {
- $this->data = $data;
}
+ $data = $result;
+ }
+
+ if ($set) {
+ // 数据对象赋值
+ $this->setAttrs($data);
+ } else {
+ $this->data = $data;
}
return $this;
}
/**
- * 批量设置数据对象值
+ * 批量追加数据对象值
* @access public
- * @param mixed $data 数据
- * @param bool $set 是否需要进行数据处理
+ * @param array $data 数据
+ * @param bool $set 是否需要进行数据处理
* @return $this
*/
- public function appendData($data, $set = false)
+ public function appendData(array $data, bool $set = false)
{
if ($set) {
- // 进行数据处理
- foreach ($data as $key => $value) {
- $this->setAttr($key, $value, $data);
- }
+ $this->setAttrs($data);
} else {
- if (is_object($data)) {
- $data = get_object_vars($data);
- }
-
$this->data = array_merge($this->data, $data);
}
return $this;
}
+ /**
+ * 刷新对象原始数据(为当前数据)
+ * @access public
+ * @return $this
+ */
+ public function refreshOrigin()
+ {
+ $this->origin = $this->data;
+ return $this;
+ }
+
/**
* 获取对象原始数据 如果不存在指定字段返回null
* @access public
- * @param string $name 字段名 留空获取全部
+ * @param string $name 字段名 留空获取全部
* @return mixed
*/
- public function getOrigin($name = null)
+ public function getOrigin(string $name = null)
{
if (is_null($name)) {
return $this->origin;
- } else {
- return array_key_exists($name, $this->origin) ? $this->origin[$name] : null;
}
+
+ $fieldName = $this->getRealFieldName($name);
+
+ return array_key_exists($fieldName, $this->origin) ? $this->origin[$fieldName] : null;
}
/**
- * 获取对象原始数据 如果不存在指定字段返回false
+ * 获取当前对象数据 如果不存在指定字段返回false
* @access public
- * @param string $name 字段名 留空获取全部
+ * @param string $name 字段名 留空获取全部
* @return mixed
* @throws InvalidArgumentException
*/
- public function getData($name = null)
+ public function getData(string $name = null)
{
if (is_null($name)) {
return $this->data;
- } elseif (array_key_exists($name, $this->data)) {
- return $this->data[$name];
- } elseif (array_key_exists($name, $this->relation)) {
- return $this->relation[$name];
- } else {
- throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}
+
+ $fieldName = $this->getRealFieldName($name);
+
+ if (array_key_exists($fieldName, $this->data)) {
+ return $this->data[$fieldName];
+ } elseif (array_key_exists($fieldName, $this->relation)) {
+ return $this->relation[$fieldName];
+ }
+
+ throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}
/**
@@ -263,26 +308,20 @@ trait Attribute
* @access public
* @return array
*/
- public function getChangedData()
+ public function getChangedData(): array
{
- if ($this->force) {
- $data = $this->data;
- } else {
- $data = array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
- if ((empty($a) || empty($b)) && $a !== $b) {
- return 1;
- }
+ $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
+ if ((empty($a) || empty($b)) && $a !== $b) {
+ return 1;
+ }
- return is_object($a) || $a != $b ? 1 : 0;
- });
- }
+ return is_object($a) || $a != $b ? 1 : 0;
+ });
- if (!empty($this->readonly)) {
- // 只读字段不允许更新
- foreach ($this->readonly as $key => $field) {
- if (isset($data[$field])) {
- unset($data[$field]);
- }
+ // 只读字段不允许更新
+ foreach ($this->readonly as $key => $field) {
+ if (array_key_exists($field, $data)) {
+ unset($data[$field]);
}
}
@@ -290,101 +329,77 @@ trait Attribute
}
/**
- * 修改器 设置数据对象值
+ * 直接设置数据对象值
* @access public
- * @param string $name 属性名
- * @param mixed $value 属性值
- * @param array $data 数据
+ * @param string $name 属性名
+ * @param mixed $value 值
* @return void
*/
- public function setAttr($name, $value, $data = [])
+ public function set(string $name, $value): void
{
- if (isset($this->set[$name])) {
- return;
- }
-
- $isRelationData = false;
+ $name = $this->getRealFieldName($name);
- if (is_null($value) && $this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
- // 自动写入的时间戳字段
- $value = $this->autoWriteTimestamp($name);
- } else {
- // 检测修改器
- $method = 'set' . Db::parseName($name, 1) . 'Attr';
-
- if (method_exists($this, $method)) {
- $value = $this->$method($value, array_merge($this->data, $data));
- } elseif (isset($this->type[$name])) {
- // 类型转换
- $value = $this->writeTransform($value, $this->type[$name]);
- }
- }
-
- // 设置数据对象属性
$this->data[$name] = $value;
- $this->set[$name] = true;
+ unset($this->get[$name]);
}
/**
- * 是否需要自动写入时间字段
+ * 通过修改器 批量设置数据对象值
* @access public
- * @param bool $auto
- * @return $this
+ * @param array $data 数据
+ * @return void
*/
- public function isAutoWriteTimestamp($auto)
+ public function setAttrs(array $data): void
{
- $this->autoWriteTimestamp = $auto;
-
- return $this;
+ // 进行数据处理
+ foreach ($data as $key => $value) {
+ $this->setAttr($key, $value, $data);
+ }
}
/**
- * 自动写入时间戳
+ * 通过修改器 设置数据对象值
* @access public
- * @param string $name 时间戳字段
- * @return mixed
+ * @param string $name 属性名
+ * @param mixed $value 属性值
+ * @param array $data 数据
+ * @return void
*/
- protected function autoWriteTimestamp($name)
+ public function setAttr(string $name, $value, array $data = []): void
{
- if (isset($this->type[$name])) {
- $type = $this->type[$name];
+ $name = $this->getRealFieldName($name);
- if (strpos($type, ':')) {
- list($type, $param) = explode(':', $type, 2);
- }
+ // 检测修改器
+ $method = 'set' . Str::studly($name) . 'Attr';
- switch ($type) {
- case 'datetime':
- case 'date':
- $format = !empty($param) ? $param : $this->dateFormat;
- $format .= strpos($format, 'u') || false !== strpos($format, '\\') ? '' : '.u';
- $value = $this->formatDateTime($format);
- break;
- case 'timestamp':
- case 'integer':
- default:
- $value = time();
- break;
+ if (method_exists($this, $method)) {
+ $array = $this->data;
+
+ $value = $this->$method($value, array_merge($this->data, $data));
+
+ if (is_null($value) && $array !== $this->data) {
+ return;
}
- } elseif (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
- 'datetime',
- 'date',
- 'timestamp',
- ])) {
- $format = strpos($this->dateFormat, 'u') || false !== strpos($this->dateFormat, '\\') ? '' : '.u';
- $value = $this->formatDateTime($this->dateFormat . $format);
- } else {
- $value = time();
+ } elseif (isset($this->type[$name])) {
+ // 类型转换
+ $value = $this->writeTransform($value, $this->type[$name]);
+ } elseif ($this->isRelationAttr($name)) {
+ $this->relation[$name] = $value;
+ } elseif ((array_key_exists($name, $this->origin) || empty($this->origin)) && is_object($value) && method_exists($value, '__toString')) {
+ // 对象类型
+ $value = $value->__toString();
}
- return $value;
+ // 设置数据对象属性
+ $this->data[$name] = $value;
+ unset($this->get[$name]);
}
/**
* 数据写入 类型转换
- * @access public
- * @param mixed $value 值
- * @param string|array $type 要转换的类型
+ * @access protected
+ * @param mixed $value 值
+ * @param string|array $type 要转换的类型
* @return mixed
*/
protected function writeTransform($value, $type)
@@ -393,10 +408,14 @@ trait Attribute
return;
}
+ if ($value instanceof Raw) {
+ return $value;
+ }
+
if (is_array($type)) {
- list($type, $param) = $type;
+ [$type, $param] = $type;
} elseif (strpos($type, ':')) {
- list($type, $param) = explode(':', $type, 2);
+ [$type, $param] = explode(':', $type, 2);
}
switch ($type) {
@@ -407,7 +426,7 @@ trait Attribute
if (empty($param)) {
$value = (float) $value;
} else {
- $value = (float) number_format($value, $param, '.', '');
+ $value = (float) number_format($value, (int) $param, '.', '');
}
break;
case 'boolean':
@@ -419,9 +438,8 @@ trait Attribute
}
break;
case 'datetime':
- $format = !empty($param) ? $param : $this->dateFormat;
- $value = is_numeric($value) ? $value : strtotime($value);
- $value = $this->formatDateTime($format, $value);
+ $value = is_numeric($value) ? $value : strtotime($value);
+ $value = $this->formatDateTime('Y-m-d H:i:s.u', $value, true);
break;
case 'object':
if (is_object($value)) {
@@ -437,6 +455,11 @@ trait Attribute
case 'serialize':
$value = serialize($value);
break;
+ default:
+ if (is_object($value) && false !== strpos($type, '\\') && method_exists($value, '__toString')) {
+ // 对象类型
+ $value = $value->__toString();
+ }
}
return $value;
@@ -445,105 +468,119 @@ trait Attribute
/**
* 获取器 获取数据对象的值
* @access public
- * @param string $name 名称
- * @param array $item 数据
+ * @param string $name 名称
* @return mixed
* @throws InvalidArgumentException
*/
- public function getAttr($name, &$item = null)
+ public function getAttr(string $name)
{
try {
- $notFound = false;
+ $relation = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
- $notFound = true;
+ $relation = $this->isRelationAttr($name);
$value = null;
}
+ return $this->getValue($name, $value, $relation);
+ }
+
+ /**
+ * 获取经过获取器处理后的数据对象的值
+ * @access protected
+ * @param string $name 字段名称
+ * @param mixed $value 字段值
+ * @param bool|string $relation 是否为关联属性或者关联名
+ * @return mixed
+ * @throws InvalidArgumentException
+ */
+ protected function getValue(string $name, $value, $relation = false)
+ {
// 检测属性获取器
- $fieldName = Db::parseName($name);
- $method = 'get' . Db::parseName($name, 1) . 'Attr';
+ $fieldName = $this->getRealFieldName($name);
+
+ if (array_key_exists($fieldName, $this->get)) {
+ return $this->get[$fieldName];
+ }
+ $method = 'get' . Str::studly($name) . 'Attr';
if (isset($this->withAttr[$fieldName])) {
- if ($notFound && $relation = $this->isRelationAttr($name)) {
- $modelRelation = $this->$relation();
- $value = $this->getRelationData($modelRelation);
+ if ($relation) {
+ $value = $this->getRelationValue($relation);
}
- $closure = $this->withAttr[$fieldName];
- $value = $closure($value, $this->data);
+ if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
+ $value = $this->getJsonValue($fieldName, $value);
+ } else {
+ $closure = $this->withAttr[$fieldName];
+ if ($closure instanceof \Closure) {
+ $value = $closure($value, $this->data);
+ }
+ }
} elseif (method_exists($this, $method)) {
- if ($notFound && $relation = $this->isRelationAttr($name)) {
- $modelRelation = $this->$relation();
- $value = $this->getRelationData($modelRelation);
+ if ($relation) {
+ $value = $this->getRelationValue($relation);
}
$value = $this->$method($value, $this->data);
- } elseif (isset($this->type[$name])) {
+ } elseif (isset($this->type[$fieldName])) {
// 类型转换
- $value = $this->readTransform($value, $this->type[$name]);
- } elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
- if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
- 'datetime',
- 'date',
- 'timestamp',
- ])) {
- $value = $this->formatDateTime($this->dateFormat, $value);
- } else {
- $value = $this->formatDateTime($this->dateFormat, $value, true);
- }
- } elseif ($notFound) {
- $value = $this->getRelationAttribute($name, $item);
+ $value = $this->readTransform($value, $this->type[$fieldName]);
+ } elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
+ $value = $this->getTimestampValue($value);
+ } elseif ($relation) {
+ $value = $this->getRelationValue($relation);
+ // 保存关联对象值
+ $this->relation[$name] = $value;
}
+
+ $this->get[$fieldName] = $value;
+
return $value;
}
/**
- * 获取关联属性值
+ * 获取JSON字段属性值
* @access protected
- * @param string $name 属性名
- * @param array $item 数据
+ * @param string $name 属性名
+ * @param mixed $value JSON数据
* @return mixed
*/
- protected function getRelationAttribute($name, &$item)
+ protected function getJsonValue($name, $value)
{
- $relation = $this->isRelationAttr($name);
-
- if ($relation) {
- $modelRelation = $this->$relation();
- if ($modelRelation instanceof Relation) {
- $value = $this->getRelationData($modelRelation);
-
- if ($item && method_exists($modelRelation, 'getBindAttr') && $bindAttr = $modelRelation->getBindAttr()) {
-
- foreach ($bindAttr as $key => $attr) {
- $key = is_numeric($key) ? $attr : $key;
-
- if (isset($item[$key])) {
- throw new Exception('bind attr has exists:' . $key);
- } else {
- $item[$key] = $value ? $value->getAttr($attr) : null;
- }
- }
-
- return false;
- }
-
- // 保存关联对象值
- $this->relation[$name] = $value;
+ if (is_null($value)) {
+ return $value;
+ }
- return $value;
+ foreach ($this->withAttr[$name] as $key => $closure) {
+ if ($this->jsonAssoc) {
+ $value[$key] = $closure($value[$key], $value);
+ } else {
+ $value->$key = $closure($value->$key, $value);
}
}
- throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
+ return $value;
+ }
+
+ /**
+ * 获取关联属性值
+ * @access protected
+ * @param string $relation 关联名
+ * @return mixed
+ */
+ protected function getRelationValue(string $relation)
+ {
+ $modelRelation = $this->$relation();
+
+ return $modelRelation instanceof Relation ? $this->getRelationData($modelRelation) : null;
}
/**
* 数据读取 类型转换
- * @access public
- * @param mixed $value 值
- * @param string|array $type 要转换的类型
+ * @access protected
+ * @param mixed $value 值
+ * @param string|array $type 要转换的类型
* @return mixed
*/
protected function readTransform($value, $type)
@@ -553,9 +590,9 @@ trait Attribute
}
if (is_array($type)) {
- list($type, $param) = $type;
+ [$type, $param] = $type;
} elseif (strpos($type, ':')) {
- list($type, $param) = explode(':', $type, 2);
+ [$type, $param] = explode(':', $type, 2);
}
switch ($type) {
@@ -566,7 +603,7 @@ trait Attribute
if (empty($param)) {
$value = (float) $value;
} else {
- $value = (float) number_format($value, $param, '.', '');
+ $value = (float) number_format($value, (int) $param, '.', '');
}
break;
case 'boolean':
@@ -594,7 +631,11 @@ trait Attribute
$value = empty($value) ? new \stdClass() : json_decode($value);
break;
case 'serialize':
- $value = unserialize($value);
+ try {
+ $value = unserialize($value);
+ } catch (\Exception $e) {
+ $value = null;
+ }
break;
default:
if (false !== strpos($type, '\\')) {
@@ -613,20 +654,25 @@ trait Attribute
* @param callable $callback 闭包获取器
* @return $this
*/
- public function withAttribute($name, $callback = null)
+ public function withAttr($name, callable $callback = null)
{
if (is_array($name)) {
foreach ($name as $key => $val) {
- $key = Db::parseName($key);
-
- $this->withAttr[$key] = $val;
+ $this->withAttr($key, $val);
}
} else {
- $name = Db::parseName($name);
+ $name = $this->getRealFieldName($name);
+
+ if (strpos($name, '.')) {
+ [$name, $key] = explode('.', $name);
- $this->withAttr[$name] = $callback;
+ $this->withAttr[$name][$key] = $callback;
+ } else {
+ $this->withAttr[$name] = $callback;
+ }
}
return $this;
}
+
}
diff --git a/src/model/concern/Conversion.php b/src/model/concern/Conversion.php
index 8df518bf26d63af7cc978c51c9891df1f80e152d..b584ba91ae3de5574dffad135d415171b9a2b79f 100644
--- a/src/model/concern/Conversion.php
+++ b/src/model/concern/Conversion.php
@@ -2,45 +2,116 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model\concern;
use think\Collection;
-use think\Db;
-use think\Exception;
+use think\db\exception\DbException as Exception;
+use think\helper\Str;
use think\Model;
use think\model\Collection as ModelCollection;
+use think\model\relation\OneToOne;
/**
* 模型数据转换处理
*/
trait Conversion
{
- // 显示属性
+ /**
+ * 数据输出显示的属性
+ * @var array
+ */
protected $visible = [];
- // 隐藏属性
+
+ /**
+ * 数据输出隐藏的属性
+ * @var array
+ */
protected $hidden = [];
- // 附加属性
+
+ /**
+ * 数据输出需要追加的属性
+ * @var array
+ */
protected $append = [];
- // 查询数据集对象
+
+ /**
+ * 场景
+ * @var array
+ */
+ protected $scene = [];
+
+ /**
+ * 数据输出字段映射
+ * @var array
+ */
+ protected $mapping = [];
+
+ /**
+ * 数据集对象名
+ * @var string
+ */
protected $resultSetType;
+ /**
+ * 数据命名是否自动转为驼峰
+ * @var bool
+ */
+ protected $convertNameToCamel;
+
+ /**
+ * 转换数据为驼峰命名(用于输出)
+ * @access public
+ * @param bool $toCamel 是否自动驼峰命名
+ * @return $this
+ */
+ public function convertNameToCamel(bool $toCamel = true)
+ {
+ $this->convertNameToCamel = $toCamel;
+ return $this;
+ }
+
/**
* 设置需要附加的输出属性
* @access public
- * @param array $append 属性列表
- * @param bool $override 是否覆盖
+ * @param array $append 属性列表
+ * @param bool $merge 是否合并
* @return $this
*/
- public function append($append = [], $override = false)
+ public function append(array $append = [], bool $merge = false)
{
- $this->append = $override ? $append : array_merge($this->append, $append);
+ if ($merge) {
+ $this->append = array_merge($this->append, $append);
+ } else {
+ $this->append = $append;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置输出层场景
+ * @access public
+ * @param string $scene 场景名称
+ * @return $this
+ */
+ public function scene(string $scene)
+ {
+ if (isset($this->scene[$scene])) {
+ $data = $this->scene[$scene];
+ foreach (['append', 'hidden', 'visible'] as $name) {
+ if (isset($data[$name])) {
+ $this->$name($data[$name]);
+ }
+ }
+ }
return $this;
}
@@ -48,18 +119,15 @@ trait Conversion
/**
* 设置附加关联对象的属性
* @access public
- * @param string $attr 关联属性
- * @param string|array $append 追加属性名
+ * @param string $attr 关联属性
+ * @param string|array $append 追加属性名
* @return $this
* @throws Exception
*/
- public function appendRelationAttr($attr, $append)
+ public function appendRelationAttr(string $attr, array $append)
{
- if (is_string($append)) {
- $append = explode(',', $append);
- }
+ $relation = Str::camel($attr);
- $relation = Db::parseName($attr, 1, false);
if (isset($this->relation[$relation])) {
$model = $this->relation[$relation];
} else {
@@ -71,9 +139,9 @@ trait Conversion
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
throw new Exception('bind attr has exists:' . $key);
- } else {
- $this->data[$key] = $model->$attr;
}
+
+ $this->data[$key] = $model->$attr;
}
}
@@ -83,13 +151,17 @@ trait Conversion
/**
* 设置需要隐藏的输出属性
* @access public
- * @param array $hidden 属性列表
- * @param bool $override 是否覆盖
+ * @param array $hidden 属性列表
+ * @param bool $merge 是否合并
* @return $this
*/
- public function hidden($hidden = [], $override = false)
+ public function hidden(array $hidden = [], bool $merge = false)
{
- $this->hidden = $override ? $hidden : array_merge($this->hidden, $hidden);
+ if ($merge) {
+ $this->hidden = array_merge($this->hidden, $hidden);
+ } else {
+ $this->hidden = $hidden;
+ }
return $this;
}
@@ -97,13 +169,30 @@ trait Conversion
/**
* 设置需要输出的属性
* @access public
- * @param array $visible
- * @param bool $override 是否覆盖
+ * @param array $visible
+ * @param bool $merge 是否合并
+ * @return $this
+ */
+ public function visible(array $visible = [], bool $merge = false)
+ {
+ if ($merge) {
+ $this->visible = array_merge($this->visible, $visible);
+ } else {
+ $this->visible = $visible;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置属性的映射输出
+ * @access public
+ * @param array $map
* @return $this
*/
- public function visible($visible = [], $override = false)
+ public function mapping(array $map)
{
- $this->visible = $override ? $visible : array_merge($this->visible, $visible);
+ $this->mapping = $map;
return $this;
}
@@ -113,106 +202,150 @@ trait Conversion
* @access public
* @return array
*/
- public function toArray()
+ public function toArray(): array
{
- $item = [];
- $visible = [];
- $hidden = [];
+ $item = [];
+ $hasVisible = false;
+
+ foreach ($this->visible as $key => $val) {
+ if (is_string($val)) {
+ if (strpos($val, '.')) {
+ [$relation, $name] = explode('.', $val);
+ $this->visible[$relation][] = $name;
+ } else {
+ $this->visible[$val] = true;
+ $hasVisible = true;
+ }
+ unset($this->visible[$key]);
+ }
+ }
+
+ foreach ($this->hidden as $key => $val) {
+ if (is_string($val)) {
+ if (strpos($val, '.')) {
+ [$relation, $name] = explode('.', $val);
+ $this->hidden[$relation][] = $name;
+ } else {
+ $this->hidden[$val] = true;
+ }
+ unset($this->hidden[$key]);
+ }
+ }
// 合并关联数据
$data = array_merge($this->data, $this->relation);
- // 过滤属性
- if (!empty($this->visible)) {
- $array = $this->parseAttr($this->visible, $visible);
- $data = array_intersect_key($data, array_flip($array));
- } elseif (!empty($this->hidden)) {
- $array = $this->parseAttr($this->hidden, $hidden, false);
- $data = array_diff_key($data, array_flip($array));
- }
-
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
- if (isset($visible[$key])) {
- $val->visible($visible[$key]);
- } elseif (isset($hidden[$key])) {
- $val->hidden($hidden[$key]);
+ if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
+ $val->visible($this->visible[$key]);
+ } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
+ $val->hidden($this->hidden[$key]);
}
// 关联模型对象
- $item[$key] = $val->toArray();
- } else {
- // 模型属性
+ if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
+ $item[$key] = $val->toArray();
+ }
+ } elseif (isset($this->visible[$key])) {
+ $item[$key] = $this->getAttr($key);
+ } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
+
+ if (isset($this->mapping[$key])) {
+ // 检查字段映射
+ $mapName = $this->mapping[$key];
+ $item[$mapName] = $item[$key];
+ unset($item[$key]);
+ }
}
// 追加属性(必须定义获取器)
- if (!empty($this->append)) {
- foreach ($this->append as $key => $name) {
- if (is_array($name)) {
- // 追加关联对象属性
- $relation = $this->getRelation($key);
+ foreach ($this->append as $key => $name) {
+ $this->appendAttrToArray($item, $key, $name);
+ }
- if (!$relation) {
- $relation = $this->getAttr($key);
- $relation->visible($name);
- }
+ if ($this->convertNameToCamel) {
+ foreach ($item as $key => $val) {
+ $name = Str::camel($key);
+ if ($name !== $key) {
+ $item[$name] = $val;
+ unset($item[$key]);
+ }
+ }
+ }
- $item[$key] = $relation->append($name)->toArray();
+ return $item;
+ }
- } elseif (strpos($name, '.')) {
- list($key, $attr) = explode('.', $name);
- // 追加关联对象属性
- $relation = $this->getRelation($key);
+ protected function appendAttrToArray(array &$item, $key, $name)
+ {
+ if (is_array($name)) {
+ // 追加关联对象属性
+ $relation = $this->getRelation($key, true);
+ $item[$key] = $relation ? $relation->append($name)
+ ->toArray() : [];
+ } elseif (strpos($name, '.')) {
+ [$key, $attr] = explode('.', $name);
+ // 追加关联对象属性
+ $relation = $this->getRelation($key, true);
+ $item[$key] = $relation ? $relation->append([$attr])
+ ->toArray() : [];
+ } else {
+ $value = $this->getAttr($name);
+ $item[$name] = $value;
- if (!$relation) {
- $relation = $this->getAttr($key);
- $relation->visible([$attr]);
- }
+ $this->getBindAttrValue($name, $value, $item);
+ }
+ }
- $item[$key] = $relation->append([$attr])->toArray();
+ protected function getBindAttrValue(string $name, $value, array &$item = [])
+ {
+ $relation = $this->isRelationAttr($name);
+ if (!$relation) {
+ return false;
+ }
- } else {
- $value = $this->getAttr($name, $item);
- if (false !== $value) {
- $item[$name] = $value;
- }
+ $modelRelation = $this->$relation();
+
+ if ($modelRelation instanceof OneToOne) {
+ $bindAttr = $modelRelation->getBindAttr();
+
+ if (!empty($bindAttr)) {
+ unset($item[$name]);
+ }
+
+ foreach ($bindAttr as $key => $attr) {
+ $key = is_numeric($key) ? $attr : $key;
+
+ if (isset($item[$key])) {
+ throw new Exception('bind attr has exists:' . $key);
}
+
+ $item[$key] = $value ? $value->getAttr($attr) : null;
}
}
-
- return $item;
}
/**
* 转换当前模型对象为JSON字符串
* @access public
- * @param integer $options json参数
+ * @param integer $options json参数
* @return string
*/
- public function toJson($options = JSON_UNESCAPED_UNICODE)
+ public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $options);
}
- /**
- * 移除当前模型的关联属性
- * @access public
- * @return $this
- */
- public function removeRelation()
- {
- $this->relation = [];
- return $this;
- }
-
public function __toString()
{
return $this->toJson();
}
// JsonSerializable
+ #[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
@@ -225,7 +358,7 @@ trait Conversion
* @param string $resultSetType 数据集类
* @return Collection
*/
- public function toCollection($collection, $resultSetType = null)
+ public function toCollection(iterable $collection = [], string $resultSetType = null): Collection
{
$resultSetType = $resultSetType ?: $this->resultSetType;
@@ -238,38 +371,4 @@ trait Conversion
return $collection;
}
- /**
- * 解析隐藏及显示属性
- * @access protected
- * @param array $attrs 属性
- * @param array $result 结果集
- * @param bool $visible
- * @return array
- */
- protected function parseAttr($attrs, &$result, $visible = true)
- {
- $array = [];
-
- foreach ($attrs as $key => $val) {
- if (is_array($val)) {
- if ($visible) {
- $array[] = $key;
- }
-
- $result[$key] = $val;
- } elseif (strpos($val, '.')) {
- list($key, $name) = explode('.', $val);
-
- if ($visible) {
- $array[] = $key;
- }
-
- $result[$key][] = $name;
- } else {
- $array[] = $val;
- }
- }
-
- return $array;
- }
}
diff --git a/src/model/concern/ModelEvent.php b/src/model/concern/ModelEvent.php
index 61de65b45d06aa6a0c8bcc822cba47f3ce2f0fc9..d2388ab4ae584441589ea79892c9cb7dd3da9caa 100644
--- a/src/model/concern/ModelEvent.php
+++ b/src/model/concern/ModelEvent.php
@@ -2,87 +2,46 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model\concern;
-use think\Db;
+use think\db\exception\ModelEventException;
+use think\helper\Str;
/**
* 模型事件处理
*/
trait ModelEvent
{
- // 回调事件
- private static $event = [];
/**
- * 模型事件观察
- * @var array
+ * Event对象
+ * @var object
*/
- protected static $observe = ['before_write', 'after_write', 'before_insert', 'after_insert', 'before_update', 'after_update', 'before_delete', 'after_delete', 'before_restore', 'after_restore'];
-
- /**
- * 绑定模型事件观察者类
- * @var array
- */
- protected $observerClass;
+ protected static $event;
/**
* 是否需要事件响应
* @var bool
*/
- private $withEvent = true;
-
- /**
- * 注册回调方法
- * @access public
- * @param string $event 事件名
- * @param callable $callback 回调方法
- * @param bool $override 是否覆盖
- * @return void
- */
- public static function event($event, $callback, $override = false)
- {
- $class = static::class;
-
- if ($override) {
- self::$event[$class][$event] = [];
- }
-
- self::$event[$class][$event][] = $callback;
- }
+ protected $withEvent = true;
/**
- * 清除回调方法
+ * 设置Event对象
* @access public
+ * @param object $event Event对象
* @return void
*/
- public static function flushEvent()
+ public static function setEvent($event)
{
- self::$event[static::class] = [];
- }
-
- /**
- * 注册一个模型观察者
- *
- * @param object|string $class
- * @return void
- */
- public static function observe($class)
- {
- foreach (static::$observe as $event) {
- $eventFuncName = Db::parseName($event, 1, false);
-
- if (method_exists($class, $eventFuncName)) {
- static::event($event, [$class, $eventFuncName]);
- }
- }
+ self::$event = $event;
}
/**
@@ -91,7 +50,7 @@ trait ModelEvent
* @param bool $event 是否需要事件响应
* @return $this
*/
- public function withEvent($event)
+ public function withEvent(bool $event)
{
$this->withEvent = $event;
return $this;
@@ -100,133 +59,30 @@ trait ModelEvent
/**
* 触发事件
* @access protected
- * @param string $event 事件名
+ * @param string $event 事件名
* @return bool
*/
- protected function trigger($event)
+ protected function trigger(string $event): bool
{
- $class = static::class;
-
- if ($this->withEvent && isset(self::$event[$class][$event])) {
- foreach (self::$event[$class][$event] as $callback) {
- $result = call_user_func_array($callback, [$this]);
-
- if (false === $result) {
- return false;
- }
- }
+ if (!$this->withEvent) {
+ return true;
}
- return true;
- }
+ $call = 'on' . Str::studly($event);
- /**
- * 模型before_insert事件快捷方法
- * @access protected
- * @param callable $callback
- * @param bool $override
- */
- protected static function beforeInsert($callback, $override = false)
- {
- self::event('before_insert', $callback, $override);
- }
-
- /**
- * 模型after_insert事件快捷方法
- * @access protected
- * @param callable $callback
- * @param bool $override
- */
- protected static function afterInsert($callback, $override = false)
- {
- self::event('after_insert', $callback, $override);
- }
-
- /**
- * 模型before_update事件快捷方法
- * @access protected
- * @param callable $callback
- * @param bool $override
- */
- protected static function beforeUpdate($callback, $override = false)
- {
- self::event('before_update', $callback, $override);
- }
-
- /**
- * 模型after_update事件快捷方法
- * @access protected
- * @param callable $callback
- * @param bool $override
- */
- protected static function afterUpdate($callback, $override = false)
- {
- self::event('after_update', $callback, $override);
- }
-
- /**
- * 模型before_write事件快捷方法
- * @access protected
- * @param callable $callback
- * @param bool $override
- */
- protected static function beforeWrite($callback, $override = false)
- {
- self::event('before_write', $callback, $override);
- }
-
- /**
- * 模型after_write事件快捷方法
- * @access protected
- * @param callable $callback
- * @param bool $override
- */
- protected static function afterWrite($callback, $override = false)
- {
- self::event('after_write', $callback, $override);
- }
-
- /**
- * 模型before_delete事件快捷方法
- * @access protected
- * @param callable $callback
- * @param bool $override
- */
- protected static function beforeDelete($callback, $override = false)
- {
- self::event('before_delete', $callback, $override);
- }
-
- /**
- * 模型after_delete事件快捷方法
- * @access protected
- * @param callable $callback
- * @param bool $override
- */
- protected static function afterDelete($callback, $override = false)
- {
- self::event('after_delete', $callback, $override);
- }
-
- /**
- * 模型before_restore事件快捷方法
- * @access protected
- * @param callable $callback
- * @param bool $override
- */
- protected static function beforeRestore($callback, $override = false)
- {
- self::event('before_restore', $callback, $override);
- }
+ try {
+ if (method_exists(static::class, $call)) {
+ $result = call_user_func([static::class, $call], $this);
+ } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) {
+ $result = self::$event->trigger('model.' . static::class . '.' . $event, $this);
+ $result = empty($result) ? true : end($result);
+ } else {
+ $result = true;
+ }
- /**
- * 模型after_restore事件快捷方法
- * @access protected
- * @param callable $callback
- * @param bool $override
- */
- protected static function afterRestore($callback, $override = false)
- {
- self::event('after_restore', $callback, $override);
+ return false === $result ? false : true;
+ } catch (ModelEventException $e) {
+ return false;
+ }
}
}
diff --git a/src/model/concern/OptimLock.php b/src/model/concern/OptimLock.php
new file mode 100644
index 0000000000000000000000000000000000000000..5e61318337ac1d702e5f84774c129002ef506ecb
--- /dev/null
+++ b/src/model/concern/OptimLock.php
@@ -0,0 +1,85 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\concern;
+
+use think\db\exception\DbException as Exception;
+
+/**
+ * 乐观锁
+ */
+trait OptimLock
+{
+ protected function getOptimLockField()
+ {
+ return property_exists($this, 'optimLock') && isset($this->optimLock) ? $this->optimLock : 'lock_version';
+ }
+
+ /**
+ * 数据检查
+ * @access protected
+ * @return void
+ */
+ protected function checkData(): void
+ {
+ $this->isExists() ? $this->updateLockVersion() : $this->recordLockVersion();
+ }
+
+ /**
+ * 记录乐观锁
+ * @access protected
+ * @return void
+ */
+ protected function recordLockVersion(): void
+ {
+ $optimLock = $this->getOptimLockField();
+
+ if ($optimLock) {
+ $this->set($optimLock, 0);
+ }
+ }
+
+ /**
+ * 更新乐观锁
+ * @access protected
+ * @return void
+ */
+ protected function updateLockVersion(): void
+ {
+ $optimLock = $this->getOptimLockField();
+
+ if ($optimLock && $lockVer = $this->getOrigin($optimLock)) {
+ // 更新乐观锁
+ $this->set($optimLock, $lockVer + 1);
+ }
+ }
+
+ public function getWhere()
+ {
+ $where = parent::getWhere();
+ $optimLock = $this->getOptimLockField();
+
+ if ($optimLock && $lockVer = $this->getOrigin($optimLock)) {
+ $where[] = [$optimLock, '=', $lockVer];
+ }
+
+ return $where;
+ }
+
+ protected function checkResult($result): void
+ {
+ if (!$result) {
+ throw new Exception('record has update');
+ }
+ }
+
+}
diff --git a/src/model/concern/RelationShip.php b/src/model/concern/RelationShip.php
index c318262b987d53a5a2cbef9ee2725a21f18bc3b6..8e0d498e64fc419a236c3e77874f96cf9b47aa3b 100644
--- a/src/model/concern/RelationShip.php
+++ b/src/model/concern/RelationShip.php
@@ -2,18 +2,21 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model\concern;
+use Closure;
use think\Collection;
-use think\Db;
-use think\db\Query;
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
+use think\helper\Str;
use think\Model;
use think\model\Relation;
use think\model\relation\BelongsTo;
@@ -21,9 +24,12 @@ use think\model\relation\BelongsToMany;
use think\model\relation\HasMany;
use think\model\relation\HasManyThrough;
use think\model\relation\HasOne;
+use think\model\relation\HasOneThrough;
use think\model\relation\MorphMany;
use think\model\relation\MorphOne;
use think\model\relation\MorphTo;
+use think\model\relation\MorphToMany;
+use think\model\relation\OneToOne;
/**
* 模型关联处理
@@ -46,21 +52,21 @@ trait RelationShip
* 关联写入定义信息
* @var array
*/
- private $together;
+ private $together = [];
/**
* 关联自动写入信息
* @var array
*/
- protected $relationWrite;
+ protected $relationWrite = [];
/**
* 设置父关联对象
* @access public
- * @param Model $model 模型对象
+ * @param Model $model 模型对象
* @return $this
*/
- public function setParent($model)
+ public function setParent(Model $model)
{
$this->parent = $model;
@@ -72,7 +78,7 @@ trait RelationShip
* @access public
* @return Model
*/
- public function getParent()
+ public function getParent(): Model
{
return $this->parent;
}
@@ -80,54 +86,93 @@ trait RelationShip
/**
* 获取当前模型的关联模型数据
* @access public
- * @param string $name 关联方法名
+ * @param string $name 关联方法名
+ * @param bool $auto 不存在是否自动获取
* @return mixed
*/
- public function getRelation($name = null)
+ public function getRelation(string $name = null, bool $auto = false)
{
if (is_null($name)) {
return $this->relation;
- } elseif (array_key_exists($name, $this->relation)) {
+ }
+
+ if (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
- } else {
- return;
+ } elseif ($auto) {
+ $relation = Str::camel($name);
+ return $this->getRelationValue($relation);
}
}
/**
* 设置关联数据对象值
* @access public
- * @param string $name 属性名
- * @param mixed $value 属性值
- * @param array $data 数据
+ * @param string $name 属性名
+ * @param mixed $value 属性值
+ * @param array $data 数据
* @return $this
*/
- public function setRelation($name, $value, $data = [])
+ public function setRelation(string $name, $value, array $data = [])
{
// 检测修改器
- $method = 'set' . Db::parseName($name, 1) . 'Attr';
+ $method = 'set' . Str::studly($name) . 'Attr';
if (method_exists($this, $method)) {
$value = $this->$method($value, array_merge($this->data, $data));
}
- $this->relation[$name] = $value;
+ $this->relation[$this->getRealFieldName($name)] = $value;
return $this;
}
/**
- * 关联数据一起更新
+ * 查询当前模型的关联数据
* @access public
- * @param mixed $relation 关联
- * @return $this
+ * @param array $relations 关联名
+ * @param array $withRelationAttr 关联获取器
+ * @return void
*/
- public function together($relation)
+ public function relationQuery(array $relations, array $withRelationAttr = []): void
{
- if (is_string($relation)) {
- $relation = explode(',', $relation);
+ foreach ($relations as $key => $relation) {
+ $subRelation = [];
+ $closure = null;
+
+ if ($relation instanceof Closure) {
+ // 支持闭包查询过滤关联条件
+ $closure = $relation;
+ $relation = $key;
+ }
+
+ if (is_array($relation)) {
+ $subRelation = $relation;
+ $relation = $key;
+ } elseif (strpos($relation, '.')) {
+ [$relation, $subRelation] = explode('.', $relation, 2);
+ }
+
+ $method = Str::camel($relation);
+ $relationName = Str::snake($relation);
+
+ $relationResult = $this->$method();
+
+ if (isset($withRelationAttr[$relationName])) {
+ $relationResult->withAttr($withRelationAttr[$relationName]);
+ }
+
+ $this->relation[$relation] = $relationResult->getRelation((array) $subRelation, $closure);
}
+ }
+ /**
+ * 关联数据写入
+ * @access public
+ * @param array $relation 关联
+ * @return $this
+ */
+ public function together(array $relation)
+ {
$this->together = $relation;
$this->checkAutoRelationWrite();
@@ -143,76 +188,55 @@ trait RelationShip
* @param integer $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public static function has($relation, $operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ public static function has(string $relation, string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query
{
- $relation = (new static())->$relation();
-
- if (is_array($operator) || $operator instanceof \Closure) {
- return $relation->hasWhere($operator);
- }
-
- return $relation->has($operator, $count, $id, $joinType);
+ return (new static())
+ ->$relation()
+ ->has($operator, $count, $id, $joinType, $query);
}
/**
* 根据关联条件查询当前模型
* @access public
- * @param string $relation 关联方法名
- * @param mixed $where 查询条件(数组或者闭包)
- * @param mixed $fields 字段
+ * @param string $relation 关联方法名
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public static function hasWhere($relation, $where = [], $fields = '*')
+ public static function hasWhere(string $relation, $where = [], string $fields = '*', string $joinType = '', Query $query = null): Query
{
- return (new static())->$relation()->hasWhere($where, $fields);
+ return (new static())
+ ->$relation()
+ ->hasWhere($where, $fields, $joinType, $query);
}
/**
- * 查询当前模型的关联数据
+ * 预载入关联查询 JOIN方式
* @access public
- * @param string|array $relations 关联名
- * @param array $withRelationAttr 关联获取器
- * @return $this
+ * @param Query $query Query对象
+ * @param string $relation 关联方法名
+ * @param mixed $field 字段
+ * @param string $joinType JOIN类型
+ * @param Closure $closure 闭包
+ * @param bool $first
+ * @return bool
*/
- public function relationQuery($relations, $withRelationAttr = [])
+ public function eagerly(Query $query, string $relation, $field, string $joinType = '', Closure $closure = null, bool $first = false): bool
{
- if (is_string($relations)) {
- $relations = explode(',', $relations);
- }
-
- foreach ($relations as $key => $relation) {
- $subRelation = '';
- $closure = null;
-
- if ($relation instanceof \Closure) {
- // 支持闭包查询过滤关联条件
- $closure = $relation;
- $relation = $key;
- }
-
- if (is_array($relation)) {
- $subRelation = $relation;
- $relation = $key;
- } elseif (strpos($relation, '.')) {
- list($relation, $subRelation) = explode('.', $relation, 2);
- }
-
- $method = Db::parseName($relation, 1, false);
- $relationName = Db::parseName($relation);
-
- $relationResult = $this->$method();
-
- if (isset($withRelationAttr[$relationName])) {
- $relationResult->withAttr($withRelationAttr[$relationName]);
- }
-
- $this->relation[$relation] = $relationResult->getRelation($subRelation, $closure);
+ $relation = Str::camel($relation);
+ $class = $this->$relation();
+ if ($class instanceof OneToOne) {
+ $class->eagerly($query, $relation, $field, $joinType, $closure, $first);
+ return true;
+ } else {
+ return false;
}
-
- return $this;
}
/**
@@ -221,18 +245,17 @@ trait RelationShip
* @param array $resultSet 数据集
* @param string $relation 关联名
* @param array $withRelationAttr 关联获取器
- * @param bool $join 是否为JOIN方式
- * @return array
+ * @param bool $join 是否为JOIN方式
+ * @param mixed $cache 关联缓存
+ * @return void
*/
- public function eagerlyResultSet(&$resultSet, $relation, $withRelationAttr = [], $join = false)
+ public function eagerlyResultSet(array &$resultSet, array $relations, array $withRelationAttr = [], bool $join = false, $cache = false): void
{
- $relations = is_string($relation) ? explode(',', $relation) : $relation;
-
foreach ($relations as $key => $relation) {
- $subRelation = '';
+ $subRelation = [];
$closure = null;
- if ($relation instanceof \Closure) {
+ if ($relation instanceof Closure) {
$closure = $relation;
$relation = $key;
}
@@ -241,11 +264,13 @@ trait RelationShip
$subRelation = $relation;
$relation = $key;
} elseif (strpos($relation, '.')) {
- list($relation, $subRelation) = explode('.', $relation, 2);
+ [$relation, $subRelation] = explode('.', $relation, 2);
+
+ $subRelation = [$subRelation];
}
- $relation = Db::parseName($relation, 1, false);
- $relationName = Db::parseName($relation);
+ $relationName = $relation;
+ $relation = Str::camel($relation);
$relationResult = $this->$relation();
@@ -253,28 +278,32 @@ trait RelationShip
$relationResult->withAttr($withRelationAttr[$relationName]);
}
- $relationResult->eagerlyResultSet($resultSet, $relation, $subRelation, $closure, $join);
+ if (is_scalar($cache)) {
+ $relationCache = [$cache];
+ } else {
+ $relationCache = $cache[$relationName] ?? $cache;
+ }
+
+ $relationResult->eagerlyResultSet($resultSet, $relationName, $subRelation, $closure, $relationCache, $join);
}
}
/**
* 预载入关联查询 返回模型对象
* @access public
- * @param Model $result 数据对象
- * @param string $relation 关联名
- * @param array $withRelationAttr 关联获取器
- * @param bool $join 是否为JOIN方式
- * @return Model
+ * @param array $relations 关联
+ * @param array $withRelationAttr 关联获取器
+ * @param bool $join 是否为JOIN方式
+ * @param mixed $cache 关联缓存
+ * @return void
*/
- public function eagerlyResult(&$result, $relation, $withRelationAttr = [], $join = false)
+ public function eagerlyResult(array $relations, array $withRelationAttr = [], bool $join = false, $cache = false): void
{
- $relations = is_string($relation) ? explode(',', $relation) : $relation;
-
foreach ($relations as $key => $relation) {
- $subRelation = '';
+ $subRelation = [];
$closure = null;
- if ($relation instanceof \Closure) {
+ if ($relation instanceof Closure) {
$closure = $relation;
$relation = $key;
}
@@ -283,11 +312,13 @@ trait RelationShip
$subRelation = $relation;
$relation = $key;
} elseif (strpos($relation, '.')) {
- list($relation, $subRelation) = explode('.', $relation, 2);
+ [$relation, $subRelation] = explode('.', $relation, 2);
+
+ $subRelation = [$subRelation];
}
- $relation = Db::parseName($relation, 1, false);
- $relationName = Db::parseName($relation);
+ $relationName = $relation;
+ $relation = Str::camel($relation);
$relationResult = $this->$relation();
@@ -295,25 +326,58 @@ trait RelationShip
$relationResult->withAttr($withRelationAttr[$relationName]);
}
- $relationResult->eagerlyResult($result, $relation, $subRelation, $closure, $join);
+ if (is_scalar($cache)) {
+ $relationCache = [$cache];
+ } else {
+ $relationCache = $cache[$relationName] ?? [];
+ }
+
+ $relationResult->eagerlyResult($this, $relationName, $subRelation, $closure, $relationCache, $join);
}
}
+ /**
+ * 绑定(一对一)关联属性到当前模型
+ * @access protected
+ * @param string $relation 关联名称
+ * @param array $attrs 绑定属性
+ * @return $this
+ * @throws Exception
+ */
+ public function bindAttr(string $relation, array $attrs = [])
+ {
+ $relation = $this->getRelation($relation, true);
+
+ foreach ($attrs as $key => $attr) {
+ $key = is_numeric($key) ? $attr : $key;
+ $value = $this->getOrigin($key);
+
+ if (!is_null($value)) {
+ throw new Exception('bind attr has exists:' . $key);
+ }
+
+ $this->set($key, $relation ? $relation->$attr : null);
+ }
+
+ return $this;
+ }
+
/**
* 关联统计
* @access public
- * @param Model $result 数据对象
- * @param array $relations 关联名
- * @param string $aggregate 聚合查询方法
- * @param string $field 字段
+ * @param Query $query 查询对象
+ * @param array $relations 关联名
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param bool $useSubQuery 子查询
* @return void
*/
- public function relationCount(&$result, $relations, $aggregate = 'sum', $field = '*')
+ public function relationCount(Query $query, array $relations, string $aggregate = 'sum', string $field = '*', bool $useSubQuery = true): void
{
foreach ($relations as $key => $relation) {
- $closure = null;
+ $closure = $name = null;
- if ($relation instanceof \Closure) {
+ if ($relation instanceof Closure) {
$closure = $relation;
$relation = $key;
} elseif (is_string($key)) {
@@ -321,26 +385,35 @@ trait RelationShip
$relation = $key;
}
- $relation = Db::parseName($relation, 1, false);
- $count = $this->$relation()->relationCount($result, $closure, $aggregate, $field);
+ $relation = Str::camel($relation);
+
+ if ($useSubQuery) {
+ $count = $this->$relation()->getRelationCountQuery($closure, $aggregate, $field, $name);
+ } else {
+ $count = $this->$relation()->relationCount($this, $closure, $aggregate, $field, $name);
+ }
- if (!isset($name)) {
- $name = Db::parseName($relation) . '_' . $aggregate;
+ if (empty($name)) {
+ $name = Str::snake($relation) . '_' . $aggregate;
}
- $result->setAttr($name, $count);
+ if ($useSubQuery) {
+ $query->field(['(' . $count . ')' => $name]);
+ } else {
+ $this->setAttr($name, $count);
+ }
}
}
/**
* HAS ONE 关联定义
* @access public
- * @param string $model 模型名
- * @param string $foreignKey 关联外键
- * @param string $localKey 当前主键
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前主键
* @return HasOne
*/
- public function hasOne($model, $foreignKey = '', $localKey = '')
+ public function hasOne(string $model, string $foreignKey = '', string $localKey = ''): HasOne
{
// 记录当前关联信息
$model = $this->parseModel($model);
@@ -353,19 +426,19 @@ trait RelationShip
/**
* BELONGS TO 关联定义
* @access public
- * @param string $model 模型名
- * @param string $foreignKey 关联外键
- * @param string $localKey 关联主键
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 关联主键
* @return BelongsTo
*/
- public function belongsTo($model, $foreignKey = '', $localKey = '')
+ public function belongsTo(string $model, string $foreignKey = '', string $localKey = ''): BelongsTo
{
// 记录当前关联信息
$model = $this->parseModel($model);
- $foreignKey = $foreignKey ?: $this->getForeignKey($model);
+ $foreignKey = $foreignKey ?: $this->getForeignKey((new $model)->getName());
$localKey = $localKey ?: (new $model)->getPk();
- $trace = debug_backtrace(false, 2);
- $relation = Db::parseName($trace[1]['function']);
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+ $relation = Str::snake($trace[1]['function']);
return new BelongsTo($this, $model, $foreignKey, $localKey, $relation);
}
@@ -373,12 +446,12 @@ trait RelationShip
/**
* HAS MANY 关联定义
* @access public
- * @param string $model 模型名
- * @param string $foreignKey 关联外键
- * @param string $localKey 当前主键
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前主键
* @return HasMany
*/
- public function hasMany($model, $foreignKey = '', $localKey = '')
+ public function hasMany(string $model, string $foreignKey = '', string $localKey = ''): HasMany
{
// 记录当前关联信息
$model = $this->parseModel($model);
@@ -391,66 +464,92 @@ trait RelationShip
/**
* HAS MANY 远程关联定义
* @access public
- * @param string $model 模型名
- * @param string $through 中间模型名
- * @param string $foreignKey 关联外键
- * @param string $throughKey 关联外键
- * @param string $localKey 当前主键
+ * @param string $model 模型名
+ * @param string $through 中间模型名
+ * @param string $foreignKey 关联外键
+ * @param string $throughKey 关联外键
+ * @param string $localKey 当前主键
+ * @param string $throughPk 中间表主键
* @return HasManyThrough
*/
- public function hasManyThrough($model, $through, $foreignKey = '', $throughKey = '', $localKey = '')
+ public function hasManyThrough(string $model, string $through, string $foreignKey = '', string $throughKey = '', string $localKey = '', string $throughPk = ''): HasManyThrough
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $through = $this->parseModel($through);
+ $localKey = $localKey ?: $this->getPk();
+ $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+ $throughKey = $throughKey ?: $this->getForeignKey((new $through)->getName());
+ $throughPk = $throughPk ?: (new $through)->getPk();
+
+ return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey, $throughPk);
+ }
+
+ /**
+ * HAS ONE 远程关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $through 中间模型名
+ * @param string $foreignKey 关联外键
+ * @param string $throughKey 关联外键
+ * @param string $localKey 当前主键
+ * @param string $throughPk 中间表主键
+ * @return HasOneThrough
+ */
+ public function hasOneThrough(string $model, string $through, string $foreignKey = '', string $throughKey = '', string $localKey = '', string $throughPk = ''): HasOneThrough
{
// 记录当前关联信息
$model = $this->parseModel($model);
$through = $this->parseModel($through);
$localKey = $localKey ?: $this->getPk();
$foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
- $throughKey = $throughKey ?: $this->getForeignKey($through);
+ $throughKey = $throughKey ?: $this->getForeignKey((new $through)->getName());
+ $throughPk = $throughPk ?: (new $through)->getPk();
- return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey);
+ return new HasOneThrough($this, $model, $through, $foreignKey, $throughKey, $localKey, $throughPk);
}
/**
* BELONGS TO MANY 关联定义
* @access public
- * @param string $model 模型名
- * @param string $table 中间表名
- * @param string $foreignKey 关联外键
- * @param string $localKey 当前模型关联键
+ * @param string $model 模型名
+ * @param string $middle 中间表/模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前模型关联键
* @return BelongsToMany
*/
- public function belongsToMany($model, $table = '', $foreignKey = '', $localKey = '')
+ public function belongsToMany(string $model, string $middle = '', string $foreignKey = '', string $localKey = ''): BelongsToMany
{
// 记录当前关联信息
$model = $this->parseModel($model);
- $name = Db::parseName(basename(str_replace('\\', '/', $model)));
- $table = $table ?: Db::parseName($this->name) . '_' . $name;
+ $name = Str::snake(class_basename($model));
+ $middle = $middle ?: Str::snake($this->name) . '_' . $name;
$foreignKey = $foreignKey ?: $name . '_id';
$localKey = $localKey ?: $this->getForeignKey($this->name);
- return new BelongsToMany($this, $model, $table, $foreignKey, $localKey);
+ return new BelongsToMany($this, $model, $middle, $foreignKey, $localKey);
}
/**
* MORPH One 关联定义
* @access public
- * @param string $model 模型名
- * @param string|array $morph 多态字段信息
- * @param string $type 多态类型
+ * @param string $model 模型名
+ * @param string|array $morph 多态字段信息
+ * @param string $type 多态类型
* @return MorphOne
*/
- public function morphOne($model, $morph = null, $type = '')
+ public function morphOne(string $model, $morph = null, string $type = ''): MorphOne
{
// 记录当前关联信息
$model = $this->parseModel($model);
if (is_null($morph)) {
- $trace = debug_backtrace(false, 2);
- $morph = Db::parseName($trace[1]['function']);
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+ $morph = Str::snake($trace[1]['function']);
}
if (is_array($morph)) {
- list($morphType, $foreignKey) = $morph;
+ [$morphType, $foreignKey] = $morph;
} else {
$morphType = $morph . '_type';
$foreignKey = $morph . '_id';
@@ -464,25 +563,25 @@ trait RelationShip
/**
* MORPH MANY 关联定义
* @access public
- * @param string $model 模型名
- * @param string|array $morph 多态字段信息
- * @param string $type 多态类型
+ * @param string $model 模型名
+ * @param string|array $morph 多态字段信息
+ * @param string $type 多态类型
* @return MorphMany
*/
- public function morphMany($model, $morph = null, $type = '')
+ public function morphMany(string $model, $morph = null, string $type = ''): MorphMany
{
// 记录当前关联信息
$model = $this->parseModel($model);
if (is_null($morph)) {
- $trace = debug_backtrace(false, 2);
- $morph = Db::parseName($trace[1]['function']);
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+ $morph = Str::snake($trace[1]['function']);
}
$type = $type ?: get_class($this);
if (is_array($morph)) {
- list($morphType, $foreignKey) = $morph;
+ [$morphType, $foreignKey] = $morph;
} else {
$morphType = $morph . '_type';
$foreignKey = $morph . '_id';
@@ -494,14 +593,14 @@ trait RelationShip
/**
* MORPH TO 关联定义
* @access public
- * @param string|array $morph 多态字段信息
- * @param array $alias 多态别名定义
+ * @param string|array $morph 多态字段信息
+ * @param array $alias 多态别名定义
* @return MorphTo
*/
- public function morphTo($morph = null, $alias = [])
+ public function morphTo($morph = null, array $alias = []): MorphTo
{
- $trace = debug_backtrace(false, 2);
- $relation = Db::parseName($trace[1]['function']);
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+ $relation = Str::snake($trace[1]['function']);
if (is_null($morph)) {
$morph = $relation;
@@ -509,7 +608,7 @@ trait RelationShip
// 记录当前关联信息
if (is_array($morph)) {
- list($morphType, $foreignKey) = $morph;
+ [$morphType, $foreignKey] = $morph;
} else {
$morphType = $morph . '_type';
$foreignKey = $morph . '_id';
@@ -519,17 +618,76 @@ trait RelationShip
}
/**
- * 解析模型的完整命名空间
+ * MORPH TO MANY关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $middle 中间表名/模型名
+ * @param string|array $morph 多态字段信息
+ * @param string $localKey 当前模型关联键
+ * @return MorphToMany
+ */
+ public function morphToMany(string $model, string $middle, $morph = null, string $localKey = null): MorphToMany
+ {
+ if (is_null($morph)) {
+ $morph = $middle;
+ }
+
+ // 记录当前关联信息
+ if (is_array($morph)) {
+ [$morphType, $morphKey] = $morph;
+ } else {
+ $morphType = $morph . '_type';
+ $morphKey = $morph . '_id';
+ }
+
+ $model = $this->parseModel($model);
+ $name = Str::snake(class_basename($model));
+ $localKey = $localKey ?: $this->getForeignKey($name);
+
+ return new MorphToMany($this, $model, $middle, $morphType, $morphKey, $localKey);
+ }
+
+ /**
+ * MORPH BY MANY关联定义
* @access public
- * @param string $model 模型名(或者完整类名)
+ * @param string $model 模型名
+ * @param string $middle 中间表名/模型名
+ * @param string|array $morph 多态字段信息
+ * @param string $foreignKey 关联外键
+ * @return MorphToMany
+ */
+ public function morphByMany(string $model, string $middle, $morph = null, string $foreignKey = null): MorphToMany
+ {
+ if (is_null($morph)) {
+ $morph = $middle;
+ }
+
+ // 记录当前关联信息
+ if (is_array($morph)) {
+ [$morphType, $morphKey] = $morph;
+ } else {
+ $morphType = $morph . '_type';
+ $morphKey = $morph . '_id';
+ }
+
+ $model = $this->parseModel($model);
+ $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+
+ return new MorphToMany($this, $model, $middle, $morphType, $morphKey, $foreignKey, true);
+ }
+
+ /**
+ * 解析模型的完整命名空间
+ * @access protected
+ * @param string $model 模型名(或者完整类名)
* @return string
*/
- protected function parseModel($model)
+ protected function parseModel(string $model): string
{
if (false === strpos($model, '\\')) {
$path = explode('\\', static::class);
array_pop($path);
- array_push($path, Db::parseName($model, 1));
+ array_push($path, Str::studly($model));
$model = implode('\\', $path);
}
@@ -538,30 +696,30 @@ trait RelationShip
/**
* 获取模型的默认外键名
- * @access public
- * @param string $name 模型名
+ * @access protected
+ * @param string $name 模型名
* @return string
*/
- protected function getForeignKey($name)
+ protected function getForeignKey(string $name): string
{
if (strpos($name, '\\')) {
- $name = basename(str_replace('\\', '/', $name));
+ $name = class_basename($name);
}
- return Db::parseName($name) . '_id';
+ return Str::snake($name) . '_id';
}
/**
* 检查属性是否为关联属性 如果是则返回关联方法名
- * @access public
- * @param string $attr 关联属性名
+ * @access protected
+ * @param string $attr 关联属性名
* @return string|false
*/
- protected function isRelationAttr($attr)
+ protected function isRelationAttr(string $attr)
{
- $relation = Db::parseName($attr, 1, false);
+ $relation = Str::camel($attr);
- if (method_exists($this, $relation) && !method_exists('think\Model', $relation)) {
+ if ((method_exists($this, $relation) && !method_exists('think\Model', $relation)) || isset(static::$macro[static::class][$relation])) {
return $relation;
}
@@ -570,35 +728,34 @@ trait RelationShip
/**
* 智能获取关联模型数据
- * @access public
- * @param Relation $modelRelation 模型关联对象
+ * @access protected
+ * @param Relation $modelRelation 模型关联对象
* @return mixed
*/
protected function getRelationData(Relation $modelRelation)
{
- if ($this->parent && !$modelRelation->isSelfRelation() && get_class($this->parent) == get_class($modelRelation->getModel())) {
- $value = $this->parent;
- } else {
- // 获取关联数据
- $value = $modelRelation->getRelation();
+ if ($this->parent && !$modelRelation->isSelfRelation()
+ && get_class($this->parent) == get_class($modelRelation->getModel())) {
+ return $this->parent;
}
- return $value;
+ // 获取关联数据
+ return $modelRelation->getRelation();
}
/**
* 关联数据自动写入检查
- * @access public
+ * @access protected
* @return void
*/
- protected function checkAutoRelationWrite()
+ protected function checkAutoRelationWrite(): void
{
foreach ($this->together as $key => $name) {
if (is_array($name)) {
if (key($name) === 0) {
$this->relationWrite[$key] = [];
// 绑定关联属性
- foreach ((array) $name as $val) {
+ foreach ($name as $val) {
if (isset($this->data[$val])) {
$this->relationWrite[$key][$val] = $this->data[$val];
}
@@ -618,18 +775,19 @@ trait RelationShip
/**
* 自动关联数据更新(针对一对一关联)
- * @access public
+ * @access protected
* @return void
*/
- protected function autoRelationUpdate()
+ protected function autoRelationUpdate(): void
{
foreach ($this->relationWrite as $name => $val) {
if ($val instanceof Model) {
- $val->save();
+ $val->exists(true)->save();
} else {
- $model = $this->getRelation($name);
+ $model = $this->getRelation($name, true);
+
if ($model instanceof Model) {
- $model->save($val);
+ $model->exists(true)->save($val);
}
}
}
@@ -637,35 +795,47 @@ trait RelationShip
/**
* 自动关联数据写入(针对一对一关联)
- * @access public
+ * @access protected
* @return void
*/
- protected function autoRelationInsert()
+ protected function autoRelationInsert(): void
{
foreach ($this->relationWrite as $name => $val) {
- $method = Db::parseName($name, 1, false);
+ $method = Str::camel($name);
$this->$method()->save($val);
}
}
/**
* 自动关联数据删除(支持一对一及一对多关联)
- * @access public
+ * @access protected
+ * @param bool $force 强制删除
* @return void
*/
- protected function autoRelationDelete()
+ protected function autoRelationDelete($force = false): void
{
foreach ($this->relationWrite as $key => $name) {
$name = is_numeric($key) ? $name : $key;
- $result = $this->getRelation($name);
+ $result = $this->getRelation($name, true);
if ($result instanceof Model) {
- $result->delete();
+ $result->force($force)->delete();
} elseif ($result instanceof Collection) {
foreach ($result as $model) {
- $model->delete();
+ $model->force($force)->delete();
}
}
}
}
+
+ /**
+ * 移除当前模型的关联属性
+ * @access public
+ * @return $this
+ */
+ public function removeRelation()
+ {
+ $this->relation = [];
+ return $this;
+ }
}
diff --git a/src/model/concern/SoftDelete.php b/src/model/concern/SoftDelete.php
index 0829e4708568db8fce289b1585dec838bf14dda5..b7b10921b2f0b79d4121fa7e8d24c06e14f140c2 100644
--- a/src/model/concern/SoftDelete.php
+++ b/src/model/concern/SoftDelete.php
@@ -1,26 +1,42 @@
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model\concern;
-use think\db\Query;
+use think\db\BaseQuery as Query;
+use think\Model;
/**
* 数据软删除
+ * @mixin Model
+ * @method $this withTrashed()
+ * @method $this onlyTrashed()
*/
trait SoftDelete
{
- /**
- * 是否包含软删除数据
- * @var bool
- */
- protected $withTrashed = false;
+
+ public function db($scope = []): Query
+ {
+ $query = parent::db($scope);
+ $this->withNoTrashed($query);
+ return $query;
+ }
/**
* 判断当前实例是否被软删除
* @access public
- * @return boolean
+ * @return bool
*/
- public function trashed()
+ public function trashed(): bool
{
$field = $this->getDeleteTimeField();
@@ -31,47 +47,18 @@ trait SoftDelete
return false;
}
- /**
- * 查询软删除数据
- * @access public
- * @return Query
- */
- public static function withTrashed()
- {
- $model = new static();
-
- return $model->withTrashedData(true)->db(false);
- }
-
- /**
- * 是否包含软删除数据
- * @access protected
- * @param bool $withTrashed 是否包含软删除数据
- * @return $this
- */
- protected function withTrashedData($withTrashed)
+ public function scopeWithTrashed(Query $query)
{
- $this->withTrashed = $withTrashed;
- return $this;
+ $query->removeOption('soft_delete');
}
- /**
- * 只查询软删除数据
- * @access public
- * @return Query
- */
- public static function onlyTrashed()
+ public function scopeOnlyTrashed(Query $query)
{
- $model = new static();
- $field = $model->getDeleteTimeField(true);
+ $field = $this->getDeleteTimeField(true);
if ($field) {
- return $model
- ->db(false)
- ->useSoftDelete($field, $model->getWithTrashedExp());
+ $query->useSoftDelete($field, $this->getWithTrashedExp());
}
-
- return $model->db(false);
}
/**
@@ -79,32 +66,30 @@ trait SoftDelete
* @access protected
* @return array
*/
- protected function getWithTrashedExp()
+ protected function getWithTrashedExp(): array
{
- return is_null($this->defaultSoftDelete) ?
- ['notnull', ''] : ['<>', $this->defaultSoftDelete];
+ return is_null($this->defaultSoftDelete) ? ['notnull', ''] : ['<>', $this->defaultSoftDelete];
}
/**
* 删除当前的记录
* @access public
- * @param bool $force 是否强制删除
* @return bool
*/
- public function delete($force = false)
+ public function delete(): bool
{
- if (!$this->isExists() || false === $this->trigger('before_delete', $this)) {
+ if (!$this->isExists() || $this->isEmpty() || false === $this->trigger('BeforeDelete')) {
return false;
}
- $force = $force ?: $this->isForce();
$name = $this->getDeleteTimeField();
+ $force = $this->isForce();
if ($name && !$force) {
// 软删除
- $this->data($name, $this->autoWriteTimestamp($name));
+ $this->set($name, $this->autoWriteTimestamp());
- $result = $this->isUpdate()->withEvent(false)->save();
+ $this->exists()->withEvent(false)->save();
$this->withEvent(true);
} else {
@@ -112,18 +97,20 @@ trait SoftDelete
$where = $this->getWhere();
// 删除当前模型数据
- $this->db(false)
+ $this->db()
->where($where)
->removeOption('soft_delete')
->delete();
+
+ $this->lazySave(false);
}
// 关联删除
if (!empty($this->relationWrite)) {
- $this->autoRelationDelete();
+ $this->autoRelationDelete($force);
}
- $this->trigger('after_delete', $this);
+ $this->trigger('AfterDelete');
$this->exists(false);
@@ -134,19 +121,29 @@ trait SoftDelete
* 删除记录
* @access public
* @param mixed $data 主键列表 支持闭包查询条件
- * @param bool $force 是否强制删除
+ * @param bool $force 是否强制删除
* @return bool
*/
- public static function destroy($data, $force = false)
+ public static function destroy($data, bool $force = false): bool
{
- // 包含软删除数据
- $query = self::withTrashed();
+ // 传入空值(包括空字符串和空数组)的时候不会做任何的数据删除操作,但传入0则是有效的
+ if (empty($data) && 0 !== $data) {
+ return false;
+ }
+ $model = (new static());
+
+ $query = $model->db(false);
+
+ // 仅当强制删除时包含软删除数据
+ if ($force) {
+ $query->removeOption('soft_delete');
+ }
if (is_array($data) && key($data) !== 0) {
$query->where($data);
$data = null;
} elseif ($data instanceof \Closure) {
- call_user_func_array($data, [ & $query]);
+ call_user_func_array($data, [&$query]);
$data = null;
} elseif (is_null($data)) {
return false;
@@ -154,10 +151,9 @@ trait SoftDelete
$resultSet = $query->select($data);
- if ($resultSet) {
- foreach ($resultSet as $data) {
- $data->force($force)->delete();
- }
+ foreach ($resultSet as $result) {
+ /** @var Model $result */
+ $result->force($force)->delete();
}
return true;
@@ -169,43 +165,39 @@ trait SoftDelete
* @param array $where 更新条件
* @return bool
*/
- public function restore($where = [])
+ public function restore($where = []): bool
{
$name = $this->getDeleteTimeField();
- if ($name) {
- if (false === $this->trigger('before_restore')) {
- return false;
- }
+ if (!$name || false === $this->trigger('BeforeRestore')) {
+ return false;
+ }
- if (empty($where)) {
- $pk = $this->getPk();
- if (is_string($pk)) {
- $where[] = [$pk, '=', $this->getData($pk)];
- }
+ if (empty($where)) {
+ $pk = $this->getPk();
+ if (is_string($pk)) {
+ $where[] = [$pk, '=', $this->getData($pk)];
}
+ }
- // 恢复删除
- $this->db(false)
- ->where($where)
- ->useSoftDelete($name, $this->getWithTrashedExp())
- ->update([$name => $this->defaultSoftDelete]);
-
- $this->trigger('after_restore');
+ // 恢复删除
+ $this->db(false)
+ ->where($where)
+ ->useSoftDelete($name, $this->getWithTrashedExp())
+ ->update([$name => $this->defaultSoftDelete]);
- return true;
- }
+ $this->trigger('AfterRestore');
- return false;
+ return true;
}
/**
* 获取软删除字段
- * @access public
- * @param bool $read 是否查询操作 写操作的时候会自动去掉表别名
+ * @access protected
+ * @param bool $read 是否查询操作 写操作的时候会自动去掉表别名
* @return string|false
*/
- protected function getDeleteTimeField($read = false)
+ protected function getDeleteTimeField(bool $read = false)
{
$field = property_exists($this, 'deleteTime') && isset($this->deleteTime) ? $this->deleteTime : 'delete_time';
@@ -213,7 +205,7 @@ trait SoftDelete
return false;
}
- if (!strpos($field, '.')) {
+ if (false === strpos($field, '.')) {
$field = '__TABLE__.' . $field;
}
@@ -228,15 +220,16 @@ trait SoftDelete
/**
* 查询的时候默认排除软删除数据
* @access protected
- * @param Query $query
+ * @param Query $query
* @return void
*/
- protected function withNoTrashed($query)
+ protected function withNoTrashed(Query $query): void
{
$field = $this->getDeleteTimeField(true);
if ($field) {
- $query->useSoftDelete($field, $this->defaultSoftDelete);
+ $condition = is_null($this->defaultSoftDelete) ? ['null', ''] : ['=', $this->defaultSoftDelete];
+ $query->useSoftDelete($field, $condition);
}
}
}
diff --git a/src/model/concern/TimeStamp.php b/src/model/concern/TimeStamp.php
index aa6dddb2a73ea8d4c3f96dfddd223f423b94db9c..9440c3e6b6897606f2eed65ce204a1059780be9d 100644
--- a/src/model/concern/TimeStamp.php
+++ b/src/model/concern/TimeStamp.php
@@ -2,42 +2,184 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model\concern;
use DateTime;
+
/**
* 自动时间戳
*/
trait TimeStamp
{
- // 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型
+ /**
+ * 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型
+ * @var bool|string
+ */
protected $autoWriteTimestamp;
- // 创建时间字段
+
+ /**
+ * 创建时间字段 false表示关闭
+ * @var false|string
+ */
protected $createTime = 'create_time';
- // 更新时间字段
+
+ /**
+ * 更新时间字段 false表示关闭
+ * @var false|string
+ */
protected $updateTime = 'update_time';
- // 时间字段取出后的默认时间格式
+
+ /**
+ * 时间字段显示格式
+ * @var string
+ */
protected $dateFormat;
+ /**
+ * 是否需要自动写入时间字段
+ * @access public
+ * @param bool|string $auto
+ * @return $this
+ */
+ public function isAutoWriteTimestamp($auto)
+ {
+ $this->autoWriteTimestamp = $this->checkTimeFieldType($auto);
+
+ return $this;
+ }
+
+ /**
+ * 检测时间字段的实际类型
+ * @access public
+ * @param bool|string $type
+ * @return mixed
+ */
+ protected function checkTimeFieldType($type)
+ {
+ if (true === $type) {
+ if (isset($this->type[$this->createTime])) {
+ $type = $this->type[$this->createTime];
+ } elseif (isset($this->schema[$this->createTime]) && in_array($this->schema[$this->createTime], ['datetime', 'date', 'timestamp', 'int'])) {
+ $type = $this->schema[$this->createTime];
+ } else {
+ $type = $this->getFieldType($this->createTime);
+ }
+ }
+
+ return $type;
+ }
+
+ /**
+ * 设置时间字段名称
+ * @access public
+ * @param string $createTime
+ * @param string $updateTime
+ * @return $this
+ */
+ public function setTimeField(string $createTime, string $updateTime)
+ {
+ $this->createTime = $createTime;
+ $this->updateTime = $updateTime;
+
+ return $this;
+ }
+
+ /**
+ * 获取自动写入时间字段
+ * @access public
+ * @return bool|string
+ */
+ public function getAutoWriteTimestamp()
+ {
+ return $this->autoWriteTimestamp;
+ }
+
+ /**
+ * 设置时间字段格式化
+ * @access public
+ * @param string|false $format
+ * @return $this
+ */
+ public function setDateFormat($format)
+ {
+ $this->dateFormat = $format;
+
+ return $this;
+ }
+
+ /**
+ * 获取自动写入时间字段
+ * @access public
+ * @return string|false
+ */
+ public function getDateFormat()
+ {
+ return $this->dateFormat;
+ }
+
+ /**
+ * 自动写入时间戳
+ * @access protected
+ * @return mixed
+ */
+ protected function autoWriteTimestamp()
+ {
+ // 检测时间字段类型
+ $type = $this->checkTimeFieldType($this->autoWriteTimestamp);
+
+ return is_string($type) ? $this->getTimeTypeValue($type) : time();
+ }
+
+ /**
+ * 获取指定类型的时间字段值
+ * @access protected
+ * @param string $type 时间字段类型
+ * @return mixed
+ */
+ protected function getTimeTypeValue(string $type)
+ {
+ $value = time();
+
+ switch ($type) {
+ case 'datetime':
+ case 'date':
+ case 'timestamp':
+ $value = $this->formatDateTime('Y-m-d H:i:s.u');
+ break;
+ default:
+ if (false !== strpos($type, '\\')) {
+ // 对象数据写入
+ $obj = new $type();
+ if (method_exists($obj, '__toString')) {
+ // 对象数据写入
+ $value = $obj->__toString();
+ }
+ }
+ }
+
+ return $value;
+ }
+
/**
* 时间日期字段格式化处理
* @access protected
* @param mixed $format 日期格式
* @param mixed $time 时间日期表达式
- * @param bool $timestamp 是否进行时间戳转换
+ * @param bool $timestamp 时间表达式是否为时间戳
* @return mixed
*/
- protected function formatDateTime($format, $time = 'now', $timestamp = false)
+ protected function formatDateTime($format, $time = 'now', bool $timestamp = false)
{
if (empty($time)) {
- return;
+ return $time;
}
if (false === $format) {
@@ -46,9 +188,11 @@ trait TimeStamp
return new $format($time);
}
- if ($timestamp) {
+ if ($time instanceof DateTime) {
+ $dateTime = $time;
+ } elseif ($timestamp) {
$dateTime = new DateTime();
- $dateTime->setTimestamp($time);
+ $dateTime->setTimestamp((int) $time);
} else {
$dateTime = new DateTime($time);
}
@@ -56,16 +200,24 @@ trait TimeStamp
return $dateTime->format($format);
}
- protected function checkTimeStampWrite()
+ /**
+ * 获取时间字段值
+ * @access protected
+ * @param mixed $value
+ * @return mixed
+ */
+ protected function getTimestampValue($value)
{
- // 自动写入创建时间和更新时间
- if ($this->autoWriteTimestamp) {
- if ($this->createTime && !isset($this->data[$this->createTime])) {
- $this->data[$this->createTime] = $this->autoWriteTimestamp($this->createTime);
- }
- if ($this->updateTime && !isset($this->data[$this->updateTime])) {
- $this->data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
- }
+ $type = $this->checkTimeFieldType($this->autoWriteTimestamp);
+
+ if (is_string($type) && in_array(strtolower($type), [
+ 'datetime', 'date', 'timestamp',
+ ])) {
+ $value = $this->formatDateTime($this->dateFormat, $value);
+ } else {
+ $value = $this->formatDateTime($this->dateFormat, $value, true);
}
+
+ return $value;
}
}
diff --git a/src/model/concern/Virtual.php b/src/model/concern/Virtual.php
new file mode 100644
index 0000000000000000000000000000000000000000..66cdfb7ea3cfa5f968223eb865c9498f1d4902d3
--- /dev/null
+++ b/src/model/concern/Virtual.php
@@ -0,0 +1,90 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\concern;
+
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
+
+/**
+ * 虚拟模型
+ */
+trait Virtual
+{
+ /**
+ * 获取当前模型的数据库查询对象
+ * @access public
+ * @param array $scope 设置不使用的全局查询范围
+ * @return Query
+ */
+ public function db($scope = []): Query
+ {
+ throw new Exception('virtual model not support db query');
+ }
+
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @param string $field 字段名
+ * @return string|null
+ */
+ public function getFieldType(string $field)
+ {}
+
+ /**
+ * 保存当前数据对象
+ * @access public
+ * @param array $data 数据
+ * @param string $sequence 自增序列名
+ * @return bool
+ */
+ public function save(array $data = [], string $sequence = null): bool
+ {
+ // 数据对象赋值
+ $this->setAttrs($data);
+
+ if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
+ return false;
+ }
+
+ // 写入回调
+ $this->trigger('AfterWrite');
+
+ $this->exists(true);
+
+ return true;
+ }
+
+ /**
+ * 删除当前的记录
+ * @access public
+ * @return bool
+ */
+ public function delete(): bool
+ {
+ if (!$this->isExists() || $this->isEmpty() || false === $this->trigger('BeforeDelete')) {
+ return false;
+ }
+
+ // 关联删除
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationDelete();
+ }
+
+ $this->trigger('AfterDelete');
+
+ $this->exists(false);
+
+ return true;
+ }
+
+}
diff --git a/src/model/relation/BelongsTo.php b/src/model/relation/BelongsTo.php
index 22c84cbf53f9c7a6c6adef5a26719fdab5c4a607..741a2e8d4874a82c43810713943f698a88f0a91a 100644
--- a/src/model/relation/BelongsTo.php
+++ b/src/model/relation/BelongsTo.php
@@ -2,36 +2,41 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model\relation;
-use think\Db;
+use Closure;
+use think\db\BaseQuery as Query;
+use think\helper\Str;
use think\Model;
+/**
+ * BelongsTo关联类
+ */
class BelongsTo extends OneToOne
{
/**
* 架构函数
* @access public
- * @param Model $parent 上级模型对象
- * @param string $model 模型名
- * @param string $foreignKey 关联外键
- * @param string $localKey 关联主键
- * @param string $relation 关联名
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 关联主键
+ * @param string $relation 关联名
*/
- public function __construct(Model $parent, $model, $foreignKey, $localKey, $relation = null)
+ public function __construct(Model $parent, string $model, string $foreignKey, string $localKey, string $relation = null)
{
$this->parent = $parent;
$this->model = $model;
$this->foreignKey = $foreignKey;
$this->localKey = $localKey;
- $this->joinType = 'INNER';
$this->query = (new $model)->db();
$this->relation = $relation;
@@ -42,15 +47,15 @@ class BelongsTo extends OneToOne
/**
* 延迟获取关联数据
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包查询条件
* @access public
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包查询条件
* @return Model
*/
- public function getRelation($subRelation = '', $closure = null)
+ public function getRelation(array $subRelation = [], Closure $closure = null)
{
if ($closure) {
- $closure($this->query);
+ $closure($this->getClosureType($closure));
}
$foreignKey = $this->foreignKey;
@@ -62,7 +67,14 @@ class BelongsTo extends OneToOne
->find();
if ($relationModel) {
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($this->parent, $relationModel);
+ }
+
$relationModel->setParent(clone $this->parent);
+ } else {
+ $relationModel = $this->getDefaultModel();
}
return $relationModel;
@@ -71,15 +83,16 @@ class BelongsTo extends OneToOne
/**
* 创建关联统计子查询
* @access public
- * @param \Closure $closure 闭包
- * @param string $aggregate 聚合查询方法
- * @param string $field 字段
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 聚合字段别名
* @return string
*/
- public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*')
+ public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', &$name = ''): string
{
if ($closure) {
- $closure($this->query);
+ $closure($this->getClosureType($closure), $name);
}
return $this->query
@@ -88,67 +101,111 @@ class BelongsTo extends OneToOne
->$aggregate($field);
}
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 数据对象
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
+ * @return integer
+ */
+ public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
+ {
+ $foreignKey = $this->foreignKey;
+
+ if (!isset($result->$foreignKey)) {
+ return 0;
+ }
+
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
+ }
+
+ return $this->query
+ ->where($this->localKey, '=', $result->$foreignKey)
+ ->$aggregate($field);
+ }
+
/**
* 根据关联条件查询当前模型
* @access public
- * @param string $operator 比较操作符
- * @param integer $count 个数
- * @param string $id 关联表的统计字段
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function has($operator = '>=', $count = 1, $id = '*')
+ public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query
{
$table = $this->query->getTable();
- $model = basename(str_replace('\\', '/', get_class($this->parent)));
- $relation = basename(str_replace('\\', '/', $this->model));
+ $model = class_basename($this->parent);
+ $relation = class_basename($this->model);
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
-
- return $this->parent->db()
- ->alias($model)
- ->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey) {
- $query->table([$table => $relation])
- ->field($relation . '.' . $localKey)
- ->whereExp($model . '.' . $foreignKey, '=' . $relation . '.' . $localKey);
- });
+ $softDelete = $this->query->getOptions('soft_delete');
+ $query = $query ?: $this->parent->db()->alias($model);
+
+ return $query->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey, $softDelete) {
+ $query->table([$table => $relation])
+ ->field($relation . '.' . $localKey)
+ ->whereExp($model . '.' . $foreignKey, '=' . $relation . '.' . $localKey)
+ ->when($softDelete, function ($query) use ($softDelete, $relation) {
+ $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
+ });
+ });
}
/**
* 根据关联条件查询当前模型
* @access public
- * @param mixed $where 查询条件(数组或者闭包)
- * @param mixed $fields 字段
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function hasWhere($where = [], $fields = null)
+ public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null): Query
{
$table = $this->query->getTable();
- $model = basename(str_replace('\\', '/', get_class($this->parent)));
- $relation = basename(str_replace('\\', '/', $this->model));
+ $model = class_basename($this->parent);
+ $relation = class_basename($this->model);
if (is_array($where)) {
$this->getQueryWhere($where, $relation);
+ } elseif ($where instanceof Query) {
+ $where->via($relation);
+ } elseif ($where instanceof Closure) {
+ $where($this->query->via($relation));
+ $where = $this->query;
}
- $fields = $this->getRelationQueryFields($fields, $model);
+ $fields = $this->getRelationQueryFields($fields, $model);
+ $softDelete = $this->query->getOptions('soft_delete');
+ $query = $query ?: $this->parent->db();
- return $this->parent->db()
- ->alias($model)
+ return $query->alias($model)
->field($fields)
- ->join($table . ' ' . $relation, $model . '.' . $this->foreignKey . '=' . $relation . '.' . $this->localKey, $this->joinType)
+ ->join([$table => $relation], $model . '.' . $this->foreignKey . '=' . $relation . '.' . $this->localKey, $joinType ?: $this->joinType)
+ ->when($softDelete, function ($query) use ($softDelete, $relation) {
+ $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
+ })
->where($where);
}
/**
* 预载入关联查询(数据集)
- * @access public
- * @param array $resultSet 数据集
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @access protected
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure)
+ protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
@@ -166,28 +223,25 @@ class BelongsTo extends OneToOne
$data = $this->eagerlyWhere([
[$localKey, 'in', $range],
- ], $localKey, $relation, $subRelation, $closure);
-
- // 关联属性名
- $attr = Db::parseName($relation);
+ ], $localKey, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
// 关联模型
if (!isset($data[$result->$foreignKey])) {
- $relationModel = null;
+ $relationModel = $this->getDefaultModel();
} else {
$relationModel = $data[$result->$foreignKey];
$relationModel->setParent(clone $result);
- $relationModel->isUpdate(true);
+ $relationModel->exists(true);
}
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
if (!empty($this->bindAttr)) {
// 绑定关联属性
- $this->bindAttr($relationModel, $result);
- } else {
- // 设置关联属性
- $result->setRelation($attr, $relationModel);
+ $this->bindAttr($result, $relationModel);
+ $result->hidden([$relation], true);
}
}
}
@@ -195,52 +249,53 @@ class BelongsTo extends OneToOne
/**
* 预载入关联查询(数据)
- * @access public
- * @param Model $result 数据对象
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @access protected
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- protected function eagerlyOne(&$result, $relation, $subRelation, $closure)
+ protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
+ $this->query->removeWhereField($localKey);
+
$data = $this->eagerlyWhere([
[$localKey, '=', $result->$foreignKey],
- ], $localKey, $relation, $subRelation, $closure);
+ ], $localKey, $subRelation, $closure, $cache);
// 关联模型
if (!isset($data[$result->$foreignKey])) {
- $relationModel = null;
+ $relationModel = $this->getDefaultModel();
} else {
$relationModel = $data[$result->$foreignKey];
$relationModel->setParent(clone $result);
- $relationModel->isUpdate(true);
+ $relationModel->exists(true);
}
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
+
if (!empty($this->bindAttr)) {
// 绑定关联属性
- $this->bindAttr($relationModel, $result);
- } else {
- // 设置关联属性
- $result->setRelation(Db::parseName($relation), $relationModel);
+ $this->bindAttr($result, $relationModel);
+ $result->hidden([$relation], true);
}
}
/**
* 添加关联数据
* @access public
- * @param Model $model 关联模型对象
+ * @param Model $model关联模型对象
* @return Model
*/
- public function associate($model)
+ public function associate(Model $model): Model
{
- $foreignKey = $this->foreignKey;
- $pk = $model->getPk();
-
- $this->parent->setAttr($foreignKey, $model->$pk);
+ $this->parent->setAttr($this->foreignKey, $model->getKey());
$this->parent->save();
return $this->parent->setRelation($this->relation, $model);
@@ -251,7 +306,7 @@ class BelongsTo extends OneToOne
* @access public
* @return Model
*/
- public function dissociate()
+ public function dissociate(): Model
{
$foreignKey = $this->foreignKey;
@@ -266,7 +321,7 @@ class BelongsTo extends OneToOne
* @access protected
* @return void
*/
- protected function baseQuery()
+ protected function baseQuery(): void
{
if (empty($this->baseQuery)) {
if (isset($this->parent->{$this->foreignKey})) {
diff --git a/src/model/relation/BelongsToMany.php b/src/model/relation/BelongsToMany.php
index 2fff121bb161896a82eb23dc6bb404512c92dedc..eb6812e378b26c1101b0b207237b0ec359b92ab7 100644
--- a/src/model/relation/BelongsToMany.php
+++ b/src/model/relation/BelongsToMany.php
@@ -2,7 +2,7 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
@@ -11,46 +11,65 @@
namespace think\model\relation;
+use Closure;
use think\Collection;
-use think\Db;
-use think\db\Query;
-use think\Exception;
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
+use think\db\Raw;
use think\Model;
use think\model\Pivot;
use think\model\Relation;
+/**
+ * 多对多关联类
+ */
class BelongsToMany extends Relation
{
- // 中间表表名
+ /**
+ * 中间表表名
+ * @var string
+ */
protected $middle;
- // 中间表模型名称
+
+ /**
+ * 中间表模型名称
+ * @var string
+ */
protected $pivotName;
- // 中间表模型对象
+
+ /**
+ * 中间表模型对象
+ * @var Pivot
+ */
protected $pivot;
- // 中间表数据名称
+
+ /**
+ * 中间表数据名称
+ * @var string
+ */
protected $pivotDataName = 'pivot';
/**
* 架构函数
* @access public
- * @param Model $parent 上级模型对象
- * @param string $model 模型名
- * @param string $table 中间表名
- * @param string $foreignKey 关联模型外键
- * @param string $localKey 当前模型关联键
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $middle 中间表/模型名
+ * @param string $foreignKey 关联模型外键
+ * @param string $localKey 当前模型关联键
*/
- public function __construct(Model $parent, $model, $table, $foreignKey, $localKey)
+ public function __construct(Model $parent, string $model, string $middle, string $foreignKey, string $localKey)
{
$this->parent = $parent;
$this->model = $model;
$this->foreignKey = $foreignKey;
$this->localKey = $localKey;
- if (false !== strpos($table, '\\')) {
- $this->pivotName = $table;
- $this->middle = basename(str_replace('\\', '/', $table));
+ if (false !== strpos($middle, '\\')) {
+ $this->pivotName = $middle;
+ $this->middle = class_basename($middle);
} else {
- $this->middle = $table;
+ $this->middle = $middle;
}
$this->query = (new $model)->db();
@@ -59,10 +78,11 @@ class BelongsToMany extends Relation
/**
* 设置中间表模型
- * @param $pivot
+ * @access public
+ * @param $pivot
* @return $this
*/
- public function pivot($pivot)
+ public function pivot(string $pivot)
{
$this->pivotName = $pivot;
return $this;
@@ -74,177 +94,89 @@ class BelongsToMany extends Relation
* @param string $name
* @return $this
*/
- public function pivotDataName($name)
+ public function name(string $name)
{
$this->pivotDataName = $name;
return $this;
}
- /**
- * 获取中间表更新条件
- * @param $data
- * @return array
- */
- protected function getUpdateWhere($data)
- {
- return [
- $this->localKey => $data[$this->localKey],
- $this->foreignKey => $data[$this->foreignKey],
- ];
- }
-
/**
* 实例化中间表模型
- * @param $data
+ * @access public
+ * @param $data
* @return Pivot
* @throws Exception
*/
- protected function newPivot($data = [], $isUpdate = false)
+ protected function newPivot(array $data = []): Pivot
{
- $class = $this->pivotName ?: '\\think\\model\\Pivot';
+ $class = $this->pivotName ?: Pivot::class;
$pivot = new $class($data, $this->parent, $this->middle);
if ($pivot instanceof Pivot) {
- return $isUpdate ? $pivot->isUpdate(true, $this->getUpdateWhere($data)) : $pivot;
- }
-
- throw new Exception('pivot model must extends: \think\model\Pivot');
- }
-
- /**
- * 合成中间表模型
- * @param array|Collection|Paginator $models
- */
- protected function hydratePivot($models)
- {
- foreach ($models as $model) {
- $pivot = [];
-
- foreach ($model->getData() as $key => $val) {
- if (strpos($key, '__')) {
- list($name, $attr) = explode('__', $key, 2);
-
- if ('pivot' == $name) {
- $pivot[$attr] = $val;
- unset($model->$key);
- }
- }
- }
-
- $model->setRelation($this->pivotDataName, $this->newPivot($pivot, true));
+ return $pivot;
+ } else {
+ throw new Exception('pivot model must extends: \think\model\Pivot');
}
}
- /**
- * 创建关联查询Query对象
- * @return Query
- */
- protected function buildQuery()
- {
- $foreignKey = $this->foreignKey;
- $localKey = $this->localKey;
-
- // 关联查询
- $pk = $this->parent->getPk();
-
- $condition[] = ['pivot.' . $localKey, '=', $this->parent->$pk];
-
- return $this->belongsToManyQuery($foreignKey, $localKey, $condition);
- }
-
/**
* 延迟获取关联数据
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包查询条件
+ * @access public
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包查询条件
* @return Collection
*/
- public function getRelation($subRelation = '', $closure = null)
+ public function getRelation(array $subRelation = [], Closure $closure = null): Collection
{
if ($closure) {
- $closure($this->query);
+ $closure($this->getClosureType($closure));
}
- $result = $this->buildQuery()->relation($subRelation)->select();
- $this->hydratePivot($result);
-
- return $result;
- }
-
- /**
- * 重载select方法
- * @param null $data
- * @return Collection
- */
- public function select($data = null)
- {
- $result = $this->buildQuery()->select($data);
- $this->hydratePivot($result);
-
- return $result;
- }
-
- /**
- * 重载paginate方法
- * @param null $listRows
- * @param bool $simple
- * @param array $config
- * @return Paginator
- */
- public function paginate($listRows = null, $simple = false, $config = [])
- {
- $result = $this->buildQuery()->paginate($listRows, $simple, $config);
- $this->hydratePivot($result);
-
- return $result;
+ return $this->relation($subRelation)
+ ->select()
+ ->setParent(clone $this->parent);
}
/**
- * 重载find方法
- * @param null $data
- * @return Model
+ * 组装Pivot模型
+ * @access public
+ * @param Model $result 模型对象
+ * @return array
*/
- public function find($data = null)
+ protected function matchPivot(Model $result): array
{
- $result = $this->buildQuery()->find($data);
- if ($result) {
- $this->hydratePivot([$result]);
+ $pivot = [];
+ foreach ($result->getData() as $key => $val) {
+ if (strpos($key, '__')) {
+ [$name, $attr] = explode('__', $key, 2);
+
+ if ('pivot' == $name) {
+ $pivot[$attr] = $val;
+ unset($result->$key);
+ }
+ }
}
- return $result;
- }
+ $pivotData = $this->pivot->newInstance($pivot, [
+ [$this->localKey, '=', $this->parent->getKey(), null],
+ [$this->foreignKey, '=', $result->getKey(), null],
+ ]);
- /**
- * 查找多条记录 如果不存在则抛出异常
- * @access public
- * @param array|string|Query|\Closure $data
- * @return Collection
- */
- public function selectOrFail($data = null)
- {
- return $this->failException(true)->select($data);
- }
-
- /**
- * 查找单条记录 如果不存在则抛出异常
- * @access public
- * @param array|string|Query|\Closure $data
- * @return Model
- */
- public function findOrFail($data = null)
- {
- return $this->failException(true)->find($data);
+ $result->setRelation($this->pivotDataName, $pivotData);
+ return $pivot;
}
/**
* 根据关联条件查询当前模型
* @access public
- * @param string $operator 比较操作符
- * @param integer $count 个数
- * @param string $id 关联表的统计字段
- * @param string $joinType JOIN类型
- * @return Query
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
+ * @return Model
*/
- public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ public function has(string $operator = '>=', $count = 1, $id = '*', string $joinType = 'INNER', Query $query = null)
{
return $this->parent;
}
@@ -252,21 +184,24 @@ class BelongsToMany extends Relation
/**
* 根据关联条件查询当前模型
* @access public
- * @param mixed $where 查询条件(数组或者闭包)
- * @param mixed $fields 字段
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
* @throws Exception
*/
- public function hasWhere($where = [], $fields = null)
+ public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: hasWhere');
}
/**
* 设置中间表的查询条件
- * @param string $field
- * @param null $op
- * @param null $condition
+ * @access public
+ * @param string $field
+ * @param string $op
+ * @param mixed $condition
* @return $this
*/
public function wherePivot($field, $op = null, $condition = null)
@@ -278,19 +213,19 @@ class BelongsToMany extends Relation
/**
* 预载入关联查询(数据集)
* @access public
- * @param array $resultSet 数据集
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
+ public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
- $localKey = $this->localKey;
- $foreignKey = $this->foreignKey;
+ $localKey = $this->localKey;
+ $pk = $resultSet[0]->getPk();
+ $range = [];
- $pk = $resultSet[0]->getPk();
- $range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (isset($result->$pk)) {
@@ -302,10 +237,7 @@ class BelongsToMany extends Relation
// 查询关联数据
$data = $this->eagerlyManyToMany([
['pivot.' . $localKey, 'in', $range],
- ], $relation, $subRelation, $closure);
-
- // 关联属性名
- $attr = Db::parseName($relation);
+ ], $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
@@ -313,7 +245,7 @@ class BelongsToMany extends Relation
$data[$result->$pk] = [];
}
- $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk]));
+ $result->setRelation($relation, $this->resultSetBuild($data[$result->$pk], clone $this->parent));
}
}
}
@@ -321,13 +253,14 @@ class BelongsToMany extends Relation
/**
* 预载入关联查询(单个数据)
* @access public
- * @param Model $result 数据对象
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- public function eagerlyResult(&$result, $relation, $subRelation, $closure)
+ public function eagerlyResult(Model $result, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$pk = $result->getPk();
@@ -336,87 +269,102 @@ class BelongsToMany extends Relation
// 查询管理数据
$data = $this->eagerlyManyToMany([
['pivot.' . $this->localKey, '=', $pk],
- ], $relation, $subRelation, $closure);
+ ], $subRelation, $closure, $cache);
// 关联数据封装
if (!isset($data[$pk])) {
$data[$pk] = [];
}
- $result->setRelation(Db::parseName($relation), $this->resultSetBuild($data[$pk]));
+ $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
}
}
/**
* 关联统计
* @access public
- * @param Model $result 数据对象
- * @param \Closure $closure 闭包
+ * @param Model $result 数据对象
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
* @return integer
*/
- public function relationCount($result, $closure)
+ public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): float
{
- $pk = $result->getPk();
- $count = 0;
+ $pk = $result->getPk();
- if (isset($result->$pk)) {
- $pk = $result->$pk;
- $count = $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
- ['pivot.' . $this->localKey, '=', $pk],
- ])->count();
+ if (!isset($result->$pk)) {
+ return 0;
+ }
+
+ $pk = $result->$pk;
+
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
}
- return $count;
+ return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
+ ['pivot.' . $this->localKey, '=', $pk],
+ ])->$aggregate($field);
}
/**
* 获取关联统计子查询
* @access public
- * @param \Closure $closure 闭包
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
* @return string
*/
- public function getRelationCountQuery($closure)
+ public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
+ }
+
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
[
- 'pivot.' . $this->localKey, 'exp', Db::raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk()),
+ 'pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->db(false)->getTable() . '.' . $this->parent->getPk()),
],
- ])->fetchSql()->count();
+ ])->fetchSql()->$aggregate($field);
}
/**
* 多对多 关联模型预查询
- * @access public
- * @param array $where 关联预查询条件
- * @param string $relation 关联名
- * @param string $subRelation 子关联
- * @param \Closure $closure 闭包
+ * @access protected
+ * @param array $where 关联预查询条件
+ * @param array $subRelation 子关联
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return array
*/
- protected function eagerlyManyToMany($where, $relation, $subRelation = '', $closure = null)
+ protected function eagerlyManyToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array
{
+ if ($closure) {
+ $closure($this->getClosureType($closure));
+ }
+
// 预载入关联查询 支持嵌套预载入
- $list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where, $closure)
+ $list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)
->with($subRelation)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select();
// 组装模型数据
- $data = [];
+ $data = [];
+ $withLimit = $this->withLimit ?: $this->query->getOptions('with_limit');
+
foreach ($list as $set) {
- $pivot = [];
- foreach ($set->getData() as $key => $val) {
- if (strpos($key, '__')) {
- list($name, $attr) = explode('__', $key, 2);
- if ('pivot' == $name) {
- $pivot[$attr] = $val;
- unset($set->$key);
- }
- }
- }
+ $pivot = $this->matchPivot($set);
+ $key = $pivot[$this->localKey];
- $set->setRelation($this->pivotDataName, $this->newPivot($pivot, true));
+ if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
+ continue;
+ }
- $data[$pivot[$this->localKey]][] = $set;
+ $data[$key][] = $set;
}
return $data;
@@ -424,42 +372,46 @@ class BelongsToMany extends Relation
/**
* BELONGS TO MANY 关联查询
- * @access public
- * @param string $foreignKey 关联模型关联键
- * @param string $localKey 当前模型关联键
- * @param array $condition 关联查询条件
- * @param \Closure $closure 闭包
+ * @access protected
+ * @param string $foreignKey 关联模型关联键
+ * @param string $localKey 当前模型关联键
+ * @param array $condition 关联查询条件
* @return Query
*/
- protected function belongsToManyQuery($foreignKey, $localKey, $condition = [], $closure = null)
+ protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query
{
- if ($closure) {
- $closure($this->query);
- }
-
// 关联查询封装
- $tableName = $this->query->getTable();
- $table = $this->pivot->getTable();
- $fields = $this->getQueryFields($tableName);
+ if (empty($this->baseQuery)) {
+ $tableName = $this->query->getTable();
+ $table = $this->pivot->db()->getTable();
+
+ if ($this->withoutField) {
+ $this->query->withoutField($this->withoutField);
+ }
- $query = $this->query
- ->field($fields)
- ->field(true, false, $table, 'pivot', 'pivot__');
+ $fields = $this->getQueryFields($tableName);
- if (empty($this->baseQuery)) {
- $relationFk = $this->query->getPk();
- $query->join($table . ' pivot', 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk)
+ $withLimit = $this->withLimit ?: $this->query->getOptions('with_limit');
+ if ($withLimit) {
+ $this->query->limit($withLimit);
+ }
+
+ $this->query
+ ->field($fields)
+ ->tableField(true, $table, 'pivot', 'pivot__')
+ ->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $this->query->getPk())
->where($condition);
+
}
- return $query;
+ return $this->query;
}
/**
* 保存(新增)当前关联数据对象
* @access public
- * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
- * @param array $pivot 中间表额外数据
+ * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
+ * @param array $pivot 中间表额外数据
* @return array|Pivot
*/
public function save($data, array $pivot = [])
@@ -471,18 +423,18 @@ class BelongsToMany extends Relation
/**
* 批量保存当前关联数据对象
* @access public
- * @param array $dataSet 数据集
- * @param array $pivot 中间表额外数据
- * @param bool $samePivot 额外数据是否相同
+ * @param iterable $dataSet 数据集
+ * @param array $pivot 中间表额外数据
+ * @param bool $samePivot 额外数据是否相同
* @return array|false
*/
- public function saveAll(array $dataSet, array $pivot = [], $samePivot = false)
+ public function saveAll(iterable $dataSet, array $pivot = [], bool $samePivot = false)
{
$result = [];
foreach ($dataSet as $key => $data) {
if (!$samePivot) {
- $pivotData = isset($pivot[$key]) ? $pivot[$key] : [];
+ $pivotData = $pivot[$key] ?? [];
} else {
$pivotData = $pivot;
}
@@ -496,12 +448,12 @@ class BelongsToMany extends Relation
/**
* 附加关联的一个中间表数据
* @access public
- * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
- * @param array $pivot 中间表额外数据
+ * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
+ * @param array $pivot 中间表额外数据
* @return array|Pivot
* @throws Exception
*/
- public function attach($data, $pivot = [])
+ public function attach($data, array $pivot = [])
{
if (is_array($data)) {
if (key($data) === 0) {
@@ -516,22 +468,21 @@ class BelongsToMany extends Relation
$id = $data;
} elseif ($data instanceof Model) {
// 根据关联表主键直接写入中间表
- $relationFk = $data->getPk();
- $id = $data->$relationFk;
+ $id = $data->getKey();
}
- if ($id) {
+ if (!empty($id)) {
// 保存中间表数据
- $pk = $this->parent->getPk();
- $pivot[$this->localKey] = $this->parent->$pk;
- $ids = (array) $id;
+ $pivot[$this->localKey] = $this->parent->getKey();
+ $ids = (array) $id;
foreach ($ids as $id) {
$pivot[$this->foreignKey] = $id;
$this->pivot->replace()
->exists(false)
+ ->data([])
->save($pivot);
- $result[] = $this->newPivot($pivot, true);
+ $result[] = $this->newPivot($pivot);
}
if (count($result) == 1) {
@@ -548,9 +499,8 @@ class BelongsToMany extends Relation
/**
* 判断是否存在关联数据
* @access public
- * @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键
- * @return Pivot
- * @throws Exception
+ * @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键
+ * @return Pivot|false
*/
public function attached($data)
{
@@ -571,11 +521,11 @@ class BelongsToMany extends Relation
/**
* 解除关联的一个中间表数据
* @access public
- * @param integer|array $data 数据 可以使用关联对象的主键
- * @param bool $relationDel 是否同时删除关联表数据
+ * @param integer|array $data 数据 可以使用关联对象的主键
+ * @param bool $relationDel 是否同时删除关联表数据
* @return integer
*/
- public function detach($data = null, $relationDel = false)
+ public function detach($data = null, bool $relationDel = false): int
{
if (is_array($data)) {
$id = $data;
@@ -584,13 +534,12 @@ class BelongsToMany extends Relation
$id = $data;
} elseif ($data instanceof Model) {
// 根据关联表主键直接写入中间表
- $relationFk = $data->getPk();
- $id = $data->$relationFk;
+ $id = $data->getKey();
}
// 删除中间表数据
- $pk = $this->parent->getPk();
- $pivot[] = [$this->localKey, '=', $this->parent->$pk];
+ $pivot = [];
+ $pivot[] = [$this->localKey, '=', $this->parent->getKey()];
if (isset($id)) {
$pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id];
@@ -609,11 +558,12 @@ class BelongsToMany extends Relation
/**
* 数据同步
- * @param array $ids
- * @param bool $detaching
+ * @access public
+ * @param array $ids
+ * @param bool $detaching
* @return array
*/
- public function sync($ids, $detaching = true)
+ public function sync(array $ids, bool $detaching = true): array
{
$changes = [
'attached' => [],
@@ -621,10 +571,8 @@ class BelongsToMany extends Relation
'updated' => [],
];
- $pk = $this->parent->getPk();
-
$current = $this->pivot
- ->where($this->localKey, $this->parent->$pk)
+ ->where($this->localKey, $this->parent->getKey())
->column($this->foreignKey);
$records = [];
@@ -661,15 +609,25 @@ class BelongsToMany extends Relation
* @access protected
* @return void
*/
- protected function baseQuery()
+ protected function baseQuery(): void
{
- if (empty($this->baseQuery) && $this->parent->getData()) {
- $pk = $this->parent->getPk();
- $table = $this->pivot->getTable();
+ if (empty($this->baseQuery)) {
+ $foreignKey = $this->foreignKey;
+ $localKey = $this->localKey;
+
+ $this->query->filter(function ($result, $options) {
+ $this->matchPivot($result);
+ });
+
+ // 关联查询
+ if (null === $this->parent->getKey()) {
+ $condition = ['pivot.' . $localKey, 'exp', new Raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk())];
+ } else {
+ $condition = ['pivot.' . $localKey, '=', $this->parent->getKey()];
+ }
+
+ $this->belongsToManyQuery($foreignKey, $localKey, [$condition]);
- $this->query
- ->join($table . ' pivot', 'pivot.' . $this->foreignKey . '=' . $this->query->getTable() . '.' . $this->query->getPk())
- ->where('pivot.' . $this->localKey, $this->parent->$pk);
$this->baseQuery = true;
}
}
diff --git a/src/model/relation/HasMany.php b/src/model/relation/HasMany.php
index aec4da612c6016275ca7af3d0e4d3ebf0e22471a..dc034e4966d06d164387488d350029e848aecaa8 100644
--- a/src/model/relation/HasMany.php
+++ b/src/model/relation/HasMany.php
@@ -2,31 +2,37 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model\relation;
-use think\Db;
-use think\db\Query;
+use Closure;
+use think\Collection;
+use think\db\BaseQuery as Query;
+use think\helper\Str;
use think\Model;
use think\model\Relation;
+/**
+ * 一对多关联类
+ */
class HasMany extends Relation
{
/**
* 架构函数
* @access public
- * @param Model $parent 上级模型对象
- * @param string $model 模型名
- * @param string $foreignKey 关联外键
- * @param string $localKey 当前模型主键
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前模型主键
*/
- public function __construct(Model $parent, $model, $foreignKey, $localKey)
+ public function __construct(Model $parent, string $model, string $foreignKey, string $localKey)
{
$this->parent = $parent;
$this->model = $model;
@@ -41,40 +47,40 @@ class HasMany extends Relation
/**
* 延迟获取关联数据
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包查询条件
- * @return \think\Collection
+ * @access public
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包查询条件
+ * @return Collection
*/
- public function getRelation($subRelation = '', $closure = null)
+ public function getRelation(array $subRelation = [], Closure $closure = null): Collection
{
if ($closure) {
- $closure($this->query);
+ $closure($this->getClosureType($closure));
}
- $list = $this->query
- ->where($this->foreignKey, $this->parent->{$this->localKey})
- ->relation($subRelation)
- ->select();
-
- $parent = clone $this->parent;
-
- foreach ($list as &$model) {
- $model->setParent($parent);
+ $withLimit = $this->withLimit ?: $this->query->getOptions('with_limit');
+ if ($withLimit) {
+ $this->query->limit($withLimit);
}
- return $list;
+ return $this->query
+ ->where($this->foreignKey, $this->parent->{$this->localKey})
+ ->relation($subRelation)
+ ->select()
+ ->setParent(clone $this->parent);
}
/**
* 预载入关联查询
* @access public
- * @param array $resultSet 数据集
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
+ public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$range = [];
@@ -87,13 +93,9 @@ class HasMany extends Relation
}
if (!empty($range)) {
- $where = [
+ $data = $this->eagerlyOneToMany([
[$this->foreignKey, 'in', $range],
- ];
- $data = $this->eagerlyOneToMany($where, $relation, $subRelation, $closure);
-
- // 关联属性名
- $attr = Db::parseName($relation);
+ ], $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
@@ -102,11 +104,7 @@ class HasMany extends Relation
$data[$pk] = [];
}
- foreach ($data[$pk] as &$relationModel) {
- $relationModel->setParent(clone $result);
- }
-
- $result->setRelation($attr, $this->resultSetBuild($data[$pk]));
+ $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
}
}
}
@@ -114,87 +112,90 @@ class HasMany extends Relation
/**
* 预载入关联查询
* @access public
- * @param Model $result 数据对象
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- public function eagerlyResult(&$result, $relation, $subRelation, $closure)
+ public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
if (isset($result->$localKey)) {
- $pk = $result->$localKey;
- $where = [
+ $pk = $result->$localKey;
+ $data = $this->eagerlyOneToMany([
[$this->foreignKey, '=', $pk],
- ];
- $data = $this->eagerlyOneToMany($where, $relation, $subRelation, $closure);
+ ], $subRelation, $closure, $cache);
// 关联数据封装
if (!isset($data[$pk])) {
$data[$pk] = [];
}
- foreach ($data[$pk] as &$relationModel) {
- $relationModel->setParent(clone $result);
- }
-
- $result->setRelation(Db::parseName($relation), $this->resultSetBuild($data[$pk]));
+ $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
}
}
/**
* 关联统计
* @access public
- * @param Model $result 数据对象
- * @param \Closure $closure 闭包
+ * @param Model $result 数据对象
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
* @return integer
*/
- public function relationCount($result, $closure)
+ public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$localKey = $this->localKey;
- $count = 0;
- if (isset($result->$localKey)) {
- if ($closure) {
- $closure($this->query);
- }
+ if (!isset($result->$localKey)) {
+ return 0;
+ }
- $count = $this->query->where($this->foreignKey, '=', $result->$localKey)->count();
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
}
- return $count;
+ return $this->query
+ ->where($this->foreignKey, '=', $result->$localKey)
+ ->$aggregate($field);
}
/**
* 创建关联统计子查询
* @access public
- * @param \Closure $closure 闭包
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
* @return string
*/
- public function getRelationCountQuery($closure)
+ public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
- $closure($this->query);
+ $closure($this->getClosureType($closure), $name);
}
- return $this->query
- ->whereExp($this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->parent->getPk())
+ return $this->query->alias($aggregate . '_table')
+ ->whereExp($aggregate . '_table.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey)
->fetchSql()
- ->count();
+ ->$aggregate($field);
}
/**
* 一对多 关联模型预查询
* @access public
- * @param array $where 关联预查询条件
- * @param string $relation 关联名
- * @param string $subRelation 子关联
- * @param \Closure $closure
+ * @param array $where 关联预查询条件
+ * @param array $subRelation 子关联
+ * @param Closure $closure
+ * @param array $cache 关联缓存
* @return array
*/
- protected function eagerlyOneToMany($where, $relation, $subRelation = '', $closure = null)
+ protected function eagerlyOneToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array
{
$foreignKey = $this->foreignKey;
@@ -202,16 +203,32 @@ class HasMany extends Relation
// 预载入关联查询 支持嵌套预载入
if ($closure) {
- $closure($this->query);
+ $this->baseQuery = true;
+ $closure($this->getClosureType($closure));
}
- $list = $this->query->where($where)->with($subRelation)->select();
+ if ($this->withoutField) {
+ $this->query->withoutField($this->withoutField);
+ }
+
+ $list = $this->query
+ ->where($where)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
+ ->with($subRelation)
+ ->select();
// 组装模型数据
- $data = [];
+ $data = [];
+ $withLimit = $this->withLimit ?: $this->query->getOptions('with_limit');
foreach ($list as $set) {
- $data[$set->$foreignKey][] = $set;
+ $key = $set->$foreignKey;
+
+ if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
+ continue;
+ }
+
+ $data[$key][] = $set;
}
return $data;
@@ -220,11 +237,23 @@ class HasMany extends Relation
/**
* 保存(新增)当前关联数据对象
* @access public
- * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
- * @param boolean $replace 是否自动识别更新和写入
+ * @param mixed $data 数据 可以使用数组 关联模型对象
+ * @param boolean $replace 是否自动识别更新和写入
* @return Model|false
*/
- public function save($data, $replace = true)
+ public function save($data, bool $replace = true)
+ {
+ $model = $this->make($data);
+
+ return $model->replace($replace)->save() ? $model : false;
+ }
+
+ /**
+ * 创建关联对象实例
+ * @param array|Model $data
+ * @return Model
+ */
+ public function make($data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
@@ -233,19 +262,17 @@ class HasMany extends Relation
// 保存关联表数据
$data[$this->foreignKey] = $this->parent->{$this->localKey};
- $model = new $this->model;
-
- return $model->replace($replace)->save($data) ? $model : false;
+ return new $this->model($data);
}
/**
* 批量保存当前关联数据对象
* @access public
- * @param array $dataSet 数据集
- * @param boolean $replace 是否自动识别更新和写入
+ * @param iterable $dataSet 数据集
+ * @param boolean $replace 是否自动识别更新和写入
* @return array|false
*/
- public function saveAll(array $dataSet, $replace = true)
+ public function saveAll(iterable $dataSet, bool $replace = true)
{
$result = [];
@@ -259,22 +286,32 @@ class HasMany extends Relation
/**
* 根据关联条件查询当前模型
* @access public
- * @param string $operator 比较操作符
- * @param integer $count 个数
- * @param string $id 关联表的统计字段
- * @param string $joinType JOIN类型
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = 'INNER', Query $query = null): Query
{
- $table = $this->query->getTable();
- $model = basename(str_replace('\\', '/', get_class($this->parent)));
- $relation = basename(str_replace('\\', '/', $this->model));
+ $table = $this->query->getTable();
+
+ $model = class_basename($this->parent);
+ $relation = class_basename($this->model);
+
+ if ('*' != $id) {
+ $id = $relation . '.' . (new $this->model)->getPk();
+ }
+
+ $softDelete = $this->query->getOptions('soft_delete');
+ $query = $query ?: $this->parent->db()->alias($model);
- return $this->parent->db()
- ->alias($model)
- ->field($model . '.*')
- ->join($table . ' ' . $relation, $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType)
+ return $query->field($model . '.*')
+ ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType)
+ ->when($softDelete, function ($query) use ($softDelete, $relation) {
+ $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
+ })
->group($relation . '.' . $this->foreignKey)
->having('count(' . $id . ')' . $operator . $count);
}
@@ -282,26 +319,38 @@ class HasMany extends Relation
/**
* 根据关联条件查询当前模型
* @access public
- * @param mixed $where 查询条件(数组或者闭包)
- * @param mixed $fields 字段
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function hasWhere($where = [], $fields = null)
+ public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null): Query
{
$table = $this->query->getTable();
- $model = basename(str_replace('\\', '/', get_class($this->parent)));
- $relation = basename(str_replace('\\', '/', $this->model));
+ $model = class_basename($this->parent);
+ $relation = class_basename($this->model);
if (is_array($where)) {
$this->getQueryWhere($where, $relation);
+ } elseif ($where instanceof Query) {
+ $where->via($relation);
+ } elseif ($where instanceof Closure) {
+ $where($this->query->via($relation));
+ $where = $this->query;
}
- $fields = $this->getRelationQueryFields($fields, $model);
+ $fields = $this->getRelationQueryFields($fields, $model);
+ $softDelete = $this->query->getOptions('soft_delete');
+ $query = $query ?: $this->parent->db();
- return $this->parent->db()
- ->alias($model)
+ return $query->alias($model)
+ ->group($model . '.' . $this->localKey)
->field($fields)
- ->join($table . ' ' . $relation, $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey)
+ ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType)
+ ->when($softDelete, function ($query) use ($softDelete, $relation) {
+ $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
+ })
->where($where);
}
@@ -310,7 +359,7 @@ class HasMany extends Relation
* @access protected
* @return void
*/
- protected function baseQuery()
+ protected function baseQuery(): void
{
if (empty($this->baseQuery)) {
if (isset($this->parent->{$this->localKey})) {
diff --git a/src/model/relation/HasManyThrough.php b/src/model/relation/HasManyThrough.php
index 7108b624ea620525f584a4c85cb8470decc963a4..2d6fa583e52980081655e9afd2c2a5c3ee089fa1 100644
--- a/src/model/relation/HasManyThrough.php
+++ b/src/model/relation/HasManyThrough.php
@@ -2,7 +2,7 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
@@ -11,132 +11,375 @@
namespace think\model\relation;
-use think\db\Query;
-use think\Exception;
-use think\Db;
+use Closure;
+use think\Collection;
+use think\db\BaseQuery as Query;
+use think\helper\Str;
use think\Model;
use think\model\Relation;
+/**
+ * 远程一对多关联类
+ */
class HasManyThrough extends Relation
{
- // 中间关联表外键
+ /**
+ * 中间关联表外键
+ * @var string
+ */
protected $throughKey;
- // 中间表模型
+
+ /**
+ * 中间主键
+ * @var string
+ */
+ protected $throughPk;
+
+ /**
+ * 中间表查询对象
+ * @var Query
+ */
protected $through;
/**
* 架构函数
- * @access public
- * @param Model $parent 上级模型对象
- * @param string $model 模型名
- * @param string $through 中间模型名
- * @param string $foreignKey 关联外键
- * @param string $throughKey 关联外键
- * @param string $localKey 当前主键
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 关联模型名
+ * @param string $through 中间模型名
+ * @param string $foreignKey 关联外键
+ * @param string $throughKey 中间关联外键
+ * @param string $localKey 当前模型主键
+ * @param string $throughPk 中间模型主键
*/
- public function __construct(Model $parent, $model, $through, $foreignKey, $throughKey, $localKey)
+ public function __construct(Model $parent, string $model, string $through, string $foreignKey, string $throughKey, string $localKey, string $throughPk)
{
$this->parent = $parent;
$this->model = $model;
- $this->through = $through;
+ $this->through = (new $through)->db();
$this->foreignKey = $foreignKey;
$this->throughKey = $throughKey;
$this->localKey = $localKey;
+ $this->throughPk = $throughPk;
$this->query = (new $model)->db();
}
/**
* 延迟获取关联数据
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包查询条件
- * @return \think\Collection
+ * @access public
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包查询条件
+ * @return Collection
*/
- public function getRelation($subRelation = '', $closure = null)
+ public function getRelation(array $subRelation = [], Closure $closure = null)
{
if ($closure) {
- $closure($this->query);
+ $closure($this->getClosureType($closure));
}
$this->baseQuery();
- return $this->query->relation($subRelation)->select();
+ $withLimit = $this->withLimit ?: $this->query->getOptions('with_limit');
+ if ($withLimit) {
+ $this->query->limit($withLimit);
+ }
+
+ return $this->query->relation($subRelation)
+ ->select()
+ ->setParent(clone $this->parent);
}
/**
* 根据关联条件查询当前模型
* @access public
- * @param string $operator 比较操作符
- * @param integer $count 个数
- * @param string $id 关联表的统计字段
- * @param string $joinType JOIN类型
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query
{
- return $this->parent;
+ $model = Str::snake(class_basename($this->parent));
+ $throughTable = $this->through->getTable();
+ $pk = $this->throughPk;
+ $throughKey = $this->throughKey;
+ $relation = new $this->model;
+ $relationTable = $relation->getTable();
+ $softDelete = $this->query->getOptions('soft_delete');
+
+ if ('*' != $id) {
+ $id = $relationTable . '.' . $relation->getPk();
+ }
+ $query = $query ?: $this->parent->db()->alias($model);
+
+ return $query->field($model . '.*')
+ ->join($throughTable, $throughTable . '.' . $this->foreignKey . '=' . $model . '.' . $this->localKey)
+ ->join($relationTable, $relationTable . '.' . $throughKey . '=' . $throughTable . '.' . $this->throughPk)
+ ->when($softDelete, function ($query) use ($softDelete, $relationTable) {
+ $query->where($relationTable . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
+ })
+ ->group($relationTable . '.' . $this->throughKey)
+ ->having('count(' . $id . ')' . $operator . $count);
}
/**
* 根据关联条件查询当前模型
* @access public
- * @param mixed $where 查询条件(数组或者闭包)
- * @param mixed $fields 字段
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function hasWhere($where = [], $fields = null)
+ public function hasWhere($where = [], $fields = null, $joinType = '', Query $query = null): Query
{
- throw new Exception('relation not support: hasWhere');
+ $model = Str::snake(class_basename($this->parent));
+ $throughTable = $this->through->getTable();
+ $pk = $this->throughPk;
+ $throughKey = $this->throughKey;
+ $modelTable = (new $this->model)->getTable();
+
+ if (is_array($where)) {
+ $this->getQueryWhere($where, $modelTable);
+ } elseif ($where instanceof Query) {
+ $where->via($modelTable);
+ } elseif ($where instanceof Closure) {
+ $where($this->query->via($modelTable));
+ $where = $this->query;
+ }
+
+ $fields = $this->getRelationQueryFields($fields, $model);
+ $softDelete = $this->query->getOptions('soft_delete');
+ $query = $query ?: $this->parent->db();
+
+ return $query->alias($model)
+ ->join($throughTable, $throughTable . '.' . $this->foreignKey . '=' . $model . '.' . $this->localKey)
+ ->join($modelTable, $modelTable . '.' . $throughKey . '=' . $throughTable . '.' . $this->throughPk, $joinType)
+ ->when($softDelete, function ($query) use ($softDelete, $modelTable) {
+ $query->where($modelTable . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
+ })
+ ->group($modelTable . '.' . $this->throughKey)
+ ->where($where)
+ ->field($fields);
}
/**
- * 预载入关联查询
- * @access public
- * @param array $resultSet 数据集
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * 预载入关联查询(数据集)
+ * @access protected
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
- {}
+ public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
+ {
+ $localKey = $this->localKey;
+ $foreignKey = $this->foreignKey;
+
+ $range = [];
+ foreach ($resultSet as $result) {
+ // 获取关联外键列表
+ if (isset($result->$localKey)) {
+ $range[] = $result->$localKey;
+ }
+ }
+
+ if (!empty($range)) {
+ $this->query->removeWhereField($foreignKey);
+
+ $data = $this->eagerlyWhere([
+ [$this->foreignKey, 'in', $range],
+ ], $foreignKey, $subRelation, $closure, $cache);
+
+ // 关联数据封装
+ foreach ($resultSet as $result) {
+ $pk = $result->$localKey;
+ if (!isset($data[$pk])) {
+ $data[$pk] = [];
+ }
+
+ // 设置关联属性
+ $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
+ }
+ }
+ }
/**
- * 预载入关联查询 返回模型对象
- * @access public
- * @param Model $result 数据对象
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * 预载入关联查询(数据)
+ * @access protected
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- public function eagerlyResult(&$result, $relation, $subRelation, $closure)
- {}
+ public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
+ {
+ $localKey = $this->localKey;
+ $foreignKey = $this->foreignKey;
+ $pk = $result->$localKey;
+
+ $this->query->removeWhereField($foreignKey);
+
+ $data = $this->eagerlyWhere([
+ [$foreignKey, '=', $pk],
+ ], $foreignKey, $subRelation, $closure, $cache);
+
+ // 关联数据封装
+ if (!isset($data[$pk])) {
+ $data[$pk] = [];
+ }
+
+ $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
+ }
+
+ /**
+ * 关联模型预查询
+ * @access public
+ * @param array $where 关联预查询条件
+ * @param string $key 关联键名
+ * @param array $subRelation 子关联
+ * @param Closure $closure
+ * @param array $cache 关联缓存
+ * @return array
+ */
+ protected function eagerlyWhere(array $where, string $key, array $subRelation = [], Closure $closure = null, array $cache = []): array
+ {
+ // 预载入关联查询 支持嵌套预载入
+ $throughList = $this->through->where($where)->select();
+ $keys = $throughList->column($this->throughPk, $this->throughPk);
+
+ if ($closure) {
+ $this->baseQuery = true;
+ $closure($this->getClosureType($closure));
+ }
+
+ $throughKey = $this->throughKey;
+
+ if ($this->baseQuery) {
+ $throughKey = Str::snake(class_basename($this->model)) . "." . $this->throughKey;
+ }
+
+ $list = $this->query
+ ->where($throughKey, 'in', $keys)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
+ ->select();
+
+ // 组装模型数据
+ $data = [];
+ $keys = $throughList->column($this->foreignKey, $this->throughPk);
+
+ foreach ($list as $set) {
+ $key = $keys[$set->{$this->throughKey}];
+
+ if ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->withLimit) {
+ continue;
+ }
+
+ $data[$key][] = $set;
+ }
+
+ return $data;
+ }
/**
* 关联统计
* @access public
- * @param Model $result 数据对象
- * @param \Closure $closure 闭包
- * @return integer
+ * @param Model $result 数据对象
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
+ * @return mixed
+ */
+ public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
+ {
+ $localKey = $this->localKey;
+
+ if (!isset($result->$localKey)) {
+ return 0;
+ }
+
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
+ }
+
+ $alias = Str::snake(class_basename($this->model));
+ $throughTable = $this->through->getTable();
+ $pk = $this->throughPk;
+ $throughKey = $this->throughKey;
+ $modelTable = $this->parent->getTable();
+
+ if (false === strpos($field, '.')) {
+ $field = $alias . '.' . $field;
+ }
+
+ return $this->query
+ ->alias($alias)
+ ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
+ ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
+ ->where($throughTable . '.' . $this->foreignKey, $result->$localKey)
+ ->$aggregate($field);
+ }
+
+ /**
+ * 创建关联统计子查询
+ * @access public
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
+ * @return string
*/
- public function relationCount($result, $closure)
- {}
+ public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
+ {
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
+ }
+
+ $alias = Str::snake(class_basename($this->model));
+ $throughTable = $this->through->getTable();
+ $pk = $this->throughPk;
+ $throughKey = $this->throughKey;
+ $modelTable = $this->parent->getTable();
+
+ if (false === strpos($field, '.')) {
+ $field = $alias . '.' . $field;
+ }
+
+ return $this->query
+ ->alias($alias)
+ ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
+ ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
+ ->whereExp($throughTable . '.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey)
+ ->fetchSql()
+ ->$aggregate($field);
+ }
/**
* 执行基础查询(仅执行一次)
* @access protected
* @return void
*/
- protected function baseQuery()
+ protected function baseQuery(): void
{
if (empty($this->baseQuery) && $this->parent->getData()) {
- $through = $this->through;
- $alias = Db::parseName(basename(str_replace('\\', '/', $this->model)));
- $throughTable = $through::getTable();
- $pk = (new $through)->getPk();
+ $alias = Str::snake(class_basename($this->model));
+ $throughTable = $this->through->getTable();
+ $pk = $this->throughPk;
$throughKey = $this->throughKey;
$modelTable = $this->parent->getTable();
- $fields = $this->getQueryFields($alias);
+
+ if ($this->withoutField) {
+ $this->query->withoutField($this->withoutField);
+ }
+
+ $fields = $this->getQueryFields($alias);
$this->query
->field($fields)
diff --git a/src/model/relation/HasOne.php b/src/model/relation/HasOne.php
index 3d13455b9d1d4d0fddfd711e2903ac9b654500f3..5e5e50112782f45e657c59510978cb665951f6b7 100644
--- a/src/model/relation/HasOne.php
+++ b/src/model/relation/HasOne.php
@@ -2,36 +2,40 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types = 1);
namespace think\model\relation;
-use think\Db;
-use think\db\Query;
+use Closure;
+use think\db\BaseQuery as Query;
+use think\helper\Str;
use think\Model;
+/**
+ * HasOne 关联类
+ */
class HasOne extends OneToOne
{
/**
* 架构函数
* @access public
- * @param Model $parent 上级模型对象
- * @param string $model 模型名
- * @param string $foreignKey 关联外键
- * @param string $localKey 当前模型主键
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前模型主键
*/
- public function __construct(Model $parent, $model, $foreignKey, $localKey)
+ public function __construct(Model $parent, string $model, string $foreignKey, string $localKey)
{
$this->parent = $parent;
$this->model = $model;
$this->foreignKey = $foreignKey;
$this->localKey = $localKey;
- $this->joinType = 'INNER';
$this->query = (new $model)->db();
if (get_class($parent) == $model) {
@@ -41,16 +45,17 @@ class HasOne extends OneToOne
/**
* 延迟获取关联数据
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包查询条件
+ * @access public
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包查询条件
* @return Model
*/
- public function getRelation($subRelation = '', $closure = null)
+ public function getRelation(array $subRelation = [], Closure $closure = null)
{
$localKey = $this->localKey;
if ($closure) {
- $closure($this->query);
+ $closure($this->getClosureType($closure));
}
// 判断关联类型执行查询
@@ -61,7 +66,14 @@ class HasOne extends OneToOne
->find();
if ($relationModel) {
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($this->parent, $relationModel);
+ }
+
$relationModel->setParent(clone $this->parent);
+ } else {
+ $relationModel = $this->getDefaultModel();
}
return $relationModel;
@@ -70,81 +82,129 @@ class HasOne extends OneToOne
/**
* 创建关联统计子查询
* @access public
- * @param \Closure $closure 闭包
- * @param string $aggregate 聚合查询方法
- * @param string $field 字段
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
* @return string
*/
- public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*')
+ public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
- $closure($this->query);
+ $closure($this->getClosureType($closure), $name);
}
return $this->query
- ->whereExp($this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->parent->getPk())
+ ->whereExp($this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey)
->fetchSql()
->$aggregate($field);
}
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 数据对象
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
+ * @return integer
+ */
+ public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
+ {
+ $localKey = $this->localKey;
+
+ if (!isset($result->$localKey)) {
+ return 0;
+ }
+
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
+ }
+
+ return $this->query
+ ->where($this->foreignKey, '=', $result->$localKey)
+ ->$aggregate($field);
+ }
+
/**
* 根据关联条件查询当前模型
* @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function has()
+ public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query
{
$table = $this->query->getTable();
- $model = basename(str_replace('\\', '/', get_class($this->parent)));
- $relation = basename(str_replace('\\', '/', $this->model));
+ $model = class_basename($this->parent);
+ $relation = class_basename($this->model);
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
+ $softDelete = $this->query->getOptions('soft_delete');
+ $query = $query ?: $this->parent->db()->alias($model);
- return $this->parent->db()
- ->alias($model)
- ->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey) {
- $query->table([$table => $relation])
- ->field($relation . '.' . $foreignKey)
- ->whereExp($model . '.' . $localKey, '=' . $relation . '.' . $foreignKey);
- });
+ return $query->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey, $softDelete) {
+ $query->table([$table => $relation])
+ ->field($relation . '.' . $foreignKey)
+ ->whereExp($model . '.' . $localKey, '=' . $relation . '.' . $foreignKey)
+ ->when($softDelete, function ($query) use ($softDelete, $relation) {
+ $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
+ });
+ });
}
/**
* 根据关联条件查询当前模型
* @access public
- * @param mixed $where 查询条件(数组或者闭包)
- * @param mixed $fields 字段
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function hasWhere($where = [], $fields = null)
+ public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null): Query
{
$table = $this->query->getTable();
- $model = basename(str_replace('\\', '/', get_class($this->parent)));
- $relation = basename(str_replace('\\', '/', $this->model));
+ $model = class_basename($this->parent);
+ $relation = class_basename($this->model);
if (is_array($where)) {
$this->getQueryWhere($where, $relation);
+ } elseif ($where instanceof Query) {
+ $where->via($relation);
+ } elseif ($where instanceof Closure) {
+ $where($this->query->via($relation));
+ $where = $this->query;
}
- $fields = $this->getRelationQueryFields($fields, $model);
+ $fields = $this->getRelationQueryFields($fields, $model);
+ $softDelete = $this->query->getOptions('soft_delete');
+ $query = $query ?: $this->parent->db();
- return $this->parent->db()
- ->alias($model)
+ return $query->alias($model)
->field($fields)
- ->join($table . ' ' . $relation, $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $this->joinType)
+ ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType ?: $this->joinType)
+ ->when($softDelete, function ($query) use ($softDelete, $relation) {
+ $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
+ })
->where($where);
}
/**
* 预载入关联查询(数据集)
- * @access public
- * @param array $resultSet 数据集
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @access protected
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure)
+ protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
@@ -162,28 +222,25 @@ class HasOne extends OneToOne
$data = $this->eagerlyWhere([
[$foreignKey, 'in', $range],
- ], $foreignKey, $relation, $subRelation, $closure);
-
- // 关联属性名
- $attr = Db::parseName($relation);
+ ], $foreignKey, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
// 关联模型
if (!isset($data[$result->$localKey])) {
- $relationModel = null;
+ $relationModel = $this->getDefaultModel();
} else {
$relationModel = $data[$result->$localKey];
$relationModel->setParent(clone $result);
- $relationModel->isUpdate(true);
+ $relationModel->exists(true);
}
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
if (!empty($this->bindAttr)) {
// 绑定关联属性
- $this->bindAttr($relationModel, $result, $this->bindAttr);
- } else {
- // 设置关联属性
- $result->setRelation($attr, $relationModel);
+ $this->bindAttr($result, $relationModel);
+ $result->hidden([$relation], true);
}
}
}
@@ -191,35 +248,41 @@ class HasOne extends OneToOne
/**
* 预载入关联查询(数据)
- * @access public
- * @param Model $result 数据对象
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @access protected
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- protected function eagerlyOne(&$result, $relation, $subRelation, $closure)
+ protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
- $data = $this->eagerlyWhere([
+
+ $this->query->removeWhereField($foreignKey);
+
+ $data = $this->eagerlyWhere([
[$foreignKey, '=', $result->$localKey],
- ], $foreignKey, $relation, $subRelation, $closure);
+ ], $foreignKey, $subRelation, $closure, $cache);
// 关联模型
if (!isset($data[$result->$localKey])) {
- $relationModel = null;
+ $relationModel = $this->getDefaultModel();
} else {
$relationModel = $data[$result->$localKey];
$relationModel->setParent(clone $result);
- $relationModel->isUpdate(true);
+ $relationModel->exists(true);
}
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
+
if (!empty($this->bindAttr)) {
// 绑定关联属性
- $this->bindAttr($relationModel, $result, $this->bindAttr);
- } else {
- $result->setRelation(Db::parseName($relation), $relationModel);
+ $this->bindAttr($result, $relationModel);
+ $result->hidden([$relation], true);
}
}
@@ -228,7 +291,7 @@ class HasOne extends OneToOne
* @access protected
* @return void
*/
- protected function baseQuery()
+ protected function baseQuery(): void
{
if (empty($this->baseQuery)) {
if (isset($this->parent->{$this->localKey})) {
diff --git a/src/model/relation/HasOneThrough.php b/src/model/relation/HasOneThrough.php
new file mode 100644
index 0000000000000000000000000000000000000000..0278533eedcffad3eb8778018759fb76f6944d0a
--- /dev/null
+++ b/src/model/relation/HasOneThrough.php
@@ -0,0 +1,165 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use Closure;
+use think\helper\Str;
+use think\Model;
+
+/**
+ * 远程一对一关联类
+ */
+class HasOneThrough extends HasManyThrough
+{
+
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包查询条件
+ * @return Model
+ */
+ public function getRelation(array $subRelation = [], Closure $closure = null)
+ {
+ if ($closure) {
+ $closure($this->getClosureType($closure));
+ }
+
+ $this->baseQuery();
+
+ $relationModel = $this->query->relation($subRelation)->find();
+
+ if ($relationModel) {
+ $relationModel->setParent(clone $this->parent);
+ } else {
+ $relationModel = $this->getDefaultModel();
+ }
+
+ return $relationModel;
+ }
+
+ /**
+ * 预载入关联查询(数据集)
+ * @access protected
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
+ * @return void
+ */
+ public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
+ {
+ $localKey = $this->localKey;
+ $foreignKey = $this->foreignKey;
+
+ $range = [];
+ foreach ($resultSet as $result) {
+ // 获取关联外键列表
+ if (isset($result->$localKey)) {
+ $range[] = $result->$localKey;
+ }
+ }
+
+ if (!empty($range)) {
+ $this->query->removeWhereField($foreignKey);
+
+ $data = $this->eagerlyWhere([
+ [$this->foreignKey, 'in', $range],
+ ], $foreignKey, $subRelation, $closure, $cache);
+
+ // 关联数据封装
+ foreach ($resultSet as $result) {
+ // 关联模型
+ if (!isset($data[$result->$localKey])) {
+ $relationModel = $this->getDefaultModel();
+ } else {
+ $relationModel = $data[$result->$localKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->exists(true);
+ }
+
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询(数据)
+ * @access protected
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
+ * @return void
+ */
+ public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
+ {
+ $localKey = $this->localKey;
+ $foreignKey = $this->foreignKey;
+
+ $this->query->removeWhereField($foreignKey);
+
+ $data = $this->eagerlyWhere([
+ [$foreignKey, '=', $result->$localKey],
+ ], $foreignKey, $subRelation, $closure, $cache);
+
+ // 关联模型
+ if (!isset($data[$result->$localKey])) {
+ $relationModel = $this->getDefaultModel();
+ } else {
+ $relationModel = $data[$result->$localKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->exists(true);
+ }
+
+ $result->setRelation($relation, $relationModel);
+ }
+
+ /**
+ * 关联模型预查询
+ * @access public
+ * @param array $where 关联预查询条件
+ * @param string $key 关联键名
+ * @param array $subRelation 子关联
+ * @param Closure $closure
+ * @param array $cache 关联缓存
+ * @return array
+ */
+ protected function eagerlyWhere(array $where, string $key, array $subRelation = [], Closure $closure = null, array $cache = []): array
+ {
+ // 预载入关联查询 支持嵌套预载入
+ $keys = $this->through->where($where)->column($this->throughPk, $this->foreignKey);
+
+ if ($closure) {
+ $closure($this->getClosureType($closure));
+ }
+
+ $list = $this->query
+ ->where($this->throughKey, 'in', $keys)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
+ ->select();
+
+ // 组装模型数据
+ $data = [];
+ $keys = array_flip($keys);
+
+ foreach ($list as $set) {
+ $data[$keys[$set->{$this->throughKey}]] = $set;
+ }
+
+ return $data;
+ }
+
+}
diff --git a/src/model/relation/MorphMany.php b/src/model/relation/MorphMany.php
index 2e9f58d66ac2c48499cbfe041ac37ca43d8deabc..5cd3dfc4e88d4ab58ec6a0e2d09a3bc02417c069 100644
--- a/src/model/relation/MorphMany.php
+++ b/src/model/relation/MorphMany.php
@@ -2,7 +2,7 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
@@ -11,30 +11,48 @@
namespace think\model\relation;
-use think\Db;
-use think\db\Query;
-use think\Exception;
+use Closure;
+use think\Collection;
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
+use think\helper\Str;
use think\Model;
use think\model\Relation;
+/**
+ * 多态一对多关联
+ */
class MorphMany extends Relation
{
- // 多态字段
+
+ /**
+ * 多态关联外键
+ * @var string
+ */
protected $morphKey;
+
+ /**
+ * 多态字段名
+ * @var string
+ */
protected $morphType;
- // 多态类型
+
+ /**
+ * 多态类型
+ * @var string
+ */
protected $type;
/**
* 架构函数
* @access public
- * @param Model $parent 上级模型对象
- * @param string $model 模型名
- * @param string $morphKey 关联外键
- * @param string $morphType 多态字段名
- * @param string $type 多态类型
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $morphKey 关联外键
+ * @param string $morphType 多态字段名
+ * @param string $type 多态类型
*/
- public function __construct(Model $parent, $model, $morphKey, $morphType, $type)
+ public function __construct(Model $parent, string $model, string $morphKey, string $morphType, string $type)
{
$this->parent = $parent;
$this->model = $model;
@@ -46,38 +64,40 @@ class MorphMany extends Relation
/**
* 延迟获取关联数据
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包查询条件
- * @return \think\Collection
+ * @access public
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包查询条件
+ * @return Collection
*/
- public function getRelation($subRelation = '', $closure = null)
+ public function getRelation(array $subRelation = [], Closure $closure = null): Collection
{
if ($closure) {
- $closure($this->query);
+ $closure($this->getClosureType($closure));
}
$this->baseQuery();
- $list = $this->query->relation($subRelation)->select();
- $parent = clone $this->parent;
-
- foreach ($list as &$model) {
- $model->setParent($parent);
+ $withLimit = $this->withLimit ?: $this->query->getOptions('with_limit');
+ if ($withLimit) {
+ $this->query->limit($withLimit);
}
- return $list;
+ return $this->query->relation($subRelation)
+ ->select()
+ ->setParent(clone $this->parent);
}
/**
* 根据关联条件查询当前模型
* @access public
- * @param string $operator 比较操作符
- * @param integer $count 个数
- * @param string $id 关联表的统计字段
- * @param string $joinType JOIN类型
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: has');
}
@@ -85,11 +105,13 @@ class MorphMany extends Relation
/**
* 根据关联条件查询当前模型
* @access public
- * @param mixed $where 查询条件(数组或者闭包)
- * @param mixed $fields 字段
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function hasWhere($where = [], $fields = null)
+ public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: hasWhere');
}
@@ -97,13 +119,14 @@ class MorphMany extends Relation
/**
* 预载入关联查询
* @access public
- * @param array $resultSet 数据集
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
+ public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$morphType = $this->morphType;
$morphKey = $this->morphKey;
@@ -123,10 +146,7 @@ class MorphMany extends Relation
[$morphKey, 'in', $range],
[$morphType, '=', $type],
];
- $data = $this->eagerlyMorphToMany($where, $relation, $subRelation, $closure);
-
- // 关联属性名
- $attr = Db::parseName($relation);
+ $data = $this->eagerlyMorphToMany($where, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
@@ -134,12 +154,7 @@ class MorphMany extends Relation
$data[$result->$pk] = [];
}
- foreach ($data[$result->$pk] as &$relationModel) {
- $relationModel->setParent(clone $result);
- $relationModel->isUpdate(true);
- }
-
- $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk]));
+ $result->setRelation($relation, $this->resultSetBuild($data[$result->$pk], clone $this->parent));
}
}
}
@@ -147,111 +162,122 @@ class MorphMany extends Relation
/**
* 预载入关联查询
* @access public
- * @param Model $result 数据对象
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- public function eagerlyResult(&$result, $relation, $subRelation, $closure)
+ public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$pk = $result->getPk();
if (isset($result->$pk)) {
- $key = $result->$pk;
- $where = [
+ $key = $result->$pk;
+ $data = $this->eagerlyMorphToMany([
[$this->morphKey, '=', $key],
[$this->morphType, '=', $this->type],
- ];
- $data = $this->eagerlyMorphToMany($where, $relation, $subRelation, $closure);
+ ], $subRelation, $closure, $cache);
if (!isset($data[$key])) {
$data[$key] = [];
}
- foreach ($data[$key] as &$relationModel) {
- $relationModel->setParent(clone $result);
- $relationModel->isUpdate(true);
- }
-
- $result->setRelation(Db::parseName($relation), $this->resultSetBuild($data[$key]));
+ $result->setRelation($relation, $this->resultSetBuild($data[$key], clone $this->parent));
}
}
/**
* 关联统计
* @access public
- * @param Model $result 数据对象
- * @param \Closure $closure 闭包
- * @return integer
+ * @param Model $result 数据对象
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
+ * @return mixed
*/
- public function relationCount($result, $closure)
+ public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
- $pk = $result->getPk();
- $count = 0;
+ $pk = $result->getPk();
- if (isset($result->$pk)) {
- if ($closure) {
- $closure($this->query);
- }
+ if (!isset($result->$pk)) {
+ return 0;
+ }
- $count = $this->query
- ->where([
- [$this->morphKey, '=', $result->$pk],
- [$this->morphType, '=', $this->type],
- ])
- ->count();
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
}
- return $count;
+ return $this->query
+ ->where([
+ [$this->morphKey, '=', $result->$pk],
+ [$this->morphType, '=', $this->type],
+ ])
+ ->$aggregate($field);
}
/**
* 获取关联统计子查询
* @access public
- * @param \Closure $closure 闭包
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
* @return string
*/
- public function getRelationCountQuery($closure)
+ public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
- $closure($this->query);
+ $closure($this->getClosureType($closure), $name);
}
return $this->query
- ->where([
- [$this->morphKey, 'exp', Db::raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk())],
- [$this->morphType, '=', $this->type],
- ])
+ ->whereExp($this->morphKey, '=' . $this->parent->getTable() . '.' . $this->parent->getPk())
+ ->where($this->morphType, '=', $this->type)
->fetchSql()
- ->count();
+ ->$aggregate($field);
}
/**
* 多态一对多 关联模型预查询
- * @access public
- * @param array $where 关联预查询条件
- * @param string $relation 关联名
- * @param string $subRelation 子关联
- * @param \Closure $closure 闭包
+ * @access protected
+ * @param array $where 关联预查询条件
+ * @param array $subRelation 子关联
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return array
*/
- protected function eagerlyMorphToMany($where, $relation, $subRelation = '', $closure = null)
+ protected function eagerlyMorphToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array
{
// 预载入关联查询 支持嵌套预载入
- $this->query->removeOptions('where');
+ $this->query->removeOption('where');
if ($closure) {
- $closure($this->query);
+ $this->baseQuery = true;
+ $closure($this->getClosureType($closure));
}
- $list = $this->query->where($where)->with($subRelation)->select();
+ $list = $this->query
+ ->where($where)
+ ->with($subRelation)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
+ ->select();
$morphKey = $this->morphKey;
// 组装模型数据
- $data = [];
+ $data = [];
+ $withLimit = $this->withLimit ?: $this->query->getOptions('with_limit');
+
foreach ($list as $set) {
- $data[$set->$morphKey][] = $set;
+ $key = $set->$morphKey;
+
+ if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
+ continue;
+ }
+
+ $data[$key][] = $set;
}
return $data;
@@ -260,10 +286,23 @@ class MorphMany extends Relation
/**
* 保存(新增)当前关联数据对象
* @access public
- * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
+ * @param mixed $data 数据 可以使用数组 关联模型对象
+ * @param bool $replace 是否自动识别更新和写入
* @return Model|false
*/
- public function save($data)
+ public function save($data, bool $replace = true)
+ {
+ $model = $this->make();
+
+ return $model->replace($replace)->save($data) ? $model : false;
+ }
+
+ /**
+ * 创建关联对象实例
+ * @param array|Model $data
+ * @return Model
+ */
+ public function make($data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
@@ -272,37 +311,63 @@ class MorphMany extends Relation
// 保存关联表数据
$pk = $this->parent->getPk();
- $model = new $this->model;
-
$data[$this->morphKey] = $this->parent->$pk;
$data[$this->morphType] = $this->type;
- return $model->save($data) ? $model : false;
+ return new $this->model($data);
}
/**
* 批量保存当前关联数据对象
* @access public
- * @param array $dataSet 数据集
+ * @param iterable $dataSet 数据集
+ * @param boolean $replace 是否自动识别更新和写入
* @return array|false
*/
- public function saveAll(array $dataSet)
+ public function saveAll(iterable $dataSet, bool $replace = true)
{
$result = [];
foreach ($dataSet as $key => $data) {
- $result[] = $this->save($data);
+ $result[] = $this->save($data, $replace);
}
return empty($result) ? false : $result;
}
+ /**
+ * 获取多态关联外键
+ * @return string
+ */
+ public function getMorphKey()
+ {
+ return $this->morphKey;
+ }
+
+ /**
+ * 获取多态字段名
+ * @return string
+ */
+ public function getMorphType()
+ {
+ return $this->morphType;
+ }
+
+ /**
+ * 获取多态类型
+ * @return string
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
/**
* 执行基础查询(仅执行一次)
* @access protected
* @return void
*/
- protected function baseQuery()
+ protected function baseQuery(): void
{
if (empty($this->baseQuery) && $this->parent->getData()) {
$pk = $this->parent->getPk();
diff --git a/src/model/relation/MorphOne.php b/src/model/relation/MorphOne.php
index 6f9fe8cf3e227a16ee5cf70bd294abe7af8ec75d..b5fd5a7407b3bf3c7bde4a63175c4e056b07efbb 100644
--- a/src/model/relation/MorphOne.php
+++ b/src/model/relation/MorphOne.php
@@ -2,7 +2,7 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
@@ -11,30 +11,52 @@
namespace think\model\relation;
-use think\Db;
-use think\db\Query;
-use think\Exception;
+use Closure;
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
+use think\helper\Str;
use think\Model;
use think\model\Relation;
+/**
+ * 多态一对一关联类
+ */
class MorphOne extends Relation
{
- // 多态字段
+ /**
+ * 多态关联外键
+ * @var string
+ */
protected $morphKey;
+
+ /**
+ * 多态字段
+ * @var string
+ */
protected $morphType;
- // 多态类型
+
+ /**
+ * 多态类型
+ * @var string
+ */
protected $type;
+ /**
+ * 绑定的关联属性
+ * @var array
+ */
+ protected $bindAttr = [];
+
/**
* 构造函数
* @access public
- * @param Model $parent 上级模型对象
- * @param string $model 模型名
- * @param string $morphKey 关联外键
- * @param string $morphType 多态字段名
- * @param string $type 多态类型
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $morphKey 关联外键
+ * @param string $morphType 多态字段名
+ * @param string $type 多态类型
*/
- public function __construct(Model $parent, $model, $morphKey, $morphType, $type)
+ public function __construct(Model $parent, string $model, string $morphKey, string $morphType, string $type)
{
$this->parent = $parent;
$this->model = $model;
@@ -46,14 +68,15 @@ class MorphOne extends Relation
/**
* 延迟获取关联数据
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包查询条件
+ * @access public
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包查询条件
* @return Model
*/
- public function getRelation($subRelation = '', $closure = null)
+ public function getRelation(array $subRelation = [], Closure $closure = null)
{
if ($closure) {
- $closure($this->query);
+ $closure($this->getClosureType($closure));
}
$this->baseQuery();
@@ -61,7 +84,14 @@ class MorphOne extends Relation
$relationModel = $this->query->relation($subRelation)->find();
if ($relationModel) {
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($this->parent, $relationModel);
+ }
+
$relationModel->setParent(clone $this->parent);
+ } else {
+ $relationModel = $this->getDefaultModel();
}
return $relationModel;
@@ -70,13 +100,14 @@ class MorphOne extends Relation
/**
* 根据关联条件查询当前模型
* @access public
- * @param string $operator 比较操作符
- * @param integer $count 个数
- * @param string $id 关联表的统计字段
- * @param string $joinType JOIN类型
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null)
{
return $this->parent;
}
@@ -84,11 +115,13 @@ class MorphOne extends Relation
/**
* 根据关联条件查询当前模型
* @access public
- * @param mixed $where 查询条件(数组或者闭包)
- * @param mixed $fields 字段
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
* @return Query
*/
- public function hasWhere($where = [], $fields = null)
+ public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: hasWhere');
}
@@ -96,13 +129,14 @@ class MorphOne extends Relation
/**
* 预载入关联查询
* @access public
- * @param array $resultSet 数据集
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
+ public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$morphType = $this->morphType;
$morphKey = $this->morphKey;
@@ -121,22 +155,25 @@ class MorphOne extends Relation
$data = $this->eagerlyMorphToOne([
[$morphKey, 'in', $range],
[$morphType, '=', $type],
- ], $relation, $subRelation, $closure);
-
- // 关联属性名
- $attr = Db::parseName($relation);
+ ], $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
if (!isset($data[$result->$pk])) {
- $relationModel = null;
+ $relationModel = $this->getDefaultModel();
} else {
$relationModel = $data[$result->$pk];
$relationModel->setParent(clone $result);
- $relationModel->isUpdate(true);
+ $relationModel->exists(true);
}
- $result->setRelation($attr, $relationModel);
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($result, $relationModel);
+ } else {
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
+ }
}
}
}
@@ -144,13 +181,14 @@ class MorphOne extends Relation
/**
* 预载入关联查询
* @access public
- * @param Model $result 数据对象
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return void
*/
- public function eagerlyResult(&$result, $relation, $subRelation, $closure)
+ public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$pk = $result->getPk();
@@ -159,37 +197,48 @@ class MorphOne extends Relation
$data = $this->eagerlyMorphToOne([
[$this->morphKey, '=', $pk],
[$this->morphType, '=', $this->type],
- ], $relation, $subRelation, $closure);
+ ], $subRelation, $closure, $cache);
if (isset($data[$pk])) {
$relationModel = $data[$pk];
$relationModel->setParent(clone $result);
- $relationModel->isUpdate(true);
+ $relationModel->exists(true);
} else {
- $relationModel = null;
+ $relationModel = $this->getDefaultModel();
}
- $result->setRelation(Db::parseName($relation), $relationModel);
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($result, $relationModel);
+ } else {
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
+ }
}
}
/**
* 多态一对一 关联模型预查询
- * @access public
- * @param array $where 关联预查询条件
- * @param string $relation 关联名
- * @param string $subRelation 子关联
- * @param \Closure $closure 闭包
+ * @access protected
+ * @param array $where 关联预查询条件
+ * @param array $subRelation 子关联
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
* @return array
*/
- protected function eagerlyMorphToOne($where, $relation, $subRelation = '', $closure = null)
+ protected function eagerlyMorphToOne(array $where, array $subRelation = [], $closure = null, array $cache = []): array
{
// 预载入关联查询 支持嵌套预载入
if ($closure) {
- $closure($this->query);
+ $this->baseQuery = true;
+ $closure($this->getClosureType($closure));
}
- $list = $this->query->where($where)->with($subRelation)->select();
+ $list = $this->query
+ ->where($where)
+ ->with($subRelation)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
+ ->select();
$morphKey = $this->morphKey;
// 组装模型数据
@@ -205,22 +254,34 @@ class MorphOne extends Relation
/**
* 保存(新增)当前关联数据对象
* @access public
- * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
+ * @param mixed $data 数据 可以使用数组 关联模型对象
+ * @param boolean $replace 是否自动识别更新和写入
* @return Model|false
*/
- public function save($data)
+ public function save($data, bool $replace = true)
+ {
+ $model = $this->make($data);
+ return $model->replace($replace)->save() ? $model : false;
+ }
+
+ /**
+ * 创建关联对象实例
+ * @param array|Model $data
+ * @return Model
+ */
+ public function make($data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
}
+
// 保存关联表数据
$pk = $this->parent->getPk();
- $model = new $this->model;
-
$data[$this->morphKey] = $this->parent->$pk;
$data[$this->morphType] = $this->type;
- return $model->save($data) ? $model : false;
+
+ return new $this->model($data);
}
/**
@@ -228,7 +289,7 @@ class MorphOne extends Relation
* @access protected
* @return void
*/
- protected function baseQuery()
+ protected function baseQuery(): void
{
if (empty($this->baseQuery) && $this->parent->getData()) {
$pk = $this->parent->getPk();
@@ -241,4 +302,48 @@ class MorphOne extends Relation
}
}
+ /**
+ * 绑定关联表的属性到父模型属性
+ * @access public
+ * @param array $attr 要绑定的属性列表
+ * @return $this
+ */
+ public function bind(array $attr)
+ {
+ $this->bindAttr = $attr;
+
+ return $this;
+ }
+
+ /**
+ * 获取绑定属性
+ * @access public
+ * @return array
+ */
+ public function getBindAttr(): array
+ {
+ return $this->bindAttr;
+ }
+
+ /**
+ * 绑定关联属性到父模型
+ * @access protected
+ * @param Model $result 父模型对象
+ * @param Model $model 关联模型对象
+ * @return void
+ * @throws Exception
+ */
+ protected function bindAttr(Model $result, Model $model = null): void
+ {
+ foreach ($this->bindAttr as $key => $attr) {
+ $key = is_numeric($key) ? $attr : $key;
+ $value = $result->getOrigin($key);
+
+ if (!is_null($value)) {
+ throw new Exception('bind attr has exists:' . $key);
+ }
+
+ $result->setAttr($key, $model ? $model->$attr : null);
+ }
+ }
}
diff --git a/src/model/relation/MorphTo.php b/src/model/relation/MorphTo.php
index e72a451b7c28421089617f9f020496ca15d5610f..040f2c42d761a7a8bb944a6f58c95d5a06271b99 100644
--- a/src/model/relation/MorphTo.php
+++ b/src/model/relation/MorphTo.php
@@ -1,303 +1,382 @@
-
-// +----------------------------------------------------------------------
-
-namespace think\model\relation;
-
-use think\Db;
-use think\Exception;
-use think\Model;
-use think\model\Relation;
-
-class MorphTo extends Relation
-{
- // 多态字段
- protected $morphKey;
- protected $morphType;
- // 多态别名
- protected $alias;
- // 关联名
- protected $relation;
-
- /**
- * 架构函数
- * @access public
- * @param Model $parent 上级模型对象
- * @param string $morphType 多态字段名
- * @param string $morphKey 外键名
- * @param array $alias 多态别名定义
- * @param string $relation 关联名
- */
- public function __construct(Model $parent, $morphType, $morphKey, $alias = [], $relation = null)
- {
- $this->parent = $parent;
- $this->morphType = $morphType;
- $this->morphKey = $morphKey;
- $this->alias = $alias;
- $this->relation = $relation;
- }
-
- /**
- * 获取当前的关联模型类的实例
- * @access public
- * @return Model
- */
- public function getModel()
- {
- $morphType = $this->morphType;
- $model = $this->parseModel($this->parent->$morphType);
-
- return (new $model);
- }
-
- /**
- * 延迟获取关联数据
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包查询条件
- * @return Model
- */
- public function getRelation($subRelation = '', $closure = null)
- {
- $morphKey = $this->morphKey;
- $morphType = $this->morphType;
-
- // 多态模型
- $model = $this->parseModel($this->parent->$morphType);
-
- // 主键数据
- $pk = $this->parent->$morphKey;
-
- $relationModel = (new $model)->relation($subRelation)->find($pk);
-
- if ($relationModel) {
- $relationModel->setParent(clone $this->parent);
- }
-
- return $relationModel;
- }
-
- /**
- * 根据关联条件查询当前模型
- * @access public
- * @param string $operator 比较操作符
- * @param integer $count 个数
- * @param string $id 关联表的统计字段
- * @param string $joinType JOIN类型
- * @return Query
- */
- public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
- {
- return $this->parent;
- }
-
- /**
- * 根据关联条件查询当前模型
- * @access public
- * @param mixed $where 查询条件(数组或者闭包)
- * @param mixed $fields 字段
- * @return Query
- */
- public function hasWhere($where = [], $fields = null)
- {
- throw new Exception('relation not support: hasWhere');
- }
-
- /**
- * 解析模型的完整命名空间
- * @access protected
- * @param string $model 模型名(或者完整类名)
- * @return string
- */
- protected function parseModel($model)
- {
- if (isset($this->alias[$model])) {
- $model = $this->alias[$model];
- }
-
- if (false === strpos($model, '\\')) {
- $path = explode('\\', get_class($this->parent));
- array_pop($path);
- array_push($path, Db::parseName($model, 1));
- $model = implode('\\', $path);
- }
-
- return $model;
- }
-
- /**
- * 设置多态别名
- * @access public
- * @param array $alias 别名定义
- * @return $this
- */
- public function setAlias($alias)
- {
- $this->alias = $alias;
-
- return $this;
- }
-
- /**
- * 移除关联查询参数
- * @access public
- * @return $this
- */
- public function removeOption()
- {
- return $this;
- }
-
- /**
- * 预载入关联查询
- * @access public
- * @param array $resultSet 数据集
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
- * @return void
- * @throws Exception
- */
- public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
- {
- $morphKey = $this->morphKey;
- $morphType = $this->morphType;
- $range = [];
-
- foreach ($resultSet as $result) {
- // 获取关联外键列表
- if (!empty($result->$morphKey)) {
- $range[$result->$morphType][] = $result->$morphKey;
- }
- }
-
- if (!empty($range)) {
- // 关联属性名
- $attr = Db::parseName($relation);
-
- foreach ($range as $key => $val) {
- // 多态类型映射
- $model = $this->parseModel($key);
- $obj = new $model;
- $pk = $obj->getPk();
- $list = $obj->all($val, $subRelation);
- $data = [];
-
- foreach ($list as $k => $vo) {
- $data[$vo->$pk] = $vo;
- }
-
- foreach ($resultSet as $result) {
- if ($key == $result->$morphType) {
- // 关联模型
- if (!isset($data[$result->$morphKey])) {
- $relationModel = null;
- } else {
- $relationModel = $data[$result->$morphKey];
- $relationModel->setParent(clone $result);
- $relationModel->isUpdate(true);
- }
-
- $result->setRelation($attr, $relationModel);
- }
- }
- }
- }
- }
-
- /**
- * 预载入关联查询
- * @access public
- * @param Model $result 数据对象
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
- * @return void
- */
- public function eagerlyResult(&$result, $relation, $subRelation, $closure)
- {
- $morphKey = $this->morphKey;
- $morphType = $this->morphType;
- // 多态类型映射
- $model = $this->parseModel($result->{$this->morphType});
-
- $this->eagerlyMorphToOne($model, $relation, $result, $subRelation);
- }
-
- /**
- * 关联统计
- * @access public
- * @param Model $result 数据对象
- * @param \Closure $closure 闭包
- * @return integer
- */
- public function relationCount($result, $closure)
- {}
-
- /**
- * 多态MorphTo 关联模型预查询
- * @access public
- * @param object $model 关联模型对象
- * @param string $relation 关联名
- * @param Model $result
- * @param string $subRelation 子关联
- * @return void
- */
- protected function eagerlyMorphToOne($model, $relation, &$result, $subRelation = '')
- {
- // 预载入关联查询 支持嵌套预载入
- $pk = $this->parent->{$this->morphKey};
- $data = (new $model)->with($subRelation)->find($pk);
-
- if ($data) {
- $data->setParent(clone $result);
- $data->isUpdate(true);
- }
-
- $result->setRelation(Db::parseName($relation), $data ?: null);
- }
-
- /**
- * 添加关联数据
- * @access public
- * @param Model $model 关联模型对象
- * @param string $type 多态类型
- * @return Model
- */
- public function associate($model, $type = '')
- {
- $morphKey = $this->morphKey;
- $morphType = $this->morphType;
- $pk = $model->getPk();
-
- $this->parent->setAttr($morphKey, $model->$pk);
- $this->parent->setAttr($morphType, $type ?: get_class($model));
- $this->parent->save();
-
- return $this->parent->setRelation($this->relation, $model);
- }
-
- /**
- * 注销关联数据
- * @access public
- * @return Model
- */
- public function dissociate()
- {
- $morphKey = $this->morphKey;
- $morphType = $this->morphType;
-
- $this->parent->setAttr($morphKey, null);
- $this->parent->setAttr($morphType, null);
- $this->parent->save();
-
- return $this->parent->setRelation($this->relation, null);
- }
-
-}
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use Closure;
+use think\db\exception\DbException as Exception;
+use think\db\Query;
+use think\helper\Str;
+use think\Model;
+use think\model\Relation;
+
+/**
+ * 多态关联类
+ */
+class MorphTo extends Relation
+{
+ /**
+ * 多态关联外键
+ * @var string
+ */
+ protected $morphKey;
+
+ /**
+ * 多态字段
+ * @var string
+ */
+ protected $morphType;
+
+ /**
+ * 多态别名
+ * @var array
+ */
+ protected $alias = [];
+
+ /**
+ * 关联名
+ * @var string
+ */
+ protected $relation;
+
+ protected $queryCaller = [];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $morphType 多态字段名
+ * @param string $morphKey 外键名
+ * @param array $alias 多态别名定义
+ * @param ?string $relation 关联名
+ */
+ public function __construct(Model $parent, string $morphType, string $morphKey, array $alias = [], string $relation = null)
+ {
+ $this->parent = $parent;
+ $this->morphType = $morphType;
+ $this->morphKey = $morphKey;
+ $this->alias = $alias;
+ $this->relation = $relation;
+ }
+
+ /**
+ * 获取当前的关联模型类的实例
+ * @access public
+ * @return Model
+ */
+ public function getModel(): Model
+ {
+ $morphType = $this->morphType;
+ $model = $this->parseModel($this->parent->$morphType);
+
+ return (new $model);
+ }
+
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param array $subRelation 子关联名
+ * @param ?Closure $closure 闭包查询条件
+ * @return Model
+ */
+ public function getRelation(array $subRelation = [], Closure $closure = null)
+ {
+ $morphKey = $this->morphKey;
+ $morphType = $this->morphType;
+
+ // 多态模型
+ $model = $this->parseModel($this->parent->$morphType);
+
+ // 主键数据
+ $pk = $this->parent->$morphKey;
+
+ $relationModel = $this->buildQuery((new $model)->relation($subRelation))->find($pk);
+
+ if ($relationModel) {
+ $relationModel->setParent(clone $this->parent);
+ }
+
+ return $relationModel;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
+ * @return Query
+ */
+ public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null)
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @param ?Query $query Query对象
+ * @return Query
+ */
+ public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
+ {
+ $alias = class_basename($this->parent);
+
+ $types = $this->parent->distinct()->column($this->morphType);
+
+ $query = $query ?: $this->parent->db();
+
+ return $query->alias($alias)
+ ->where(function (Query $query) use ($types, $where, $alias) {
+ foreach ($types as $type) {
+ if ($type) {
+ $query->whereExists(function (Query $query) use ($type, $where, $alias) {
+ $class = $this->parseModel($type);
+ /** @var Model $model */
+ $model = new $class();
+
+ $table = $model->getTable();
+ $query
+ ->table($table)
+ ->where($alias . '.' . $this->morphType, $type)
+ ->whereRaw("`{$alias}`.`{$this->morphKey}`=`{$table}`.`{$model->getPk()}`")
+ ->where($where);
+ }, 'OR');
+ }
+ }
+ });
+ }
+
+ /**
+ * 解析模型的完整命名空间
+ * @access protected
+ * @param string $model 模型名(或者完整类名)
+ * @return Model
+ */
+ protected function parseModel(string $model): string
+ {
+ if (isset($this->alias[$model])) {
+ $model = $this->alias[$model];
+ }
+
+ if (false === strpos($model, '\\')) {
+ $path = explode('\\', get_class($this->parent));
+ array_pop($path);
+ array_push($path, Str::studly($model));
+ $model = implode('\\', $path);
+ }
+
+ return $model;
+ }
+
+ /**
+ * 设置多态别名
+ * @access public
+ * @param array $alias 别名定义
+ * @return $this
+ */
+ public function setAlias(array $alias)
+ {
+ $this->alias = $alias;
+
+ return $this;
+ }
+
+ /**
+ * 移除关联查询参数
+ * @access public
+ * @return $this
+ */
+ public function removeOption(string $option = '')
+ {
+ return $this;
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param ?Closure $closure 闭包
+ * @param array $cache 关联缓存
+ * @return void
+ * @throws Exception
+ */
+ public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
+ {
+ $morphKey = $this->morphKey;
+ $morphType = $this->morphType;
+ $range = [];
+
+ foreach ($resultSet as $result) {
+ // 获取关联外键列表
+ if (!empty($result->$morphKey)) {
+ $range[$result->$morphType][] = $result->$morphKey;
+ }
+ }
+
+ if (!empty($range)) {
+
+ foreach ($range as $key => $val) {
+ // 多态类型映射
+ $model = $this->parseModel($key);
+ $obj = new $model;
+ if (!\is_null($closure)) {
+ $obj = $closure($obj);
+ }
+ $pk = $obj->getPk();
+ $list = $obj->with($subRelation)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
+ ->select($val);
+ $data = [];
+
+ foreach ($list as $k => $vo) {
+ $data[$vo->$pk] = $vo;
+ }
+
+ foreach ($resultSet as $result) {
+ if ($key == $result->$morphType) {
+ // 关联模型
+ if (!isset($data[$result->$morphKey])) {
+ $relationModel = null;
+ } else {
+ $relationModel = $data[$result->$morphKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->exists(true);
+ }
+
+ $result->setRelation($relation, $relationModel);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param ?Closure $closure 闭包
+ * @param array $cache 关联缓存
+ * @return void
+ */
+ public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
+ {
+ // 多态类型映射
+ $model = $this->parseModel($result->{$this->morphType});
+
+ $this->eagerlyMorphToOne($model, $relation, $result, $subRelation, $cache);
+ }
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 数据对象
+ * @param ?Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @return integer
+ */
+ public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*')
+ {
+ }
+
+ /**
+ * 多态MorphTo 关联模型预查询
+ * @access protected
+ * @param string $model 关联模型对象
+ * @param string $relation 关联名
+ * @param Model $result
+ * @param array $subRelation 子关联
+ * @param array $cache 关联缓存
+ * @return void
+ */
+ protected function eagerlyMorphToOne(string $model, string $relation, Model $result, array $subRelation = [], array $cache = []): void
+ {
+ // 预载入关联查询 支持嵌套预载入
+ $pk = $this->parent->{$this->morphKey};
+
+ $data = null;
+
+ if (\class_exists($model)) {
+ $data = (new $model)->with($subRelation)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
+ ->find($pk);
+
+ if ($data) {
+ $data->setParent(clone $result);
+ $data->exists(true);
+ }
+ }
+
+ $result->setRelation($relation, $data ?: null);
+ }
+
+ /**
+ * 添加关联数据
+ * @access public
+ * @param Model $model 关联模型对象
+ * @param string $type 多态类型
+ * @return Model
+ */
+ public function associate(Model $model, string $type = ''): Model
+ {
+ $morphKey = $this->morphKey;
+ $morphType = $this->morphType;
+ $pk = $model->getPk();
+
+ $this->parent->setAttr($morphKey, $model->$pk);
+ $this->parent->setAttr($morphType, $type ?: get_class($model));
+ $this->parent->save();
+
+ return $this->parent->setRelation($this->relation, $model);
+ }
+
+ /**
+ * 注销关联数据
+ * @access public
+ * @return Model
+ */
+ public function dissociate(): Model
+ {
+ $morphKey = $this->morphKey;
+ $morphType = $this->morphType;
+
+ $this->parent->setAttr($morphKey, null);
+ $this->parent->setAttr($morphType, null);
+ $this->parent->save();
+
+ return $this->parent->setRelation($this->relation, null);
+ }
+
+ protected function buildQuery(Query $query)
+ {
+ foreach ($this->queryCaller as $caller) {
+ call_user_func_array([$query, $caller[0]], $caller[1]);
+ }
+
+ return $query;
+ }
+
+ public function __call($method, $args)
+ {
+ $this->queryCaller[] = [$method, $args];
+ return $this;
+ }
+}
diff --git a/src/model/relation/MorphToMany.php b/src/model/relation/MorphToMany.php
new file mode 100644
index 0000000000000000000000000000000000000000..7a914da9343049295cd1e1565ed5e8db12e3a5fa
--- /dev/null
+++ b/src/model/relation/MorphToMany.php
@@ -0,0 +1,493 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use Closure;
+use Exception;
+use think\db\BaseQuery as Query;
+use think\db\Raw;
+use think\Model;
+use think\model\Pivot;
+
+/**
+ * 多态多对多关联
+ */
+class MorphToMany extends BelongsToMany
+{
+
+ /**
+ * 多态关系的模型名映射别名的数组
+ *
+ * @var array
+ */
+ protected static $morphMap = [];
+
+ /**
+ * 多态字段名
+ * @var string
+ */
+ protected $morphType;
+
+ /**
+ * 多态模型名
+ * @var string
+ */
+ protected $morphClass;
+
+ /**
+ * 是否反向关联
+ * @var bool
+ */
+ protected $inverse;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $middle 中间表名/模型名
+ * @param string $morphKey 关联外键
+ * @param string $morphType 多态字段名
+ * @param string $localKey 当前模型关联键
+ * @param bool $inverse 反向关联
+ */
+ public function __construct(Model $parent, string $model, string $middle, string $morphType, string $morphKey, string $localKey, bool $inverse = false)
+ {
+ $this->morphType = $morphType;
+ $this->inverse = $inverse;
+ $this->morphClass = $inverse ? $model : get_class($parent);
+ if (isset(static::$morphMap[$this->morphClass])) {
+ $this->morphClass = static::$morphMap[$this->morphClass];
+ }
+
+ $foreignKey = $inverse ? $morphKey : $localKey;
+ $localKey = $inverse ? $localKey : $morphKey;
+
+ parent::__construct($parent, $model, $middle, $foreignKey, $localKey);
+ }
+
+ /**
+ * 预载入关联查询(数据集)
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
+ * @return void
+ */
+ public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
+ {
+ $pk = $resultSet[0]->getPk();
+ $range = [];
+
+ foreach ($resultSet as $result) {
+ // 获取关联外键列表
+ if (isset($result->$pk)) {
+ $range[] = $result->$pk;
+ }
+ }
+
+ if (!empty($range)) {
+ // 查询关联数据
+ $data = $this->eagerlyManyToMany([
+ ['pivot.' . $this->localKey, 'in', $range],
+ ['pivot.' . $this->morphType, '=', $this->morphClass],
+ ], $subRelation, $closure, $cache);
+
+ // 关联数据封装
+ foreach ($resultSet as $result) {
+ if (!isset($data[$result->$pk])) {
+ $data[$result->$pk] = [];
+ }
+
+ $result->setRelation($relation, $this->resultSetBuild($data[$result->$pk], clone $this->parent));
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询(单个数据)
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
+ * @return void
+ */
+ public function eagerlyResult(Model $result, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
+ {
+ $pk = $result->getPk();
+
+ if (isset($result->$pk)) {
+ $pk = $result->$pk;
+ // 查询管理数据
+ $data = $this->eagerlyManyToMany([
+ ['pivot.' . $this->localKey, '=', $pk],
+ ['pivot.' . $this->morphType, '=', $this->morphClass],
+ ], $subRelation, $closure, $cache);
+
+ // 关联数据封装
+ if (!isset($data[$pk])) {
+ $data[$pk] = [];
+ }
+
+ $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
+ }
+ }
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 数据对象
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
+ * @return integer
+ */
+ public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): float
+ {
+ $pk = $result->getPk();
+
+ if (!isset($result->$pk)) {
+ return 0;
+ }
+
+ $pk = $result->$pk;
+
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
+ }
+
+ return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
+ ['pivot.' . $this->localKey, '=', $pk],
+ ['pivot.' . $this->morphType, '=', $this->morphClass],
+ ])->$aggregate($field);
+ }
+
+ /**
+ * 获取关联统计子查询
+ * @access public
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
+ * @return string
+ */
+ public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
+ {
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
+ }
+
+ return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
+ ['pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->db(false)->getTable() . '.' . $this->parent->getPk())],
+ ['pivot.' . $this->morphType, '=', $this->morphClass],
+ ])->fetchSql()->$aggregate($field);
+ }
+
+ /**
+ * BELONGS TO MANY 关联查询
+ * @access protected
+ * @param string $foreignKey 关联模型关联键
+ * @param string $localKey 当前模型关联键
+ * @param array $condition 关联查询条件
+ * @return Query
+ */
+ protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query
+ {
+ // 关联查询封装
+ $tableName = $this->query->getTable();
+ $table = $this->pivot->db()->getTable();
+
+ if ($this->withoutField) {
+ $this->query->withoutField($this->withoutField);
+ }
+
+ $fields = $this->getQueryFields($tableName);
+
+ $withLimit = $this->withLimit ?: $this->query->getOptions('with_limit');
+ if ($withLimit) {
+ $this->query->limit($withLimit);
+ }
+
+ $query = $this->query
+ ->field($fields)
+ ->tableField(true, $table, 'pivot', 'pivot__');
+
+ if (empty($this->baseQuery)) {
+ $relationFk = $this->query->getPk();
+ $query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk)
+ ->where($condition);
+ }
+
+ return $query;
+ }
+
+ /**
+ * 多对多 关联模型预查询
+ * @access protected
+ * @param array $where 关联预查询条件
+ * @param array $subRelation 子关联
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
+ * @return array
+ */
+ protected function eagerlyManyToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array
+ {
+ if ($closure) {
+ $closure($this->getClosureType($closure));
+ }
+
+ // 预载入关联查询 支持嵌套预载入
+ $list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)
+ ->with($subRelation)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
+ ->select();
+
+ // 组装模型数据
+ $data = [];
+ $withLimit = $this->withLimit ?: $this->query->getOptions('with_limit');
+
+ foreach ($list as $set) {
+ $pivot = [];
+ foreach ($set->getData() as $key => $val) {
+ if (strpos($key, '__')) {
+ [$name, $attr] = explode('__', $key, 2);
+ if ('pivot' == $name) {
+ $pivot[$attr] = $val;
+ unset($set->$key);
+ }
+ }
+ }
+
+ $key = $pivot[$this->localKey];
+
+ if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
+ continue;
+ }
+
+ $set->setRelation($this->pivotDataName, $this->newPivot($pivot));
+
+ $data[$key][] = $set;
+ }
+
+ return $data;
+ }
+
+ /**
+ * 附加关联的一个中间表数据
+ * @access public
+ * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
+ * @param array $pivot 中间表额外数据
+ * @return array|Pivot
+ */
+ public function attach($data, array $pivot = [])
+ {
+ if (is_array($data)) {
+ if (key($data) === 0) {
+ $id = $data;
+ } else {
+ // 保存关联表数据
+ $model = new $this->model;
+ $id = $model->insertGetId($data);
+ }
+ } else if (is_numeric($data) || is_string($data)) {
+ // 根据关联表主键直接写入中间表
+ $id = $data;
+ } else if ($data instanceof Model) {
+ // 根据关联表主键直接写入中间表
+ $id = $data->getKey();
+ }
+
+ if (!empty($id)) {
+ // 保存中间表数据
+ $pivot[$this->localKey] = $this->parent->getKey();
+ $pivot[$this->morphType] = $this->morphClass;
+ $ids = (array) $id;
+
+ $result = [];
+
+ foreach ($ids as $id) {
+ $pivot[$this->foreignKey] = $id;
+
+ $this->pivot->replace()
+ ->exists(false)
+ ->data([])
+ ->save($pivot);
+ $result[] = $this->newPivot($pivot);
+ }
+
+ if (count($result) == 1) {
+ // 返回中间表模型对象
+ $result = $result[0];
+ }
+
+ return $result;
+ } else {
+ throw new Exception('miss relation data');
+ }
+ }
+
+ /**
+ * 判断是否存在关联数据
+ * @access public
+ * @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键
+ * @return Pivot|false
+ */
+ public function attached($data)
+ {
+ if ($data instanceof Model) {
+ $id = $data->getKey();
+ } else {
+ $id = $data;
+ }
+
+ $pivot = $this->pivot
+ ->where($this->localKey, $this->parent->getKey())
+ ->where($this->morphType, $this->morphClass)
+ ->where($this->foreignKey, $id)
+ ->find();
+
+ return $pivot ?: false;
+ }
+
+ /**
+ * 解除关联的一个中间表数据
+ * @access public
+ * @param integer|array $data 数据 可以使用关联对象的主键
+ * @param bool $relationDel 是否同时删除关联表数据
+ * @return integer
+ */
+ public function detach($data = null, bool $relationDel = false): int
+ {
+ if (is_array($data)) {
+ $id = $data;
+ } else if (is_numeric($data) || is_string($data)) {
+ // 根据关联表主键直接写入中间表
+ $id = $data;
+ } else if ($data instanceof Model) {
+ // 根据关联表主键直接写入中间表
+ $id = $data->getKey();
+ }
+
+ // 删除中间表数据
+ $pivot = [
+ [$this->localKey, '=', $this->parent->getKey()],
+ [$this->morphType, '=', $this->morphClass],
+ ];
+
+ if (isset($id)) {
+ $pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id];
+ }
+
+ $result = $this->pivot->where($pivot)->delete();
+
+ // 删除关联表数据
+ if (isset($id) && $relationDel) {
+ $model = $this->model;
+ $model::destroy($id);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 数据同步
+ * @access public
+ * @param array $ids
+ * @param bool $detaching
+ * @return array
+ */
+ public function sync(array $ids, bool $detaching = true): array
+ {
+ $changes = [
+ 'attached' => [],
+ 'detached' => [],
+ 'updated' => [],
+ ];
+
+ $current = $this->pivot
+ ->where($this->localKey, $this->parent->getKey())
+ ->where($this->morphType, $this->morphClass)
+ ->column($this->foreignKey);
+
+ $records = [];
+
+ foreach ($ids as $key => $value) {
+ if (!is_array($value)) {
+ $records[$value] = [];
+ } else {
+ $records[$key] = $value;
+ }
+ }
+
+ $detach = array_diff($current, array_keys($records));
+
+ if ($detaching && count($detach) > 0) {
+ $this->detach($detach);
+ $changes['detached'] = $detach;
+ }
+
+ foreach ($records as $id => $attributes) {
+ if (!in_array($id, $current)) {
+ $this->attach($id, $attributes);
+ $changes['attached'][] = $id;
+ } else if (count($attributes) > 0 && $this->attach($id, $attributes)) {
+ $changes['updated'][] = $id;
+ }
+ }
+
+ return $changes;
+ }
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery(): void
+ {
+ if (empty($this->baseQuery)) {
+ $foreignKey = $this->foreignKey;
+ $localKey = $this->localKey;
+
+ // 关联查询
+ $this->belongsToManyQuery($foreignKey, $localKey, [
+ ['pivot.' . $localKey, '=', $this->parent->getKey()],
+ ['pivot.' . $this->morphType, '=', $this->morphClass],
+ ]);
+
+ $this->baseQuery = true;
+ }
+ }
+
+ /**
+ * 设置或获取多态关系的模型名映射别名的数组
+ *
+ * @param array|null $map
+ * @param bool $merge
+ * @return array
+ */
+ public static function morphMap(array $map = null, $merge = true): array
+ {
+ if (is_array($map)) {
+ static::$morphMap = $merge && static::$morphMap
+ ? $map+static::$morphMap : $map;
+ }
+
+ return static::$morphMap;
+ }
+
+}
diff --git a/src/model/relation/OneToOne.php b/src/model/relation/OneToOne.php
index 2b9d5bab9f548f66f4a5377cf9d1223540fdb2a0..035e57955ebe35547912563404d1a29d1b1fbe95 100644
--- a/src/model/relation/OneToOne.php
+++ b/src/model/relation/OneToOne.php
@@ -2,7 +2,7 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
@@ -11,35 +11,44 @@
namespace think\model\relation;
-use think\Db;
-use think\db\Query;
-use think\Exception;
+use Closure;
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
+use think\helper\Str;
use think\Model;
use think\model\Relation;
/**
- * Class OneToOne
+ * 一对一关联基础类
* @package think\model\relation
- *
*/
abstract class OneToOne extends Relation
{
- // 预载入方式 0 -JOIN 1 -IN
- protected $eagerlyType = 1;
- // 当前关联的JOIN类型
- protected $joinType;
- // 要绑定的属性
+ /**
+ * JOIN类型
+ * @var string
+ */
+ protected $joinType = 'INNER';
+
+ /**
+ * 绑定的关联属性
+ * @var array
+ */
protected $bindAttr = [];
- // 关联名
+
+ /**
+ * 关联名
+ * @var string
+ */
protected $relation;
/**
* 设置join类型
* @access public
- * @param string $type JOIN类型
+ * @param string $type JOIN类型
* @return $this
*/
- public function joinType($type)
+ public function joinType(string $type)
{
$this->joinType = $type;
return $this;
@@ -48,17 +57,17 @@ abstract class OneToOne extends Relation
/**
* 预载入关联查询(JOIN方式)
* @access public
- * @param Query $query 查询对象
- * @param string $relation 关联名
- * @param mixed $field 关联字段
- * @param string $joinType JOIN方式
- * @param \Closure $closure 闭包条件
- * @param bool $first
+ * @param Query $query 查询对象
+ * @param string $relation 关联名
+ * @param mixed $field 关联字段
+ * @param string $joinType JOIN方式
+ * @param Closure $closure 闭包条件
+ * @param bool $first
* @return void
*/
- public function eagerly(Query $query, $relation, $field, $joinType, $closure, $first)
+ public function eagerly(Query $query, string $relation, $field = true, string $joinType = '', Closure $closure = null, bool $first = false): void
{
- $name = Db::parseName(basename(str_replace('\\', '/', get_class($this->parent))));
+ $name = Str::snake(class_basename($this->parent));
if ($first) {
$table = $query->getTable();
@@ -71,7 +80,7 @@ abstract class OneToOne extends Relation
$masterField = true;
}
- $query->field($masterField, false, $table, $name);
+ $query->tableField($masterField, $table, $name);
}
// 预载入封装
@@ -82,143 +91,146 @@ abstract class OneToOne extends Relation
$query->via($joinAlias);
if ($this instanceof BelongsTo) {
- $joinOn = $name . '.' . $this->foreignKey . '=' . $joinAlias . '.' . $this->localKey;
+
+ $foreignKeyExp = $this->foreignKey;
+
+ if (strpos($foreignKeyExp, '.') === false) {
+ $foreignKeyExp = $name . '.' . $this->foreignKey;
+ }
+
+ $joinOn = $foreignKeyExp . '=' . $joinAlias . '.' . $this->localKey;
} else {
- $joinOn = $name . '.' . $this->localKey . '=' . $joinAlias . '.' . $this->foreignKey;
+
+ $foreignKeyExp = $this->foreignKey;
+
+ if (strpos($foreignKeyExp, '.') === false) {
+ $foreignKeyExp = $joinAlias . '.' . $this->foreignKey;
+ }
+
+ $joinOn = $name . '.' . $this->localKey . '=' . $foreignKeyExp;
}
if ($closure) {
// 执行闭包查询
- $closure($query);
- // 使用withField指定获取关联的字段,如
- // $query->where(['id'=>1])->withField('id,name');
- if ($query->getOptions('with_field')) {
- $field = $query->getOptions('with_field');
- $query->removeOption('with_field');
+ $closure($this->getClosureType($closure));
+
+ // 使用withField指定获取关联的字段
+ if ($this->withField) {
+ $field = $this->withField;
}
}
$query->join([$joinTable => $joinAlias], $joinOn, $joinType)
- ->field($field, false, $joinTable, $joinAlias, $relation . '__');
+ ->tableField($field, $joinTable, $joinAlias, $relation . '__');
}
/**
* 预载入关联查询(数据集)
- * @param array $resultSet
- * @param string $relation
- * @param string $subRelation
- * @param \Closure $closure
+ * @access protected
+ * @param array $resultSet
+ * @param string $relation
+ * @param array $subRelation
+ * @param Closure $closure
* @return mixed
*/
- abstract protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure);
+ abstract protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null);
/**
* 预载入关联查询(数据)
- * @param Model $result
- * @param string $relation
- * @param string $subRelation
- * @param \Closure $closure
+ * @access protected
+ * @param Model $result
+ * @param string $relation
+ * @param array $subRelation
+ * @param Closure $closure
* @return mixed
*/
- abstract protected function eagerlyOne(&$result, $relation, $subRelation, $closure);
+ abstract protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], Closure $closure = null);
/**
* 预载入关联查询(数据集)
* @access public
- * @param array $resultSet 数据集
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
- * @param bool $join 是否为JOIN方式
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
+ * @param bool $join 是否为JOIN方式
* @return void
*/
- public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure, $join = false)
+ public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = [], bool $join = false): void
{
- if ($join || 0 == $this->eagerlyType) {
+ if ($join) {
// 模型JOIN关联组装
foreach ($resultSet as $result) {
$this->match($this->model, $relation, $result);
}
} else {
// IN查询
- $this->eagerlySet($resultSet, $relation, $subRelation, $closure);
+ $this->eagerlySet($resultSet, $relation, $subRelation, $closure, $cache);
}
}
/**
* 预载入关联查询(数据)
* @access public
- * @param Model $result 数据对象
- * @param string $relation 当前关联名
- * @param string $subRelation 子关联名
- * @param \Closure $closure 闭包
- * @param bool $join 是否为JOIN方式
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
+ * @param bool $join 是否为JOIN方式
* @return void
*/
- public function eagerlyResult(&$result, $relation, $subRelation, $closure, $join = false)
+ public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = [], bool $join = false): void
{
- if (0 == $this->eagerlyType || $join) {
+ if ($join) {
// 模型JOIN关联组装
$this->match($this->model, $relation, $result);
} else {
// IN查询
- $this->eagerlyOne($result, $relation, $subRelation, $closure);
+ $this->eagerlyOne($result, $relation, $subRelation, $closure, $cache);
}
}
/**
* 保存(新增)当前关联数据对象
* @access public
- * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
+ * @param mixed $data 数据 可以使用数组 关联模型对象
+ * @param boolean $replace 是否自动识别更新和写入
* @return Model|false
*/
- public function save($data)
+ public function save($data, bool $replace = true)
{
- if ($data instanceof Model) {
- $data = $data->getData();
- }
-
- $model = new $this->model;
- // 保存关联表数据
- $data[$this->foreignKey] = $this->parent->{$this->localKey};
+ $model = $this->make($data);
- return $model->save($data) ? $model : false;
+ return $model->replace($replace)->save() ? $model : false;
}
/**
- * 设置预载入方式
- * @access public
- * @param integer $type 预载入方式 0 JOIN查询 1 IN查询
- * @return $this
+ * 创建关联对象实例
+ * @param array|Model $data
+ * @return Model
*/
- public function setEagerlyType($type)
+ public function make($data = []): Model
{
- $this->eagerlyType = $type;
+ if ($data instanceof Model) {
+ $data = $data->getData();
+ }
- return $this;
- }
+ // 保存关联表数据
+ $data[$this->foreignKey] = $this->parent->{$this->localKey};
- /**
- * 获取预载入方式
- * @access public
- * @return integer
- */
- public function getEagerlyType()
- {
- return $this->eagerlyType;
+ return new $this->model($data);
}
/**
* 绑定关联表的属性到父模型属性
* @access public
- * @param mixed $attr 要绑定的属性列表
+ * @param array $attr 要绑定的属性列表
* @return $this
*/
- public function bind($attr)
+ public function bind(array $attr)
{
- if (is_string($attr)) {
- $attr = explode(',', $attr);
- }
$this->bindAttr = $attr;
return $this;
@@ -229,35 +241,25 @@ abstract class OneToOne extends Relation
* @access public
* @return array
*/
- public function getBindAttr()
+ public function getBindAttr(): array
{
return $this->bindAttr;
}
- /**
- * 关联统计
- * @access public
- * @param Model $result 数据对象
- * @param \Closure $closure 闭包
- * @return integer
- */
- public function relationCount($result, $closure)
- {}
-
/**
* 一对一 关联模型预查询拼装
* @access public
- * @param string $model 模型名称
- * @param string $relation 关联名
- * @param Model $result 模型对象实例
+ * @param string $model 模型名称
+ * @param string $relation 关联名
+ * @param Model $result 模型对象实例
* @return void
*/
- protected function match($model, $relation, &$result)
+ protected function match(string $model, string $relation, Model $result): void
{
// 重新组装模型数据
foreach ($result->getData() as $key => $val) {
if (strpos($key, '__')) {
- list($name, $attr) = explode('__', $key, 2);
+ [$name, $attr] = explode('__', $key, 2);
if ($name == $relation) {
$list[$name][$attr] = $val;
unset($result->$key);
@@ -273,67 +275,78 @@ abstract class OneToOne extends Relation
} else {
$relationModel = new $model($list[$relation]);
$relationModel->setParent(clone $result);
- $relationModel->isUpdate(true);
+ $relationModel->exists(true);
}
if (!empty($this->bindAttr)) {
- $this->bindAttr($relationModel, $result, $this->bindAttr);
+ $this->bindAttr($result, $relationModel);
}
} else {
$relationModel = null;
}
- $result->setRelation(Db::parseName($relation), $relationModel);
+ $result->setRelation($relation, $relationModel);
}
/**
* 绑定关联属性到父模型
* @access protected
- * @param Model $model 关联模型对象
- * @param Model $result 父模型对象
+ * @param Model $result 父模型对象
+ * @param Model $model 关联模型对象
* @return void
* @throws Exception
*/
- protected function bindAttr($model, &$result)
+ protected function bindAttr(Model $result, Model $model = null): void
{
foreach ($this->bindAttr as $key => $attr) {
- $key = is_numeric($key) ? $attr : $key;
- if (isset($result->$key)) {
+ $key = is_numeric($key) ? $attr : $key;
+ $value = $result->getOrigin($key);
+
+ if (!is_null($value)) {
throw new Exception('bind attr has exists:' . $key);
- } else {
- $result->setAttr($key, $model ? $model->$attr : null);
}
+
+ $result->setAttr($key, $model ? $model->$attr : null);
}
}
/**
* 一对一 关联模型预查询(IN方式)
* @access public
- * @param array $where 关联预查询条件
- * @param string $key 关联键名
- * @param string $relation 关联名
- * @param string $subRelation 子关联
- * @param \Closure $closure
+ * @param array $where 关联预查询条件
+ * @param string $key 关联键名
+ * @param array $subRelation 子关联
+ * @param Closure $closure
+ * @param array $cache 关联缓存
* @return array
*/
- protected function eagerlyWhere($where, $key, $relation, $subRelation = '', $closure = null)
+ protected function eagerlyWhere(array $where, string $key, array $subRelation = [], Closure $closure = null, array $cache = [])
{
// 预载入关联查询 支持嵌套预载入
if ($closure) {
- $closure($this->query);
+ $this->baseQuery = true;
+ $closure($this->getClosureType($closure));
+ }
- if ($field = $this->query->getOptions('with_field')) {
- $this->query->field($field)->removeOption('with_field');
- }
+ if ($this->withField) {
+ $this->query->field($this->withField);
+ } elseif ($this->withoutField) {
+ $this->query->withoutField($this->withoutField);
}
- $list = $this->query->where($where)->with($subRelation)->select();
+ $list = $this->query
+ ->where($where)
+ ->with($subRelation)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
+ ->select();
// 组装模型数据
$data = [];
foreach ($list as $set) {
- $data[$set->$key] = $set;
+ if (!isset($data[$set->$key])) {
+ $data[$set->$key] = $set;
+ }
}
return $data;
diff --git a/src/paginator/Collection.php b/src/paginator/Collection.php
deleted file mode 100644
index d3e9c93b743983607d33d53c61357b95dbae4ea9..0000000000000000000000000000000000000000
--- a/src/paginator/Collection.php
+++ /dev/null
@@ -1,74 +0,0 @@
-
-// +----------------------------------------------------------------------
-
-namespace think\paginator;
-
-use Exception;
-use think\Paginator;
-
-/**
- * Class Collection
- * @package think\paginator
- * @method integer total()
- * @method integer listRows()
- * @method integer currentPage()
- * @method string render()
- * @method Paginator fragment($fragment)
- * @method Paginator appends($key, $value)
- * @method integer lastPage()
- * @method boolean hasPages()
- */
-class Collection extends \think\Collection
-{
-
- /** @var Paginator */
- protected $paginator;
-
- public function __construct($items = [], Paginator $paginator = null)
- {
- $this->paginator = $paginator;
- parent::__construct($items);
- }
-
- public static function make($items = [], Paginator $paginator = null)
- {
- return new static($items, $paginator);
- }
-
- public function toArray()
- {
- if ($this->paginator) {
- try {
- $total = $this->total();
- } catch (Exception $e) {
- $total = null;
- }
-
- return [
- 'total' => $total,
- 'per_page' => $this->listRows(),
- 'current_page' => $this->currentPage(),
- 'data' => parent::toArray(),
- ];
- } else {
- return parent::toArray();
- }
- }
-
- public function __call($method, $args)
- {
- if ($this->paginator && method_exists($this->paginator, $method)) {
- return call_user_func_array([$this->paginator, $method], $args);
- } else {
- throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
- }
- }
-}
diff --git a/src/paginator/driver/Bootstrap.php b/src/paginator/driver/Bootstrap.php
index de44202f1bf80b756e12c993b23985f074df62af..6d55c394440852270c92ec901ce1df9f3943f1b4 100644
--- a/src/paginator/driver/Bootstrap.php
+++ b/src/paginator/driver/Bootstrap.php
@@ -2,7 +2,7 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
@@ -13,6 +13,9 @@ namespace think\paginator\driver;
use think\Paginator;
+/**
+ * Bootstrap 分页驱动
+ */
class Bootstrap extends Paginator
{
@@ -21,7 +24,7 @@ class Bootstrap extends Paginator
* @param string $text
* @return string
*/
- protected function getPreviousButton($text = "«")
+ protected function getPreviousButton(string $text = "«"): string
{
if ($this->currentPage() <= 1) {
@@ -40,7 +43,7 @@ class Bootstrap extends Paginator
* @param string $text
* @return string
*/
- protected function getNextButton($text = '»')
+ protected function getNextButton(string $text = '»'): string
{
if (!$this->hasMore) {
return $this->getDisabledTextWrapper($text);
@@ -55,7 +58,7 @@ class Bootstrap extends Paginator
* 页码按钮
* @return string
*/
- protected function getLinks()
+ protected function getLinks(): string
{
if ($this->simple) {
return '';
@@ -131,10 +134,10 @@ class Bootstrap extends Paginator
* 生成一个可点击的按钮
*
* @param string $url
- * @param int $page
+ * @param string $page
* @return string
*/
- protected function getAvailablePageWrapper($url, $page)
+ protected function getAvailablePageWrapper(string $url, string $page): string
{
return '' . $page . '';
}
@@ -145,7 +148,7 @@ class Bootstrap extends Paginator
* @param string $text
* @return string
*/
- protected function getDisabledTextWrapper($text)
+ protected function getDisabledTextWrapper(string $text): string
{
return '' . $text . '';
}
@@ -156,7 +159,7 @@ class Bootstrap extends Paginator
* @param string $text
* @return string
*/
- protected function getActivePageWrapper($text)
+ protected function getActivePageWrapper(string $text): string
{
return '' . $text . '';
}
@@ -166,7 +169,7 @@ class Bootstrap extends Paginator
*
* @return string
*/
- protected function getDots()
+ protected function getDots(): string
{
return $this->getDisabledTextWrapper('...');
}
@@ -177,7 +180,7 @@ class Bootstrap extends Paginator
* @param array $urls
* @return string
*/
- protected function getUrlLinks(array $urls)
+ protected function getUrlLinks(array $urls): string
{
$html = '';
@@ -192,10 +195,10 @@ class Bootstrap extends Paginator
* 生成普通页码按钮
*
* @param string $url
- * @param int $page
+ * @param string $page
* @return string
*/
- protected function getPageLinkWrapper($url, $page)
+ protected function getPageLinkWrapper(string $url, string $page): string
{
if ($this->currentPage() == $page) {
return $this->getActivePageWrapper($page);
diff --git a/src/Exception.php b/stubs/Exception.php
similarity index 83%
rename from src/Exception.php
rename to stubs/Exception.php
index 034c85b6479329b78ee618b2adde4e5cdce71fd6..0fdba9c554e58b881a09491dea1437e7b1239a8b 100644
--- a/src/Exception.php
+++ b/stubs/Exception.php
@@ -2,18 +2,22 @@
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
-// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// | Copyright (c) 2006~2021 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
-// | Author: 麦当苗儿
+// | Author: liu21st
// +----------------------------------------------------------------------
+declare (strict_types=1);
namespace think;
+/**
+ * 异常基础类
+ * @package think
+ */
class Exception extends \Exception
{
-
/**
* 保存异常页面显示的额外Debug数据
* @var array
@@ -33,10 +37,11 @@ class Exception extends \Exception
* key1 value1
* key2 value2
*
+ * @access protected
* @param string $label 数据分类,用于异常页面显示
* @param array $data 需要显示的数据,必须为关联数组
*/
- final protected function setData($label, array $data)
+ final protected function setData(string $label, array $data)
{
$this->data[$label] = $data;
}
@@ -44,11 +49,11 @@ class Exception extends \Exception
/**
* 获取异常额外Debug数据
* 主要用于输出到异常页面便于调试
+ * @access public
* @return array 由setData设置的Debug数据
*/
final public function getData()
{
return $this->data;
}
-
}
diff --git a/stubs/Facade.php b/stubs/Facade.php
new file mode 100644
index 0000000000000000000000000000000000000000..d801d8b0fbc24397250d4f3f44f5048db8f4dc14
--- /dev/null
+++ b/stubs/Facade.php
@@ -0,0 +1,65 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+class Facade
+{
+ /**
+ * 始终创建新的对象实例
+ * @var bool
+ */
+ protected static $alwaysNewInstance;
+
+ protected static $instance;
+
+ /**
+ * 获取当前Facade对应类名
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {}
+
+ /**
+ * 创建Facade实例
+ * @static
+ * @access protected
+ * @param bool $newInstance 是否每次创建新的实例
+ * @return object
+ */
+ protected static function createFacade(bool $newInstance = false)
+ {
+ $class = static::getFacadeClass() ?: 'think\DbManager';
+
+ if (static::$alwaysNewInstance) {
+ $newInstance = true;
+ }
+
+ if ($newInstance) {
+ return new $class();
+ }
+
+ if (!self::$instance) {
+ self::$instance = new $class();
+ }
+
+ return self::$instance;
+
+ }
+
+ // 调用实际类的方法
+ public static function __callStatic($method, $params)
+ {
+ return call_user_func_array([static::createFacade(), $method], $params);
+ }
+}
diff --git a/stubs/load_stubs.php b/stubs/load_stubs.php
new file mode 100644
index 0000000000000000000000000000000000000000..5854cda569c4356b0c64058a3d331aed7ea08ce5
--- /dev/null
+++ b/stubs/load_stubs.php
@@ -0,0 +1,9 @@
+=')) {
+ $this->assertMatchesRegularExpression($pattern, $string, $message);
+ } else {
+ $this->assertRegExp($pattern, $string, $message);
+ }
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000000000000000000000000000000000000..e712c59408e0a6bd00a233e2071514fa0b85fcab
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,53 @@
+ 'mysql',
+ // 数据库连接信息
+ 'connections' => [
+ 'mysql' => [
+ // 数据库类型
+ 'type' => 'mysql',
+ // 主机地址
+ 'hostname' => getenv('TESTS_DB_MYSQL_HOSTNAME'),
+ // 数据库名
+ 'database' => getenv('TESTS_DB_MYSQL_DATABASE'),
+ // 用户名
+ 'username' => getenv('TESTS_DB_MYSQL_USERNAME'),
+ // 密码
+ 'password' => getenv('TESTS_DB_MYSQL_PASSWORD'),
+ // 数据库编码默认采用utf8
+ 'charset' => 'utf8',
+ // 数据库表前缀
+ 'prefix' => 'test_',
+ // 是否需要断线重连
+ 'break_reconnect' => false,
+ // 断线标识字符串
+ 'break_match_str' => [],
+ // 数据库调试模式
+ 'debug' => false,
+ ],
+ 'mysql_manage' => [
+ // 数据库类型
+ 'type' => 'mysql',
+ // 主机地址
+ 'hostname' => getenv('TESTS_DB_MYSQL_HOSTNAME'),
+ // 数据库名
+ 'database' => getenv('TESTS_DB_MYSQL_DATABASE'),
+ // 用户名
+ 'username' => getenv('TESTS_DB_MYSQL_USERNAME'),
+ // 密码
+ 'password' => getenv('TESTS_DB_MYSQL_PASSWORD'),
+ // 数据库编码默认采用utf8
+ 'charset' => 'utf8',
+ // 数据库表前缀
+ 'prefix' => 'test_',
+ // 数据库调试模式
+ 'debug' => false,
+ ],
+ ],
+]);
\ No newline at end of file
diff --git a/tests/functions.php b/tests/functions.php
new file mode 100644
index 0000000000000000000000000000000000000000..d531610d81bf58205ef3063f746df5bb2f2d2a0d
--- /dev/null
+++ b/tests/functions.php
@@ -0,0 +1,54 @@
+ $key) {
+ if (is_callable($key)) {
+ $item[$index] = call_user_func($key, $val);
+ } elseif (is_int($index)) {
+ $item[$key] = $val[$key];
+ } else {
+ $item[$key] = $val[$index];
+ }
+ }
+ return $item;
+ }, $arr);
+
+ if (!empty($key)) {
+ $result = array_combine(array_column($arr, 'id'), $result);
+ }
+
+ return $result;
+}
+
+function array_value_sort(array $arr)
+{
+ foreach ($arr as &$value) {
+ sort($value);
+ }
+}
+
+function query_mysql_connection_id(ConnectionInterface $connect): int
+{
+ $cid = $connect->query('SELECT CONNECTION_ID() as cid')[0]['cid'];
+ return (int) $cid;
+}
+
+function mysql_kill_connection(string $name, $cid)
+{
+ Db::connect($name)->execute("KILL {$cid}");
+}
\ No newline at end of file
diff --git a/tests/orm/DbTest.php b/tests/orm/DbTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d7886309b1424b8bcc542f857e25f701d66bbf4c
--- /dev/null
+++ b/tests/orm/DbTest.php
@@ -0,0 +1,177 @@
+ 1, 'type' => 3, 'username' => 'qweqwe', 'nickname' => 'asdasd', 'password' => '123123'],
+ ['id' => 2, 'type' => 2, 'username' => 'rtyrty', 'nickname' => 'fghfgh', 'password' => '456456'],
+ ['id' => 3, 'type' => 1, 'username' => 'uiouio', 'nickname' => 'jkljkl', 'password' => '789789'],
+ ['id' => 5, 'type' => 2, 'username' => 'qazqaz', 'nickname' => 'wsxwsx', 'password' => '098098'],
+ ['id' => 7, 'type' => 2, 'username' => 'rfvrfv', 'nickname' => 'tgbtgb', 'password' => '765765'],
+ ];
+ Db::table('test_user')->insertAll(self::$testUserData);
+ }
+
+ public function testColumn()
+ {
+ $users = self::$testUserData;
+
+ // 获取全部列
+ $result = Db::table('test_user')->column('*', 'id');
+
+ $this->assertCount(5, $result);
+ $this->assertEquals($users, array_values($result));
+ $this->assertEquals(array_column($users, 'id'), array_keys($result));
+
+ // 获取某一个字段
+ $result = Db::table('test_user')->column('username');
+ $this->assertEquals(array_column($users, 'username'), $result);
+
+ // 获取某字段唯一
+ $result = Db::table('test_user')->column('DISTINCT type');
+ $expected = array_unique(array_column($users, 'type'));
+ $this->assertEquals($expected, $result);
+
+ // 字段别名
+ $result = Db::table('test_user')->column('username as name2');
+ $expected = array_column($users, 'username');
+ $this->assertEquals($expected, $result);
+
+ // 表别名
+ $result = Db::table('test_user')->alias('test2')->column('test2.username');
+ $expected = array_column($users, 'username');
+ $this->assertEquals($expected, $result);
+
+ // 获取若干列
+ $result = Db::table('test_user')->column('username,nickname', 'id');
+ $expected = array_column_ex($users, ['username', 'nickname', 'id'], 'id');
+ $this->assertEquals($expected, $result);
+
+ // 获取若干列不指定key时不报错
+ $result = Db::table('test_user')->column('username,nickname,id');
+ $expected = array_column_ex($users, ['username', 'nickname', 'id']);
+ $this->assertEquals($expected, $result);
+
+ // 数组方式获取
+ $result = Db::table('test_user')->column(['username', 'nickname', 'type'], 'id');
+ $expected = array_column_ex($users, ['username', 'nickname', 'type', 'id'], 'id');
+ $this->assertEquals($expected, $result);
+
+ // 数组方式获取(重命名字段)
+ $result = Db::table('test_user')->column(['username' => 'my_name', 'nickname'], 'id');
+ $expected = array_column_ex($users, ['username' => 'my_name', 'nickname', 'id'], 'id');
+ array_value_sort($result);
+ array_value_sort($expected);
+ $this->assertEquals($expected, $result);
+
+ // 数组方式获取(定义表达式)
+ $result = Db::table('test_user')
+ ->column([
+ 'username' => 'my_name',
+ 'nickname',
+ new Raw('`type`+1000 as type2'),
+ ], 'id');
+ $expected = array_column_ex(
+ $users,
+ [
+ 'username' => 'my_name',
+ 'nickname',
+ 'type2' => function ($value) {
+ return $value['type'] + 1000;
+ },
+ 'id'
+ ],
+ 'id'
+ );
+ array_value_sort($result);
+ array_value_sort($expected);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testWhereIn()
+ {
+ $sqlLogs = [];
+ Db::listen(function ($sql) use (&$sqlLogs) {
+ $sqlLogs[] = $sql;
+ });
+
+ $expected = Collection::make(self::$testUserData)->whereIn('type', [1, 3])->values()->toArray();
+ $result = Db::table('test_user')->whereIn('type', [1, 3])->column('*');
+ $this->assertEquals($expected, $result);
+
+ $expected = Collection::make(self::$testUserData)->whereIn('type', [1])->values()->toArray();
+ $result = Db::table('test_user')->whereIn('type', [1])->column('*');
+ $this->assertEquals($expected, $result);
+
+ $expected = Collection::make(self::$testUserData)->whereIn('type', [1, ''])->values()->toArray();
+ $result = Db::table('test_user')->whereIn('type', [1, ''])->column('*');
+ $this->assertEquals($expected, $result);
+
+ $result = Db::table('test_user')->whereIn('type', [])->column('*');
+ $this->assertEquals([], $result);
+
+ $expected = Collection::make(self::$testUserData)->whereNotIn('type', [1, 3])->values()->toArray();
+ $result = Db::table('test_user')->whereNotIn('type', [1, 3])->column('*');
+ $this->assertEquals($expected, $result);
+
+ $expected = Collection::make(self::$testUserData)->values()->toArray();
+ $result = Db::table('test_user')->whereNotIn('type', [])->column('*');
+ $this->assertEquals($expected, $result);
+
+ $this->assertEquals([
+ "SELECT * FROM `test_user` WHERE `type` IN (1,3)",
+ "SELECT * FROM `test_user` WHERE `type` = 1",
+ "SELECT * FROM `test_user` WHERE `type` IN (1,0)",
+ "SELECT * FROM `test_user` WHERE 0 = 1",
+ "SELECT * FROM `test_user` WHERE `type` NOT IN (1,3)",
+ "SELECT * FROM `test_user` WHERE 1 = 1",
+ ], $sqlLogs);
+ }
+
+ public function testException()
+ {
+ $this->expectException(DbException::class);
+ try {
+ Db::query("wrong syntax");
+ } catch (DbException $exception) {
+ $this->assertInstanceOf(ThinkException::class, $exception);
+ throw $exception;
+ }
+ }
+}
diff --git a/tests/orm/DbTransactionTest.php b/tests/orm/DbTransactionTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9e02e544f1a1f3d7a0188350fdd7a2a0a44cf2c9
--- /dev/null
+++ b/tests/orm/DbTransactionTest.php
@@ -0,0 +1,238 @@
+ 1, 'type' => 9, 'username' => '1-9-a'],
+ ['id' => 2, 'type' => 8, 'username' => '2-8-a'],
+ ['id' => 3, 'type' => 7, 'username' => '3-7-a'],
+ ];
+ }
+
+ public function testTransaction()
+ {
+ $testData = self::$testData;
+ $connect = Db::connect();
+
+ $connect->table('test_tran_a')->startTrans();
+ $connect->table('test_tran_a')->insertAll($testData);
+ $connect->table('test_tran_a')->rollback();
+
+ $this->assertEmpty($connect->table('test_tran_a')->column('*'));
+
+ $connect->execute('TRUNCATE TABLE `test_tran_a`;');
+ $connect->table('test_tran_a')->startTrans();
+ $connect->table('test_tran_a')->insertAll($testData);
+ $connect->table('test_tran_a')->commit();
+ $this->assertEquals($testData, $connect->table('test_tran_a')->column('*'));
+ $connect->table('test_tran_a')->startTrans();
+ $connect->table('test_tran_a')->where('id', '=', 2)->update([
+ 'username' => '2-8-b',
+ ]);
+ $connect->table('test_tran_a')->commit();
+ $this->assertEquals(
+ '2-8-b',
+ $connect->table('test_tran_a')->where('id', '=', 2)->value('username')
+ );
+ }
+
+ public function testBreakReconnect()
+ {
+ $testData = self::$testData;
+ // 初始化配置
+ $config = Db::getConfig();
+ $config['connections']['mysql']['break_reconnect'] = true;
+ $config['connections']['mysql']['break_match_str'] = [
+ 'query execution was interrupted',
+ ];
+ Db::setConfig($config);
+ // 初始化数据
+ $connect = Db::connect(null, true);
+ $connect->table('test_tran_a')->insertAll($testData);
+
+ $cid = query_mysql_connection_id($connect);
+ mysql_kill_connection('mysql_manage', $cid);
+ // 触发重连
+ $connect->table('test_tran_a')->where('id', '=', 2)->value('username');
+
+ $newCid = query_mysql_connection_id($connect);
+ $this->assertNotEquals($cid, $newCid);
+ $cid = $newCid;
+
+ // 事务前重连
+ mysql_kill_connection('mysql_manage', $cid);
+ Db::table('test_tran_a')->startTrans();
+ $connect->table('test_tran_a')->where('id', '=', 2)->update([
+ 'username' => '2-8-b',
+ ]);
+ Db::table('test_tran_a')->commit();
+ $newCid = query_mysql_connection_id($connect);
+ $this->assertNotEquals($cid, $newCid);
+ $cid = $newCid;
+ $this->assertEquals(
+ '2-8-b',
+ Db::table('test_tran_a')->where('id', '=', 2)->value('username')
+ );
+
+ // 事务中不能重连
+ try {
+ Db::table('test_tran_a')->startTrans();
+ $connect->table('test_tran_a')->where('id', '=', 2)->update([
+ 'username' => '2-8-c',
+ ]);
+ mysql_kill_connection('mysql_manage', $cid);
+ $connect->table('test_tran_a')->where('id', '=', 3)->update([
+ 'username' => '3-7-b',
+ ]);
+ Db::table('test_tran_a')->commit();
+ } catch (Throwable | Exception $exception) {
+ try {
+ Db::table('test_tran_a')->rollback();
+ } catch (Exception $rollbackException) {
+ // Ignore exception
+ $this->proxyAssertMatchesRegularExpression(
+ '~(server has gone away)~',
+ $rollbackException->getMessage()
+ );
+ }
+ // Ignore exception
+ $this->proxyAssertMatchesRegularExpression(
+ '~(server has gone away)~',
+ $exception->getMessage()
+ );
+ }
+ // 预期应该没有发生任何更改
+ $this->assertEquals(
+ '2-8-b',
+ Db::table('test_tran_a')->where('id', '=', 2)->value('username')
+ );
+ $this->assertEquals(
+ '3-7-a',
+ Db::table('test_tran_a')->where('id', '=', 3)->value('username')
+ );
+ }
+
+ public function testTransactionSavepoint()
+ {
+ $testData = self::$testData;
+ // 初始化数据
+ $connect = Db::connect(null, true);
+ $connect->table('test_tran_a')->insertAll($testData);
+
+ Db::table('test_tran_a')->transaction(function () use ($connect) {
+ $cid = query_mysql_connection_id($connect);
+ // tran 1
+ Db::table('test_tran_a')->startTrans();
+ $connect->table('test_tran_a')->where('id', '=', 2)->update([
+ 'username' => '2-8-c',
+ ]);
+ Db::table('test_tran_a')->commit();
+ // tran 2
+ Db::table('test_tran_a')->startTrans();
+ $connect->table('test_tran_a')->where('id', '=', 3)->update([
+ 'username' => '3-7-b',
+ ]);
+ Db::table('test_tran_a')->commit();
+ });
+
+ // 预期变化
+ $this->assertEquals(
+ '2-8-c',
+ Db::table('test_tran_a')->where('id', '=', 2)->value('username')
+ );
+ $this->assertEquals(
+ '3-7-b',
+ Db::table('test_tran_a')->where('id', '=', 3)->value('username')
+ );
+ }
+
+ public function testTransactionSavepointBreakReconnect()
+ {
+ $testData = self::$testData;
+ // 初始化配置
+ $config = Db::getConfig();
+ $config['connections']['mysql']['break_reconnect'] = true;
+ $config['connections']['mysql']['break_match_str'] = [
+ 'query execution was interrupted',
+ ];
+ Db::setConfig($config);
+ // 初始化数据
+ $connect = Db::connect(null, true);
+ $connect->table('test_tran_a')->insertAll($testData);
+
+ // 事务中不能重连
+ try {
+ // tran 0
+ Db::table('test_tran_a')->startTrans();
+ $cid = query_mysql_connection_id($connect);
+ // tran 1
+ Db::table('test_tran_a')->startTrans();
+ $connect->table('test_tran_a')->where('id', '=', 2)->update([
+ 'username' => '2-8-c',
+ ]);
+ Db::table('test_tran_a')->commit();
+ // kill
+ mysql_kill_connection('mysql_manage', $cid);
+ // tran 2
+ Db::table('test_tran_a')->startTrans();
+ $connect->table('test_tran_a')->where('id', '=', 3)->update([
+ 'username' => '3-7-b',
+ ]);
+ Db::table('test_tran_a')->commit();
+ // tran 0
+ Db::table('test_tran_a')->commit();
+ } catch (Throwable | Exception $exception) {
+ try {
+ Db::table('test_tran_a')->rollback();
+ } catch (Exception $rollbackException) {
+ // Ignore exception
+ $this->proxyAssertMatchesRegularExpression(
+ '~(server has gone away)~',
+ $rollbackException->getMessage()
+ );
+ }
+ // Ignore exception
+ $this->proxyAssertMatchesRegularExpression(
+ '~(server has gone away)~',
+ $exception->getMessage()
+ );
+ }
+ // 预期应该没有发生任何更改
+ $this->assertEquals(
+ '2-8-a',
+ Db::table('test_tran_a')->where('id', '=', 2)->value('username')
+ );
+ $this->assertEquals(
+ '3-7-a',
+ Db::table('test_tran_a')->where('id', '=', 3)->value('username')
+ );
+ }
+}