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') + ); + } +}