This commit is contained in:
gaofeng
2026-05-12 18:27:28 +08:00
commit 6d9aee81aa
3664 changed files with 274415 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
/.github export-ignore
/tests export-ignore
/phpunit.xml export-ignore

201
vendor/topthink/think-orm/LICENSE vendored Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

43
vendor/topthink/think-orm/README.md vendored Normal file
View File

@@ -0,0 +1,43 @@
# ThinkORM4.0
基于PHP8.0+ 和PDO实现的轻量级ORM完全重构的Model层支持实体模型和分层[新特性盘点](https://doc.thinkphp.cn/@think-orm/v4_0/new-features.html)并基本兼容3.0(参考[升级指导](https://doc.thinkphp.cn/@think-orm/v4_0/upgrade.html))。
## 特性
* 基于PDO和PHP强类型实现
* 原生查询支持
* 灵活的查询构造器和链式查询
* 自动参数绑定和预查询
* 聚合查询
* 强大的模型及关联定义
* 模型获取器和修改器
* 虚拟模型支持
* 实体模型和视图模型支持
* 支持ActiveRecord模式和仓储模式
* 模型事件和类型自动转换
* 数据自动写入和延迟写入
* 搜索器和查询范围
* 预载入关联查询和延迟关联查询
* 数据写入自动验证
* 多数据库和`MongoDb`支持
* 分布式及事务、断点重连
* `JSON`及枚举类支持
* `PSR-16`缓存及`PSR-3`日志规范
## 安装
~~~
composer require topthink/think-orm
~~~
## 文档
详细参考 [ThinkORM开发指南](https://doc.thinkphp.cn/@think-orm)
基于官方手册的数据训练和提供精准解答服务
[官方专家智能体](https://chat.topthink.com/chat/e7m6qe)
你可以使用官方提供的[ThinkChat](https://chat.topthink.com/),让你在学习 ThinkPHP 的旅途中享受私人 AI 助理服务!
[![](https://www.topthink.com/uploads/assistant/20230630/4d1a3f0ad2958b49bb8189b7ef824cb0.png)](https://chat.topthink.com/)
ThinkPHP 生态服务由[顶想云](https://www.topthink.com)TOPThink Cloud提供为生态提供专业的开发者服务和价值之选。

47
vendor/topthink/think-orm/composer.json vendored Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "topthink/think-orm",
"description": "the PHP Database&ORM Framework",
"keywords": [
"orm",
"database"
],
"license": "Apache-2.0",
"authors": [
{
"name": "liu21st",
"email": "liu21st@gmail.com"
}
],
"require": {
"php": ">=8.0.0",
"ext-json": "*",
"ext-pdo": "*",
"psr/simple-cache": ">=1.0",
"psr/log": ">=1.0",
"topthink/think-helper":"^3.1",
"topthink/think-validate":"^3.0"
},
"require-dev": {
"phpunit/phpunit": "^9.6|^10"
},
"autoload": {
"psr-4": {
"think\\": "src"
},
"files": [
"src/helper.php",
"stubs/load_stubs.php"
]
},
"autoload-dev": {
"psr-4": {
"tests\\": "tests"
}
},
"suggest": {
"ext-mongodb": "provide mongodb support"
},
"config": {
"sort-packages": true
}
}

View File

@@ -0,0 +1,400 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think;
use Closure;
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.
*
* @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 = [];
/**
* 查询次数.
*
* @var int
*/
protected $queryTimes = 0;
/**
* 查询缓存对象
*
* @var CacheInterface
*/
protected $cache;
/**
* 查询日志对象
*
* @var LoggerInterface
*/
protected $log;
/**
* 架构函数.
*/
public function __construct()
{
$this->modelMaker();
}
/**
* 注入模型对象
*
* @return void
*/
protected function modelMaker()
{
Model::maker(function (Model $model) {
$model->setOption('db', $this);
if (is_object($this->event)) {
$model->setOption('event', $this->event);
}
$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.
*
* @return void
*/
public function triggerSql(): void
{
}
/**
* 初始化配置参数.
*
* @param array $config 连接配置
*
* @return void
*/
public function setConfig($config): void
{
$this->config = $config;
}
/**
* 设置缓存对象
*
* @param CacheInterface $cache 缓存对象
*
* @return void
*/
public function setCache(CacheInterface $cache): void
{
$this->cache = $cache;
}
/**
* 设置日志对象
*
* @param LoggerInterface|Closure $log 日志对象
*
* @return void
*/
public function setLog(LoggerInterface | Closure $log): void
{
$this->log = $log;
}
/**
* 记录SQL日志.
*
* @param string $log SQL日志信息
* @param string $type 日志类型
*
* @return void
*/
public function log(string $log, string $type = 'sql')
{
if ($this->log) {
if ($this->log instanceof Closure) {
call_user_func_array($this->log, [$type, $log]);
} else {
$this->log->log($type, $log);
}
}
}
/**
* 获得查询日志(没有设置日志对象使用).
*
* @deprecated
* @param bool $clear 是否清空
* @return array
*/
public function getDbLog(bool $clear = false): array
{
return [];
}
/**
* 获取配置参数.
*
* @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;
}
/**
* 创建/切换数据库连接查询.
*
* @param string|array|null $name 连接配置信息
* @param bool $force 强制重新连接
*
* @return ConnectionInterface
*/
public function connect(string|array|null $name = null, bool $force = false)
{
return $this->instance($name, $force);
}
/**
* 创建数据库连接实例.
*
* @param string|array|null $name 连接标识
* @param bool $force 强制重新连接
*
* @return ConnectionInterface
*/
protected function instance(string|array|null $name = null, bool $force = false): ConnectionInterface
{
if (empty($name)) {
$name = $this->getConfig('default', 'mysql');
}
$key = is_array($name) ? md5(json_encode($name)) : $name;
if ($force || !isset($this->instance[$key])) {
$this->instance[$key] = $this->createConnection($name);
}
return $this->instance[$key];
}
/**
* 获取连接配置.
*
* @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 string|array $config
*
* @return ConnectionInterface
*/
protected function createConnection(string|array $config): ConnectionInterface
{
$config = is_array($config) ? $config : $this->getConnectionConfig($config);
$type = !empty($config['type']) ? $config['type'] : 'mysql';
if (str_contains($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;
}
/**
* 使用表达式设置数据.
*
* @param string $value 表达式
*
* @return Raw
*/
public function raw(string $value, array $bind = []): Raw
{
return new Raw($value, $bind);
}
/**
* 更新查询次数.
* @deprecated
* @return void
*/
public function updateQueryTimes(): void
{
}
/**
* 重置查询次数.
* @deprecated
* @return void
*/
public function clearQueryTimes(): void
{
$this->queryTimes = 0;
}
/**
* 获得查询次数.
* @deprecated
* @return int
*/
public function getQueryTimes(): int
{
return $this->queryTimes;
}
/**
* 监听SQL执行.
*
* @param callable $callback 回调方法
*
* @return void
*/
public function listen(callable $callback): void
{
$this->listen[] = $callback;
}
/**
* 获取监听SQL执行.
*
* @return array
*/
public function getListen(): array
{
return $this->listen;
}
/**
* 获取所有连接实列.
*
* @return array
*/
public function getInstance(): array
{
return $this->instance;
}
/**
* 注册回调方法.
*
* @param string $event 事件名
* @param callable $callback 回调方法
*
* @return void
*/
public function event(string $event, callable $callback): void
{
$this->event[$event][] = $callback;
}
/**
* 触发事件.
*
* @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);
}
}

327
vendor/topthink/think-orm/src/Entity.php vendored Normal file
View File

@@ -0,0 +1,327 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think;
use ArrayAccess;
use InvalidArgumentException;
use JsonSerializable;
use ReflectionClass;
use think\contract\Arrayable;
use think\contract\Jsonable;
use think\model\contract\Modelable;
use WeakMap;
/**
* Class Entity.
* @mixin Model
*/
abstract class Entity implements JsonSerializable, ArrayAccess, Arrayable, Jsonable, Modelable
{
private static ?WeakMap $weakMap = null;
/**
* 架构函数.
*
* @param Model $model 模型连接对象
*/
public function __construct(?Model $model = null)
{
$this->initWeakMap();
// 获取实体模型参数
$options = $this->getOptions();
if (is_null($model)) {
$class = !empty($options['modelClass']) ? $options['modelClass'] : str_replace('\\entity\\', '\\model\\', static::class);
$model = new $class();
$model->entity($this);
}
self::$weakMap[$this] = [
'model' => $model,
];
// 初始化模型
$this->setOptions($options);
$this->init($options);
}
protected function initWeakMap()
{
if (!self::$weakMap) {
self::$weakMap = new WeakMap;
}
}
/**
* 在实体模型中定义 返回相关配置参数.
*
* @return array
*/
protected function getOptions(): array
{
return [];
}
/**
* 批量设置模型参数
* @param array $options 值
* @return void
*/
public function setOptions(array $options): void
{
foreach ($options as $name => $value) {
$this->setOption($name, $value);
}
}
/**
* 设置模型参数
*
* @param string $name 参数名
* @param mixed $value 值
*
* @return $this
*/
public function setOption(string $name, $value)
{
self::$weakMap[$this][$name] = $value;
return $this;
}
/**
* 获取模型参数
*
* @param string $name 参数名
* @param mixed $default 默认值
*
* @return mixed
*/
public function getOption(string $name, $default = null)
{
return self::$weakMap[$this][$name] ?? $default;
}
/**
* 创建新的实例.
*
* @param Model $model 模型连接对象
*/
public function newInstance(?Model $model)
{
$entity = new static();
return $entity->setModel($model);
}
/**
* 初始化模型.
*
* @param array $options 模型参数
* @return void
*/
protected function init(array $options = []): void {}
/**
* 获取模型对象实例.
* @return Model
*/
public function model()
{
return self::$weakMap[$this]['model'];
}
/**
* 设置模型.
*
* @param Model $model 模型对象
* @return $this
*/
public function setModel(Model $model)
{
self::$weakMap[$this]['model'] = $model;
return $this;
}
/**
* 获取克隆的模型实例.
*
* @return static
*/
public function clone()
{
$model = new static();
self::$weakMap[$model] = self::$weakMap[$this];
return $model;
}
/**
* 克隆模型实例
*
* @return void
*/
public function __clone()
{
throw new InvalidArgumentException('use $modelObj->clone() replace clone $modelObj');
}
/**
* 序列化模型对象
*
* @return array
*/
public function __serialize(): array
{
return array_diff_key(self::$weakMap[$this]);
}
/**
* 反序列化模型对象
*
* @param array $data
* @return void
*/
public function __unserialize(array $data)
{
$this->initWeakMap();
self::$weakMap[$this] = $data;
}
/**
* 获取属性 支持获取器
*
* @param string $name 名称
*
* @return mixed
*/
public function __get(string $name)
{
return $this->model()->get($name);
}
/**
* 设置数据 支持类型自动转换
*
* @param string $name 名称
* @param mixed $value 值
*
* @return void
*/
public function __set(string $name, $value): void
{
$this->model()->set($name, $value);
}
/**
* 检测数据对象的值
*
* @param string $name 名称
*
* @return bool
*/
public function __isset(string $name): bool
{
return $this->model()->__isset($name);
}
/**
* 销毁数据对象的值
*
* @param string $name 名称
*
* @return void
*/
public function __unset(string $name): void
{
$this->model()->__unset($name);
}
public function __toString()
{
return $this->model()->toJson();
}
public function __debugInfo()
{
return $this->model()->getData();
}
// JsonSerializable
public function jsonSerialize(): array
{
return $this->model()->toArray();
}
/**
* 模型数据转数组.
*
* @return array
*/
public function toArray(): array
{
return $this->model()->toArray();
}
/**
* 模型数据转Json.
*
* @param int $options json参数
* @return string
*/
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return $this->model()->toJson($options);
}
// ArrayAccess
public function offsetSet(mixed $name, mixed $value): void
{
$this->__set($name, $value);
}
public function offsetGet(mixed $name): mixed
{
return $this->__get($name);
}
public function offsetExists(mixed $name): bool
{
return $this->__isset($name);
}
public function offsetUnset(mixed $name): void
{
$this->__unset($name);
}
public static function __callStatic($method, $args)
{
$entity = new static();
if (in_array($method, ['destroy', 'create', 'update', 'saveAll'])) {
// 调用model的静态方法
$db = $entity->model();
} else {
// 调用Query类查询方法
$db = $entity->model()->db();
}
return call_user_func_array([$db, $method], $args);
}
public function __call($method, $args)
{
// 调用Model类方法
return call_user_func_array([$this->model(), $method], $args);
}
}

892
vendor/topthink/think-orm/src/Model.php vendored Normal file
View File

@@ -0,0 +1,892 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
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;
use think\db\Express;
use think\exception\ValidateException;
use think\model\Collection;
use think\model\contract\Modelable;
use think\model\View;
use WeakMap;
/**
* Class Model.
* @mixin \think\db\Query
*
* @method static void onAfterRead(Model $model) after_read事件定义
* @method static mixed onBeforeInsert(Model $model) before_insert事件定义
* @method static void onAfterInsert(Model $model) after_insert事件定义
* @method static mixed onBeforeUpdate(Model $model) before_update事件定义
* @method static void onAfterUpdate(Model $model) after_update事件定义
* @method static mixed onBeforeWrite(Model $model) before_write事件定义
* @method static void onAfterWrite(Model $model) after_write事件定义
* @method static mixed onBeforeDelete(Model $model) before_write事件定义
* @method static void onAfterDelete(Model $model) after_delete事件定义
* @method static void onBeforeRestore(Model $model) before_restore事件定义
* @method static void onAfterRestore(Model $model) after_restore事件定义
*/
abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable, Modelable
{
use model\concern\Attribute;
use model\concern\AutoWriteData;
use model\concern\Conversion;
use model\concern\DbConnect;
use model\concern\ModelEvent;
use model\concern\RelationShip;
private static ?WeakMap $weakMap = null;
/**
* 服务注入.
*
* @var Closure[]
*/
protected static array $_maker = [];
/**
* 设置服务注入.
*/
public static function maker(Closure $maker): void
{
static::$_maker[] = $maker;
}
/**
* 设置容器对象的依赖注入方法.(用于兼容)
*
* @param callable $callable 依赖注入方法
*
* @return void
*/
public static function setInvoker(callable $callable): void
{
}
/**
* 调用反射执行模型方法 支持参数绑定.
*
* @param mixed $method
* @param array $vars 参数
*
* @return mixed
*/
public function invoke($method, array $vars = [])
{
if (is_string($method)) {
$method = [$this, $method];
}
$invoker = $this->getOption('invoker');
if ($invoker) {
return $invoker($method instanceof Closure ? $method : Closure::fromCallable($method), $vars);
}
return call_user_func_array($method, $vars);
}
/**
* 架构函数.
*
* @param array|object $data 实体模型数据
*/
public function __construct(array | object $data = [])
{
// 获取实体模型参数
$options = $this->getOptions();
if (!self::$weakMap) {
self::$weakMap = new WeakMap;
}
self::$weakMap[$this] = [
'get' => [],
'data' => [],
'origin' => [],
'relation' => [],
'together' => [],
'allow' => [],
'withAttr' => [],
'schema' => $options['schema'] ?? [],
'updateTime' => $options['updateTime'] ?? 'update_time',
'createTime' => $options['createTime'] ?? 'create_time',
'suffix' => $options['suffix'] ?? '',
'validate' => $options['validate'] ?? $this->parseValidate(),
'type' => $options['type'] ?? [],
'readonly' => $options['readonly'] ?? [],
'disuse' => $options['disuse'] ?? [],
'hidden' => $options['hidden'] ?? [],
'visible' => $options['visible'] ?? [],
'append' => $options['append'] ?? [],
'mapping' => $options['mapping'] ?? [],
'strict' => $options['strict'] ?? true,
'bindAttr' => $options['bindAttr'] ?? [],
'autoRelation' => $options['autoRelation'] ?? [],
];
// 设置额外参数
$this->setOptions(array_diff_key($options, self::$weakMap[$this]));
// 模型初始化
$this->initialize();
// 初始化数据
$this->initializeData($data);
}
protected function initialize()
{
if (!empty(static::$_maker)) {
foreach (static::$_maker as $maker) {
call_user_func($maker, $this);
}
}
// 初始化模型
$this->init();
}
/**
* 初始化模型.
*
* @return void
*/
protected function init()
{}
/**
* 在实体模型中定义 返回相关配置参数.
*
* @return array
*/
protected function getOptions(): array
{
return [];
}
/**
* 批量设置模型参数
* @param array $options 值
* @return void
*/
public function setOptions(array $options): void
{
foreach ($options as $name => $value) {
$this->setOption($name, $value);
}
}
/**
* 设置模型参数
*
* @param string $name 参数名
* @param mixed $value 值
*
* @return $this
*/
public function setOption(string $name, $value)
{
self::$weakMap[$this][$name] = $value;
if (property_exists($this, $name)) {
$this->$name = $value;
}
return $this;
}
/**
* 获取模型参数
*
* @param string $name 参数名
* @param mixed $default 默认值
*
* @return mixed
*/
public function getOption(string $name, $default = null)
{
// 兼容读取3.0版本的属性参数定义
if (property_exists($this, $name) && isset($this->$name)) {
return $this->$name;
}
return self::$weakMap[$this][$name] ?? $default;
}
private function setWeakData(string $key, string $name, $value): void
{
self::$weakMap[$this][$key][$name] = $value;
}
private function getWeakData(string $key, string $name, $default = null)
{
return self::$weakMap[$this][$key][$name] ?? $default;
}
/**
* 创建新的模型实例.
*
* @param array|object $data
*
* @return Model|Entity
*/
public function newInstance(array | object $data = [])
{
$model = new static($data);
if (!empty($data)) {
$model->exists(true);
}
if ($this->getEntity()) {
// 存在对应实体模型实例
return $this->getEntity()->newInstance($model);
}
return $this->fetchModel($model);
}
/**
* 获取克隆的模型实例.
*
* @return static
*/
public function clone()
{
$model = new static();
self::$weakMap[$model] = self::$weakMap[$this];
return $model;
}
/**
* 获取实际模型实例.
*
* @param Model $model
*
* @return Modelable
*/
protected function fetchModel(Model $model): Modelable
{
$class = $model->getOption('entityClass', str_replace('\\model\\', '\\entity\\', static::class));
if (class_exists($class) && is_subclass_of($class, Entity::class)) {
$entity = new $class($model);
$model->entity($entity);
return $entity;
}
return $model;
}
public function entity(Entity $entity): void
{
$this->setOption('entity', $entity);
}
public function getEntity(): ?Entity
{
return $this->getOption('entity');
}
/**
* 解析对应验证类.
*
* @return string
*/
protected function parseValidate(): string
{
$auto = $this->getOption('autoValidate', false);
$validate = $auto && str_contains(static::class, '\\model\\') ? str_replace('\\model\\', '\\validate\\', static::class) : '';
return $validate && class_exists($validate) ? $validate : '';
}
/**
* 设置验证场景.
*
* @param string|array $scene 场景名或数组
* @return $this
*/
public function scene(string|array $scene)
{
return $this->setOption('scene', $scene);
}
/**
* 验证模型数据.
*
* @param array $data 数据
* @param array $allow 需要验证的字段
*
* @throws ValidateException
* @return void
*/
protected function validate(array $data, array $allow = []): void
{
$validater = $this->getOption('validate');
if (!empty($validater)) {
validate($validater)
->scene($this->getOption('scene') ?: $allow)
->check($data);
}
}
/**
* 保存模型实例数据.
*
* @param array|object $data 数据
* @param mixed $where 更新条件 true为强制新增
* @param bool $refresh 是否刷新数据
* @return bool
*/
public function save(array | object $data = [], $where = [], bool $refresh = false): bool
{
if (!empty($data)) {
// 初始化模型数据
$this->initializeData($data, true);
}
if (false === $this->trigger('BeforeWrite')) {
return false;
}
if (true === $where) {
$isUpdate = false;
$where = [];
} elseif (!empty($where)) {
$isUpdate = true;
} else {
$isUpdate = $this->isExists() ? true : false;
}
if (false === $this->trigger($isUpdate ? 'BeforeUpdate' : 'BeforeInsert')) {
return false;
}
[$data, $relations, $allow] = $this->validateAndFilterData($isUpdate);
if (empty($data)) {
// 保存关联数据
if ($isUpdate && $this->getOption('together')) {
$this->relationSave($relations, $isUpdate);
}
return true;
}
// 自动写入数据
$this->autoWriteData($data, $isUpdate, $allow);
$db = $this->getDbWhere($where);
$result = $db->field($allow)
->removeOption('data')
->save($data, !$isUpdate);
if (!$isUpdate) {
$this->exists(true);
// 写入自增键值
$key = $db->getAutoInc();
$val = $db->getLastInsID();
if ($key && $val) {
$this->setData($key, $val);
}
} elseif ($refresh) {
// 刷新数据
$this->refresh();
}
$this->trigger($isUpdate ? 'AfterUpdate' : 'AfterInsert');
$this->trigger('AfterWrite');
// 保存关联数据
if ($this->getOption('together')) {
$this->relationSave($relations, $isUpdate);
}
// 重置原始数据
$this->refreshOrigin();
return true;
}
/**
* 验证和过滤数据
* @param bool $isUpdate 是否更新
* @return array [$data, $relations, $allow]
*/
protected function validateAndFilterData(bool $isUpdate): array
{
$data = $this->getData();
$origin = $this->getOrigin();
$allow = $this->getOption('allow') ?: array_keys($this->getFields());
$readonly = $this->getOption('readonly');
$disuse = $this->getOption('disuse');
$allow = array_diff($allow, $disuse, $isUpdate ? $readonly : []);
$together = $this->getOption('together');
// 验证数据
$this->validate($data, $allow);
$relations = [];
foreach ($data as $name => &$val) {
if ($val instanceof Modelable || in_array($name, $together)) {
$relations[$name] = $val;
unset($data[$name]);
} elseif ($val instanceof Collection || (!empty($allow) && !in_array($name, $allow))) {
unset($data[$name]);
} elseif ($isUpdate && !$this->isForce() && $this->isNotRequireUpdate($name, $val, $origin)) {
unset($data[$name]);
} else {
$val = $this->setWithAttr($name, $val);
}
}
return [$data, $relations, $allow];
}
/**
* 数据检查.
* @param array $data 数据
* @param bool $isUpdate 是否更新
* @return void
*/
protected function checkData(array &$data, bool $isUpdate): void
{
}
protected function getDbWhere($where = [])
{
$db = $this->db();
// 检查条件
if (!empty($where)) {
$db->where($where);
} elseif ($this->getKey()) {
$db->setKey($this->getKey());
} else {
$db->where($this->getOrigin());
}
if ($this->isForce()) {
$db->removeOption('soft_delete');
}
return $db;
}
/**
* 检查字段是否有更新(主键无需更新).
*
* @param string $name 字段
* @param mixed $val 值
* @param array $origin 原始数据
* @return bool
*/
protected function isNotRequireUpdate(string $name, $val, array $origin): bool
{
return (array_key_exists($name, $origin) && $val === $origin[$name]) || $this->getPk() == $name;
}
/**
* 获取更新数据.
*
* @return array
*/
public function getChangedData(): array
{
$data = $this->getData();
$origin = $this->getOrigin();
$change = [];
foreach ($data as $name => $val) {
if (!array_key_exists($name, $origin) || $val !== $origin[$name]) {
$change[$name] = $val;
}
}
return $change;
}
/**
* 判断数据是否有更新.
*
* @param string $name 字段
* @return bool
*/
public function isChange(string $name): bool
{
return $this->getData($name) !== $this->getOrigin($name);
}
/**
* 刷新模型数据.
*
* @return static
*/
public function refresh()
{
if ($this->isExists() && $this->getKey()) {
$data = $this->db()->find($this->getKey())->getData();
$this->data($data);
}
return $this;
}
/**
* 保存多个数据到当前数据对象
*
* @param iterable $dataSet 数据
* @param bool $replace 是否replace
*
* @return Collection
*/
public static function saveAll(iterable $dataSet, bool $replace = true): Collection
{
$result = [];
$model = new static;
$pk = $model->getPk();
foreach ($dataSet as $key => $data) {
$model = new static;
if ($replace) {
$exists = true;
foreach ((array) $pk as $field) {
if (is_string($field) && !isset($data[$field])) {
$exists = false;
}
}
} else {
$exists = false;
}
$model->replace($replace)->exists($exists)->save($data);
$result[$key] = $model->fetchModel($model);
}
return $model->toCollection($result);
}
/**
* 删除模型数据.
*
* @return bool
*/
public function delete(): bool
{
if ($this->isEmpty() || false === $this->trigger('BeforeDelete')) {
return false;
}
foreach ($this->getData() as $name => $val) {
if ($val instanceof Model || $val instanceof Collection) {
$relations[$name] = $val;
}
}
$result = $this->getDbWhere()->delete();
$this->trigger('AfterDelete');
if ($result && !empty($relations)) {
// 删除关联数据
$this->relationDelete($relations);
}
$this->exists(false);
$this->clear();
return true;
}
/**
* 写入数据.
*
* @param array|object $data 数据
* @param array $allowField 允许字段
* @param bool $replace 使用Replace
* @param string $suffix 数据表后缀
* @return Modelable
*/
public static function create(array | object $data, array $allowField = [], bool $replace = false, string $suffix = ''): Modelable
{
$model = new static();
if (!empty($suffix)) {
$model->setSuffix($suffix);
}
$model->allowField($allowField)->replace($replace)->save($data, true);
return $model->fetchModel($model);
}
/**
* 更新数据.
*
* @param array|object $data 数据
* @param mixed $where 更新条件
* @param array $allowField 允许字段
* @param string $suffix 数据表后缀
* @param bool $refresh 是否刷新数据
* @return Modelable
*/
public static function update(array | object $data, $where = [], array $allowField = [], string $suffix = '', bool $refresh = false): Modelable
{
$model = new static();
if (!empty($suffix)) {
$model->setSuffix($suffix);
}
$model->allowField($allowField)->exists(true)->save($data, $where, $refresh);
return $model->fetchModel($model);
}
/**
* 删除记录.
*
* @param mixed $data 主键列表 支持闭包查询条件
* @param bool $force 是否强制删除
*
* @return bool
*/
public static function destroy($data, bool $force = false): bool
{
$model = new static();
$db = $model->db();
if (is_array($data) && key($data) !== 0) {
$db->where($data);
$data = [];
} elseif ($data instanceof Closure) {
$data($db);
$data = [];
}
$resultSet = $db->select((array) $data);
foreach ($resultSet as $result) {
$result->force($force)->delete();
}
return true;
}
/**
* 字段值增长
*
* @param string $field 字段名
* @param float|int $step 增长值
* @param int $lazyTime 延迟时间(秒)
*
* @return $this
*/
public function inc(string $field, float|int $step = 1, int $lazyTime = 0)
{
return $this->set($field, new Express('+', $step, $lazyTime));
}
/**
* 字段值减少.
*
* @param string $field 字段名
* @param float|int $step 增长值
* @param int $lazyTime 延迟时间(秒)
*
* @return $this
*/
public function dec(string $field, float|int $step = 1, int $lazyTime = 0)
{
return $this->set($field, new Express('-', $step, $lazyTime));
}
/**
* 查询缓存 数据为空不缓存.
*
* @param mixed $key 缓存key
* @param int|\DateTime $expire 缓存有效期
* @param string|array $tag 缓存标签
*
* @return $this
*/
public function setCache($key = true, $expire = null, $tag = null)
{
return $this->setOption('cache', [$key, $expire, $tag]);
}
/**
* 允许写入字段.
*
* @param array $allow 允许字段
*
* @return $this
*/
public function allowField(array $allow)
{
return $this->setOption('allow', $allow);
}
/**
* 动态设置只读字段.
*
* @param array $fields 只读字段
*
* @return $this
*/
public function readonly(array $fields)
{
return $this->setOption('readonly', $fields);
}
/**
* 强制写入或删除
*
* @param bool $force 强制更新
*
* @return $this
*/
public function force(bool $force = true)
{
return $this->setOption('force', $force);
}
/**
* 判断数据是否强制写入或删除.
*
* @return bool
*/
public function isForce(): bool
{
return $this->getOption('force', false);
}
/**
* 获取属性 支持获取器
*
* @param string $name 名称
*
* @return mixed
*/
public function __get(string $name)
{
return $this->get($name);
}
/**
* 设置数据 支持类型自动转换
*
* @param string $name 名称
* @param mixed $value 值
*
* @return void
*/
public function __set(string $name, $value): void
{
if ($value instanceof Modelable && $bind = $this->getBindAttr($this->getOption('bindAttr'), $name)) {
// 关联属性绑定
$this->bindRelationAttr($value, $bind);
} else {
$this->set($name, $value);
}
}
/**
* 检测数据对象的值
*
* @param string $name 名称
*
* @return bool
*/
public function __isset(string $name): bool
{
return !is_null($this->get($name));
}
/**
* 销毁数据对象的值
*
* @param string $name 名称
*
* @return void
*/
public function __unset(string $name): void
{
$name = $this->getRealFieldName($name);
$this->setWeakData('data', $name, null);
$this->setWeakData('get', $name, null);
}
public function __toString()
{
return $this->toJson();
}
public function __debugInfo()
{
return [
'data' => $this->getOption('data'),
'origin' => $this->getOption('origin'),
'schema' => $this->getOption('schema'),
];
}
// JsonSerializable
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* 序列化模型对象
*
* @return array
*/
public function __serialize(): array
{
$removeKeys = ['invoker', 'db', 'event'];
return array_diff_key(self::$weakMap[$this], array_flip($removeKeys));
}
/**
* 反序列化模型对象
*
* @param array $data
* @return void
*/
public function __unserialize(array $data)
{
if (!self::$weakMap) {
self::$weakMap = new WeakMap;
}
self::$weakMap[$this] = $data;
// 重新初始化
$this->initialize();
}
/**
* 克隆模型实例
*
* @return void
*/
public function __clone()
{
throw new InvalidArgumentException('use $modelObj->clone() replace clone $modelObj');
}
// ArrayAccess
public function offsetSet(mixed $name, mixed $value): void
{
$this->set($name, $value);
}
public function offsetGet(mixed $name): mixed
{
return $this->get($name);
}
public function offsetExists(mixed $name): bool
{
return $this->__isset($name);
}
public function offsetUnset(mixed $name): void
{
$this->__unset($name);
}
}

View File

@@ -0,0 +1,568 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 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
*/
protected $simple = false;
/**
* 数据集.
*
* @var Collection
*/
protected $items;
/**
* 当前页.
*
* @var int
*/
protected $currentPage;
/**
* 最后一页.
*
* @var int
*/
protected $lastPage;
/**
* 数据总数.
*
* @var int|null
*/
protected $total;
/**
* 每页数量.
*
* @var int
*/
protected $listRows;
/**
* 是否有下一页.
*
* @var bool
*/
protected $hasMore;
/**
* 分页配置.
*
* @var array
*/
protected $options = [
'var_page' => 'page',
'path' => '/',
'query' => [],
'fragment' => '',
];
/**
* 获取当前页码
*
* @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);
$this->options['path'] = '/' != $this->options['path'] ? rtrim($this->options['path'], '/') : $this->options['path'];
$this->simple = $simple;
$this->listRows = $listRows;
if (!$items instanceof Collection) {
$items = Collection::make($items);
}
if ($simple) {
$this->currentPage = $this->setCurrentPage($currentPage);
$this->hasMore = count($items) > ($this->listRows);
$items = $items->slice(0, $this->listRows);
} else {
$this->total = $total;
$this->lastPage = (int) ceil($total / $listRows);
$this->currentPage = $this->setCurrentPage($currentPage);
$this->hasMore = $this->currentPage < $this->lastPage;
}
$this->items = $items;
}
/**
* @param mixed $items
* @param int $listRows
* @param int $currentPage
* @param int $total
* @param bool $simple
* @param array $options
*
* @return Paginator
*/
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)
{
static::$maker = $resolver;
}
protected function setCurrentPage(int $currentPage): int
{
if (!$this->simple && $currentPage > $this->lastPage) {
return $this->lastPage > 0 ? $this->lastPage : 1;
}
return $currentPage;
}
/**
* 获取页码对应的链接.
*
* @param int $page
*
* @return string
*/
protected function url(int $page): string
{
if ($page <= 0) {
$page = 1;
}
if (!str_contains($this->options['path'], '[PAGE]')) {
$parameters = [$this->options['var_page'] => $page];
$path = $this->options['path'];
} else {
$parameters = [];
$path = str_replace('[PAGE]', (string) $page, $this->options['path']);
}
if (count($this->options['query']) > 0) {
$parameters = array_merge($this->options['query'], $parameters);
}
$url = $path;
if (!empty($parameters)) {
$url .= '?' . http_build_query($parameters, '', '&');
}
return $url . $this->buildFragment();
}
/**
* 自动获取当前页码
*
* @param string $varPage
* @param int $default
*
* @return int
*/
public static function getCurrentPage(string $varPage = 'page', int $default = 1): int
{
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.
*
* @param string $default
*
* @return string
*/
public static function getCurrentPath($default = '/'): string
{
if (isset(static::$currentPathResolver)) {
return call_user_func(static::$currentPathResolver);
}
return $default;
}
/**
* 设置获取当前路径闭包.
*
* @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');
}
return $this->total;
}
/**
* 获取每页数量.
*
* @return int
*/
public function listRows(): int
{
return $this->listRows;
}
/**
* 获取当前页页码
*
* @return bool
*/
public function hasMore(): bool
{
return $this->hasMore;
}
/**
* 获取当前页页码
*
* @return int
*/
public function currentPage(): int
{
return $this->currentPage;
}
/**
* 获取最后一页页码
*
* @return int
*/
public function lastPage(): int
{
if ($this->simple) {
throw new DomainException('not support last');
}
return $this->lastPage;
}
/**
* 数据是否足够分页.
*
* @return bool
*/
public function hasPages(): bool
{
return !(1 == $this->currentPage && !$this->hasMore);
}
/**
* 创建一组分页链接.
*
* @param int $start
* @param int $end
*
* @return array
*/
public function getUrlRange(int $start, int $end): array
{
$urls = [];
for ($page = $start; $page <= $end; $page++) {
$urls[$page] = $this->url($page);
}
return $urls;
}
/**
* 设置URL锚点.
*
* @param string|null $fragment
*
* @return $this
*/
public function fragment(?string $fragment = null)
{
$this->options['fragment'] = $fragment;
return $this;
}
/**
* 添加URL参数.
*
* @param array $append
*
* @return $this
*/
public function appends(array $append)
{
foreach ($append as $k => $v) {
if ($k !== $this->options['var_page']) {
$this->options['query'][$k] = $v;
}
}
return $this;
}
/**
* 构造锚点字符串.
*
* @return string
*/
protected function buildFragment(): string
{
return $this->options['fragment'] ? '#' . $this->options['fragment'] : '';
}
/**
* 渲染分页html.
*
* @return mixed
*/
abstract public function render();
public function items()
{
return $this->items->all();
}
/**
* 获取数据集.
*
* @return Collection|\think\model\Collection
*/
public function getCollection()
{
return $this->items;
}
/**
* 设置数据集.
*
* @param Collection $items
*
* @return $this
*/
public function setCollection(Collection $items)
{
$this->items = $items;
return $this;
}
public function isEmpty(): bool
{
return $this->items->isEmpty();
}
/**
* 给每个元素执行个回调.
*
* @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;
}
/**
* Retrieve an external iterator.
*
* @return Traversable An instance of an object implementing <b>Iterator</b> or
* <b>Traversable</b>
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->items->all());
}
/**
* Whether a offset exists.
*
* @param mixed $offset
*
* @return bool
*/
public function offsetExists(mixed $offset): bool
{
return $this->items->offsetExists($offset);
}
/**
* Offset to retrieve.
*
* @param mixed $offset
*
* @return mixed
*/
public function offsetGet(mixed $offset): mixed
{
return $this->items->offsetGet($offset);
}
/**
* Offset to set.
*
* @param mixed $offset
* @param mixed $value
*/
public function offsetSet(mixed $offset, mixed $value): void
{
$this->items->offsetSet($offset, $value);
}
/**
* Offset to unset.
*
* @param mixed $offset
*
* @return void
*
* @since 5.0.0
*/
public function offsetUnset(mixed $offset): void
{
$this->items->offsetUnset($offset);
}
/**
* 统计数据集条数.
*
* @return int
*/
public function count(): int
{
return $this->items->count();
}
public function __toString()
{
return (string) $this->render();
}
/**
* 转换为数组.
*
* @return array
*/
public function toArray(): array
{
try {
$total = $this->total();
} catch (DomainException $e) {
$total = null;
}
return [
'total' => $total,
'per_page' => $this->listRows(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage,
'data' => $this->items->toArray(),
'has_more' => $this->hasMore,
];
}
/**
* Specify data which should be serialized to JSON.
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
public function __call($name, $arguments)
{
$result = call_user_func_array([$this->items, $name], $arguments);
if ($result instanceof Collection) {
$this->items = $result;
return $this;
}
return $result;
}
}

View File

@@ -0,0 +1,971 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db;
use BackedEnum;
use Closure;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use UnitEnum;
/**
* Db Base Builder.
*/
abstract class BaseBuilder
{
/**
* Connection对象
*
* @var ConnectionInterface
*/
protected $connection;
/**
* 查询表达式映射.
*
* @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'],
'parseBetween' => ['NOT BETWEEN', 'BETWEEN'],
'parseIn' => ['NOT IN', 'IN'],
'parseExp' => ['EXP'],
'parseNull' => ['NOT NULL', 'NULL'],
'parseBetweenTime' => ['BETWEEN TIME', 'NOT BETWEEN TIME'],
'parseTime' => ['< TIME', '> TIME', '<= TIME', '>= TIME'],
'parseExists' => ['NOT EXISTS', 'EXISTS'],
'parseColumn' => ['COLUMN'],
];
/**
* SELECT SQL表达式.
*
* @var string
*/
protected $selectSql = 'SELECT%DISTINCT%%EXTRA% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT% %LOCK%%COMMENT%';
/**
* INSERT SQL表达式.
*
* @var string
*/
protected $insertSql = '%INSERT%%EXTRA% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
/**
* INSERT ALL SQL表达式.
*
* @var string
*/
protected $insertAllSql = '%INSERT%%EXTRA% INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
/**
* UPDATE SQL表达式.
*
* @var string
*/
protected $updateSql = 'UPDATE%EXTRA% %TABLE% SET %SET%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%';
/**
* DELETE SQL表达式.
*
* @var string
*/
protected $deleteSql = 'DELETE%EXTRA% FROM %TABLE%%USING%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%';
/**
* 架构函数.
*
* @param ConnectionInterface $connection 数据库连接对象实例
*/
public function __construct(?ConnectionInterface $connection)
{
$this->connection = $connection;
}
/**
* 获取当前的连接对象实例.
*
* @return ConnectionInterface
*/
public function getConnection(): ConnectionInterface
{
return $this->connection;
}
/**
* 注册查询表达式解析.
*
* @param string $name 解析方法
* @param array $parser 匹配表达式数据
*
* @return $this
*/
public function bindParser(string $name, array $parser)
{
$this->parser[$name] = $parser;
return $this;
}
/**
* 数据分析.
*
* @param Query $query 查询对象
* @param array $data 数据
* @param array $fields 字段信息
* @param array $bind 参数绑定
*
* @return array
*/
abstract protected function parseData(Query $query, array $data = [], array $fields = [], array $bind = []): array;
/**
* 数据绑定处理.
*
* @param Query $query 查询对象
* @param string $key 字段名
* @param mixed $data 数据
* @param array $bind 绑定数据
*
* @return string
*/
abstract protected function parseDataBind(Query $query, string $key, $data, array $bind = []): string;
/**
* 字段名分析.
*
* @param Query $query 查询对象
* @param mixed $key 字段名
* @param bool $strict 严格检测
*
* @return string
*/
abstract public function parseKey(Query $query, string | int | Raw $key, bool $strict = false): string;
/**
* 查询额外参数分析.
*
* @param Query $query 查询对象
* @param string $extra 额外参数
*
* @return string
*/
abstract protected function parseExtra(Query $query, string $extra): string;
/**
* field分析.
*
* @param Query $query 查询对象
* @param array $fields 字段名
*
* @return string
*/
abstract protected function parseField(Query $query, array $fields): string;
/**
* table分析.
*
* @param Query $query 查询对象
* @param array|string $tables 表名
*
* @return string
*/
abstract protected function parseTable(Query $query, array | string $tables): string;
/**
* where分析.
*
* @param Query $query 查询对象
* @param array $where 查询条件
*
* @return string
*/
abstract protected function parseWhere(Query $query, array $where): string;
/**
* 生成查询条件SQL.
*
* @param Query $query 查询对象
* @param array $where 查询条件
*
* @return string
*/
public function buildWhere(Query $query, array $where): string
{
if (empty($where)) {
$where = [];
}
$whereStr = '';
$binds = $query->getFieldsBindType();
foreach ($where as $logic => $val) {
$str = $this->parseWhereLogic($query, $logic, $val, $binds);
$whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($logic) + 1) : implode(' ', $str);
}
return $whereStr;
}
/**
* 不同字段使用相同查询条件AND.
*
* @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) {
// 使用闭包查询
$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 (str_contains($field, '|')) {
$where[] = $this->parseFieldsOr($query, $value, $field, $logic, $binds);
} elseif (str_contains($field, '&')) {
$where[] = $this->parseFieldsAnd($query, $value, $field, $logic, $binds);
} else {
// 对字段使用表达式查询
$field = is_string($field) ? $field : '';
$where[] = ' ' . $logic . ' ' . $this->parseWhereItem($query, $field, $value, $binds);
}
}
return $where;
}
/**
* 不同字段使用相同查询条件AND.
*
* @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 ' ' . $logic . ' ( ' . implode(' AND ', $item) . ' )';
}
/**
* 不同字段使用相同查询条件OR.
*
* @param Query $query 查询对象
* @param array $value 查询条件
* @param string $field 查询字段
* @param string $logic Logic
* @param array $binds 参数绑定
*
* @return string
*/
protected function parseFieldsOr(Query $query, array $value, string $field, string $logic, array $binds): string
{
$item = [];
foreach (explode('|', $field) as $k) {
$item[] = $this->parseWhereItem($query, $k, $value, $binds);
}
return ' ' . $logic . ' ( ' . implode(' OR ', $item) . ' )';
}
/**
* 闭包查询.
*
* @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->getOption('where') ?: []);
if (!empty($whereClosure)) {
$query->bind($newQuery->getBind(false));
$where = ' ' . $logic . ' ( ' . $whereClosure . ' )';
}
return $where ?? '';
}
/**
* 复合条件查询.
*
* @param Query $query 查询对象
* @param mixed $value 查询条件
* @param mixed $field 查询字段
* @param string $logic Logic
* @param array $binds 参数绑定
*
* @return string
*/
protected function parseMultiWhereField(Query $query, array $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子单元分析.
*
* @param Query $query 查询对象
* @param mixed $field 查询字段
* @param array $val 查询条件
* @param array $binds 参数绑定
*
* @return string
*/
abstract protected function parseWhereItem(Query $query, $field, array $val, array $binds = []): string;
/**
* 模糊查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param array $value
* @param string $field
* @param int $bindType
* @param string $logic
*
* @return string
*/
abstract protected function parseLike(Query $query, string $key, string $exp, $value, $field, int $bindType, string $logic): string;
/**
* 表达式查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param Raw $value
* @param string $field
* @param int $bindType
*
* @return string
*/
protected function parseExp(Query $query, string $key, string $exp, Raw $value, string $field, int $bindType): string
{
// 表达式查询
return '( ' . $key . ' ' . $this->parseRaw($query, $value) . ' )';
}
/**
* 表达式查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param array $value
* @param string $field
* @param int $bindType
*
* @return string
*/
protected function parseColumn(Query $query, string $key, $exp, array $value, string $field, int $bindType): string
{
// 字段比较查询
[$op, $field] = $value;
if (!in_array(trim($op), ['=', '<>', '>', '>=', '<', '<='])) {
throw new Exception('where express error:' . var_export($value, true));
}
return '( ' . $key . ' ' . $op . ' ' . $this->parseKey($query, $field, true) . ' )';
}
/**
* Null查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
* @param int $bindType
*
* @return string
*/
abstract protected function parseNull(Query $query, string $key, string $exp, $value, $field, int $bindType): string;
/**
* 范围查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
* @param int $bindType
*
* @return string
*/
abstract protected function parseBetween(Query $query, string $key, string $exp, array | string $value, $field, int $bindType): string;
/**
* Exists查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param Raw|Closure $value
* @param string $field
* @param int $bindType
*
* @return string
*/
protected function parseExists(Query $query, string $key, string $exp, Raw | Closure $value, string $field, int $bindType): string
{
// EXISTS 查询
if ($value instanceof Closure) {
$value = $this->parseClosure($query, $value, false);
} elseif ($value instanceof Raw) {
$value = $this->parseRaw($query, $value);
}
return $exp . ' ( ' . $value . ' )';
}
/**
* 时间比较查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
* @param int $bindType
*
* @return string
*/
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);
}
/**
* 大小比较查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
* @param int $bindType
* @param string $param
*
* @return string
*/
protected function parseCompare(Query $query, string $key, string $exp, $value, $field, int $bindType, ?string $param = null): string
{
if (is_array($value)) {
throw new Exception('where express error:' . $exp . var_export($value, true));
}
// 比较运算
if ($value instanceof Closure) {
$value = $this->parseClosure($query, $value);
} elseif ($value instanceof Raw) {
$value = $this->parseRaw($query, $value);
} elseif ($value instanceof BackedEnum) {
$value = $value->value;
} elseif ($value instanceof UnitEnum) {
$value = $value->name;
}
if ('=' == $exp && is_null($value)) {
return $key . ' IS NULL';
}
if (is_null($param)) {
return $key . ' ' . $exp . ' ' . $value;
} else {
return '( ' . $key . ' ' . $exp . ' ' . $value . ' ) ' . $param;
}
}
/**
* 时间范围查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
* @param int $bindType
*
* @return string
*/
protected function parseBetweenTime(Query $query, string $key, string $exp, $value, $field, int $bindType): string
{
if (is_string($value)) {
$value = explode(',', $value);
}
return $key . ' ' . substr($exp, 0, -4)
. $this->parseDateTime($query, $value[0], $field, $bindType)
. ' AND '
. $this->parseDateTime($query, $value[1], $field, $bindType);
}
/**
* IN查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
* @param int $bindType
*
* @return string
*/
abstract protected function parseIn(Query $query, string $key, string $exp, $value, $field, int $bindType): string;
/**
* 闭包子查询.
*
* @param Query $query 查询对象
* @param Closure $call
* @param bool $show
*
* @return string
*/
protected function parseClosure(Query $query, Closure $call, bool $show = true): string
{
$newQuery = $query->newQuery()->removeOption();
$call($newQuery);
return $newQuery->buildSql($show);
}
/**
* 日期时间条件解析.
*
* @param Query $query 查询对象
* @param mixed $value
* @param string $key
* @param int $bindType
*
* @return string
*/
abstract protected function parseDateTime(Query $query, $value, string $key, int $bindType): string;
/**
* limit分析.
*
* @param Query $query 查询对象
* @param mixed $limit
*
* @return string
*/
abstract protected function parseLimit(Query $query, string $limit): string;
/**
* join分析.
*
* @param Query $query 查询对象
* @param array $join
*
* @return string
*/
abstract protected function parseJoin(Query $query, array $join): string;
/**
* order分析.
*
* @param Query $query 查询对象
* @param array $order
*
* @return string
*/
abstract protected function parseOrder(Query $query, array $order): string;
/**
* 分析Raw对象
*
* @param Query $query 查询对象
* @param Raw $raw Raw对象
*
* @return string
*/
abstract protected function parseRaw(Query $query, Raw $raw): string;
/**
* 随机排序.
*
* @param Query $query 查询对象
*
* @return string
*/
abstract protected function parseRand(Query $query): string;
/**
* group分析.
*
* @param Query $query 查询对象
* @param mixed $group
*
* @return string
*/
abstract protected function parseGroup(Query $query, string | array $group): string;
/**
* having分析.
*
* @param Query $query 查询对象
* @param string $having
*
* @return string
*/
abstract protected function parseHaving(Query $query, string $having): string;
/**
* comment分析.
*
* @param Query $query 查询对象
* @param string $comment
*
* @return string
*/
protected function parseComment(Query $query, string $comment): string
{
if (str_contains($comment, '*/')) {
$comment = strstr($comment, '*/', true);
}
return !empty($comment) ? ' /* ' . $comment . ' */' : '';
}
/**
* distinct分析.
*
* @param Query $query 查询对象
* @param mixed $distinct
*
* @return string
*/
abstract protected function parseDistinct(Query $query, bool $distinct): string;
/**
* union分析.
*
* @param Query $query 查询对象
* @param array $union
*
* @return string
*/
protected function parseUnion(Query $query, array $union): string
{
if (empty($union)) {
return '';
}
$type = $union['type'];
unset($union['type']);
foreach ($union as $u) {
if ($u instanceof Closure) {
$sql[] = $type . ' ' . $this->parseClosure($query, $u);
} elseif (is_string($u)) {
$sql[] = $type . ' ( ' . $u . ' )';
}
}
return ' ' . implode(' ', $sql);
}
/**
* index分析可在操作链中指定需要强制使用的索引.
*
* @param Query $query 查询对象
* @param mixed $index
*
* @return string
*/
abstract protected function parseForce(Query $query, string | array $index): string;
/**
* 设置锁机制.
*
* @param Query $query 查询对象
* @param bool|string $lock
*
* @return string
*/
abstract protected function parseLock(Query $query, bool | string $lock = false): string;
/**
* 生成查询SQL.
*
* @param Query $query 查询对象
* @param bool $one 是否仅获取一个记录
*
* @return string
*/
public function select(Query $query, bool $one = false): string
{
$options = $query->getOptions();
return str_replace(
['%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->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.
*
* @param Query $query 查询对象
*
* @return string
*/
public function insert(Query $query): string
{
$options = $query->getOptions();
// 分析并处理数据
$data = $this->parseData($query, $options['data']);
if (empty($data)) {
return '';
}
$fields = array_keys($data);
$values = array_values($data);
return str_replace(
['%INSERT%', '%TABLE%', '%EXTRA%', '%FIELD%', '%DATA%', '%COMMENT%'],
[
!empty($options['replace']) ? 'REPLACE' : 'INSERT',
$this->parseTable($query, $options['table']),
$this->parseExtra($query, $options['extra']),
implode(' , ', $fields),
implode(' , ', $values),
$this->parseComment($query, $options['comment']),
],
$this->insertSql
);
}
/**
* 生成insertall SQL.
*
* @param Query $query 查询对象
* @param array $dataSet 数据集
*
* @return string
*/
public function insertAll(Query $query, array $dataSet): string
{
$options = $query->getOptions();
// 获取绑定信息
$bind = $query->getFieldsBindType();
// 获取合法的字段
if (empty($options['field']) || '*' == $options['field']) {
$allowFields = array_keys($bind);
} else {
$allowFields = $options['field'];
}
$fields = [];
$values = [];
foreach ($dataSet as $k => $data) {
$data = $this->parseData($query, $data, $allowFields, $bind);
$values[] = 'SELECT ' . implode(',', array_values($data));
if (!isset($insertFields)) {
$insertFields = array_keys($data);
}
}
foreach ($insertFields as $field) {
$fields[] = $this->parseKey($query, $field);
}
return str_replace(
['%INSERT%', '%TABLE%', '%EXTRA%', '%FIELD%', '%DATA%', '%COMMENT%'],
[
!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']),
],
$this->insertAllSql
);
}
/**
* 生成slect insert SQL.
*
* @param Query $query 查询对象
* @param array $fields 数据
* @param string $table 数据表
*
* @return string
*/
public function selectInsert(Query $query, array $fields, string $table): string
{
foreach ($fields as &$field) {
$field = $this->parseKey($query, $field, true);
}
return 'INSERT INTO ' . $this->parseTable($query, $table) . ' (' . implode(',', $fields) . ') ' . $this->select($query);
}
/**
* 生成update SQL.
*
* @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%', '%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']),
$this->parseOrder($query, $options['order']),
$this->parseLimit($query, $options['limit']),
$this->parseLock($query, $options['lock']),
$this->parseComment($query, $options['comment']),
],
$this->updateSql
);
}
/**
* 生成delete SQL.
*
* @param Query $query 查询对象
*
* @return string
*/
public function delete(Query $query): string
{
$options = $query->getOptions();
return str_replace(
['%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']),
$this->parseOrder($query, $options['order']),
$this->parseLimit($query, $options['limit']),
$this->parseLock($query, $options['lock']),
$this->parseComment($query, $options['comment']),
],
$this->deleteSql
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,839 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db;
use BackedEnum;
use Closure;
use Stringable;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use UnitEnum;
/**
* Db Builder.
*/
class Builder extends BaseBuilder
{
/**
* 数据分析.
*
* @param Query $query 查询对象
* @param array $data 数据
* @param array $fields 字段信息
* @param array $bind 参数绑定
*
* @return array
*/
protected function parseData(Query $query, array $data = [], array $fields = [], array $bind = []): array
{
if (empty($data)) {
return [];
}
$options = $query->getOptions();
// 获取绑定信息
if (empty($bind)) {
$bind = $query->getFieldsBindType();
}
if (empty($fields)) {
if (empty($options['field']) || '*' == $options['field']) {
$fields = array_keys($bind);
} else {
$fields = $options['field'];
}
}
$result = [];
foreach ($data as $key => $val) {
$item = $this->parseKey($query, $key, true);
if ($val instanceof UnitEnum) {
$val = $this->parseEnum($val);
} elseif ($val instanceof Raw) {
$result[$item] = $this->parseRaw($query, $val);
continue;
} elseif (is_null($val) && in_array($key, $fields, true)) {
$result[$item] = 'NULL';
continue;
} elseif (!is_scalar($val) && (in_array($key, (array) $query->getOption('json')) || 'json' == $query->getFieldType($key))) {
$val = json_encode($val);
}
if (str_contains($key, '->')) {
[$key, $name] = explode('->', $key, 2);
$item = $this->parseKey($query, $key);
$result[$item . '->' . $name] = 'json_set(' . $item . ', \'$.' . $name . '\', ' . $this->parseDataBind($query, $key . '->' . $name, $val, $bind) . ')';
} elseif (!str_contains($key, '.') && !in_array($key, $fields, true)) {
if ($options['strict']) {
throw new Exception('fields not exists:[' . $key . ']');
}
} elseif ($val instanceof Express) {
if ($val->getLazyTime() && in_array($val->getType(), ['+', '-'])) {
$step = $query->lazyWrite($key, $val->getType() == '+' ? 'inc' : 'dec', $val->getStep(), $val->getLazyTime());
if (false === $step) {
continue;
}
$result[$item] = $item . ' + ' . $step;
} else {
$result[$item] = $item . $this->parseExpress($query, $val);
}
} elseif (is_array($val) && !empty($val) && is_string($val[0])) {
if (in_array(strtoupper($val[0]), ['INC', 'DEC'])) {
$result[$item] = match (strtoupper($val[0])) {
'INC' => $item . ' + ' . floatval($val[1]),
'DEC' => $item . ' - ' . floatval($val[1]),
};
}
} elseif (is_scalar($val)) {
// 过滤非标量数据
if (!$query->isAutoBind() && Connection::PARAM_STR == $bind[$key]) {
$val = '\'' . $val . '\'';
}
$result[$item] = !$query->isAutoBind() ? $val : $this->parseDataBind($query, $key, $val, $bind);
}
}
return $result;
}
/**
* 数据绑定处理.
*
* @param Query $query 查询对象
* @param string $key 字段名
* @param mixed $data 数据
* @param array $bind 绑定数据
*
* @return string
*/
protected function parseDataBind(Query $query, string $key, $data, array $bind = []): string
{
if ($data instanceof Raw) {
return $this->parseRaw($query, $data);
}
$name = $query->bindValue($data, $bind[$key] ?? Connection::PARAM_STR);
return ':' . $name;
}
/**
* 字段名分析.
*
* @param Query $query 查询对象
* @param mixed $key 字段名
* @param bool $strict 严格检测
*
* @return string
*/
public function parseKey(Query $query, string | int | Raw $key, bool $strict = false): string
{
return $key;
}
/**
* 查询额外参数分析.
*
* @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分析.
*
* @param Query $query 查询对象
* @param array $fields 字段名
*
* @return string
*/
protected function parseField(Query $query, array $fields): string
{
if (empty($fields)) {
return '*';
}
// 支持 'field1' => 'field2' 这样的字段别名定义
$array = array_map(function ($field, $key) use ($query) {
if ($field instanceof Raw) {
return $this->parseRaw($query, $field);
} elseif (!is_numeric($key)) {
// 字段别名定义
return $this->parseKey($query, $key) . ' AS ' . $this->parseKey($query, $field, true);
} else {
return $this->parseKey($query, $field);
}
}, $fields, array_keys($fields));
return implode(',', $array);
}
/**
* table分析.
*
* @param Query $query 查询对象
* @param array|string $tables 表名
*
* @return string
*/
protected function parseTable(Query $query, array | string $tables): string
{
$item = [];
$options = $query->getOptions();
foreach ((array) $tables as $key => $table) {
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 {
$item[] = $this->parseKey($query, $table);
}
}
return implode(',', $item);
}
/**
* where分析.
*
* @param Query $query 查询对象
* @param array $where 查询条件
*
* @return string
*/
protected function parseWhere(Query $query, array $where): string
{
$options = $query->getOptions();
$whereStr = $this->buildWhere($query, $where);
if (!empty($options['soft_delete'])) {
// 附加软删除条件
[$field, $condition] = $options['soft_delete'];
$binds = $query->getFieldsBindType();
$whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : '';
$whereStr = $whereStr . $this->parseWhereItem($query, $field, $condition, $binds);
}
return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
}
/**
* where子单元分析.
*
* @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));
}
$param = $val[2] ?? null;
$exp = strtoupper($exp);
if (isset($this->exp[$exp])) {
$exp = $this->exp[$exp];
}
if (is_string($field) && 'LIKE' != $exp) {
$bindType = $binds[$field] ?? Connection::PARAM_STR;
} else {
$bindType = Connection::PARAM_STR;
}
if ($value instanceof Raw) {
} elseif ($value instanceof Stringable) {
// 对象数据写入
$value = $value->__toString();
} elseif ($value instanceof UnitEnum) {
$value = $this->parseEnum($value);
}
if (is_scalar($value) && !in_array($exp, ['EXP', 'NOT NULL', 'NULL', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) && !str_contains($exp, 'TIME')) {
if (is_string($value) && str_starts_with($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)) {
return $this->$fun($query, $key, $exp, $value, $field, $bindType, $param);
}
}
throw new Exception('where express error:' . $exp);
}
/**
* 模糊查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param array $value
* @param string $field
* @param int $bindType
* @param string $logic
*
* @return string
*/
protected function parseLike(Query $query, string $key, string $exp, $value, $field, int $bindType, ?string $logic = null): string
{
// 模糊匹配
$logic = $logic ?: 'AND';
if (is_array($value)) {
$array = [];
foreach ($value as $item) {
$name = $query->bindValue($item, Connection::PARAM_STR);
$array[] = $key . ' ' . $exp . ' :' . $name;
}
$whereStr = '(' . implode(' ' . strtoupper($logic) . ' ', $array) . ')';
} else {
$whereStr = $key . ' ' . $exp . ' ' . $value;
}
return $whereStr;
}
/**
* 表达式查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param Raw $value
* @param string $field
* @param int $bindType
*
* @return string
*/
protected function parseExp(Query $query, string $key, string $exp, Raw $value, string $field, int $bindType): string
{
// 表达式查询
return '( ' . $key . ' ' . $this->parseRaw($query, $value) . ' )';
}
/**
* Null查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
* @param int $bindType
*
* @return string
*/
protected function parseNull(Query $query, string $key, string $exp, $value, $field, int $bindType): string
{
// NULL 查询
return $key . ' IS ' . $exp;
}
/**
* 范围查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
* @param int $bindType
*
* @return string
*/
protected function parseBetween(Query $query, string $key, string $exp, array | string $value, $field, int $bindType): string
{
// BETWEEN 查询
$data = is_array($value) ? $value : explode(',', $value);
$min = $query->bindValue($data[0], $bindType);
$max = $query->bindValue($data[1], $bindType);
return $key . ' ' . $exp . ' :' . $min . ' AND :' . $max . ' ';
}
/**
* 解析枚举类型值
*
* @param UnitEnum $value
*
* @return mixed
*/
protected function parseEnum(UnitEnum $value)
{
if ($value instanceof BackedEnum) {
$value = $value->value;
} else {
$value = $value->name;
}
return $value;
}
/**
* IN查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
* @param int $bindType
*
* @return string
*/
protected function parseIn(Query $query, string $key, string $exp, $value, $field, int $bindType): string
{
// IN 查询
if ($value instanceof Closure) {
$value = $this->parseClosure($query, $value, false);
} elseif ($value instanceof Raw) {
$value = $this->parseRaw($query, $value);
} else {
// 检查枚举类型
if (is_subclass_of($value, UnitEnum::class, false)) {
if (is_subclass_of($value, BackedEnum::class, false)) {
$value = array_column($value::cases(), 'value');
} else {
$value = array_column($value::cases(), 'name');
}
} else {
$value = is_array($value) ? $value : array_unique(explode(',', (string) $value));
}
if (count($value) === 0) {
return 'IN' == $exp ? '0 = 1' : '1 = 1';
}
if ($query->isAutoBind()) {
$array = [];
foreach ($value as $v) {
if ($v instanceof UnitEnum) {
$v = $this->parseEnum($v);
}
$name = $query->bindValue($v, $bindType);
$array[] = ':' . $name;
}
$value = implode(',', $array);
} else{
foreach ($value as &$v) {
if ($v instanceof UnitEnum) {
$v = $this->parseEnum($v);
}
}
if (Connection::PARAM_STR == $bindType) {
$value = '\'' . implode('\',\'', $value) . '\'';
} else {
$value = implode(',', $value);
}
}
if (!str_contains($value, ',')) {
return $key . ('IN' == $exp ? ' = ' : ' <> ') . $value;
}
}
return $key . ' ' . $exp . ' (' . $value . ')';
}
/**
* 日期时间条件解析.
*
* @param Query $query 查询对象
* @param mixed $value
* @param string $key
* @param int $bindType
*
* @return string
*/
protected function parseDateTime(Query $query, $value, string $key, int $bindType): string
{
$options = $query->getOptions();
// 获取时间字段类型
if (str_contains($key, '.')) {
[$table, $key] = explode('.', $key);
if (isset($options['alias']) && $pos = array_search($table, $options['alias'])) {
$table = $pos;
}
} else {
$table = $options['table'];
}
$type = $query->getFieldType($key);
if ($type) {
if (is_string($value)) {
$value = strtotime($value) ?: $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->bindValue($value, $bindType);
return ':' . $name;
}
/**
* limit分析.
*
* @param Query $query 查询对象
* @param mixed $limit
*
* @return string
*/
protected function parseLimit(Query $query, string $limit): string
{
return (!empty($limit) && !str_contains($limit, '(')) ? ' LIMIT ' . $limit . ' ' : '';
}
/**
* join分析.
*
* @param Query $query 查询对象
* @param array $join
*
* @return string
*/
protected function parseJoin(Query $query, array $join): string
{
$joinStr = '';
foreach ($join as $item) {
[$table, $type, $on] = $item;
if (str_contains($on, '=')) {
[$val1, $val2] = explode('=', $on, 2);
$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;
}
/**
* order分析.
*
* @param Query $query 查询对象
* @param array $order
*
* @return string
*/
protected function parseOrder(Query $query, array $order): string
{
$array = [];
foreach ($order as $key => $val) {
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)) {
[$key, $sort] = explode(' ', str_contains($val, ' ') ? $val : $val . ' ');
} else {
$sort = $val;
}
if (preg_match('/^[\w\-\>\.]+$/', $key)) {
$sort = strtoupper($sort);
$sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : '';
$array[] = $this->parseKey($query, $key, true) . $sort;
} else {
throw new Exception('order express error:' . $key);
}
}
}
return empty($array) ? '' : ' ORDER BY ' . implode(',', $array);
}
/**
* 分析Raw对象
*
* @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;
}
/**
* 分析Express对象
*
* @param Query $query 查询对象
* @param Express $express Express对象
*
* @return string
*/
protected function parseExpress(Query $query, Express $express): string
{
return $express->getValue();
}
/**
* 随机排序.
*
* @param Query $query 查询对象
*
* @return string
*/
protected function parseRand(Query $query): string
{
return '';
}
/**
* orderField分析.
*
* @param Query $query 查询对象
* @param string $key
* @param array $val
*
* @return string
*/
protected function parseOrderField(Query $query, string $key, array $val): string
{
if (isset($val['sort'])) {
$sort = $val['sort'];
unset($val['sort']);
} else {
$sort = '';
}
$sort = strtoupper($sort);
$sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : '';
$bind = $query->getFieldsBindType();
foreach ($val as $k => $item) {
$val[$k] = $this->parseDataBind($query, $key, $item, $bind);
}
return 'field(' . $this->parseKey($query, $key, true) . ',' . implode(',', $val) . ')' . $sort;
}
/**
* group分析.
*
* @param Query $query 查询对象
* @param mixed $group
*
* @return string
*/
protected function parseGroup(Query $query, string | array $group): string
{
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分析.
*
* @param Query $query 查询对象
* @param string $having
*
* @return string
*/
protected function parseHaving(Query $query, string $having): string
{
return !empty($having) ? ' HAVING ' . $having : '';
}
/**
* comment分析.
*
* @param Query $query 查询对象
* @param string $comment
*
* @return string
*/
protected function parseComment(Query $query, string $comment): string
{
if (str_contains($comment, '*/')) {
$comment = strstr($comment, '*/', true);
}
return !empty($comment) ? ' /* ' . $comment . ' */' : '';
}
/**
* distinct分析.
*
* @param Query $query 查询对象
* @param mixed $distinct
*
* @return string
*/
protected function parseDistinct(Query $query, bool $distinct): string
{
return !empty($distinct) ? ' DISTINCT ' : '';
}
/**
* index分析可在操作链中指定需要强制使用的索引.
*
* @param Query $query 查询对象
* @param mixed $index
*
* @return string
*/
protected function parseForce(Query $query, string | array $index): string
{
if (empty($index)) {
return '';
}
if (is_array($index)) {
$index = implode(',', $index);
}
return sprintf(' FORCE INDEX ( %s ) ', $index);
}
/**
* 设置锁机制.
*
* @param Query $query 查询对象
* @param bool|string $lock
*
* @return string
*/
protected function parseLock(Query $query, bool | string $lock = false): string
{
if (is_bool($lock)) {
return $lock ? ' FOR UPDATE ' : '';
}
if (is_string($lock) && !empty($lock)) {
return ' ' . trim($lock) . ' ';
}
return '';
}
/**
* 生成insertall SQL.
*
* @param Query $query 查询对象
* @param array $keys 字段名
* @param array $datas 数据
*
* @return string
*/
public function insertAllByKeys(Query $query, array $keys, array $datas): string
{
$options = $query->getOptions();
// 获取绑定信息
$bind = $query->getFieldsBindType();
$fields = [];
$values = [];
foreach ($keys as $field) {
$fields[] = $this->parseKey($query, $field);
}
foreach ($datas as $k => $data) {
foreach ($data as $key => &$val) {
if (!$query->isAutoBind()) {
$val = Connection::PARAM_STR == $bind[$keys[$key]] ? '\'' . $val . '\'' : $val;
} else {
$val = $this->parseDataBind($query, $keys[$key], $val, $bind);
}
}
$values[] = 'SELECT ' . implode(',', $data);
}
return str_replace(
['%INSERT%', '%TABLE%', '%EXTRA%', '%FIELD%', '%DATA%', '%COMMENT%'],
[
!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']),
],
$this->insertAllSql
);
}
}

View File

@@ -0,0 +1,220 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
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;
}
/**
* 为此缓存项设置「键」.
*
* @param string $key
*
* @return $this
*/
public function setKey(string $key)
{
$this->key = $key;
return $this;
}
/**
* 返回当前缓存项的「键」.
*
* @return string
*/
public function getKey()
{
return $this->key;
}
/**
* 返回当前缓存项的有效期
*
* @return DateTimeInterface|int|null
*/
public function getExpire()
{
if ($this->expire instanceof DateTimeInterface) {
return $this->expire;
}
return $this->expire ? $this->expire - time() : null;
}
/**
* 获取缓存Tag.
*
* @return string|array
*/
public function getTag()
{
return $this->tag;
}
/**
* 凭借此缓存项的「键」从缓存系统里面取出缓存项.
*
* @return mixed
*/
public function get()
{
return $this->value;
}
/**
* 确认缓存项的检查是否命中.
*
* @return bool
*/
public function isHit(): bool
{
return $this->isHit;
}
/**
* 为此缓存项设置「值」.
*
* @param mixed $value
*
* @return $this
*/
public function set($value)
{
$this->value = $value;
$this->isHit = true;
return $this;
}
/**
* 为此缓存项设置所属标签.
*
* @param string|array $tag
*
* @return $this
*/
public function tag($tag = null)
{
$this->tag = $tag;
return $this;
}
/**
* 设置缓存项的有效期
*
* @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;
}
/**
* 设置缓存项的准确过期时间点.
*
* @param DateTimeInterface $expiration
*
* @return $this
*/
public function expiresAt(DateTimeInterface $expiration)
{
$this->expire = $expiration;
return $this;
}
/**
* 设置缓存项的过期时间.
*
* @param int|DateInterval $timeInterval
*
* @throws InvalidArgumentException
*
* @return $this
*/
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;
}
}

View File

@@ -0,0 +1,383 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db;
use Psr\SimpleCache\CacheInterface;
use think\DbManager;
/**
* 数据库连接基础类.
*/
abstract class Connection implements ConnectionInterface
{
const PARAM_INT = 1;
const PARAM_STR = 2;
const PARAM_BOOL = 5;
const PARAM_FLOAT = 21;
/**
* 当前SQL指令.
*
* @var string
*/
protected $queryStr = '';
/**
* 返回或者影响记录数.
*
* @var int
*/
protected $numRows = 0;
/**
* 事务指令数.
*
* @var int
*/
protected $transTimes = 0;
/**
* 错误信息.
*
* @var string
*/
protected $error = '';
/**
* 数据库连接ID 支持多个连接.
*
* @var array
*/
protected $links = [];
/**
* 当前连接ID.
*
* @var object
*/
protected $linkID;
/**
* 当前读连接ID.
*
* @var object
*/
protected $linkRead;
/**
* 当前写连接ID.
*
* @var object
*/
protected $linkWrite;
/**
* 数据表信息.
*
* @var array
*/
protected $info = [];
/**
* 查询开始时间.
*
* @var float
*/
protected $queryStartTime;
/**
* Builder对象
*
* @var Builder
*/
protected $builder;
/**
* Db对象
*
* @var DbManager
*/
protected $db;
/**
* 是否读取主库.
*
* @var bool
*/
protected $readMaster = false;
/**
* 数据库连接参数配置.
*
* @var array
*/
protected $config = [];
/**
* 缓存对象
*
* @var CacheInterface
*/
protected $cache;
/**
* 架构函数 读取数据库配置信息.
*
* @param array $config 数据库配置数组
*/
public function __construct(array $config = [])
{
if (!empty($config)) {
$this->config = array_merge($this->config, $config);
}
// 创建Builder对象
$class = $this->getBuilderClass();
$this->builder = new $class($this);
}
/**
* 获取当前的builder实例对象
*
* @return Builder
*/
public function getBuilder()
{
return $this->builder;
}
/**
* 创建查询对象
*/
public function newQuery(): BaseQuery
{
$class = $this->getQueryClass();
return new $class($this);
}
/**
* 设置当前的数据库Db对象
*
* @param DbManager $db
*
* @return void
*/
public function setDb(DbManager $db)
{
$this->db = $db;
}
/**
* 设置当前的缓存对象
*
* @param CacheInterface $cache
*
* @return void
*/
public function setCache(CacheInterface $cache)
{
$this->cache = $cache;
}
/**
* 获取当前的缓存对象
*
* @return CacheInterface|null
*/
public function getCache()
{
return $this->cache;
}
/**
* 获取数据库的配置参数.
*
* @param string $config 配置名称
*
* @return mixed
*/
public function getConfig(string $config = '')
{
if ('' === $config) {
return $this->config;
}
return $this->config[$config] ?? null;
}
/**
* 数据库SQL监控.
*
* @param string $sql 执行的SQL语句 留空自动获取
* @param bool $master 主从标记
*
* @return void
*/
protected function trigger(string $sql = '', bool $master = false): void
{
$listen = $this->db->getListen();
if (empty($listen)) {
$listen[] = function ($sql, $time, $master) {
if (str_starts_with($sql, 'CONNECT:')) {
$this->db->log($sql);
return;
}
// 记录SQL
if (is_bool($master)) {
// 分布式记录当前操作的主从
$master = $master ? 'master|' : 'slave|';
} else {
$master = '';
}
$this->db->log($sql . ' [ ' . $master . 'RunTime:' . $time . 's ]');
};
}
$runtime = number_format((microtime(true) - $this->queryStartTime), 6);
$sql = $sql ?: $this->getLastsql();
if (empty($this->config['deploy'])) {
$master = null;
}
foreach ($listen as $callback) {
if (is_callable($callback)) {
$callback($sql, $runtime, $master);
}
}
}
/**
* 缓存数据.
*
* @param CacheItem $cacheItem 缓存Item
*/
protected function cacheData(CacheItem $cacheItem)
{
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());
}
}
/**
* 分析缓存Key.
*
* @param BaseQuery $query 查询对象
* @param string $method 查询方法
*
* @return string
*/
protected function getCacheKey(BaseQuery $query, string $method = ''): string
{
if (!empty($query->getOption('key')) && empty($method)) {
$key = 'think_' . $this->getConfig('database') . '.' . var_export($query->getTable(), true) . '|' . var_export($query->getOption('key'), true);
} else {
$key = $query->getQueryGuid();
}
return $key;
}
/**
* 分析缓存.
*
* @param BaseQuery $query 查询对象
* @param array $cache 缓存信息
* @param string $method 查询方法
*
* @return CacheItem
*/
protected function parseCache(BaseQuery $query, array $cache, string $method = ''): CacheItem
{
[$key, $expire, $tag] = $cache;
if ($key instanceof CacheItem) {
$cacheItem = $key;
} else {
if (true === $key) {
$key = $this->getCacheKey($query, $method);
}
$cacheItem = new CacheItem($key);
$cacheItem->expire($expire);
$cacheItem->tag($tag);
}
return $cacheItem;
}
/**
* 获取返回或者影响的记录数.
*
* @return int
*/
public function getNumRows(): int
{
return $this->numRows;
}
/**
* 获取最终的SQL语句.
*
* @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] : self::PARAM_STR;
if (self::PARAM_FLOAT == $type || self::PARAM_STR == $type) {
$value = '\'' . addslashes($value) . '\'';
} elseif (self::PARAM_INT == $type && '' === $value) {
$value = '0';
}
// 判断占位符
$sql = is_numeric($key) ?
substr_replace($sql, $value, strpos($sql, '?'), 1) :
str_replace(
[':' . $key . ' ', ':' . $key . ',', ':' . $key . ')'],
[$value . ' ', $value . ',', $value . ')'],
$sql . ' ');
}
return rtrim($sql);
}
/**
* 析构方法.
*/
public function __destruct()
{
// 关闭连接
$this->close();
}
public function __call($method, $args)
{
// 调用Query类方法
return call_user_func_array([$this->newQuery(), $method], $args);
}
}

View File

@@ -0,0 +1,208 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db;
use Psr\SimpleCache\CacheInterface;
use think\DbManager;
/**
* Connection interface.
*/
interface ConnectionInterface
{
/**
* 获取当前连接器类对应的Query类.
*
* @return string
*/
public function getQueryClass(): string;
/**
* 连接数据库方法.
*
* @param array $config 接参数
* @param int $linkNum 连接序号
*
* @return mixed
*/
public function connect(array $config = [], $linkNum = 0);
/**
* 设置当前的数据库Db对象
*
* @param DbManager $db
*
* @return void
*/
public function setDb(DbManager $db);
/**
* 设置当前的缓存对象
*
* @param CacheInterface $cache
*
* @return void
*/
public function setCache(CacheInterface $cache);
/**
* 获取数据库的配置参数.
*
* @param string $config 配置名称
*
* @return mixed
*/
public function getConfig(string $config = '');
/**
* 关闭数据库(或者重新连接).
*
* @return $this
*/
public function close();
/**
* 查找单条记录.
*
* @param BaseQuery $query 查询对象
*
* @return array
*/
public function find(BaseQuery $query): array;
/**
* 查找记录.
*
* @param BaseQuery $query 查询对象
*
* @return array
*/
public function select(BaseQuery $query): array;
/**
* 插入记录.
*
* @param BaseQuery $query 查询对象
* @param bool $getLastInsID 返回自增主键
*
* @return mixed
*/
public function insert(BaseQuery $query, bool $getLastInsID = false);
/**
* 批量插入记录.
*
* @param BaseQuery $query 查询对象
* @param mixed $dataSet 数据集
*
* @return int
*/
public function insertAll(BaseQuery $query, array $dataSet = []): int;
/**
* 更新记录.
*
* @param BaseQuery $query 查询对象
*
* @return int
*/
public function update(BaseQuery $query): int;
/**
* 删除记录.
*
* @param BaseQuery $query 查询对象
*
* @return int
*/
public function delete(BaseQuery $query): int;
/**
* 得到某个字段的值
*
* @param BaseQuery $query 查询对象
* @param string $field 字段名
* @param mixed $default 默认值
*
* @return mixed
*/
public function value(BaseQuery $query, string $field, $default = null);
/**
* 得到某个列的数组.
*
* @param BaseQuery $query 查询对象
* @param string|array $column 字段名 多个字段用逗号分隔
* @param string $key 索引
*
* @return array
*/
public function column(BaseQuery $query, string|array $column, string $key = ''): array;
/**
* 执行数据库事务
*
* @param callable $callback 数据操作方法回调
*
* @return mixed
*/
public function transaction(callable $callback);
/**
* 启动事务
*
* @return void
*/
public function startTrans();
/**
* 用于非自动提交状态下面的查询提交.
*
* @return void
*/
public function commit();
/**
* 事务回滚.
*
* @return void
*/
public function rollback();
/**
* 取得数据表的字段信息.
*
* @param string $tableName
*
* @return array
*/
public function getTableFields(string $tableName): array;
/**
* 获取最近一次查询的sql语句.
*
* @return string
*/
public function getLastSql(): string;
/**
* 获取最近插入的ID.
*
* @param BaseQuery $query 查询对象
* @param string $sequence 自增序列名
*
* @return mixed
*/
public function getLastInsID(BaseQuery $query, ?string $sequence = null);
}

View File

@@ -0,0 +1,64 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db;
/**
* SQL Express.
*/
class Express
{
/**
* 创建一个SQL运算表达式.
*
* @param string $type
* @param float $value
* @param int $lazyTime
*
* @return void
*/
public function __construct(protected string $type, protected float $step = 1, protected int $lazyTime = 0)
{
}
public function getStep()
{
return $this->step;
}
public function getType()
{
return $this->type;
}
public function getLazyTime()
{
return $this->lazyTime;
}
/**
* 获取表达式.
*
* @return string
*/
public function getValue(): string
{
return match ($this->type) {
'+' => ' + ' . $this->step,
'-' => ' - ' . $this->step,
'*' => ' * ' . $this->step,
'/' => ' / ' . $this->step,
default => ' + 0',
};
}
}

View File

@@ -0,0 +1,544 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use think\helper\Str;
/**
* SQL获取类.
*/
class Fetch
{
/**
* Connection对象
*
* @var Connection
*/
protected $connection;
/**
* Builder对象
*
* @var Builder
*/
protected $builder;
/**
* 创建一个查询SQL获取对象
*
* @param Query $query 查询对象
*/
public function __construct(protected Query $query)
{
$this->connection = $query->getConnection();
$this->builder = $this->connection->getBuilder();
}
/**
* 聚合查询.
*
* @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);
}
/**
* 得到某个字段的值
*
* @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);
}
/**
* 得到某个列的数组.
*
* @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);
}
/**
* 插入记录.
*
* @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.
*
* @param array $data 数据
*
* @return string
*/
public function insertGetId(array $data = []): string
{
return $this->insert($data);
}
/**
* 保存数据 自动判断insert或者update.
*
* @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->getOption('data', []), $data);
$this->query->setOption('data', $data);
if ($this->query->getOption('where')) {
$isUpdate = true;
} else {
$isUpdate = $this->query->parseUpdateData($data);
}
return $isUpdate ? $this->update() : $this->insert();
}
/**
* 批量插入记录.
*
* @param array $dataSet 数据集
* @param int $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方式插入记录.
*
* @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);
}
/**
* 字段值增长
*
* @param string $field 字段名
* @param float|int $step 步进值
* @param int $lazyTime 延迟时间(秒)
*
* @return string
*/
public function setInc(string $field, float|int $step = 1, int $lazyTime = 0)
{
return $this->inc($field, $step)->update();
}
/**
* 字段值减少
*
* @param string $field 字段名
* @param float|int $step 步进值
* @param int $lazyTime 延迟时间(秒)
*
* @return string
*/
public function setDec(string $field, float|int $step = 1, int $lazyTime = 0)
{
return $this->dec($field, $step)->update();
}
/**
* 更新记录.
*
* @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->getOption('where'))) {
// 如果没有任何更新条件则不执行
throw new Exception('miss update condition');
}
}
// 更新数据
$this->query->setOption('data', $data);
// 生成UPDATE SQL语句
$sql = $this->builder->update($this->query);
return $this->fetch($sql);
}
/**
* 删除记录.
*
* @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->update($this->query);
return $this->fetch($sql);
}
}
// 生成删除SQL语句
$sql = $this->builder->delete($this->query);
return $this->fetch($sql);
}
/**
* 查找记录 返回SQL.
*
* @param array $data
*
* @return string
*/
public function select(array $data = []): string
{
$this->query->parseOptions();
if (!empty($data)) {
// 主键条件分析
$this->query->parsePkWhere($data);
}
// 生成查询SQL
$sql = $this->builder->select($this->query);
return $this->fetch($sql);
}
/**
* 查找单条记录 返回SQL语句.
*
* @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);
}
/**
* 查找多条记录 如果不存在则抛出异常.
*
* @param mixed $data
*
* @return string
*/
public function selectOrFail($data = null): string
{
return $this->select($data);
}
/**
* 查找单条记录 如果不存在则抛出异常.
*
* @param mixed $data
*
* @return string
*/
public function findOrFail($data = null): string
{
return $this->find($data);
}
/**
* 查找单条记录 不存在返回空数据(或者空模型).
*
* @param mixed $data 数据
*
* @return string
*/
public function findOrEmpty($data = null)
{
return $this->find($data);
}
/**
* 获取实际的SQL语句.
*
* @param string $sql
*
* @return string
*/
public function fetch(string $sql): string
{
$bind = $this->query->getBind();
return $this->connection->getRealSql($sql, $bind);
}
/**
* COUNT查询.
*
* @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', '*');
}
return $this->aggregate('COUNT', $field);
}
/**
* SUM查询.
*
* @param string $field 字段名
*
* @return string
*/
public function sum(string $field): string
{
return $this->aggregate('SUM', $field);
}
/**
* MIN查询.
*
* @param string $field 字段名
*
* @return string
*/
public function min(string $field): string
{
return $this->aggregate('MIN', $field);
}
/**
* MAX查询.
*
* @param string $field 字段名
*
* @return string
*/
public function max(string $field): string
{
return $this->aggregate('MAX', $field);
}
/**
* AVG查询.
*
* @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();
}
if (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;
}
}

View File

@@ -0,0 +1,802 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db;
use MongoDB\Driver\Command;
use MongoDB\Driver\Cursor;
use MongoDB\Driver\Exception\AuthenticationException;
use MongoDB\Driver\Exception\ConnectionException;
use MongoDB\Driver\Exception\InvalidArgumentException;
use MongoDB\Driver\Exception\RuntimeException;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern;
use think\db\exception\DbException as Exception;
use think\Paginator;
class Mongo extends BaseQuery
{
/**
* 当前数据库连接对象
*
* @var \think\db\connector\Mongo
*/
protected $connection;
/**
* 执行指令 返回数据集.
*
* @param Command $command 指令
* @param string $dbName
* @param ReadPreference $readPreference readPreference
* @param string|array $typeMap 指定返回的typeMap
*
* @throws AuthenticationException
* @throws InvalidArgumentException
* @throws ConnectionException
* @throws RuntimeException
*
* @return mixed
*/
public function command(Command $command, string $dbName = '', ?ReadPreference $readPreference = null, $typeMap = null)
{
return $this->connection->command($command, $dbName, $readPreference, $typeMap);
}
/**
* 执行command.
*
* @param string|array|object $command 指令
* @param mixed $extra 额外参数
* @param string $db 数据库名
*
* @return array
*/
public function cmd($command, $extra = null, string $db = ''): array
{
$this->parseOptions();
return $this->connection->cmd($this, $command, $extra, $db);
}
/**
* 指定distinct查询.
*
* @param string $field 字段名
*
* @return array
*/
public function getDistinct(string $field)
{
$result = $this->cmd('distinct', $field);
return $result[0]['values'];
}
/**
* 获取数据库的所有collection.
*
* @param string $db 数据库名称 留空为当前数据库
*
* @throws Exception
*/
public function listCollections(string $db = '')
{
$cursor = $this->cmd('listCollections', null, $db);
$result = [];
foreach ($cursor as $collection) {
$result[] = $collection['name'];
}
return $result;
}
/**
* COUNT查询.
*
* @param string $field 字段名
*
* @return int
*/
public function count(?string $field = null): int
{
$result = $this->cmd('count');
return $result[0]['n'];
}
/**
* 聚合查询.
*
* @param string $aggregate 聚合指令
* @param string $field 字段名
* @param bool $force 强制转为数字类型
*
* @return mixed
*/
public function aggregate(string $aggregate, $field, bool $force = false, bool $one = false)
{
$result = $this->cmd('aggregate', [strtolower($aggregate), $field]);
$value = $result[0]['aggregate'] ?? 0;
if ($force) {
$value += 0;
}
return $value;
}
/**
* 多聚合操作.
*
* @param array $aggregate 聚合指令, 可以聚合多个参数, 如 ['sum' => 'field1', 'avg' => 'field2']
* @param array $groupBy 类似mysql里面的group字段, 可以传入多个字段, 如 ['field_a', 'field_b', 'field_c']
*
* @return array 查询结果
*/
public function multiAggregate(array $aggregate, array $groupBy): array
{
$result = $this->cmd('multiAggregate', [$aggregate, $groupBy]);
foreach ($result as &$row) {
if (isset($row['_id']) && !empty($row['_id'])) {
foreach ($row['_id'] as $k => $v) {
$row[$k] = $v;
}
unset($row['_id']);
}
}
return $result;
}
/**
* 字段值增长
*
* @param string $field 字段名
* @param float|int $step 增长值
*
* @return $this
*/
public function inc(string $field, float|int $step = 1)
{
$this->options['data'][$field] = ['$inc', $step];
return $this;
}
/**
* 字段值减少.
*
* @param string $field 字段名
* @param float|int $step 减少值
*
* @return $this
*/
public function dec(string $field, float|int $step = 1)
{
return $this->inc($field, -1 * $step);
}
/**
* 指定当前操作的Collection.
*
* @param string $table 表名
*
* @return $this
*/
public function table($table)
{
$this->options['table'] = $table;
return $this;
}
/**
* table方法的别名.
*
* @param string $collection
*
* @return $this
*/
public function collection(string $collection)
{
return $this->table($collection);
}
/**
* 设置typeMap.
*
* @param string|array $typeMap
*
* @return $this
*/
public function typeMap($typeMap)
{
$this->options['typeMap'] = $typeMap;
return $this;
}
/**
* awaitData.
*
* @param bool $awaitData
*
* @return $this
*/
public function awaitData(bool $awaitData)
{
$this->options['awaitData'] = $awaitData;
return $this;
}
/**
* batchSize.
*
* @param int $batchSize
*
* @return $this
*/
public function batchSize(int $batchSize)
{
$this->options['batchSize'] = $batchSize;
return $this;
}
/**
* exhaust.
*
* @param bool $exhaust
*
* @return $this
*/
public function exhaust(bool $exhaust)
{
$this->options['exhaust'] = $exhaust;
return $this;
}
/**
* 设置modifiers.
*
* @param array $modifiers
*
* @return $this
*/
public function modifiers(array $modifiers)
{
$this->options['modifiers'] = $modifiers;
return $this;
}
/**
* 设置noCursorTimeout.
*
* @param bool $noCursorTimeout
*
* @return $this
*/
public function noCursorTimeout(bool $noCursorTimeout)
{
$this->options['noCursorTimeout'] = $noCursorTimeout;
return $this;
}
/**
* 设置oplogReplay.
*
* @param bool $oplogReplay
*
* @return $this
*/
public function oplogReplay(bool $oplogReplay)
{
$this->options['oplogReplay'] = $oplogReplay;
return $this;
}
/**
* 设置partial.
*
* @param bool $partial
*
* @return $this
*/
public function partial(bool $partial)
{
$this->options['partial'] = $partial;
return $this;
}
/**
* maxTimeMS.
*
* @param string $maxTimeMS
*
* @return $this
*/
public function maxTimeMS(string $maxTimeMS)
{
$this->options['maxTimeMS'] = $maxTimeMS;
return $this;
}
/**
* collation.
*
* @param array $collation
*
* @return $this
*/
public function collation(array $collation)
{
$this->options['collation'] = $collation;
return $this;
}
/**
* 设置是否REPLACE.
*
* @param bool $replace 是否使用REPLACE写入数据
*
* @return $this
*/
public function replace(bool $replace = true)
{
return $this;
}
/**
* 设置返回字段.
*
* @param mixed $field 字段信息
*
* @return $this
*/
public function field($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] = 1;
} else {
$projection[$key] = $val;
}
}
$this->options['projection'] = $projection;
return $this;
}
/**
* 指定要排除的查询字段.
*
* @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] = 0;
} else {
$projection[$key] = $val;
}
}
$this->options['projection'] = $projection;
return $this;
}
/**
* 设置skip.
*
* @param int $skip
*
* @return $this
*/
public function skip(int $skip)
{
$this->options['skip'] = $skip;
return $this;
}
/**
* 设置slaveOk.
*
* @param bool $slaveOk
*
* @return $this
*/
public function slaveOk(bool $slaveOk)
{
$this->options['slaveOk'] = $slaveOk;
return $this;
}
/**
* 指定查询数量.
*
* @param int $offset 起始位置
* @param int $length 查询数量
*
* @return $this
*/
public function limit(int $offset, ?int $length = null)
{
if (is_null($length)) {
$length = $offset;
$offset = 0;
}
$this->options['skip'] = $offset;
$this->options['limit'] = $length;
return $this;
}
/**
* 设置sort.
*
* @param array|string $field
* @param string $order
*
* @return $this
*/
public function order($field, string $order = '')
{
if (is_array($field)) {
$this->options['sort'] = array_map(function ($val) {
return 'asc' == strtolower($val) ? 1 : -1;
}, $field);
} else {
$this->options['sort'][$field] = 'asc' == strtolower($order) ? 1 : -1;
}
return $this;
}
/**
* 设置tailable.
*
* @param bool $tailable
*
* @return $this
*/
public function tailable(bool $tailable)
{
$this->options['tailable'] = $tailable;
return $this;
}
/**
* 设置writeConcern对象
*
* @param WriteConcern $writeConcern
*
* @return $this
*/
public function writeConcern(WriteConcern $writeConcern)
{
$this->options['writeConcern'] = $writeConcern;
return $this;
}
/**
* 获取当前数据表的主键.
*
* @return string|array
*/
public function getPk()
{
return $this->pk ?: $this->connection->getConfig('pk');
}
/**
* 执行查询但只返回Cursor对象
*
* @return Cursor
*/
public function cursor(): Cursor
{
return $this->getCursor();
}
/**
* 执行查询但只返回Cursor对象
*
* @return Cursor
*/
public function getCursor(): Cursor
{
$this->parseOptions();
return $this->connection->cursor($this);
}
/**
* 分页查询.
*
* @param int|array $listRows 每页数量 数组表示配置参数
* @param int|bool $simple 是否简洁模式或者总记录数
*
* @throws Exception
*
* @return Paginator
*/
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 = max($page, 1);
$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);
}
/**
* 分批数据返回处理.
*
* @param int $count 每次处理的数据数量
* @param callable $callback 处理回调方法
* @param string|array $column 分批处理的字段名
* @param string $order 字段排序
*
* @throws Exception
*
* @return bool
*/
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']);
}
if (is_array($column)) {
$times = 1;
$query = $this->options($options)->page($times, $count);
} else {
$query = $this->options($options)->limit($count);
if (str_contains($column, '.')) {
[$alias, $key] = explode('.', $column);
} else {
$key = $column;
if ($key == '_id' && $this->connection->getConfig('pk_convert_id')) {
$key = 'id';
}
}
}
$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;
}
/**
* 分析表达式(可用于查询或者写入操作).
*
* @return array
*/
public function parseOptions(): array
{
$options = $this->options;
// 获取数据表
if (empty($options['table'])) {
$options['table'] = $this->getTable();
}
foreach (['where', 'data', 'projection', 'filter', 'json', 'with_attr', 'with_relation_attr'] as $name) {
if (!isset($options[$name])) {
$options[$name] = [];
}
}
$modifiers = empty($options['modifiers']) ? [] : $options['modifiers'];
if (isset($options['comment'])) {
$modifiers['$comment'] = $options['comment'];
}
if (isset($options['maxTimeMS'])) {
$modifiers['$maxTimeMS'] = $options['maxTimeMS'];
}
if (!empty($modifiers)) {
$options['modifiers'] = $modifiers;
}
if (!isset($options['typeMap'])) {
$options['typeMap'] = $this->getConfig('type_map');
}
if (!isset($options['limit'])) {
$options['limit'] = 0;
}
foreach (['master', 'fetch_sql', 'fetch_cursor'] as $name) {
if (!isset($options[$name])) {
$options[$name] = false;
}
}
if (isset($options['page'])) {
// 根据页数计算limit
[$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;
return $options;
}
/**
* 获取字段类型信息.
*
* @return array
*/
public function getFieldsType(): array
{
if (!empty($this->options['field_type'])) {
return $this->options['field_type'];
}
return [];
}
/**
* 获取字段类型信息.
*
* @param string $field 字段名
*
* @return string|null
*/
public function getFieldType(string $field)
{
$fieldType = $this->getFieldsType();
return $fieldType[$field] ?? null;
}
/**
* 获取字段类型
*
* @return array
*/
public function getType()
{
return $this->getFieldsType();
}
/**
* 获取自增主键
*
* @return string
*/
public function getAutoInc()
{
return '';
}
/**
* 设置自增主键
*
* @param string $autoInc
* @return static
*/
public function autoInc(?string $autoInc)
{
return $this;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,648 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db;
use PDOStatement;
use think\db\exception\DbException as Exception;
/**
* PDO数据查询类.
*/
class Query extends BaseQuery
{
use concern\JoinAndViewQuery;
use concern\TableFieldInfo;
use concern\Transaction;
/**
* 表达式方式指定Field排序.
*
* @param string $field 排序字段
* @param array $bind 参数绑定
*
* @return $this
*/
public function orderRaw(string $field, array $bind = [])
{
$this->options['order'][] = new Raw($field, $bind);
return $this;
}
/**
* 表达式方式指定查询字段.
*
* @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').
*
* @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;
}
/**
* 随机排序.
*
* @return $this
*/
public function orderRand()
{
$this->options['order'][] = '[rand]';
return $this;
}
/**
* 使用表达式设置数据.
*
* @param string $field 字段名
* @param string $value 字段值
*
* @return $this
*/
public function exp(string $field, string $value)
{
$this->options['data'][$field] = new Raw($value);
return $this;
}
/**
* 表达式方式指定当前操作的数据表.
*
* @param mixed $table 表名
*
* @return $this
*/
public function tableRaw(string $table)
{
$this->options['table'] = new Raw($table);
return $this;
}
/**
* 获取执行的SQL语句而不进行实际的查询.
*
* @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操作.
*
* @param array $sql SQL批处理指令
*
* @return bool
*/
public function batchQuery(array $sql = []): bool
{
return $this->connection->batchQuery($sql);
}
/**
* USING支持 用于多表删除.
*
* @param mixed $using USING
*
* @return $this
*/
public function using($using)
{
$this->options['using'] = $using;
return $this;
}
/**
* 存储过程调用.
*
* @param bool $procedure 是否为存储过程查询
*
* @return $this
*/
public function procedure(bool $procedure = true)
{
$this->options['procedure'] = $procedure;
return $this;
}
/**
* 指定group查询.
*
* @param string|array $group GROUP
*
* @return $this
*/
public function group($group)
{
$this->options['group'] = $group;
return $this;
}
/**
* 指定having查询.
*
* @param string $having having
*
* @return $this
*/
public function having(string $having)
{
$this->options['having'] = $having;
return $this;
}
/**
* 指定distinct查询.
*
* @param bool $distinct 是否唯一
*
* @return $this
*/
public function distinct(bool $distinct = true)
{
$this->options['distinct'] = $distinct;
return $this;
}
/**
* 指定强制索引.
*
* @param string $force 索引名称
*
* @return $this
*/
public function force(string $force)
{
$this->options['force'] = $force;
return $this;
}
/**
* 查询注释.
*
* @param string $comment 注释
*
* @return $this
*/
public function comment(string $comment)
{
$this->options['comment'] = $comment;
return $this;
}
/**
* 设置是否REPLACE.
*
* @param bool $replace 是否使用REPLACE写入数据
*
* @return $this
*/
public function replace(bool $replace = true)
{
$this->options['replace'] = $replace;
return $this;
}
/**
* 设置当前查询所在的分区.
*
* @param string|array $partition 分区名称
*
* @return $this
*/
public function partition($partition)
{
$this->options['partition'] = $partition;
return $this;
}
/**
* 设置DUPLICATE.
*
* @param array|string|Raw $duplicate DUPLICATE信息
*
* @return $this
*/
public function duplicate($duplicate)
{
$this->options['duplicate'] = $duplicate;
return $this;
}
/**
* 设置查询的额外参数.
*
* @param string $extra 额外信息
*
* @return $this
*/
public function extra(string $extra)
{
$this->options['extra'] = $extra;
return $this;
}
/**
* 创建子查询SQL.
*
* @param bool $sub 是否添加括号
*
* @throws Exception
*
* @return string
*/
public function buildSql(bool $sub = true): string
{
return $sub ? '( ' . $this->fetchSql()->select() . ' )' : $this->fetchSql()->select();
}
/**
* 获取当前数据表的主键.
*
* @return string|array|null
*/
public function getPk()
{
if (empty($this->pk)) {
$this->pk = $this->connection->getPk($this->getTable());
}
return $this->pk;
}
/**
* 指定数据表自增主键.
*
* @param string $autoinc 自增键
*
* @return $this
*/
public function autoinc(?string $autoinc)
{
$this->autoinc = $autoinc;
return $this;
}
/**
* 获取当前数据表的自增主键.
*
* @return string|null
*/
public function getAutoInc()
{
$tableName = $this->getTable();
if (empty($this->autoinc) && $tableName) {
$this->autoinc = $this->connection->getAutoInc($tableName);
}
return $this->autoinc;
}
/**
* 字段值增长
*
* @param string $field 字段名
* @param float|int $step 增长值
* @param int $lazyTime 延迟时间(秒)
*
* @return $this
*/
public function inc(string $field, float|int $step = 1, int $lazyTime = 0)
{
if ($lazyTime > 0) {
$step = $this->lazyWrite($field, 'inc', $step, $lazyTime);
if (false === $step) {
return $this;
}
}
$this->options['data'][$field] = new Express('+', $step);
return $this;
}
/**
* 字段值减少.
*
* @param string $field 字段名
* @param float|int $step 增长值
* @param int $lazyTime 延迟时间(秒)
*
* @return $this
*/
public function dec(string $field, float|int $step = 1, int $lazyTime = 0)
{
if ($lazyTime > 0) {
$step = $this->lazyWrite($field, 'dec', $step, $lazyTime);
if (false === $step) {
return $this;
}
return $this->inc($field, $step);
}
$this->options['data'][$field] = new Express('-', $step);
return $this;
}
/**
* 字段值增长(支持延迟写入)
*
* @param string $field 字段名
* @param float|int $step 步进值
* @param int $lazyTime 延迟时间(秒)
*
* @return int|false
*/
public function setInc(string $field, float|int $step = 1, int $lazyTime = 0)
{
return $this->inc($field, $step, $lazyTime)->update();
}
/**
* 字段值减少(支持延迟写入)
*
* @param string $field 字段名
* @param float|int $step 步进值
* @param int $lazyTime 延迟时间(秒)
*
* @return int|false
*/
public function setDec(string $field, float|int $step = 1, int $lazyTime = 0)
{
return $this->dec($field, $step, $lazyTime)->update();
}
/**
* 延时更新检查 返回false表示需要延时
* 否则返回实际写入的数值
* @access public
* @param string $field 字段名
* @param string $type 自增或者自减
* @param float|int $step 写入步进值
* @param int $lazyTime 延时时间(s)
* @return false|integer
*/
public function lazyWrite(string $field, string $type, float|int $step, int $lazyTime)
{
$guid = $this->getLazyFieldCacheKey($field);
$cache = $this->getCache();
if (!$cache->has($guid . '_time')) {
// 计时开始
$cache->set($guid . '_time', time());
$cache->$type($guid, $step);
} elseif (time() > $cache->get($guid . '_time') + $lazyTime) {
// 删除缓存
$value = $cache->$type($guid, $step);
$cache->delete($guid);
$cache->delete($guid . '_time');
return 0 === $value ? false : $value;
} else {
// 更新缓存
$cache->$type($guid, $step);
}
return false;
}
/**
* 获取延迟写入字段值.
*
* @param string $field 字段名称
* @param mixed $id 主键值
*
* @return int
*/
protected function getLazyFieldValue(string $field, $id = null): int
{
return (int) $this->getCache()->get($this->getLazyFieldCacheKey($field, $id));
}
/**
* 获取延迟写入字段的缓存Key
*
* @param string $field 字段名
* @param mixed $id 主键值
*
* @return string
*/
protected function getLazyFieldCacheKey(string $field, $id = null): string
{
return 'lazy_' . $this->getTable() . '_' . $field . '_' . ($id ?: $this->getKey());
}
/**
* 执行查询但只返回PDOStatement对象
*
* @return PDOStatement
*/
public function getPdo(): PDOStatement
{
return $this->connection->pdo($this);
}
/**
* 使用游标查找记录.
*
* @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);
}
/**
* 分批数据返回处理.
*
* @param int $count 每次处理的数据数量
* @param callable $callback 处理回调方法
* @param string|array|null $column 分批处理的字段名
* @param string $order 字段排序
*
* @throws Exception
*
* @return bool
*/
public function chunk(int $count, callable $callback, string | array | null $column = null, string $order = 'asc'): bool
{
if ($count < 1) {
throw new Exception('The chunk size should be at least 1');
}
$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 (str_contains($column, '.')) {
[$alias, $key] = explode('.', $column);
} else {
$key = $column;
}
}
$resultSet = $query->order($column, $order)->select();
while (true) {
if (false === call_user_func($callback, $resultSet)) {
return false;
}
if (count($resultSet) < $count) {
break;
}
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;
}
/**
* 惰性分批遍历数据
* @param int $count 每批处理的数量
* @param string|null $column 分批处理的字段名
* @param string $order 字段排序
* @return \Generator
*/
public function lazy(int $count = 1000, ?string $column = null, string $order = 'desc')
{
if ($count < 1) {
throw new Exception('The chunk size should be at least 1');
}
$limit = (int) $this->getOption('limit', 0);
$column = $column ?: $this->getPk();
$length = $limit && $count >= $limit ? $limit : $count;
$options = $this->getOptions();
$bind = $this->bind;
$times = 0;
if ($this->getOption('order') || is_array($column)) {
$page = 1;
$resultSet = $this->options($options)->page($page, $length)->select();
} else {
$resultSet = $this->options($options)->order($column, $order)->limit($length)->select();
}
while (true) {
foreach ($resultSet as $item) {
yield $item;
$times++;
if ($limit > $count && $times >= $limit) {
break 2;
}
if (!isset($page)) {
$lastId = $item[$column];
}
}
if (count($resultSet) < $count) {
break;
}
if (isset($page)) {
$page++;
$query = $this->options($options)->page($page, $length);
} else {
$query = $this->options($options)
->where($column, 'asc' == strtolower($order) ? '>' : '<', $lastId)
->order($column, $order)
->limit($length);
}
$resultSet = $query->bind($bind)->select();
};
}
}

View File

@@ -0,0 +1,54 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db;
use Stringable;
/**
* SQL Raw.
*/
class Raw
{
/**
* 创建一个查询表达式.
*
* @param string|Stringable $value
* @param array $bind
*
* @return void
*/
public function __construct(protected string|Stringable $value, protected array $bind = [])
{
}
/**
* 获取表达式.
*
* @return string
*/
public function getValue(): string
{
return $this->value;
}
/**
* 获取参数绑定.
*
* @return array
*/
public function getBind(): array
{
return $this->bind;
}
}

View File

@@ -0,0 +1,175 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db;
use ArrayAccess;
/**
* 数组查询对象
*/
class Where implements ArrayAccess
{
/**
* 创建一个查询表达式.
*
* @param array $where 查询条件数组
* @param bool $enclose 是否增加括号
*/
public function __construct(protected array $where = [], protected bool $enclose = false)
{
}
/**
* 设置是否添加括号.
*
* @param bool $enclose
*
* @return $this
*/
public function enclose(bool $enclose = true)
{
$this->enclose = $enclose;
return $this;
}
/**
* 解析为Query对象可识别的查询条件数组.
*
* @return array
*/
public function parse(): array
{
$where = [];
foreach ($this->where as $key => $val) {
if ($val instanceof Raw) {
$where[] = [$key, 'exp', $val];
} elseif (is_null($val)) {
$where[] = [$key, 'NULL', ''];
} elseif (is_array($val)) {
$where[] = $this->parseItem($key, $val);
} else {
$where[] = [$key, '=', $val];
}
}
return $this->enclose ? [$where] : $where;
}
/**
* 分析查询表达式.
*
* @param string $field 查询字段
* @param array $where 查询条件
*
* @return array
*/
protected function parseItem(string $field, array $where = []): array
{
$op = $where[0];
$condition = $where[1] ?? null;
if (is_array($op)) {
// 同一字段多条件查询
array_unshift($where, $field);
} elseif (is_null($condition)) {
if (is_string($op) && in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) {
// null查询
$where = [$field, $op, ''];
} elseif (is_null($op) || '=' == $op) {
$where = [$field, 'NULL', ''];
} elseif ('<>' == $op) {
$where = [$field, 'NOTNULL', ''];
} else {
// 字段相等查询
$where = [$field, '=', $op];
}
} else {
$where = [$field, $op, $condition];
}
return $where;
}
/**
* 修改器 设置数据对象的值
*
* @param string $name 名称
* @param mixed $value 值
*
* @return void
*/
public function __set($name, $value)
{
$this->where[$name] = $value;
}
/**
* 获取器 获取数据对象的值
*
* @param string $name 名称
*
* @return mixed
*/
public function __get($name)
{
return $this->where[$name] ?? null;
}
/**
* 检测数据对象的值
*
* @param string $name 名称
*
* @return bool
*/
public function __isset($name)
{
return isset($this->where[$name]);
}
/**
* 销毁数据对象的值
*
* @param string $name 名称
*
* @return void
*/
public function __unset($name)
{
unset($this->where[$name]);
}
// ArrayAccess
public function offsetSet(mixed $name, mixed $value): void
{
$this->__set($name, $value);
}
public function offsetExists(mixed $name): bool
{
return $this->__isset($name);
}
public function offsetUnset(mixed $name): void
{
$this->__unset($name);
}
public function offsetGet(mixed $name): mixed
{
return $this->__get($name);
}
}

View File

@@ -0,0 +1,692 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\builder;
use Closure;
use MongoDB\BSON\Javascript;
use MongoDB\BSON\ObjectID;
use MongoDB\BSON\Regex;
use MongoDB\Driver\BulkWrite;
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;
class Mongo
{
// connection对象实例
protected $connection;
// 最后插入ID
protected $insertId = [];
// 查询表达式
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'];
/**
* 架构函数.
*
* @param Connection $connection 数据库连接对象实例
*/
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
/**
* 获取当前的连接对象实例.
*
* @return Connection
*/
public function getConnection(): Connection
{
return $this->connection;
}
/**
* key分析.
*
* @param string $key
*
* @return string
*/
protected function parseKey(Query $query, string $key): string
{
if (str_starts_with($key, '__TABLE__.')) {
[$collection, $key] = explode('.', $key, 2);
}
if ('id' == $key && $this->connection->getConfig('pk_convert_id')) {
$key = '_id';
}
return trim($key);
}
/**
* value分析.
*
* @param Query $query 查询对象
* @param mixed $value
* @param string $field
*
* @return string
*/
protected function parseValue(Query $query, $value, $field = '')
{
if ('_id' == $field && 'ObjectID' == $this->connection->getConfig('pk_type') && is_string($value)) {
try {
return new ObjectID($value);
} catch (InvalidArgumentException $e) {
return new ObjectID();
}
}
return $value;
}
/**
* insert数据分析.
*
* @param Query $query 查询对象
* @param array $data 数据
*
* @return array
*/
protected function parseData(Query $query, array $data): array
{
if (empty($data)) {
return [];
}
$result = [];
foreach ($data as $key => $val) {
$item = $this->parseKey($query, $key);
if (is_object($val)) {
$result[$item] = $val;
} elseif (isset($val[0]) && 'exp' == $val[0]) {
$result[$item] = $val[1];
} else {
$result[$item] = $this->parseValue($query, $val, $key);
}
}
return $result;
}
/**
* Set数据分析.
*
* @param Query $query 查询对象
* @param array $data 数据
*
* @return array
*/
protected function parseSet(Query $query, array $data): array
{
if (empty($data)) {
return [];
}
$result = [];
foreach ($data as $key => $val) {
$item = $this->parseKey($query, $key);
if (is_array($val) && isset($val[0]) && is_string($val[0]) && str_starts_with($val[0], '$')) {
$result[$val[0]][$item] = $this->parseValue($query, $val[1], $key);
} else {
$result['$set'][$item] = $this->parseValue($query, $val, $key);
}
}
return $result;
}
/**
* 生成查询过滤条件.
*
* @param Query $query 查询对象
* @param mixed $where
*
* @return array
*/
public function parseWhere(Query $query, array $where): array
{
if (empty($where)) {
$where = [];
}
$filter = [];
foreach ($where as $logic => $val) {
$logic = '$'.strtolower($logic);
foreach ($val as $field => $value) {
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)) {
throw new Exception('where express error:'.var_export($value, true));
}
if ($value instanceof Closure) {
// 使用闭包查询
$query = new Query($this->connection);
call_user_func_array($value, [&$query]);
$filter[$logic][] = $this->parseWhere($query, $query->getOption('where'));
} else {
if (str_contains($field, '|')) {
// 不同字段使用相同查询条件OR
$array = explode('|', $field);
foreach ($array as $k) {
$filter['$or'][] = $this->parseWhereItem($query, $k, $value);
}
} elseif (str_contains($field, '&')) {
// 不同字段使用相同查询条件AND
$array = explode('&', $field);
foreach ($array as $k) {
$filter['$and'][] = $this->parseWhereItem($query, $k, $value);
}
} else {
// 对字段使用表达式查询
$field = is_string($field) ? $field : '';
$filter[$logic][] = $this->parseWhereItem($query, $field, $value);
}
}
}
}
$options = $query->getOptions();
if (!empty($options['soft_delete'])) {
// 附加软删除条件
[$field, $condition] = $options['soft_delete'];
$filter['$and'][] = $this->parseWhereItem($query, $field, $condition);
}
return $filter;
}
// where子单元分析
protected function parseWhereItem(Query $query, $field, $val): array
{
$key = $field ? $this->parseKey($query, $field) : '';
// 查询规则和条件
if (!is_array($val)) {
$val = ['=', $val];
}
[$exp, $value] = $val;
// 对一个字段使用多个查询条件
if (is_array($exp)) {
$data = [];
foreach ($val as $value) {
$exp = $value[0];
$value = $value[1];
if (!in_array($exp, $this->exp)) {
$exp = strtolower($exp);
if (isset($this->exp[$exp])) {
$exp = $this->exp[$exp];
}
}
$k = '$'.$exp;
$data[$k] = $value;
}
$result[$key] = $data;
return $result;
} elseif (!in_array($exp, $this->exp)) {
$exp = strtolower($exp);
if (isset($this->exp[$exp])) {
$exp = $this->exp[$exp];
} else {
throw new Exception('where express error:'.$exp);
}
}
$result = [];
if ('=' == $exp) {
// 普通查询
$result[$key] = $this->parseValue($query, $value, $key);
} elseif (in_array($exp, ['neq', 'ne', 'gt', 'egt', 'gte', 'lt', 'lte', 'elt', 'mod'])) {
// 比较运算
$k = '$'.$exp;
$result[$key] = [$k => $this->parseValue($query, $value, $key)];
} elseif ('null' == $exp) {
// NULL 查询
$result[$key] = null;
} elseif ('not null' == $exp) {
$result[$key] = ['$ne' => null];
} elseif ('all' == $exp) {
// 满足所有指定条件
$result[$key] = ['$all', $this->parseValue($query, $value, $key)];
} elseif ('between' == $exp) {
// 区间查询
$value = is_array($value) ? $value : explode(',', $value);
$result[$key] = ['$gte' => $this->parseValue($query, $value[0], $key), '$lte' => $this->parseValue($query, $value[1], $key)];
} elseif ('not between' == $exp) {
// 范围查询
$value = is_array($value) ? $value : explode(',', $value);
$result[$key] = ['$lt' => $this->parseValue($query, $value[0], $key), '$gt' => $this->parseValue($query, $value[1], $key)];
} elseif ('exists' == $exp) {
// 字段是否存在
$result[$key] = ['$exists' => (bool) $value];
} elseif ('type' == $exp) {
// 类型查询
$result[$key] = ['$type' => intval($value)];
} elseif ('exp' == $exp) {
// 表达式查询
$result['$where'] = $value instanceof Javascript ? $value : new Javascript($value);
} elseif ('like' == $exp) {
// 模糊查询 采用正则方式
$result[$key] = $value instanceof Regex ? $value : new Regex($value, 'i');
} elseif (in_array($exp, ['nin', 'in'])) {
// IN 查询
$value = is_array($value) ? $value : explode(',', $value);
foreach ($value as $k => $val) {
$value[$k] = $this->parseValue($query, $val, $key);
}
$result[$key] = ['$'.$exp => $value];
} elseif ('regex' == $exp) {
$result[$key] = $value instanceof Regex ? $value : new Regex($value, 'i');
} elseif ('< time' == $exp) {
$result[$key] = ['$lt' => $this->parseDateTime($query, $value, $field)];
} elseif ('> time' == $exp) {
$result[$key] = ['$gt' => $this->parseDateTime($query, $value, $field)];
} elseif ('between time' == $exp) {
// 区间查询
$value = is_array($value) ? $value : explode(',', $value);
$result[$key] = ['$gte' => $this->parseDateTime($query, $value[0], $field), '$lte' => $this->parseDateTime($query, $value[1], $field)];
} elseif ('not between time' == $exp) {
// 范围查询
$value = is_array($value) ? $value : explode(',', $value);
$result[$key] = ['$lt' => $this->parseDateTime($query, $value[0], $field), '$gt' => $this->parseDateTime($query, $value[1], $field)];
} elseif ('near' == $exp) {
// 经纬度查询
$result[$key] = ['$near' => $this->parseValue($query, $value, $key)];
} elseif ('size' == $exp) {
// 元素长度查询
$result[$key] = ['$size' => intval($value)];
} else {
// 普通查询
$result[$key] = $this->parseValue($query, $value, $key);
}
return $result;
}
/**
* 日期时间条件解析.
*
* @param Query $query 查询对象
* @param string $value
* @param string $key
*
* @return string
*/
protected function parseDateTime(Query $query, $value, $key)
{
// 获取时间字段类型
$type = $query->getFieldType($key);
if ($type) {
if (is_string($value)) {
$value = strtotime($value) ?: $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);
}
}
}
return $value;
}
/**
* 获取最后写入的ID 如果是insertAll方法的话 返回所有写入的ID.
*
* @return mixed
*/
public function getLastInsID()
{
return $this->insertId;
}
/**
* 生成insert BulkWrite对象
*
* @param Query $query 查询对象
*
* @return BulkWrite
*/
public function insert(Query $query): BulkWrite
{
// 分析并处理数据
$options = $query->getOptions();
$data = $this->parseData($query, $options['data']);
$bulk = new BulkWrite();
if ($insertId = $bulk->insert($data)) {
$this->insertId = $insertId;
}
$this->log('insert', $data, $options);
return $bulk;
}
/**
* 生成insertall BulkWrite对象
*
* @param Query $query 查询对象
* @param array $dataSet 数据集
*
* @return BulkWrite
*/
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);
if ($insertId = $bulk->insert($data)) {
$this->insertId[] = $insertId;
}
}
$this->log('insert', $dataSet, $options);
return $bulk;
}
/**
* 生成update BulkWrite对象
*
* @param Query $query 查询对象
*
* @return BulkWrite
*/
public function update(Query $query): BulkWrite
{
$options = $query->getOptions();
$data = $this->parseSet($query, $options['data']);
$where = $this->parseWhere($query, $options['where']);
if (1 == $options['limit']) {
$updateOptions = ['multi' => false];
} else {
$updateOptions = ['multi' => true];
}
$bulk = new BulkWrite();
$bulk->update($where, $data, $updateOptions);
$this->log('update', $data, $where);
return $bulk;
}
/**
* 生成delete BulkWrite对象
*
* @param Query $query 查询对象
*
* @return BulkWrite
*/
public function delete(Query $query): BulkWrite
{
$options = $query->getOptions();
$where = $this->parseWhere($query, $options['where']);
$bulk = new BulkWrite();
if (1 == $options['limit']) {
$deleteOptions = ['limit' => 1];
} else {
$deleteOptions = ['limit' => 0];
}
$bulk->delete($where, $deleteOptions);
$this->log('remove', $where, $deleteOptions);
return $bulk;
}
/**
* 生成Mongo查询对象
*
* @param Query $query 查询对象
* @param bool $one 是否仅获取一个记录
*
* @return MongoQuery
*/
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);
return $query;
}
/**
* 生成Count命令.
*
* @param Query $query 查询对象
*
* @return Command
*/
public function count(Query $query): Command
{
$options = $query->getOptions();
$cmd['count'] = $options['table'];
$cmd['query'] = (object) $this->parseWhere($query, $options['where']);
foreach (['hint', 'limit', 'maxTimeMS', 'skip'] as $option) {
if (isset($options[$option])) {
$cmd[$option] = $options[$option];
}
}
$command = new Command($cmd);
$this->log('cmd', 'count', $cmd);
return $command;
}
/**
* 聚合查询命令.
*
* @param Query $query 查询对象
* @param array $extra 指令和字段
*
* @return Command
*/
public function aggregate(Query $query, array $extra): Command
{
$options = $query->getOptions();
[$fun, $field] = $extra;
if ('id' == $field && $this->connection->getConfig('pk_convert_id')) {
$field = '_id';
}
$group = isset($options['group']) ? '$'.$options['group'] : null;
$pipeline = [
['$match' => (object) $this->parseWhere($query, $options['where'])],
['$group' => ['_id' => $group, 'aggregate' => ['$'.$fun => '$'.$field]]],
];
$cmd = [
'aggregate' => $options['table'],
'allowDiskUse' => true,
'pipeline' => $pipeline,
'cursor' => new \stdClass(),
];
foreach (['explain', 'collation', 'bypassDocumentValidation', 'readConcern'] as $option) {
if (isset($options[$option])) {
$cmd[$option] = $options[$option];
}
}
$command = new Command($cmd);
$this->log('aggregate', $cmd);
return $command;
}
/**
* 多聚合查询命令, 可以对多个字段进行 group by 操作.
*
* @param Query $query 查询对象
* @param array $extra 指令和字段
*
* @return Command
*/
public function multiAggregate(Query $query, $extra): Command
{
$options = $query->getOptions();
[$aggregate, $groupBy] = $extra;
$groups = ['_id' => []];
foreach ($groupBy as $field) {
$groups['_id'][$field] = '$'.$field;
}
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,
'pipeline' => $pipeline,
'cursor' => new \stdClass(),
];
foreach (['explain', 'collation', 'bypassDocumentValidation', 'readConcern'] as $option) {
if (isset($options[$option])) {
$cmd[$option] = $options[$option];
}
}
$command = new Command($cmd);
$this->log('group', $cmd);
return $command;
}
/**
* 生成distinct命令.
*
* @param Query $query 查询对象
* @param string $field 字段名
*
* @return Command
*/
public function distinct(Query $query, $field): Command
{
$options = $query->getOptions();
$cmd = [
'distinct' => $options['table'],
'key' => $field,
];
if (!empty($options['where'])) {
$cmd['query'] = (object) $this->parseWhere($query, $options['where']);
}
if (isset($options['maxTimeMS'])) {
$cmd['maxTimeMS'] = $options['maxTimeMS'];
}
$command = new Command($cmd);
$this->log('cmd', 'distinct', $cmd);
return $command;
}
/**
* 查询所有的collection.
*
* @return Command
*/
public function listcollections(): Command
{
$cmd = ['listCollections' => 1];
$command = new Command($cmd);
$this->log('cmd', 'listCollections', $cmd);
return $command;
}
/**
* 查询数据表的状态信息.
*
* @param Query $query 查询对象
*
* @return Command
*/
public function collStats(Query $query): Command
{
$options = $query->getOptions();
$cmd = ['collStats' => $options['table']];
$command = new Command($cmd);
$this->log('cmd', 'collStats', $cmd);
return $command;
}
protected function log($type, $data, $options = [])
{
$this->connection->mongoLog($type, $data, $options);
}
}

View File

@@ -0,0 +1,525 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db\builder;
use PDO;
use think\db\BaseQuery as Query;
use think\db\Builder;
use think\db\exception\DbException as Exception;
use think\db\Raw;
/**
* mysql数据库驱动.
*/
class Mysql extends Builder
{
/**
* 查询表达式解析.
*
* @var array
*/
protected $parser = [
'parseCompare' => ['=', '!=', '<>', '>', '>=', '<', '<=', '&', '|', '^', '>>', '<<'],
'parseLike' => ['LIKE', 'NOT LIKE'],
'parseBetween' => ['NOT BETWEEN', 'BETWEEN'],
'parseIn' => ['NOT IN', 'IN'],
'parseExp' => ['EXP'],
'parseRegexp' => ['REGEXP', 'NOT REGEXP'],
'parseNull' => ['NOT NULL', 'NULL'],
'parseBetweenTime' => ['BETWEEN TIME', 'NOT BETWEEN TIME'],
'parseTime' => ['< TIME', '> TIME', '<= TIME', '>= TIME'],
'parseExists' => ['NOT EXISTS', 'EXISTS'],
'parseColumn' => ['COLUMN'],
'parseFindInSet' => ['FIND IN SET'],
];
/**
* 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.
*
* @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.
*
* @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.
*
* @param Query $query 查询对象
* @param array $dataSet 数据集
*
* @return string
*/
public function insertAll(Query $query, array $dataSet): string
{
$options = $query->getOptions();
$bind = $query->getFieldsBindType();
// 获取合法的字段
if (empty($options['field']) || '*' == $options['field']) {
$allowFields = array_keys($bind);
} else {
$allowFields = $options['field'];
}
$fields = [];
$values = [];
foreach ($dataSet as $data) {
$data = $this->parseData($query, $data, $allowFields, $bind);
$values[] = '( ' . implode(',', array_values($data)) . ' )';
if (!isset($insertFields)) {
$insertFields = array_keys($data);
}
}
foreach ($insertFields as $field) {
$fields[] = $this->parseKey($query, $field);
}
return str_replace(
['%INSERT%', '%EXTRA%', '%TABLE%', '%PARTITION%', '%FIELD%', '%DATA%', '%DUPLICATE%', '%COMMENT%'],
[
!empty($options['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
);
}
/**
* 生成insertall SQL
* @access public
* @param Query $query 查询对象
* @param array $keys 键值
* @param array $values 数据
* @return string
*/
public function insertAllByKeys(Query $query, array $keys, array $datas): string
{
$options = $query->getOptions();
$bind = $query->getFieldsBindType();
$fields = [];
$values = [];
foreach ($keys as $field) {
$fields[] = $this->parseKey($query, $field);
}
foreach ($datas as $data) {
foreach ($data as $key => &$val) {
if (!$query->isAutoBind()) {
$val = PDO::PARAM_STR == $bind[$keys[$key]] ? '\'' . $val . '\'' : $val;
} else {
$val = $this->parseDataBind($query, $keys[$key], $val, $bind);
}
}
$values[] = '( ' . implode(',', $data) . ' )';
}
return str_replace(
['%INSERT%', '%EXTRA%', '%TABLE%', '%PARTITION%', '%FIELD%', '%DATA%', '%DUPLICATE%', '%COMMENT%'],
[
!empty($options['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.
*
* @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[] = (str_contains($key, '->') ? strstr($key, '->', true) : $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.
*
* @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
);
}
/**
* 正则查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @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 查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
*
* @return string
*/
protected function parseFindInSet(Query $query, string $key, string $exp, $value, string $field): string
{
if ($value instanceof Raw) {
$value = $this->parseRaw($query, $value);
}
return 'FIND_IN_SET(' . $value . ', ' . $key . ')';
}
/**
* 字段和表名处理.
*
* @param Query $query 查询对象
* @param mixed $key 字段名
* @param bool $strict 严格检测
*
* @return string
*/
public function parseKey(Query $query, string | int | Raw $key, bool $strict = false): string
{
if (is_int($key)) {
return (string) $key;
}
if ($key instanceof Raw) {
return $this->parseRaw($query, $key);
}
$key = trim($key);
if (str_contains($key, '->>') && !str_contains($key, '(')) {
// JSON字段支持
[$field, $name] = explode('->>', $key, 2);
return $this->parseKey($query, $field, true) . '->>\'$' . (str_starts_with($name, '[') ? '' : '.') . str_replace('->>', '.', $name) . '\'';
}
if (str_contains($key, '->') && !str_contains($key, '(')) {
// JSON字段支持
[$field, $name] = explode('->', $key, 2);
return 'json_unquote(json_extract(' . $this->parseKey($query, $field, true) . ', \'$' . (str_starts_with($name, '[') ? '' : '.') . str_replace('->', '.', $name) . '\'))';
}
if (str_contains($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {
[$table, $key] = explode('.', $key, 2);
$alias = $query->getOption('alias');
if ('__TABLE__' == $table) {
$table = $query->getOption('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)) {
if (str_contains($table, '.')) {
$table = str_replace('.', '`.`', $table);
}
$key = '`' . $table . '`.' . $key;
}
return $key;
}
/**
* Null查询.
*
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
* @param int $bindType
*
* @return string
*/
protected function parseNull(Query $query, string $key, string $exp, $value, $field, int $bindType): string
{
if (str_starts_with($key, "json_unquote(json_extract")) {
if ('NULL' === $exp) {
return '(' . $key . ' is null OR ' . $key . ' = \'null\')';
} elseif ('NOT NULL' === $exp) {
return '(' . $key . ' is not null AND ' . $key . ' != \'null\')';
}
}
return parent::parseNull($query, $key, $exp, $value, $field, $bindType);
}
/**
* 随机排序.
*
* @param Query $query 查询对象
*
* @return string
*/
protected function parseRand(Query $query): string
{
return 'rand()';
}
/**
* Partition 分析.
*
* @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 分析.
*
* @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) . ' ';
}
}

View File

@@ -0,0 +1,134 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\builder;
use think\db\Builder;
use think\db\exception\DbException as Exception;
use think\db\BaseQuery as Query;
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分析.
*
* @param Query $query 查询对象
* @param mixed $limit
*
* @return string
*/
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 {
$limitStr = '(numrow>0 AND numrow<=' . $limit[0] . ')';
}
}
return $limitStr ? ' WHERE ' . $limitStr : '';
}
/**
* 设置锁机制.
*
* @param Query $query 查询对象
* @param bool|string $lock
*
* @return string
*/
protected function parseLock(Query $query, bool|string $lock = false): string
{
if (!$lock) {
return '';
}
return ' FOR UPDATE NOWAIT ';
}
/**
* 字段和表名处理.
*
* @param Query $query 查询对象
* @param string|int|Raw $key
* @param bool $strict
*
* @throws Exception
*
* @return string
*/
public function parseKey(Query $query, string|int|Raw $key, bool $strict = false): string
{
if (is_int($key)) {
return (string) $key;
} elseif ($key instanceof Raw) {
return $this->parseRaw($query, $key);
}
$key = trim($key);
if (str_contains($key, '->') && !str_contains($key, '(')) {
// JSON字段支持
[$field, $name] = explode($key, '->');
$key = $field . '."' . $name . '"';
} elseif (str_contains($key, '.') && !preg_match('/[,\'\"\(\)\[\s]/', $key)) {
[$table, $key] = explode('.', $key, 2);
$alias = $query->getOption('alias');
if ('__TABLE__' == $table) {
$table = $query->getOption('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;
}
/**
* 随机排序.
*
* @param Query $query 查询对象
*
* @return string
*/
protected function parseRand(Query $query): string
{
return 'DBMS_RANDOM.value';
}
}

View File

@@ -0,0 +1,123 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\builder;
use think\db\Builder;
use think\db\BaseQuery as Query;
use think\db\Raw;
/**
* Pgsql数据库驱动.
*/
class Pgsql extends Builder
{
/**
* 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%';
/**
* limit分析.
*
* @param Query $query 查询对象
* @param mixed $limit
*
* @return string
*/
public function parseLimit(Query $query, string $limit): string
{
$limitStr = '';
if (!empty($limit)) {
$limit = explode(',', $limit);
if (count($limit) > 1) {
$limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' ';
} else {
$limitStr .= ' LIMIT ' . $limit[0] . ' ';
}
}
return $limitStr;
}
/**
* 字段和表名处理.
*
* @param Query $query 查询对象
* @param string|int|Raw $key 字段名
* @param bool $strict 严格检测
*
* @return string
*/
public function parseKey(Query $query, string|int|Raw $key, bool $strict = false): string
{
if (is_int($key)) {
return (string) $key;
} elseif ($key instanceof Raw) {
return $this->parseRaw($query, $key);
}
$key = trim($key);
if (str_contains($key, '->') && !str_contains($key, '(')) {
// JSON字段支持
[$field, $name] = explode('->', $key);
$key = '"' . $field . '"' . '->>\'' . $name . '\'';
} elseif (str_contains($key, '.')) {
[$table, $key] = explode('.', $key, 2);
$alias = $query->getOption('alias');
if ('__TABLE__' == $table) {
$table = $query->getOption('table');
$table = is_array($table) ? array_shift($table) : $table;
}
if (isset($alias[$table])) {
$table = $alias[$table];
}
if ('*' != $key && !preg_match('/[,\"\*\(\).\s]/', $key)) {
$key = '"' . $key . '"';
}
}
if (isset($table)) {
$key = $table . '.' . $key;
}
return $key;
}
/**
* 随机排序.
*
* @param Query $query 查询对象
*
* @return string
*/
protected function parseRand(Query $query): string
{
return 'RANDOM()';
}
}

View File

@@ -0,0 +1,118 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\builder;
use think\db\Builder;
use think\db\BaseQuery as Query;
use think\db\Raw;
/**
* Sqlite数据库驱动.
*/
class Sqlite extends Builder
{
/**
* limit.
*
* @param Query $query 查询对象
* @param mixed $limit
*
* @return string
*/
public function parseLimit(Query $query, string $limit): string
{
$limitStr = '';
if (!empty($limit)) {
$limit = explode(',', $limit);
if (count($limit) > 1) {
$limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' ';
} else {
$limitStr .= ' LIMIT ' . $limit[0] . ' ';
}
}
return $limitStr;
}
/**
* 随机排序.
*
* @param Query $query 查询对象
*
* @return string
*/
protected function parseRand(Query $query): string
{
return 'RANDOM()';
}
/**
* 字段和表名处理.
*
* @param Query $query 查询对象
* @param string|int|Raw $key 字段名
* @param bool $strict 严格检测
*
* @return string
*/
public function parseKey(Query $query, string|int|Raw $key, bool $strict = false): string
{
if (is_int($key)) {
return (string) $key;
} elseif ($key instanceof Raw) {
return $this->parseRaw($query, $key);
}
$key = trim($key);
if (str_contains($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {
[$table, $key] = explode('.', $key, 2);
$alias = $query->getOption('alias');
if ('__TABLE__' == $table) {
$table = $query->getOption('table');
$table = is_array($table) ? array_shift($table) : $table;
}
if (isset($alias[$table])) {
$table = $alias[$table];
}
}
if ('*' != $key && !preg_match('/[,\'\"\*\(\)`.\s]/', $key)) {
$key = '`' . $key . '`';
}
if (isset($table)) {
$key = '`' . $table . '`.' . $key;
}
return $key;
}
/**
* 设置锁机制.
*
* @param Query $query 查询对象
* @param bool|string $lock
*
* @return string
*/
protected function parseLock(Query $query, bool|string $lock = false): string
{
return '';
}
}

View File

@@ -0,0 +1,194 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2012 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace think\db\builder;
use think\db\Builder;
use think\db\exception\DbException as Exception;
use think\db\BaseQuery as Query;
use think\db\Raw;
/**
* Sqlsrv数据库驱动.
*/
class Sqlsrv extends Builder
{
/**
* 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%';
/**
* 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分析.
*
* @param Query $query 查询对象
* @param mixed $order
*
* @return string
*/
protected function parseOrder(Query $query, array $order): string
{
if (empty($order)) {
return ' ORDER BY rand()';
}
$array = [];
foreach ($order as $key => $val) {
if ($val instanceof Raw) {
$array[] = $this->parseRaw($query, $val);
} elseif ('[rand]' == $val) {
$array[] = $this->parseRand($query);
} else {
if (is_numeric($key)) {
[$key, $sort] = explode(' ', str_contains($val, ' ') ? $val : $val . ' ');
} else {
$sort = $val;
}
$sort = in_array(strtolower($sort), ['asc', 'desc'], true) ? ' ' . $sort : '';
$array[] = $this->parseKey($query, $key, true) . $sort;
}
}
return ' ORDER BY ' . implode(',', $array);
}
/**
* 随机排序.
*
* @param Query $query 查询对象
*
* @return string
*/
protected function parseRand(Query $query): string
{
return 'rand()';
}
/**
* 字段和表名处理.
*
* @param Query $query 查询对象
* @param string|int|Raw $key 字段名
* @param bool $strict 严格检测
*
* @return string
*/
public function parseKey(Query $query, string|int|Raw $key, bool $strict = false): string
{
if (is_int($key)) {
return (string) $key;
} elseif ($key instanceof Raw) {
return $this->parseRaw($query, $key);
}
$key = trim($key);
if (str_contains($key, '.') && !preg_match('/[,\'\"\(\)\[\s]/', $key)) {
[$table, $key] = explode('.', $key, 2);
$alias = $query->getOption('alias');
if ('__TABLE__' == $table) {
$table = $query->getOption('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;
}
/**
* limit.
*
* @param Query $query 查询对象
* @param mixed $limit
*
* @return string
*/
protected function parseLimit(Query $query, string $limit): string
{
if (empty($limit)) {
return '';
}
$limit = explode(',', $limit);
if (count($limit) > 1) {
$limitStr = '(T1.ROW_NUMBER BETWEEN ' . $limit[0] . ' + 1 AND ' . $limit[0] . ' + ' . $limit[1] . ')';
} else {
$limitStr = '(T1.ROW_NUMBER BETWEEN 1 AND ' . $limit[0] . ')';
}
return 'WHERE ' . $limitStr;
}
public function selectInsert(Query $query, array $fields, string $table): string
{
$this->selectSql = $this->selectInsertSql;
return parent::selectInsert($query, $fields, $table);
}
}

View File

@@ -0,0 +1,131 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db\concern;
use think\db\exception\DbException;
use think\db\Raw;
/**
* 聚合查询.
*/
trait AggregateQuery
{
/**
* 聚合查询.
*
* @param string $aggregate 聚合方法
* @param string|Raw $field 字段名
* @param bool $force 强制转为数字类型
*
* @return mixed
*/
protected function aggregate(string $aggregate, string | Raw $field, bool $force = false)
{
return $this->connection->aggregate($this, $aggregate, $field, $force);
}
/**
* COUNT查询.
*
* @param string $field 字段名
*
* @return int
*/
public function count(string $field = '*'): int
{
if (!empty($this->options['group'])) {
return $this->countWithGroup($field);
}
return (int) $this->aggregate('COUNT', $field);
}
protected function countWithGroup(string $field): int
{
if (!preg_match('/^[\w\.\*]+$/', $field)) {
throw new DbException('Not supported data: ' . $field);
}
$options = $this->getOptions();
$cache = $options['cache'] ?? null;
if (isset($options['cache'])) {
unset($options['cache']);
}
$subSql = $this->options($options)
->field('count(' . $field . ') AS think_count')
->bind($this->bind)
->buildSql();
$query = $this->newQuery();
if ($cache) {
$query->setOption('cache', $cache);
}
$query->table([$subSql => '_group_count_']);
return (int) $query->aggregate('COUNT', '*');
}
/**
* SUM查询.
*
* @param string|Raw $field 字段名
*
* @return float
*/
public function sum(string | Raw $field): float
{
return $this->aggregate('SUM', $field, true);
}
/**
* MIN查询.
*
* @param string|Raw $field 字段名
* @param bool $force 强制转为数字类型
*
* @return mixed
*/
public function min(string | Raw $field, bool $force = true)
{
return $this->aggregate('MIN', $field, $force);
}
/**
* MAX查询.
*
* @param string|Raw $field 字段名
* @param bool $force 强制转为数字类型
*
* @return mixed
*/
public function max(string | Raw $field, bool $force = true)
{
return $this->aggregate('MAX', $field, $force);
}
/**
* AVG查询.
*
* @param string|Raw $field 字段名
*
* @return float
*/
public function avg(string | Raw $field): float
{
return $this->aggregate('AVG', $field, true);
}
}

View File

@@ -0,0 +1,241 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db\concern;
use think\db\Raw;
use think\helper\Str;
/**
* JOIN和VIEW查询.
*/
trait JoinAndViewQuery
{
/**
* 查询SQL组装 join.
*
* @param array|string|Raw $join 关联的表名
* @param mixed $condition 条件
* @param string $type JOIN类型
* @param array $bind 参数绑定
*
* @return $this
*/
public function join(array | string | Raw $join, ?string $condition = null, string $type = 'INNER', array $bind = [])
{
$table = $this->getJoinTable($join, $alias);
if (!empty($bind) && $condition) {
$this->bindParams($condition, $bind);
}
if ($alias) {
$this->options['join'][$alias] = [$table, strtoupper($type), $condition];
} else {
$this->options['join'][] = [$table, strtoupper($type), $condition];
}
return $this;
}
/**
* LEFT JOIN.
*
* @param array|string|Raw $join 关联的表名
* @param mixed $condition 条件
* @param array $bind 参数绑定
*
* @return $this
*/
public function leftJoin(array | string | Raw $join, ?string $condition = null, array $bind = [])
{
return $this->join($join, $condition, 'LEFT', $bind);
}
/**
* RIGHT JOIN.
*
* @param array|string|Raw $join 关联的表名
* @param mixed $condition 条件
* @param array $bind 参数绑定
*
* @return $this
*/
public function rightJoin(array | string | Raw $join, ?string $condition = null, array $bind = [])
{
return $this->join($join, $condition, 'RIGHT', $bind);
}
/**
* FULL JOIN.
*
* @param array|string|Raw $join 关联的表名
* @param mixed $condition 条件
* @param array $bind 参数绑定
*
* @return $this
*/
public function fullJoin(array | string | Raw $join, ?string $condition = null, array $bind = [])
{
return $this->join($join, $condition, 'FULL', $bind);
}
/**
* 获取Join表名及别名 支持
* ['prefix_table或者子查询'=>'alias'] 'table alias'.
*
* @param array|string|Raw $join JION表名
* @param string $alias 别名
*
* @return string|array
*/
protected function getJoinTable(array | string | Raw $join, ?string &$alias = null)
{
if (is_array($join)) {
$table = $join;
$alias = array_shift($join);
return $table;
}
if ($join instanceof Raw || str_contains($join, '(')) {
return $join;
}
$join = trim($join);
// 使用别名
if (str_contains($join, ' ')) {
// 使用别名
[$table, $alias] = explode(' ', $join);
} else {
$table = $join;
if (!str_contains($join, '.')) {
$alias = $join;
}
}
if ($this->prefix && !str_contains($table, '.') && !str_starts_with($table, $this->prefix)) {
$table = $this->prefix . Str::snake($table) . $this->suffix;
}
if (!empty($alias) && $table != $alias) {
$table = [$table => $alias];
}
return $table;
}
/**
* 指定JOIN查询字段.
*
* @param array|string|Raw $join 数据表
* @param string|array|bool $field 查询字段
* @param string $on JOIN条件
* @param string $type JOIN类型
* @param array $bind 参数绑定
*
* @return $this
*/
public function view(array | string | Raw $join, string | array | bool $field = true, ?string $on = null, string $type = 'INNER', array $bind = []): self
{
$this->options['view'] = true;
$fields = [];
$table = $this->getJoinTable($join, $alias);
// 处理字段
$fields = $this->processFields($field, $alias);
$this->field($fields);
// 处理连接
if ($on) {
$this->join($table, $on, $type, $bind);
} else {
$this->table($table);
}
return $this;
}
protected function processFields(string | array | bool $field, string $alias): array
{
$fields = [];
if (true === $field) {
$fields[] = $alias . '.*'; // 选取所有字段
} else {
if (is_string($field)) {
$field = explode(',', $field);
}
foreach ($field as $key => $val) {
$name = is_numeric($key) ? $alias . '.' . $val : (preg_match('/[,=\.\'\"\(\s]/', $key) ? $key : $alias . '.' . $key);
$fields[] = $name . (is_numeric($key) ? '' : ' AS ' . $val);
$this->options['map'][$val] = $name;
}
}
return $fields;
}
/**
* 视图查询处理.
*
* @param array $options 查询参数
*
* @return void
*/
protected function parseView(array &$options): void
{
foreach (['AND', 'OR'] as $logic) {
if (!isset($options['where'][$logic])) {
continue;
}
// 视图查询条件处理
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 (empty($options['order'])) {
return;
}
// 视图查询排序处理
foreach ($options['order'] as $key => $val) {
if (is_numeric($key) && is_string($val)) {
if (str_contains($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]);
}
}
}
}

View File

@@ -0,0 +1,729 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db\concern;
use Closure;
use think\helper\Str;
use think\model\Collection as ModelCollection;
use think\model\contract\Modelable as Model;
/**
* 模型及关联查询.
*/
trait ModelRelationQuery
{
/**
* 当前模型对象
*
* @var Model
*/
protected $model;
/**
* 指定模型.
*
* @param Model $model 模型对象实例
*
* @return $this
*/
public function model(Model $model)
{
$this->model = $model;
return $this;
}
/**
* 获取当前的模型对象
*
* @return Model|null
*/
public function getModel()
{
return $this->model;
}
/**
* 设置需要隐藏的输出属性.
*
* @param array $hidden 属性列表
* @param bool $merge 是否合并
*
* @return $this
*/
public function hidden(array $hidden, bool $merge = false)
{
$this->options['hidden'] = [$hidden, $merge];
return $this;
}
/**
* 设置需要输出的属性.
*
* @param array $visible 属性列表
* @param bool $merge 是否合并
*
* @return $this
*/
public function visible(array $visible, bool $merge = false)
{
$this->options['visible'] = [$visible, $merge];
return $this;
}
/**
* 设置需要附加的输出属性.
*
* @param array $append 属性列表
* @param bool $merge 是否合并
*
* @return $this
*/
public function append(array $append, bool $merge = false)
{
$this->options['append'] = [$append, $merge];
return $this;
}
/**
* 设置模型的输出映射.
*
* @param array $mapping 映射列表
*
* @return $this
*/
public function mapping(array $mapping)
{
$this->options['mapping'] = $mapping;
return $this;
}
/**
* 添加查询范围.
*
* @param array|string|Closure $scope 查询范围定义
* @param array $args 参数
*
* @return $this
*/
public function scope($scope, ...$args)
{
// 查询范围的第一个参数始终是当前查询对象
array_unshift($args, $this);
if ($scope instanceof Closure) {
$this->options['scope'][] = [$scope, $args];
return $this;
}
if ($this->model) {
if (is_string($scope)) {
$scope = explode(',', $scope);
}
// 检查模型类的查询范围方法
$entity = $this->model->getEntity();
foreach ($scope as $name) {
$method = 'scope' . trim($name);
if ($entity && method_exists($entity, $method)) {
$this->options['scope'][$name] = [[$entity, $method], $args];
} elseif (method_exists($this->model, $method)) {
$this->options['scope'][$name] = [[$this->model, $method], $args];
}
}
}
return $this;
}
/**
* 执行查询范围查询.
*
* @return $this
*/
protected function scopeQuery()
{
if (!empty($this->options['scope'])) {
foreach ($this->options['scope'] as $val) {
[$call, $args] = $val;
call_user_func_array($call, $args);
}
}
return $this;
}
/**
* 指定不使用的查询范围.
*
* @param array $scope 查询范围
*
* @return $this
*/
public function withoutScope(array $scope = [])
{
if (empty($scope)) {
$this->options['scope'] = [];
return $this;
}
foreach ($scope as $name) {
if (isset($this->options['scope'][$name])) {
unset($this->options['scope'][$name]);
}
}
return $this;
}
/**
* 设置关联查询.
*
* @param array $relation 关联名称
*
* @return $this
*/
public function relation(array $relation)
{
if (empty($this->model) || empty($relation)) {
return $this;
}
$this->options['relation'] = $relation;
return $this;
}
/**
* 使用搜索器条件搜索字段.
*
* @param string|array $fields 搜索字段
* @param mixed $data 搜索数据
* @param bool $strict 是否严格检查数据
*
* @return $this
*/
public function withSearch(string | array $fields, $data = [], bool $strict = false)
{
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);
} elseif ($this->model) {
// 检查字段是否有数据
$fieldName = is_numeric($key) ? $field : $key;
if ($strict && (!isset($data[$fieldName]) || (empty($data[$fieldName]) && !in_array($data[$fieldName], ['0', 0])))) {
continue;
}
if (is_string($key) && isset($data[$key])) {
// 默认搜索规则
$this->where($key, $field, 'like' == $field ? '%' . $data[$key] . '%' : $data[$key]);
continue;
}
$method = 'search' . Str::studly($fieldName) . 'Attr';
$entity = $this->model->getEntity();
if ($entity && method_exists($entity, $method)) {
$entity->$method($this, $data[$fieldName] ?? null, $data);
} elseif (method_exists($this->model, $method)) {
$this->model->$method($this, $data[$fieldName] ?? null, $data);
} elseif (isset($data[$field])) {
$this->where($fieldName, in_array($fieldName, $likeFields) ? 'like' : '=', in_array($fieldName, $likeFields) ? '%' . $data[$field] . '%' : $data[$field]);
}
}
}
return $this;
}
/**
* 限制关联数据的字段 已废弃直接使用field或withoutfield替代.
*
* @deprecated
*
* @param array|string $field 关联字段限制
*
* @return $this
*/
public function withField($field)
{
return $this->field($field);
}
/**
* 限制关联数据的数量 已废弃直接使用limit替代.
*
* @deprecated
*
* @param int $limit 关联数量限制
*
* @return $this
*/
public function withLimit(int $limit)
{
return $this->limit($limit);
}
/**
* 设置关联数据不存在的时候默认值
*
* @param mixed $data 默认值
*
* @return $this
*/
public function withDefault($data = null)
{
$this->options['default_model'] = $data;
return $this;
}
/**
* 设置关联模型的动态绑定
*
* @param array $attr 绑定数据
*
* @return $this
*/
public function withBind(array $attr)
{
$this->options['bind_attr'] = $attr;
return $this;
}
/**
* 设置数据字段获取器.
*
* @param string|array $name 字段名
* @param callable $callback 闭包获取器
*
* @return $this
*/
public function withAttr(string | array $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 (str_contains($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方式.
*
* @param array|string $with 关联方法名称
*
* @return $this
*/
public function with(array | string $with)
{
if (empty($this->model) || empty($with)) {
return $this;
}
$this->options['with'] = array_merge($this->options['with'] ?? [], (array) $with);
return $this;
}
/**
* 关联预载入 JOIN方式.
*
* @param array|string $with 关联方法名
* @param string $joinType JOIN方式
*
* @return $this
*/
public function withJoin(array | string $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) && str_contains($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;
}
/**
* 关联统计
*
* @param array|string $relations 关联方法名
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param bool $subQuery 是否使用子查询
*
* @return $this
*/
protected function withAggregate(string | array $relations, string $aggregate = 'count', $field = 'id', 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;
}
/**
* 关联缓存.
*
* @param string|array|bool $relation 关联方法名
* @param mixed $key 缓存key
* @param int|\DateTime $expire 缓存有效期
* @param string $tag 缓存标签
*
* @return $this
*/
public function withCache(string | array | bool $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;
}
/**
* 关联统计
*
* @param string|array $relation 关联方法名
* @param string $field 字段(默认为id)
* @param bool $subQuery 是否使用子查询
*
* @return $this
*/
public function withCount(string | array $relation, string $field = 'id', bool $subQuery = true)
{
return $this->withAggregate($relation, 'count', $field, $subQuery);
}
/**
* 关联统计Sum.
*
* @param string|array $relation 关联方法名
* @param string $field 字段
* @param bool $subQuery 是否使用子查询
*
* @return $this
*/
public function withSum(string | array $relation, string $field, bool $subQuery = true)
{
return $this->withAggregate($relation, 'sum', $field, $subQuery);
}
/**
* 关联统计Max.
*
* @param string|array $relation 关联方法名
* @param string $field 字段
* @param bool $subQuery 是否使用子查询
*
* @return $this
*/
public function withMax(string | array $relation, string $field, bool $subQuery = true)
{
return $this->withAggregate($relation, 'max', $field, $subQuery);
}
/**
* 关联统计Min.
*
* @param string|array $relation 关联方法名
* @param string $field 字段
* @param bool $subQuery 是否使用子查询
*
* @return $this
*/
public function withMin(string | array $relation, string $field, bool $subQuery = true)
{
return $this->withAggregate($relation, 'min', $field, $subQuery);
}
/**
* 关联统计Avg.
*
* @param string|array $relation 关联方法名
* @param string $field 字段
* @param bool $subQuery 是否使用子查询
*
* @return $this
*/
public function withAvg(string | array $relation, string $field, bool $subQuery = true)
{
return $this->withAggregate($relation, 'avg', $field, $subQuery);
}
/**
* 查询关联数据存在(或超过多少条)的模型数据.
*
* @param string $relation 关联方法名
* @param mixed $operator 比较操作符
* @param int $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);
}
/**
* 查询关联数据不存在的模型数据.
*
* @param string $relation 关联方法名
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
*
* @return $this
*/
public function hasNot(string $relation, string $id = '*', string $joinType = '')
{
return $this->model->has($relation, '=', 0, $id, $joinType, $this);
}
/**
* 根据关联条件查询当前模型.
*
* @param string|array $relation 关联方法名 或 ['关联方法名', '关联表别名']
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
*
* @return $this
*/
public function hasWhere(string|array $relation, $where = [], string $fields = '*', string $joinType = '')
{
return $this->model->hasWhere($relation, $where, $fields, $joinType, $this);
}
/**
* 根据关联条件查询当前模型.
*
* @param string|array $relation 关联方法名 或 ['关联方法名', '关联表别名']
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
*
* @return $this
*/
public function hasWhereOr(string|array $relation, $where = [], string $fields = '*', string $joinType = '')
{
return $this->model->hasWhereOr($relation, $where, $fields, $joinType, $this);
}
/**
* 查询数据转换为模型数据集对象
*
* @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,
$this->options['with_cache'] ?? false
);
}
}
// 模型数据集转换
return $this->model->toCollection($resultSet);
}
/**
* 查询数据转换为模型对象
*
* @param array $result 查询数据
*
* @return void
*/
protected function resultToModel(array &$result): void
{
// 实时读取延迟数据
if (!empty($this->options['lazy_fields'])) {
$id = $this->getKey($result);
foreach ($this->options['lazy_fields'] as $field) {
if (isset($result[$field])) {
$result[$field] += $this->getLazyFieldValue($field, $id);
}
}
}
$result = $this->model->newInstance($result);
if ($this->suffix) {
$result->setSuffix($this->suffix);
}
// 模型数据处理
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(
$result,
$this->options[$with],
$this->options['with_relation_attr'],
'with_join' == $with,
$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->withFieldAttr($this->options['with_attr']);
}
// 模型输出设置
foreach (['hidden', 'visible', 'append'] as $name) {
if (!empty($this->options[$name])) {
[$value, $merge] = $this->options[$name];
$result->$name($value, $merge);
}
}
// 字段映射
if (!empty($this->options['mapping'])) {
$result->mapping($this->options['mapping']);
}
}
}

View File

@@ -0,0 +1,149 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db\concern;
use think\db\Connection;
/**
* 参数绑定支持
*/
trait ParamsBind
{
/**
* 当前参数绑定.
*
* @var array
*/
protected $bind = [];
/**
* 批量参数绑定.
*
* @param array $value 绑定变量值
*
* @return $this
*/
public function bind(array $value)
{
$this->bind = array_merge($this->bind, $value);
return $this;
}
/**
* 单个参数绑定.
*
* @param mixed $value 绑定变量值
* @param int $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 ?: Connection::PARAM_STR];
return $name;
}
/**
* 检测参数是否已经绑定.
*
* @param string $key 参数名
*
* @return bool
*/
public function isBind(string $key)
{
return isset($this->bind[$key]);
}
/**
* 设置自动参数绑定.
*
* @param bool $bind 是否自动参数绑定
*
* @return $this
*/
public function autoBind(bool $bind)
{
$this->options['auto_bind'] = $bind;
return $this;
}
/**
* 检测是否开启自动参数绑定.
*
* @return bool
*/
public function isAutoBind(): bool
{
$autoBind = $this->getConfig('auto_param_bind');
if (null !== $this->getOption('auto_bind')) {
$autoBind = $this->getOption('auto_bind');
}
return (bool) $autoBind;
}
/**
* 参数绑定.
*
* @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);
} elseif (str_ends_with($sql, ':' . $key)) {
$sql = substr_replace($sql, ':' . $name . ' ', strrpos($sql, ':' . $key), strlen(':' . $key));
} else {
$sql = str_replace(
[':' . $key . ' ', ':' . $key . ',', ':' . $key . ')'],
[':' . $name . ' ', ':' . $name . ',', ':' . $name . ')'],
$sql);
}
}
}
/**
* 获取绑定的参数 并清空.
*
* @param bool $clear 是否清空绑定数据
*
* @return array
*/
public function getBind(bool $clear = true): array
{
$bind = $this->bind;
if ($clear) {
$this->bind = [];
}
return $bind;
}
}

View File

@@ -0,0 +1,266 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
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\contract\Modelable as Model;
/**
* 查询数据处理.
*/
trait ResultOperation
{
/**
* 设置数据处理(支持模型).
*
* @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;
}
/**
* 是否允许返回空数据(或空模型).
*
* @param bool $allowEmpty 是否允许为空
*
* @return $this
*/
public function allowEmpty(bool $allowEmpty = true)
{
$this->options['allow_empty'] = $allowEmpty;
return $this;
}
/**
* 设置查询数据不存在是否抛出异常.
*
* @param bool $fail 数据不存在是否抛出异常
*
* @return $this
*/
public function failException(bool $fail = true)
{
$this->options['fail'] = $fail;
return $this;
}
/**
* 处理数据.
*
* @param array $result 查询数据
*
* @return void
*/
protected function result(array &$result): void
{
// JSON数据处理
if (!empty($this->options['json'])) {
$this->jsonResult($result);
}
// 实时读取延迟数据
if (!empty($this->options['lazy_fields'])) {
$id = $this->getKey($result);
foreach ($this->options['lazy_fields'] as $field) {
$result[$field] += $this->getLazyFieldValue($field, $id);
}
}
// 查询数据处理
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']);
}
// 检查字段映射
if (!empty($this->options['mapping'])) {
foreach ($this->options['mapping'] as $name => $alias) {
if (isset($result[$name])) {
$result[$alias] = $result[$name];
unset($result[$name]);
}
}
}
}
/**
* 处理数据集.
*
* @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);
}
}
/**
* 使用获取器处理数据.
*
* @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 (str_contains($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);
}
}
}
/**
* 处理空数据.
* @param Closure $closure 闭包数据
* @throws DbException
* @throws ModelNotFoundException
* @throws DataNotFoundException
*
* @return array|Model|null|static
*/
protected function resultToEmpty(?Closure $closure = null)
{
if (!empty($this->options['fail'])) {
$this->throwNotFound();
} elseif ($closure instanceof Closure) {
return $closure($this);
} elseif (!empty($this->options['allow_empty'])) {
return !empty($this->model) ? $this->model->newInstance() : [];
}
}
/**
* 查找单条记录 不存在返回空数据(或者空模型).
*
* @param mixed $data 数据
*
* @return array|Model|static|mixed
*/
public function findOrEmpty($data = null)
{
return $this->allowEmpty(true)->find($data);
}
/**
* JSON字段数据转换.
*
* @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);
}
}
/**
* 查询失败 抛出异常.
*
* @throws ModelNotFoundException
* @throws DataNotFoundException
*
* @return void
*/
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);
}
/**
* 查找多条记录 如果不存在则抛出异常.
*
* @param array|string|Query|Closure $data 数据
*
* @throws ModelNotFoundException
* @throws DataNotFoundException
*
* @return array|Collection|static[]
*/
public function selectOrFail($data = [])
{
return $this->failException(true)->select($data);
}
/**
* 查找单条记录 如果不存在则抛出异常.
*
* @param array|string|Query|Closure $data 数据
*
* @throws ModelNotFoundException
* @throws DataNotFoundException
*
* @return array|Model|static|mixed
*/
public function findOrFail($data = null)
{
return $this->failException(true)->find($data);
}
}

View File

@@ -0,0 +1,107 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db\concern;
/**
* 数据字段信息.
*/
trait TableFieldInfo
{
/**
* 获取数据表字段信息.
*
* @param string $tableName 数据表名
*
* @return array
*/
public function getTableFields(string $tableName = ''): array
{
if ('' == $tableName && !empty($this->options['field_type'])) {
return array_keys($this->options['field_type']);
}
return $this->connection->getTableFields($tableName ?: $this->getTable());
}
/**
* 获取详细字段类型信息.
*
* @param string $tableName 数据表名称
*
* @return array
*/
public function getFields(string $tableName = ''): array
{
return $this->connection->getFields($tableName ?: $this->getTable());
}
/**
* 获取字段类型信息.
*
* @return array
*/
public function getFieldsType(): array
{
if (!empty($this->options['field_type'])) {
return $this->options['field_type'];
}
return $this->connection->getFieldsType($this->getTable());
}
public function getType(): array
{
return $this->getFieldsType();
}
/**
* 获取字段类型信息.
*
* @param string $field 字段名
*
* @return string|null
*/
public function getFieldType(string $field)
{
$fieldType = $this->getFieldsType();
return $fieldType[$field] ?? null;
}
/**
* 获取字段类型信息.
*
* @return array
*/
public function getFieldsBindType(): array
{
$fieldType = $this->getFieldsType();
return array_map([$this->connection, 'getFieldBindType'], $fieldType);
}
/**
* 获取字段类型信息.
*
* @param string $field 字段名
*
* @return int
*/
public function getFieldBindType(string $field): int
{
$fieldType = $this->getFieldType($field);
return $this->connection->getFieldBindType($fieldType ?: '');
}
}

View File

@@ -0,0 +1,227 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
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'],
];
/**
* 添加日期或者时间查询规则.
*
* @param array $rule 时间表达式
*
* @return $this
*/
public function timeRule(array $rule)
{
$this->timeRule = array_merge($this->timeRule, $rule);
return $this;
}
/**
* 查询日期或者时间.
*
* @param string $field 日期字段名
* @param string $op 比较运算符或者表达式
* @param mixed $range 比较范围
* @param string $logic AND OR
*
* @return $this
*/
public function whereTime(string $field, string $op, $range = null, string $logic = 'AND')
{
if (is_null($range)) {
$range = $this->timeRule[$op] ?? $op;
$op = is_array($range) ? 'between' : '>=';
}
return $this->parseWhereExp($logic, $field, strtolower($op) . ' time', $range, [], true);
}
/**
* 查询某个时间间隔数据.
*
* @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').
*
* @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'])) {
if($month === 'last month') {
$month = $this->timeRule['last month'][0];
}
$month = date('Y-m', strtotime($month));
}
return $this->whereTimeInterval($field, $month, 'month', $step, $logic);
}
/**
* 查询周数据 whereWeek('time_field', '2018-1-1') 从2018-1-1开始的一周数据.
*
* @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').
*
* @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').
*
* @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').
*
* @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').
*
* @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').
*
* @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').
*
* @param string $startField 开始时间字段
* @param string $endField 结束时间字段
*
* @return $this
*/
public function whereNotBetweenTimeField(string $startField, string $endField)
{
return $this->whereTime($startField, '>', time())
->whereTime($endField, '<', time(), 'OR');
}
}

View File

@@ -0,0 +1,131 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\concern;
/**
* 事务支持
*/
trait Transaction
{
/**
* 执行数据库Xa事务
*
* @param callable $callback 数据操作方法回调
* @param array $dbs 多个查询对象或者连接对象
*
* @throws \PDOException
* @throws \Exception
* @throws \Throwable
*
* @return mixed
*/
public function transactionXa(callable $callback, array $dbs = [])
{
return $this->connection->transactionXa($callback, $dbs);
}
/**
* 执行数据库事务
*
* @param callable $callback 数据操作方法回调
*
* @return mixed
*/
public function transaction(callable $callback)
{
return $this->connection->transaction($callback);
}
/**
* 启动事务
*
* @return void
*/
public function startTrans(): void
{
$this->connection->startTrans();
}
/**
* 用于非自动提交状态下面的查询提交.
*
* @throws \PDOException
*
* @return void
*/
public function commit(): void
{
$this->connection->commit();
}
/**
* 事务回滚.
*
* @throws \PDOException
*
* @return void
*/
public function rollback(): void
{
$this->connection->rollback();
}
/**
* 启动XA事务
*
* @param string $xid XA事务id
*
* @return void
*/
public function startTransXa(string $xid): void
{
$this->connection->startTransXa($xid);
}
/**
* 预编译XA事务
*
* @param string $xid XA事务id
*
* @return void
*/
public function prepareXa(string $xid): void
{
$this->connection->prepareXa($xid);
}
/**
* 提交XA事务
*
* @param string $xid XA事务id
*
* @return void
*/
public function commitXa(string $xid): void
{
$this->connection->commitXa($xid);
}
/**
* 回滚XA事务
*
* @param string $xid XA事务id
*
* @return void
*/
public function rollbackXa(string $xid): void
{
$this->connection->rollbackXa($xid);
}
}

View File

@@ -0,0 +1,670 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\db\concern;
use Closure;
use think\db\BaseQuery;
use think\db\Raw;
trait WhereQuery
{
/**
* 指定AND查询条件.
*
* @param mixed $field 查询字段
* @param mixed $op 查询表达式
* @param mixed $condition 查询条件
*
* @return $this
*/
public function where($field, $op = null, $condition = null)
{
if ($field instanceof self) {
$this->parseQueryWhere($field);
return $this;
} elseif (true === $field || 1 === $field) {
$this->options['where']['AND'][] = true;
return $this;
} elseif (empty($field)) {
return $this;
}
$pk = $this->getPk();
if ((is_null($condition) || '=' == $op) && is_string($pk) && $pk == $field) {
$this->options['key'] = is_null($condition) ? $op : $condition;
}
$logic = 'AND';
$param = func_get_args();
array_shift($param);
if (is_array($field) && !empty($field) && array_is_list($field)) {
return $this->where(function ($query) use ($param, $condition, $op, $field, $logic) {
return $query->parseWhereExp($logic, $field, $op, $condition, $param);
});
}
return $this->parseWhereExp($logic, $field, $op, $condition, $param);
}
/**
* 解析Query对象查询条件.
*
* @param BaseQuery $query 查询对象
*
* @return void
*/
protected function parseQueryWhere(BaseQuery $query): void
{
$this->options['where'] = $query->getOption('where', []);
$via = $query->getOption('via');
if ($via) {
foreach ($this->options['where'] as $logic => &$where) {
foreach ($where as $key => &$val) {
if (is_array($val) && !str_contains($val[0], '.')) {
$val[0] = $via . '.' . $val[0];
}
}
}
}
$this->bind($query->getBind(false));
}
/**
* 指定OR查询条件.
*
* @param mixed $field 查询字段
* @param mixed $op 查询表达式
* @param mixed $condition 查询条件
*
* @return $this
*/
public function whereOr($field, $op = null, $condition = null)
{
$logic = 'OR';
$param = func_get_args();
array_shift($param);
if (is_array($field) && !empty($field) && array_is_list($field)) {
return $this->where(function ($query) use ($param, $condition, $op, $field, $logic) {
return $query->parseWhereExp($logic, $field, $op, $condition, $param);
});
}
return $this->parseWhereExp($logic, $field, $op, $condition, $param);
}
/**
* 指定XOR查询条件.
*
* @param mixed $field 查询字段
* @param mixed $op 查询表达式
* @param mixed $condition 查询条件
*
* @return $this
*/
public function whereXor($field, $op = null, $condition = null)
{
$logic = 'XOR';
$param = func_get_args();
array_shift($param);
if (is_array($field) && !empty($field) && array_is_list($field)) {
return $this->where(function ($query) use ($param, $condition, $op, $field, $logic) {
return $query->parseWhereExp($logic, $field, $op, $condition, $param);
});
}
return $this->parseWhereExp($logic, $field, $op, $condition, $param);
}
/**
* 指定Null查询条件.
*
* @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查询条件.
*
* @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查询条件.
*
* @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查询条件.
*
* @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查询条件.
*
* @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查询条件.
*
* @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查询条件.
*
* @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查询条件.
*
* @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查询条件.
*
* @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查询条件.
*
* @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查询条件.
*
* @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);
}
/**
* 指定json_contains查询条件.
*
* @param mixed $field 查询字段
* @param mixed $condition 查询条件
* @param string $logic 查询逻辑 and or xor
*
* @return $this
*/
public function whereJsonContains(string $field, $condition, string $logic = 'AND')
{
$value = is_null($condition) ? 'NULL' : '\'' . json_encode($condition) . '\'';
if (str_contains($field, '->')) {
[$field, $path] = explode('->', $field, 2);
return $this->whereRaw('json_contains(' . $field . ', ' . $value . ', \'$.'. str_replace('->', '.', $path) . '\')', [], $logic);
}
return $this->whereRaw('json_contains(' . $field . ', ' . $value . ')', [], $logic);
}
public function whereOrJsonContains(string $field, $condition)
{
return $this->whereJsonContains($field, $condition, 'OR');
}
/**
* 比较两个字段.
*
* @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);
}
/**
* 设置软删除字段及条件.
*
* @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;
}
/**
* 包含软删除数据.
*
* @return $this
*/
public function withTrashed()
{
if ($this->model) {
$this->options['soft_delete'] = null;
}
return $this;
}
/**
* 指定Exp查询条件.
*
* @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查询.
*
* @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;
}
/**
* 指定表达式查询条件.
*
* @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.
*
* @param string $where 查询条件
* @param array $bind 参数绑定
*
* @return $this
*/
public function whereOrRaw(string $where, array $bind = [])
{
return $this->whereRaw($where, $bind, 'OR');
}
/**
* 分析查询表达式.
*
* @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): self
{
$logic = strtoupper($logic);
// 字段映射
$map = $this->getOption('field_map', []);
if (is_string($field) && isset($map[$field])) {
$field = $map[$field];
}
// 处理 via
if (is_string($field) && !empty($this->options['via']) && !str_contains($field, '.') && !str_contains($field, '->')) {
$field = $this->options['via'] . '.' . $field;
}
// 严格模式查询
if ($strict) {
return $this->parseStrictWhere($field, $op, $condition, $logic);
}
// 处理批量查询
if (is_array($field)) {
return $this->parseArrayWhereItems($field, $logic);
}
// 处理闭包查询
if ($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;
}
protected function parseStrictWhere($field, $op, $condition, string $logic): self
{
if ('=' === $op) {
$where = $this->whereEq($field, $condition);
} else {
$where = [$field, $op, $condition, $logic];
}
$this->options['where'][$logic][] = $where;
return $this;
}
/**
* 分析查询表达式.
*
* @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);
return $param;
}
if (is_string($field) && strpos($field, '->')) {
[$relation, $attr] = explode('->', $field, 2);
$type = $this->getFieldType($relation);
if (is_null($type)) {
// 自动关联查询
$this->hasWhere($relation, [[$attr , is_null($condition) ? '=' : $op, $condition ?? $op]]);
return [];
}
}
if ($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);
}
return $where;
}
if (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;
}
/**
* 相等查询的主键处理.
*
* @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];
}
/**
* 数组批量查询.
*
* @param array $field 批量查询
* @param string $logic 查询逻辑 and or xor
*
* @return $this
*/
protected function parseArrayWhereItems(array $field, string $logic)
{
$where = [];
foreach ($field as $key => $val) {
if (is_int($key)) {
$where[] = $val;
} elseif ($val instanceof Raw) {
$where[] = [$key, 'exp', $val];
} else {
$where[] = is_null($val) ? [$key, 'NULL', ''] : [$key, is_array($val) ? 'IN' : '=', $val];
}
}
if (!empty($where)) {
$this->options['where'][$logic] = isset($this->options['where'][$logic]) ?
array_merge($this->options['where'][$logic], $where) : $where;
}
return $this;
}
/**
* 去除某个查询条件.
*
* @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;
}
/**
* 条件查询.
*
* @param mixed $condition 满足条件(支持闭包)
* @param Closure|array $query 满足条件后执行的查询表达式(闭包或数组)
* @param Closure|array $otherwise 不满足条件后执行
*
* @return $this
*/
public function when($condition, Closure | array $query, Closure | array | null $otherwise = null)
{
// 处理条件为 Closure 的情况
if ($condition instanceof Closure) {
$condition = $condition($this);
}
// 根据条件决定执行哪个查询
if ($condition) {
$this->executeQuery($query, $condition);
} elseif ($otherwise) {
$this->executeQuery($otherwise, $condition);
}
return $this;
}
protected function executeQuery(Closure | array $query, $condition): void
{
if ($query instanceof Closure) {
$query($this, $condition);
} elseif (is_array($query)) {
$this->where($query);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\connector;
use PDO;
use think\db\PDOConnection;
/**
* mysql数据库驱动.
*/
class Mysql extends PDOConnection
{
/**
* 解析pdo连接的dsn信息.
*
* @param array $config 连接信息
*
* @return string
*/
protected function parseDsn(array $config): string
{
if (!empty($config['socket'])) {
$dsn = 'mysql:unix_socket=' . $config['socket'];
} elseif (!empty($config['hostport'])) {
$dsn = 'mysql:host=' . $config['hostname'] . ';port=' . $config['hostport'];
} else {
$dsn = 'mysql:host=' . $config['hostname'];
}
$dsn .= ';dbname=' . $config['database'];
if (!empty($config['charset'])) {
$dsn .= ';charset=' . $config['charset'];
}
return $dsn;
}
/**
* 取得数据表的字段信息.
*
* @param string $tableName
*
* @return array
*/
public function getFields(string $tableName): array
{
[$tableName] = explode(' ', $tableName);
if (!str_contains($tableName, '`')) {
if (str_contains($tableName, '.')) {
$tableName = str_replace('.', '`.`', $tableName);
}
$tableName = '`' . $tableName . '`';
}
$sql = 'SHOW FULL COLUMNS FROM ' . $tableName;
$pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
if (!empty($result)) {
foreach ($result as $key => $val) {
$val = array_change_key_case($val);
$info[$val['field']] = [
'name' => $val['field'],
'type' => $val['type'],
'notnull' => 'NO' == $val['null'],
'default' => $val['default'],
'primary' => strtolower($val['key']) == 'pri',
'autoinc' => strtolower($val['extra']) == 'auto_increment',
'comment' => $val['comment'],
];
}
}
return $this->fieldCase($info);
}
/**
* 取得数据库的表信息.
*
* @param string $dbName
*
* @return array
*/
public function getTables(string $dbName = ''): array
{
$sql = !empty($dbName) ? 'SHOW TABLES FROM ' . $dbName : 'SHOW TABLES ';
$pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
foreach ($result as $key => $val) {
$info[$key] = current($val);
}
return $info;
}
protected function supportSavepoint(): bool
{
return true;
}
/**
* 启动XA事务
*
* @param string $xid XA事务id
*
* @return void
*/
public function startTransXa(string $xid): void
{
$this->initConnect(true);
$this->linkID->exec("XA START '$xid'");
}
/**
* 预编译XA事务
*
* @param string $xid XA事务id
*
* @return void
*/
public function prepareXa(string $xid): void
{
$this->initConnect(true);
$this->linkID->exec("XA END '$xid'");
$this->linkID->exec("XA PREPARE '$xid'");
}
/**
* 提交XA事务
*
* @param string $xid XA事务id
*
* @return void
*/
public function commitXa(string $xid): void
{
$this->initConnect(true);
$this->linkID->exec("XA COMMIT '$xid'");
}
/**
* 回滚XA事务
*
* @param string $xid XA事务id
*
* @return void
*/
public function rollbackXa(string $xid): void
{
$this->initConnect(true);
$this->linkID->exec("XA ROLLBACK '$xid'");
}
}

View File

@@ -0,0 +1,125 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\connector;
use PDO;
use think\db\BaseQuery;
use think\db\PDOConnection;
/**
* Oracle数据库驱动.
*/
class Oracle extends PDOConnection
{
/**
* 解析pdo连接的dsn信息.
*
* @param array $config 连接信息
*
* @return string
*/
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;
}
/**
* 取得数据表的字段信息.
*
* @param string $tableName
*
* @return array
*/
public function getFields(string $tableName): array
{
[$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);
$info[$val['column_name']] = [
'name' => $val['column_name'],
'type' => $val['data_type'],
'notnull' => $val['notnull'],
'default' => $val['data_default'],
'primary' => $val['pk'],
'autoinc' => $val['pk'],
];
}
}
return $this->fieldCase($info);
}
/**
* 取得数据库的表信息(暂时实现取得用户表信息).
*
* @param string $dbName
*
* @return array
*/
public function getTables(string $dbName = ''): array
{
$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.
*
* @param BaseQuery $query 查询对象
* @param string|null $sequence 自增序列名
*
* @return mixed
*/
public function getLastInsID(BaseQuery $query, ?string $sequence = null)
{
if (!is_null($sequence)) {
$pdo = $this->linkID->query("select {$sequence}.currval as id from dual");
$result = $pdo->fetchColumn();
}
return $result ?? null;
}
protected function supportSavepoint(): bool
{
return true;
}
}

View File

@@ -0,0 +1,113 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\connector;
use PDO;
use think\db\PDOConnection;
/**
* Pgsql数据库驱动.
*/
class Pgsql extends PDOConnection
{
/**
* 默认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连接的dsn信息.
*
* @param array $config 连接信息
*
* @return string
*/
protected function parseDsn(array $config): string
{
$dsn = 'pgsql:dbname=' . $config['database'] . ';host=' . $config['hostname'];
if (!empty($config['hostport'])) {
$dsn .= ';port=' . $config['hostport'];
}
return $dsn;
}
/**
* 取得数据表的字段信息.
*
* @param string $tableName
*
* @return array
*/
public function getFields(string $tableName): array
{
[$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",fields_comment as "comment" from table_msg(\'' . $tableName . '\');';
$pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
if (!empty($result)) {
foreach ($result as $key => $val) {
$val = array_change_key_case($val);
$info[$val['field']] = [
'name' => $val['field'],
'type' => $val['type'],
'notnull' => (bool) ('' !== $val['null']),
'default' => $val['default'],
'primary' => !empty($val['key']),
'autoinc' => str_starts_with((string) $val['extra'], 'nextval('),
'comment' => $val['comment'],
];
}
}
return $this->fieldCase($info);
}
/**
* 取得数据库的表信息.
*
* @param string $dbName
*
* @return array
*/
public function getTables(string $dbName = ''): array
{
$sql = "select tablename as Tables_in_test from pg_tables where schemaname ='public'";
$pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
foreach ($result as $key => $val) {
$info[$key] = current($val);
}
return $info;
}
protected function supportSavepoint(): bool
{
return true;
}
}

View File

@@ -0,0 +1,97 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace think\db\connector;
use PDO;
use think\db\PDOConnection;
/**
* Sqlite数据库驱动.
*/
class Sqlite extends PDOConnection
{
/**
* 解析pdo连接的dsn信息.
*
* @param array $config 连接信息
*
* @return string
*/
protected function parseDsn(array $config): string
{
return 'sqlite:' . $config['database'];
}
/**
* 取得数据表的字段信息.
*
* @param string $tableName
*
* @return array
*/
public function getFields(string $tableName): array
{
[$tableName] = explode(' ', $tableName);
$sql = 'PRAGMA table_info( \'' . $tableName . '\' )';
$pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
if (!empty($result)) {
foreach ($result as $key => $val) {
$val = array_change_key_case($val);
$info[$val['name']] = [
'name' => $val['name'],
'type' => $val['type'],
'notnull' => 1 === $val['notnull'],
'default' => $val['dflt_value'],
'primary' => '1' == $val['pk'],
'autoinc' => '1' == $val['pk'],
];
}
}
return $this->fieldCase($info);
}
/**
* 取得数据库的表信息.
*
* @param string $dbName
*
* @return array
*/
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->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
foreach ($result as $key => $val) {
$info[$key] = current($val);
}
return $info;
}
protected function supportSavepoint(): bool
{
return true;
}
}

View File

@@ -0,0 +1,131 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2012 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\connector;
use PDO;
use think\db\PDOConnection;
/**
* Sqlsrv数据库驱动.
*/
class Sqlsrv extends PDOConnection
{
/**
* 默认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连接的dsn信息.
*
* @param array $config 连接信息
*
* @return string
*/
protected function parseDsn(array $config): string
{
$dsn = 'sqlsrv:Database=' . $config['database'] . ';Server=' . $config['hostname'];
if (!empty($config['hostport'])) {
$dsn .= ',' . $config['hostport'];
}
if (!empty($config['trust_server_certificate'])) {
$dsn .= ';TrustServerCertificate=' . $config['trust_server_certificate'];
}
return $dsn;
}
/**
* 取得数据表的字段信息.
*
* @param string $tableName
*
* @return array
*/
public function getFields(string $tableName): array
{
[$tableName] = explode(' ', $tableName);
str_contains($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
ON t.table_catalog = c.table_catalog
AND t.table_schema = c.table_schema
AND t.table_name = c.table_name
WHERE t.table_name = '$tableName'";
$pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
if (!empty($result)) {
foreach ($result as $key => $val) {
$val = array_change_key_case($val);
$info[$val['column_name']] = [
'name' => $val['column_name'],
'type' => $val['data_type'],
'notnull' => (bool) ('' === $val['is_nullable']), // not null is empty, null is yes
'default' => $val['column_default'],
'primary' => false,
'autoinc' => false,
];
}
}
$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) {
$info[$result['column_name']]['primary'] = true;
}
return $this->fieldCase($info);
}
/**
* 取得数据表的字段信息.
*
* @param string $dbName
*
* @return array
*/
public function getTables(string $dbName = ''): array
{
$sql = "SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
";
$pdo = $this->getPDOStatement($sql);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
$info = [];
foreach ($result as $key => $val) {
$info[$key] = current($val);
}
return $info;
}
}

View File

@@ -0,0 +1,117 @@
CREATE OR REPLACE FUNCTION pgsql_type(a_type varchar) RETURNS varchar AS
$BODY$
DECLARE
v_type varchar;
BEGIN
IF a_type='int8' THEN
v_type:='bigint';
ELSIF a_type='int4' THEN
v_type:='integer';
ELSIF a_type='int2' THEN
v_type:='smallint';
ELSIF a_type='bpchar' THEN
v_type:='char';
ELSE
v_type:=a_type;
END IF;
RETURN v_type;
END;
$BODY$
LANGUAGE PLPGSQL;
CREATE TYPE "public"."tablestruct" AS (
"fields_key_name" varchar(100),
"fields_name" VARCHAR(200),
"fields_type" VARCHAR(20),
"fields_length" BIGINT,
"fields_not_null" VARCHAR(10),
"fields_default" VARCHAR(500),
"fields_comment" VARCHAR(1000)
);
CREATE OR REPLACE FUNCTION "public"."table_msg" (a_schema_name varchar, a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS
$body$
DECLARE
v_ret tablestruct;
v_oid oid;
v_sql varchar;
v_rec RECORD;
v_key varchar;
BEGIN
SELECT
pg_class.oid INTO v_oid
FROM
pg_class
INNER JOIN pg_namespace ON (pg_class.relnamespace = pg_namespace.oid AND lower(pg_namespace.nspname) = a_schema_name)
WHERE
pg_class.relname=a_table_name;
IF NOT FOUND THEN
RETURN;
END IF;
v_sql='
SELECT
pg_attribute.attname AS fields_name,
pg_attribute.attnum AS fields_index,
pgsql_type(pg_type.typname::varchar) AS fields_type,
pg_attribute.atttypmod-4 as fields_length,
CASE WHEN pg_attribute.attnotnull THEN ''not null''
ELSE ''''
END AS fields_not_null,
pg_attrdef.adsrc AS fields_default,
pg_description.description AS fields_comment
FROM
pg_attribute
INNER JOIN pg_class ON pg_attribute.attrelid = pg_class.oid
INNER JOIN pg_type ON pg_attribute.atttypid = pg_type.oid
LEFT OUTER JOIN pg_attrdef ON pg_attrdef.adrelid = pg_class.oid AND pg_attrdef.adnum = pg_attribute.attnum
LEFT OUTER JOIN pg_description ON pg_description.objoid = pg_class.oid AND pg_description.objsubid = pg_attribute.attnum
WHERE
pg_attribute.attnum > 0
AND attisdropped <> ''t''
AND pg_class.oid = ' || v_oid || '
ORDER BY pg_attribute.attnum' ;
FOR v_rec IN EXECUTE v_sql LOOP
v_ret.fields_name=v_rec.fields_name;
v_ret.fields_type=v_rec.fields_type;
IF v_rec.fields_length > 0 THEN
v_ret.fields_length:=v_rec.fields_length;
ELSE
v_ret.fields_length:=NULL;
END IF;
v_ret.fields_not_null=v_rec.fields_not_null;
v_ret.fields_default=v_rec.fields_default;
v_ret.fields_comment=v_rec.fields_comment;
SELECT constraint_name INTO v_key FROM information_schema.key_column_usage WHERE table_schema=a_schema_name AND table_name=a_table_name AND column_name=v_rec.fields_name;
IF FOUND THEN
v_ret.fields_key_name=v_key;
ELSE
v_ret.fields_key_name='';
END IF;
RETURN NEXT v_ret;
END LOOP;
RETURN ;
END;
$body$
LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER;
COMMENT ON FUNCTION "public"."table_msg"(a_schema_name varchar, a_table_name varchar)
IS '获得表信息';
---
CREATE OR REPLACE FUNCTION "public"."table_msg" (a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS
$body$
DECLARE
v_ret tablestruct;
BEGIN
FOR v_ret IN SELECT * FROM table_msg('public',a_table_name) LOOP
RETURN NEXT v_ret;
END LOOP;
RETURN;
END;
$body$
LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER;
COMMENT ON FUNCTION "public"."table_msg"(a_table_name varchar)
IS '获得表信息';

View File

@@ -0,0 +1,117 @@
CREATE OR REPLACE FUNCTION pgsql_type(a_type varchar) RETURNS varchar AS
$BODY$
DECLARE
v_type varchar;
BEGIN
IF a_type='int8' THEN
v_type:='bigint';
ELSIF a_type='int4' THEN
v_type:='integer';
ELSIF a_type='int2' THEN
v_type:='smallint';
ELSIF a_type='bpchar' THEN
v_type:='char';
ELSE
v_type:=a_type;
END IF;
RETURN v_type;
END;
$BODY$
LANGUAGE PLPGSQL;
CREATE TYPE "public"."tablestruct" AS (
"fields_key_name" varchar(100),
"fields_name" VARCHAR(200),
"fields_type" VARCHAR(20),
"fields_length" BIGINT,
"fields_not_null" VARCHAR(10),
"fields_default" VARCHAR(500),
"fields_comment" VARCHAR(1000)
);
CREATE OR REPLACE FUNCTION "public"."table_msg" (a_schema_name varchar, a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS
$body$
DECLARE
v_ret tablestruct;
v_oid oid;
v_sql varchar;
v_rec RECORD;
v_key varchar;
BEGIN
SELECT
pg_class.oid INTO v_oid
FROM
pg_class
INNER JOIN pg_namespace ON (pg_class.relnamespace = pg_namespace.oid AND lower(pg_namespace.nspname) = a_schema_name)
WHERE
pg_class.relname=a_table_name;
IF NOT FOUND THEN
RETURN;
END IF;
v_sql='
SELECT
pg_attribute.attname AS fields_name,
pg_attribute.attnum AS fields_index,
pgsql_type(pg_type.typname::varchar) AS fields_type,
pg_attribute.atttypmod-4 as fields_length,
CASE WHEN pg_attribute.attnotnull THEN ''not null''
ELSE ''''
END AS fields_not_null,
pg_get_expr(pg_attrdef.adbin, pg_attrdef.adrelid) AS fields_default,
pg_description.description AS fields_comment
FROM
pg_attribute
INNER JOIN pg_class ON pg_attribute.attrelid = pg_class.oid
INNER JOIN pg_type ON pg_attribute.atttypid = pg_type.oid
LEFT OUTER JOIN pg_attrdef ON pg_attrdef.adrelid = pg_class.oid AND pg_attrdef.adnum = pg_attribute.attnum
LEFT OUTER JOIN pg_description ON pg_description.objoid = pg_class.oid AND pg_description.objsubid = pg_attribute.attnum
WHERE
pg_attribute.attnum > 0
AND attisdropped <> ''t''
AND pg_class.oid = ' || v_oid || '
ORDER BY pg_attribute.attnum' ;
FOR v_rec IN EXECUTE v_sql LOOP
v_ret.fields_name=v_rec.fields_name;
v_ret.fields_type=v_rec.fields_type;
IF v_rec.fields_length > 0 THEN
v_ret.fields_length:=v_rec.fields_length;
ELSE
v_ret.fields_length:=NULL;
END IF;
v_ret.fields_not_null=v_rec.fields_not_null;
v_ret.fields_default=v_rec.fields_default;
v_ret.fields_comment=v_rec.fields_comment;
SELECT constraint_name INTO v_key FROM information_schema.key_column_usage WHERE table_schema=a_schema_name AND table_name=a_table_name AND column_name=v_rec.fields_name;
IF FOUND THEN
v_ret.fields_key_name=v_key;
ELSE
v_ret.fields_key_name='';
END IF;
RETURN NEXT v_ret;
END LOOP;
RETURN ;
END;
$body$
LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER;
COMMENT ON FUNCTION "public"."table_msg"(a_schema_name varchar, a_table_name varchar)
IS '获得表信息';
---
CREATE OR REPLACE FUNCTION "public"."table_msg" (a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS
$body$
DECLARE
v_ret tablestruct;
BEGIN
FOR v_ret IN SELECT * FROM table_msg('public',a_table_name) LOOP
RETURN NEXT v_ret;
END LOOP;
RETURN;
END;
$body$
LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER;
COMMENT ON FUNCTION "public"."table_msg"(a_table_name varchar)
IS '获得表信息';

View File

@@ -0,0 +1,35 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿 <zuojiazi@vip.qq.com> <http://zjzit.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\exception;
/**
* PDO参数绑定异常.
*/
class BindParamException extends DbException
{
/**
* BindParamException constructor.
*
* @param string $message
* @param array $config
* @param string $sql
* @param array $bind
* @param int $code
*/
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);
}
}

View File

@@ -0,0 +1,44 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿 <zuojiazi@vip.qq.com> <http://zjzit.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\exception;
class DataNotFoundException extends DbException
{
protected $table;
/**
* DbException constructor.
*
* @param string $message
* @param string $table
* @param array $config
*/
public function __construct(string $message, string $table = '', array $config = [])
{
$this->message = $message;
$this->table = $table;
$this->setData('Database Config', $config);
}
/**
* 获取数据表名.
*
* @return string
*/
public function getTable()
{
return $this->table;
}
}

View File

@@ -0,0 +1,21 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
declare(strict_types = 1);
namespace think\db\exception;
/**
* Db事件异常.
*/
class DbEventException extends DbException
{
}

View File

@@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿 <zuojiazi@vip.qq.com> <http://zjzit.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\exception;
use think\Exception;
/**
* Database相关异常处理类.
*/
class DbException extends Exception
{
/**
* DbException constructor.
*
* @param string $message
* @param array $config
* @param string $sql
* @param int $code
*/
public function __construct(string $message, array $config = [], string $sql = '', int $code = 10500)
{
$this->message = $message;
$this->code = $code;
$this->setData('Database Status', [
'Error Code' => $code,
'Error Message' => $message,
'Error SQL' => $sql,
]);
unset($config['username'], $config['password']);
$this->setData('Database Config', $config);
}
}

View File

@@ -0,0 +1,21 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿 <zuojiazi@vip.qq.com> <http://zjzit.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\exception;
/**
* Duplicate异常处理类
*/
class DuplicateException extends PDOException
{
}

View File

@@ -0,0 +1,23 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\exception;
use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInvalidArgumentInterface;
/**
* 非法数据异常.
*/
class InvalidArgumentException extends \InvalidArgumentException implements SimpleCacheInvalidArgumentInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
declare(strict_types = 1);
namespace think\db\exception;
/**
* 模型事件异常.
*/
class ModelEventException extends DbException
{
}

View File

@@ -0,0 +1,44 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿 <zuojiazi@vip.qq.com> <http://zjzit.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\exception;
class ModelNotFoundException extends DbException
{
protected $model;
/**
* 构造方法.
*
* @param string $message
* @param string $model
* @param array $config
*/
public function __construct(string $message, string $model = '', array $config = [])
{
$this->message = $message;
$this->model = $model;
$this->setData('Database Config', $config);
}
/**
* 获取模型类名.
*
* @return string
*/
public function getModel()
{
return $this->model;
}
}

View File

@@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿 <zuojiazi@vip.qq.com> <http://zjzit.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\db\exception;
/**
* PDO异常处理类
* 重新封装了系统的\PDOException类.
*/
class PDOException extends DbException
{
/**
* PDOException constructor.
*
* @param \PDOException $exception
* @param array $config
* @param string $sql
* @param int $code
*/
public function __construct(\PDOException $exception, array $config = [], string $sql = '', int $code = 10501)
{
$error = $exception->errorInfo;
$message = $exception->getMessage();
if (!empty($error)) {
$this->setData('PDO Error Info', [
'SQLSTATE' => $error[0],
'Driver Error Code' => $error[1] ?? 0,
'Driver Error Message' => $error[2] ?? '',
]);
}
parent::__construct($message, $config, $sql, $code);
}
}

View File

@@ -0,0 +1,32 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace think\facade;
use think\Facade;
/**
* @see \think\DbManager
* @mixin \think\DbManager
*/
class Db extends Facade
{
/**
* 获取当前Facade对应类名或者已经绑定的容器对象标识.
*
* @return string
*/
protected static function getFacadeClass()
{
return 'think\DbManager';
}
}

View File

@@ -0,0 +1,52 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
//------------------------
// ThinkORM 助手函数
//-------------------------
use think\db\BaseQuery as Query;
use think\db\Express;
use think\db\Raw;
use think\facade\Db;
if (!function_exists('db')) {
function db(string $name, string|array|null $connect = null): Query
{
if ($connect) {
return Db::connect($connect)->name($name);
}
return Db::name($name);
}
}
if (!function_exists('raw')) {
function raw(string $value, array $bind = []): Raw
{
return new Raw($value, $bind);
}
}
if (!function_exists('inc')) {
function inc(float|int $step = 1, int $lazyTime = 0): Express
{
return new Express('+', $step, $lazyTime);
}
}
if (!function_exists('dec')) {
function dec(float|int $step = 1, int $lazyTime = 0): Express
{
return new Express('-', $step, $lazyTime);
}
}

View File

@@ -0,0 +1,284 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 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\contract\Modelable as Model;
use think\Paginator;
/**
* 模型数据集类.
*
* @template TKey of array-key
* @template TModel of \think\Model
*
* @extends BaseCollection<TKey, TModel>
*/
class Collection extends BaseCollection
{
/**
* 延迟预载入关联查询.
*
* @param array $relation 关联
* @param mixed $cache 关联缓存
*
* @return $this
*/
public function load(array $relation, $cache = false)
{
if (!$this->isEmpty()) {
$item = current($this->items);
$item->eagerlyResultSet($this->items, $relation, [], false, $cache);
}
return $this;
}
/**
* 删除数据集的数据.
*
* @return bool
*/
public function delete(): bool
{
$this->each(function (Model $model) {
$model->delete();
});
return true;
}
/**
* 更新数据.
*
* @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;
}
/**
* 设置需要隐藏的输出属性.
*
* @param array $hidden 属性列表
* @param bool $merge 是否合并
*
* @return $this
*/
public function hidden(array $hidden, bool $merge = false)
{
$this->each(function (Model $model) use ($hidden, $merge) {
$model->hidden($hidden, $merge);
});
return $this;
}
/**
* 设置需要输出的属性.
*
* @param array $visible
* @param bool $merge 是否合并
*
* @return $this
*/
public function visible(array $visible, bool $merge = false)
{
$this->each(function (Model $model) use ($visible, $merge) {
$model->visible($visible, $merge);
});
return $this;
}
/**
* 设置需要追加的输出属性.
*
* @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;
}
/**
* 设置属性映射.
*
* @param array $mapping 属性映射
*
* @return $this
*/
public function mapping(array $mapping)
{
$this->each(function (Model $model) use ($mapping) {
$model->mapping($mapping);
});
return $this;
}
/**
* 设置模型输出场景.
*
* @param string $scene 场景名称
*
* @return $this
*/
public function scene(string $scene)
{
$this->each(function (Model $model) use ($scene) {
$model->scene($scene);
});
return $this;
}
/**
* 设置数据字段获取器.
*
* @param string|array $name 字段名
* @param callable $callback 闭包获取器
*
* @return $this
*/
public function withAttr(string|array $name, ?callable $callback = null)
{
$this->each(function (Model $model) use ($name, $callback) {
$model->withFieldAttr($name, $callback);
});
return $this;
}
/**
* 绑定(一对一)关联属性到当前模型.
*
* @param string $relation 关联名称
* @param array $attrs 绑定属性
*
* @throws Exception
*
* @return $this
*/
public function bindAttr(string $relation, array $attrs = [])
{
$this->each(function (Model $model) use ($relation, $attrs) {
$model->bindAttr($relation, $attrs);
});
return $this;
}
/**
* 按指定键整理数据.
*
* @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;
}
/**
* 比较数据集,返回差集.
*
* @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);
}
/**
* 比较数据集,返回交集.
*
* @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);
}
}

View File

@@ -0,0 +1,77 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\model;
use think\Model;
/**
* 多对多中间表模型类.
*/
class Pivot extends Model
{
/**
* 父模型.
*
* @var Model
*/
public $parent;
protected $pivotName;
/**
* 是否时间自动写入.
*
* @var bool
*/
protected $autoWriteTimestamp = false;
/**
* 架构函数.
*
* @param array $data 数据
* @param Model|null $parent 上级模型
* @param string $table 中间数据表名
*/
public function __construct(array $data = [], ?Model $parent = null, string $table = '')
{
$this->pivotName = $table;
$this->parent = $parent;
parent::__construct($data);
}
/**
* 初始化模型.
*
* @return void
*/
protected function init()
{
if (is_null($this->getOption('name'))) {
$this->setOption('name', $this->pivotName);
}
}
/**
* 创建新的模型实例.
*
* @param array|object $data 数据
* @param array $options
*
* @return Model
*/
public function newInstance(array | object $data = [], array $options = [])
{
$this->data($data);
return $this->clone();
}
}

View File

@@ -0,0 +1,354 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model;
use Closure;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use think\model\Collection;
use think\model\contract\Modelable as Model;
/**
* 模型关联基础类.
*
* @mixin Query
*/
abstract class Relation
{
/**
* 父模型对象
*
* @var Model
*/
protected $parent;
/**
* 当前关联的模型类名.
*
* @var string
*/
protected $model;
/**
* 关联模型查询对象
*
* @var Query
*/
protected $query;
/**
* 关联表外键.
*
* @var string
*/
protected $foreignKey;
/**
* 关联表主键.
*
* @var string
*/
protected $localKey;
/**
* 是否执行关联基础查询.
*
* @var bool
*/
protected $baseQuery;
/**
* 是否为自关联.
*
* @var bool
*/
protected $selfRelation = false;
/**
* 关联数据字段限制.
*
* @var array
*/
protected $withField;
/**
* 排除关联数据字段.
*
* @var array
*/
protected $withoutField;
/**
* 默认数据.
*
* @var mixed
*/
protected $default;
/**
* 获取一条关联数据.
*
* @var bool
*/
protected $isOneofMany = false;
/**
* 获取关联的所属模型.
*
* @return Model
*/
public function getParent(): Model
{
return $this->parent;
}
/**
* 获取当前的关联模型类的Query实例.
*
* @return Query
*/
public function getQuery()
{
return $this->query;
}
/**
* 获取关联表外键.
*
* @return string
*/
public function getForeignKey(): string
{
return $this->foreignKey;
}
/**
* 获取关联表主键.
*
* @return string
*/
public function getLocalKey(): string
{
return $this->localKey;
}
/**
* 获取当前的关联模型类的实例.
*
* @return Model
*/
public function getModel(): Model
{
return $this->query->getModel();
}
/**
* 当前关联是否为自关联.
*
* @return bool
*/
public function isSelfRelation(): bool
{
return $this->selfRelation;
}
/**
* 封装关联数据集.
*
* @param array $resultSet 数据集
*
* @param array $resultSet 关联数据结果集
* @return Collection 返回模型集合对象
*/
protected function resultSetBuild(array $resultSet)
{
return (new $this->model())->toCollection($resultSet);
}
/**
* 获取关联查询的字段
*
* 根据模型名称处理查询字段
*
* @param string $model 模型名称
* @return mixed 返回处理后的查询字段
*/
protected function getQueryFields(string $model)
{
$fields = $this->query->getOption('field');
$this->query->removeOption('field');
return $this->getRelationQueryFields($fields, $model);
}
/**
* 获取关联查询的字段
*
* 处理关联查询的字段,添加表名前缀
*
* @param mixed $fields 字段定义
* @param string $model 模型名称
* @return mixed 返回处理后的查询字段
*/
protected function getRelationQueryFields($fields, string $model)
{
if (empty($fields) || '*' == $fields) {
return $model . '.*';
}
if (is_string($fields)) {
$fields = explode(',', $fields);
}
foreach ($fields as &$field) {
if (!str_contains($field, '.')) {
$field = $model . '.' . $field;
}
}
return $fields;
}
/**
* 处理关联查询条件
*
* 为查询条件添加关联表前缀
*
* @param array &$where 查询条件
* @param string $relation 关联表名
* @return void
*/
protected function getQueryWhere(array &$where, string $relation): void
{
if (array_is_list($where) && isset($where[0]) && is_string($where[0])) {
$where = [ $where ];
}
foreach ($where as $key => &$val) {
if (is_string($key)) {
$where[] = [!str_contains($key, '.') ? $relation . '.' . $key : $key, '=', $val];
unset($where[$key]);
} elseif (is_array($val) && isset($val[0]) && !str_contains($val[0], '.')) {
$val[0] = $relation . '.' . $val[0];
}
}
}
/**
* 获取关联数据默认值
*
* @param mixed $data 模型数据
*
* @return mixed
*/
protected function getDefaultModel($data)
{
if (is_array($data)) {
$model = new $this->model($data);
} elseif ($data instanceof Closure) {
$model = new $this->model();
$data($model);
} else {
$model = $data;
}
return $model;
}
/**
* 处理关联查询及软删除的关联查询
*
* @param Query $query 查询对象
* @param string $relation 关联名
* @param mixed $where 查询条件
* @param string $logic 查询逻辑
* @return Query 返回查询对象
*/
protected function getRelationSoftDelete(Query $query, $relation, $where = null, $logic = '')
{
if ($where) {
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;
}
$whereLogic = 'OR' == $logic ? 'whereOr' : 'where';
$query->$whereLogic(function ($query) use ($where) {
$query->where($where);
});
}
// 启用软删除则增加软删除条件
$softDelete = $this->query->getOption('soft_delete');
return $query->when($softDelete, function ($query) use ($softDelete, $relation) {
$query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
});
}
/**
* 获取关联的最新一条数据.
*
* @param string $field 排序字段
*
* @return $this
*/
public function first(string $field = '')
{
$field = $field ?: $this->query->getPk();
$this->query->order($field, 'desc');
$this->isOneofMany = true;
return $this;
}
/**
* 获取关联的最旧一条数据.
*
* @param string $field 排序字段
*
* @return $this
*/
public function last(string $field = '')
{
$field = $field ?: $this->query->getPk();
$this->query->order($field, 'asc');
$this->isOneofMany = true;
return $this;
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
}
public function __call($method, $args)
{
if ($this->query) {
// 执行基础查询
$this->baseQuery();
$result = call_user_func_array([$this->query, $method], $args);
return $result === $this->query ? $this : $result;
}
throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
}
}

View File

@@ -0,0 +1,744 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model;
use ReflectionClass;
use ReflectionProperty;
use think\Entity;
use think\exception\ValidateException;
use think\helper\Str;
use think\Model;
use think\model\Collection;
use think\model\contract\Modelable;
/**
* 视图模型
*/
abstract class View extends Entity
{
/**
* 架构函数.
*
* @param Model $model 模型连接对象
*/
public function __construct(?Model $model = null)
{
parent::__construct($model);
// 初始化模型数据
$this->initData();
}
/**
* 初始化实体数据属性.
*
* @return void
*/
protected function initData()
{
// 获取属性映射关系
$properties = $this->getEntityPropertiesMap();
$data = $this->model()->getData();
if (empty($data)) {
return ;
}
foreach ($properties as $key => $field) {
if (is_int($key)) {
// 主模型同名属性
$this->$field = $this->fetchViewAttr($field, $data);
} elseif (strpos($field, '->')) {
// 关联属性或JSON字段映射
$this->$key = $this->getRelationMapAttr($field, $data);
} else {
// 主模型属性映射
$this->$key = $this->fetchViewAttr($field, $data);
}
}
// 标记数据存在
$this->exists(true);
}
/**
* 获取关联或JSON字段映射的属性值.
*
* @param string $field 视图属性
* @param array $data 模型数据
*
* @return mixed
*/
private function getRelationMapAttr(string $field, array $data)
{
$items = explode('->', $field);
$relation = array_shift($items);
if (isset($data[$relation])) {
$value = $this->model()->$relation;
foreach ($items as $item) {
if (is_array($value)) {
$value = $value[$item] ?? null;
} elseif (is_object($value)) {
$value = $value->$item ?? null;
}
}
}
return $value ?? null;
}
/**
* 获取视图属性值(支持视图获取器).
*
* @param string $field 视图属性
* @param array $data 模型数据
*
* @return mixed
*/
private function fetchViewAttr(string $field, array $data)
{
$method = 'get' . Str::camel($field) . 'Attr';
$model = $this->model();
if (method_exists($this, $method)) {
// 视图获取器
$value = $this->$method($model);
} elseif ($model->hasData($field)) {
// 获取主模型数据(支持获取器)
$value = $model->$field;
} else {
// 获取自动映射的属性数据
$value = $this->getAutoRelationValue($field, $data);
}
return $value;
}
/**
* 获取autoMapping自动映射的视图属性值.
*
* @param string $field 视图属性
* @param array $data 模型数据
*
* @return mixed
*/
private function getAutoRelationValue(string $field, array $data)
{
$relations = $this->getOption('autoMapping', []);
if ($relations) {
$mapping = $this->getOption('viewMapping', []);
foreach ($relations as $relation) {
if (isset($data[$relation]) && $this->model()->$relation->hasData($field)) {
$value = $this->model()->$relation->$field;
if (!isset($mapping[$field])) {
$mapping[$field] = $relation . '->' . $field;
}
break;
}
}
$this->setOption('viewMapping', $mapping);
}
return $value ?? null;
}
/**
* 获取实体属性列表.
*
* @return array
*/
private function getEntityProperties(): array
{
$reflection = new ReflectionClass($this);
$properties = [];
foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
$properties[] = $property->getName();
}
return $properties;
}
/**
* 获取包含映射关系的实体属性列表.
*
* @return array
*/
private function getEntityPropertiesMap(): array
{
$properties = $this->getOption('viewProperties');
if (empty($properties)) {
// 获取实体属性列表
$fields = $this->getEntityProperties();
// 获取属性映射列表
$mapping = $this->getOption('viewMapping', []);
$relations = $this->getOption('autoMapping', []);
$properties = [];
foreach ($fields as $field) {
if (isset($mapping[$field])) {
// 映射属性
$properties[$field] = $mapping[$field];
if (strpos($mapping[$field], '->')) {
$relation = strstr($mapping[$field], '->', true);
if (!$this->model()->getFieldType($relation)) {
$relations[] = $relation;
}
}
} else {
// 主模型同名属性
$properties[] = $field;
}
}
$this->setOption('autoRelation', array_unique($relations));
$this->setOption('viewProperties', $properties);
}
return $properties;
}
/**
* 解析autoMapping的字段映射
*
* @return array
*/
protected function parseAutoMapping(): array
{
$fields = $this->getEntityProperties();
$mapping = $this->getOption('viewMapping', []);
$relations = $this->getOption('autoMapping', []);
if ($relations) {
array_unshift($relations, $this->model());
foreach ($fields as $field) {
if (isset($mapping[$field])) {
continue;
}
foreach ($relations as $relation) {
if (is_object($relation) && $relation->getFieldType($field)) {
break;
} elseif (is_string($relation) && !strpos($relation, '.') && $this->model()->$relation()->getFieldType($field)) {
$mapping[$field] = $relation . '->' . $field;
break;
}
}
}
$this->setOption('viewMapping', $mapping);
}
return $mapping;
}
/**
* 转换为数组. 视图模型不支持 hidden visible append
*
* @return array
*/
public function toArray(): array
{
$data = $this->getData();
foreach ($data as $name => &$val) {
if ($val instanceof Modelable || $val instanceof Collection) {
$val = $val->toArray();
}
}
return $data;
}
/**
* 设置视图模型数据
*
* @param array|object $data 数据
* @param mixed $validate 是否验证数据
* @return $this
*/
public function data(array | object $data, $validate = false)
{
// 处理对象数据
if (is_object($data)) {
$data = get_object_vars($data);
}
foreach ($this->getEntityProperties() as $field) {
$this->$field = $data[$field] ?? null;
}
// 验证数据
if ($validate) {
if (!is_bool($validate)) {
// 指定验证场景
$this->scene($validate);
}
$this->validate();
}
return $this;
}
/**
* 刷新模型数据.
*
* @return $this
*/
public function refresh()
{
$this->initData();
return $this;
}
/**
* 清空视图模型数据
*
* @return $this
*/
public function clear()
{
foreach ($this->getEntityProperties() as $field) {
$this->$field = null;
}
$this->exists(false);
return $this;
}
/**
* 获取视图模型数据
*
* @return array
*/
protected function getData(): array
{
$data = [];
foreach ($this->getEntityProperties() as $field) {
$data[$field] = $this->$field;
}
return $data;
}
/**
* 判断数据是否为空.
*
* @return bool
*/
public function isEmpty(): bool
{
return $this->model()->isEmpty();
}
/**
* 获取克隆的模型实例.
*
* @return static
*/
public function clone()
{
$model = new static();
return $model->setModel($this->model());
}
/**
* 设置模型.
*
* @param Model $model 模型对象
* @return $this
*/
public function setModel(Model $model)
{
parent::setModel($model);
$this->initData();
return $this;
}
/**
* 模型数据转Json.
*
* @param int $options json参数
* @return string
*/
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $options);
}
// JsonSerializable
public function jsonSerialize(): array
{
return $this->toArray();
}
public function __toString()
{
return $this->toJson();
}
/**
* 设置关联数据.
*
* @param string $relation 关联属性
* @param Model $model 关联数据
*
* @return void
*/
public function setRelation($relation, $model)
{
$this->model()->setRelation($relation, $model);
}
/**
* 设置关联绑定数据
*
* @param Model $model 关联对象
* @param array $bind 绑定属性
* @param string $relation 关联名称
* @return void
*/
public function bindRelationAttr($model, $bind, $relation)
{
if ($relation) {
$this->setRelation($relation, $model);
}
}
/**
* 视图模型数据转换为模型数据(用于写入 暂不支持子关联写入).
*
* @return array
*/
private function convertData(): array
{
// 获取属性映射
$properties = $this->getEntityPropertiesMap();
$data = $this->getData();
$item = [];
$together = [];
$array = [];
foreach ($properties as $key => $field) {
if (strpos($field, '->')) {
if (!isset($data[$key]) || substr_count($field, '->') > 1) {
// 排除空值 以及 多级关联属性值
continue;
}
[$relation, $field] = explode('->', $field);
if ('json' == $this->model()->getFieldType($relation)) {
// JSON数据赋值
$array[$relation][$field] = $data[$key];
} else {
// 关联数据赋值
$together[] = $relation;
if ($this->model()->hasData($relation)) {
// 关联更新
$this->model()->$relation->$field = $data[$key];
} else {
// 新增关联
$array[$relation][$field] = $data[$key];
}
}
} else {
$value = $data[is_int($key) ? $field : $key];
if (isset($value)) {
$item[$field] = $value;
}
}
}
// 关联数据或JSON数据封装
foreach ($array as $relation => $val) {
$this->model()->$relation = $val;
}
if (!empty($together)) {
// 自动关联写入
$this->model()->together(array_unique($together));
}
return $item;
}
/**
* 设置验证场景.
*
* @param string|array $scene 场景名或数组
* @return $this
*/
public function scene(string|array $scene)
{
return $this->setOption('scene', $scene);
}
/**
* 验证视图模型数据.
*
* @throws ValidateException
* @return bool
*/
private function validate(): bool
{
$validater = $this->getOption('validate');
if (!empty($validater) && !$this->getOption('dataHasValidate', false)) {
$data = $this->getData();
$result = validate($validater)
->scene($this->getOption('scene') ?: array_keys($data))
->check($data);
if ($result) {
$this->setOption('dataHasValidate', true);
}
return $result;
}
return true;
}
/**
* 设置数据是否存在.
*
* @param bool $exists
*
* @return $this
*/
public function exists(bool $exists = true)
{
return $this->setOption('exists', $exists);
}
/**
* 判断数据是否存在数据库.
*
* @return bool
*/
public function isExists(): bool
{
return $this->getOption('exists', false);
}
/**
* 保存模型实例数据.
*
* @param array|object $data 数据
* @param mixed $where 更新条件 true为强制新增
* @param bool $refresh 是否刷新数据
* @return bool
*/
public function save(array | object $data = [], $where = [], bool $refresh = false): bool
{
if ($data) {
$this->data($data);
}
// 验证数据
$this->validate();
// 根据映射关系转换为实际模型数据
$data = $this->convertData();
// 处理自动时间字段数据
foreach ($this->model()->getAutoTimeFields() as $field) {
unset($data[$field]);
}
$result = $this->model()
->exists($this->isExists())
->save($data, $where, $refresh);
if ($result) {
// 刷新数据
$this->refresh();
}
return $result;
}
/**
* 删除模型数据.
*
* @return bool
*/
public function delete(): bool
{
if ($this->model()->delete()) {
$this->clear();
return true;
}
return false;
}
/**
* 写入数据.
*
* @param array|object $data 数据
* @return static
*/
public static function create(array | object $data)
{
$entity = new static();
$entity->exists(false)->save($data, true);
return $entity;
}
/**
* 更新数据.
*
* @param array|object $data 数据
* @param mixed $where 更新条件
* @return static
*/
public static function update(array | object $data, $where = [])
{
$entity = new static();
$entity->exists(true)->save($data, $where, true);
return $entity;
}
/**
* 数据集写入
*
* @param iterable $dataSet 数据集
* @param bool $replace 是否replace
*
* @return Collection
*/
public static function saveAll(iterable $dataSet, bool $replace = true): Collection
{
$collection = [];
foreach ($dataSet as $data) {
$entity = new static();
$pk = $entity->getPk();
if ($replace) {
$exists = true;
foreach ((array) $pk as $field) {
if (is_string($field) && !isset($data[$field])) {
$exists = false;
}
}
$entity->exists($exists);
}
$entity->save($data, !$replace);
$collection[] = $entity;
}
return new Collection($collection);
}
/**
* 获取属性 支持获取器
*
* @param string $name 名称
*
* @return mixed
*/
public function __get(string $name)
{
if (property_exists($this, $name)) {
return $this->$name ?? null;
}
return $this->model()->$name;
}
/**
* 设置数据 支持类型自动转换
*
* @param string $name 名称
* @param mixed $value 值
*
* @return void
*/
public function __set(string $name, $value): void
{
if (property_exists($this, $name)) {
$this->$name = $value;
}
}
/**
* 检测数据对象的值
*
* @param string $name 名称
*
* @return bool
*/
public function __isset(string $name): bool
{
return !is_null($this->__get($name));
}
/**
* 销毁数据对象的值
*
* @param string $name 名称
*
* @return void
*/
public function __unset(string $name): void
{
unset($this->$name);
}
public function __debugInfo()
{
return [];
}
/**
* 克隆模型实例
*
* @return void
*/
public function __clone()
{
}
/**
* 序列化模型对象
*
* @return array
*/
public function __serialize(): array
{
return $this->getData();
}
/**
* 反序列化模型对象
*
* @param array $data
* @return void
*/
public function __unserialize(array $data)
{
parent::__construct();
if (!empty($data)){
$this->exists(true);
}
foreach ($data as $name => $val) {
$this->$name = $val;
}
}
public static function __callStatic($method, $args)
{
$entity = new static();
$model = $entity->model();
if (in_array($method, ['destroy'])) {
$db = $model;
} else {
// 处理映射字段的查询
$map = $entity->parseAutoMapping();
$alias = Str::snake(class_basename($model));
$db = $model->db()->alias($alias)->via($alias)->fieldMap($map);
}
$auto = $entity->getOption('autoRelation');
if (!empty($auto) && !in_array(strtolower($method), ['with','withjoin'])) {
// 自动关联查询
$db->with($auto);
}
return call_user_func_array([$db, $method], $args);
}
public function __call($method, $args)
{
if (in_array($method, ['hidden', 'visible', 'append'])) {
// 不支持输出设置
return $this;
}
// 调用Model类方法
$result = call_user_func_array([$this->model(), $method], $args);
return $result instanceof Model ? $this : $result;
}
}

View File

@@ -0,0 +1,72 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model;
use think\db\exception\DbException as Exception;
use think\Model;
use think\model\contract\Modelable;
/**
* Class Virtual.
* 虚拟模型
*/
abstract class Virtual extends Model
{
/**
* 创建数据.
*
* @param array|object $data 数据
* @param array $allowField 允许字段
* @param bool $replace 使用Replace
* @param string $suffix 数据表后缀
* @return Modelable
*/
public static function create(array | object $data, array $allowField = [], bool $replace = false, string $suffix = ''): Modelable
{
$model = new static();
if (!empty($data)) {
// 初始化模型数据
$model->data($data);
}
return $model;
}
/**
* 获取Db对象实例.
* @return Query
*/
public function getQuery()
{
throw new Exception('virtual model not support db query');
}
/**
* 获取数据表字段类型列表(或某个字段的类型).
*
* @param string|null $field 字段名
*
* @return array|string
*/
protected function getFields(?string $field = null)
{
$schema = array_merge($this->getOption('schema', []), $this->getOption('type', []));
if ($field) {
return $schema[$field] ?? null;
}
return $schema;
}
}

View File

@@ -0,0 +1,719 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\concern;
use BackedEnum;
use Stringable;
use think\db\Express;
use think\db\Raw;
use think\helper\Str;
use think\model\Collection;
use think\model\contract\EnumTransform;
use think\model\contract\FieldTypeTransform;
use think\model\contract\Modelable as Model;
use think\model\contract\Typeable;
use think\model\type\Date;
use think\model\type\DateTime;
use think\model\type\Json;
/**
* 模型数据处理.
*/
trait Attribute
{
/**
* 初始化模型数据.
*
* @param array|object $data 实体模型数据
* @param bool $fromSave
*
* @return void
*/
private function initializeData(array | object $data, bool $fromSave = false)
{
// 分析数据
$data = $this->parseData($data);
$schema = $this->getFields();
$fields = array_keys($schema);
// 模型赋值
foreach ($data as $name => $value) {
if (in_array($name, $this->getOption('disuse'))) {
// 废弃字段
continue;
}
if (str_contains($name, '__')) {
// 组装关联JOIN查询数据
[$relation, $attr] = explode('__', $name, 2);
$relations[$relation][$attr] = $value;
continue;
}
$trueName = $fromSave ? $this->getMappingName($name) : $name;
if (in_array($trueName, $fields)) {
$type = $schema[$trueName] ?? 'string';
// 读取数据后进行类型转换
if (!$fromSave || !$this->hasSetAttr($trueName)) {
$value = $this->readTransform($value, $type);
}
// 数据赋值
$this->setData($trueName, $value);
if ($trueName == $this->getPk()) {
$this->setKey($value);
}
// 记录原始数据
$origin[$trueName] = $value;
} else {
// 非数据表字段或关联数据 额外赋值
$this->setData($trueName, $value);
}
}
if (!empty($relations)) {
// 设置关联数据
$this->parseRelationData($relations);
}
if (!empty($origin) && !$fromSave) {
$this->trigger('AfterRead');
$this->setOption('origin', $origin);
$this->setOption('get', []);
}
}
/**
* 获取主键名.
*
* @return string|array
*/
public function getPk()
{
return $this->getOption('pk', 'id');
}
/**
* 获取表名(不含前后缀).
*
* @return string
*/
public function getName(): string
{
return $this->getOption('name', Str::snake(class_basename(static::class)));
}
/**
* 解析模型数据.
*
* @param array|object $data 数据
*
* @return array
*/
private function parseData(array | object $data): array
{
if ($data instanceof self) {
$data = $data->getData();
} elseif (is_object($data)) {
$data = get_object_vars($data);
}
return $data;
}
/**
* 动态设置数据字段获取器.
*
* @param array|string $attr 字段名
* @param callable $callback 闭包获取器
*
* @return $this
*/
public function withFieldAttr(array | string $attr, ?callable $callback = null)
{
if (is_array($attr)) {
foreach ($attr as $name => $closure) {
$this->withFieldAttr($name, $closure);
}
} else {
$name = $this->getRealFieldName($attr);
$this->setWeakData('withAttr', $name, $callback);
// 自动追加输出
self::$weakMap[$this]['append'][] = $name;
}
return $this;
}
/**
* 获取实际字段名.
* 严格模式下 完全和数据表字段对应一致(默认)
* 非严格模式 统一转换为snake规范支持驼峰规范读取
*
* @param string $name 字段名
*
* @return mixed
*/
protected function getRealFieldName(string $name)
{
if (false === $this->getOption('strict')) {
return Str::snake($name);
}
return $name;
}
/**
* 数据读取 类型转换.
*
* @param mixed $value 值
* @param string|array|null $type 要转换的类型
*
* @return mixed
*/
protected function readTransform($value, string | array | null $type)
{
if (is_null($type) || is_null($value) || $value instanceof Raw || $value instanceof Express) {
return $value;
}
$param = '';
if (is_array($type)) {
[$type, $param] = $type;
} elseif (str_contains($type, ':')) {
[$type, $param] = explode(':', $type, 2);
}
$typeTransform = static function (string $type, $value, $model, $param) {
if (class_exists($type) && !($value instanceof $type)) {
if (is_subclass_of($type, Typeable::class)) {
$value = $type::from($value, $model);
if ($param && $value instanceof DateTime) {
// 设置时间输出格式
$value->setFormat($param);
}
} elseif (is_subclass_of($type, FieldTypeTransform::class)) {
$value = $type::get($value, $model);
} elseif (is_subclass_of($type, BackedEnum::class)) {
$value = $type::from($value);
if (is_subclass_of($type, EnumTransform::class)) {
$value = $value->value();
} elseif ($model->getOption('enumReadName')) {
$method = $model->getOption('enumReadName');
$value = is_string($method) ? $value->$method() : $value->name;
}
} else {
// 对象类型
$value = new $type($value);
}
}
return $value;
};
return match ($type) {
'string','bigint'=> (string) $value,
'int','integer' => (int) $value,
'float' => empty($param) ? (float) $value : (float) number_format($value, (int) $param, '.', ''),
'bool','boolean' => (bool) $value,
'array' => empty($value) ? [] : (is_array($value) ? $value : json_decode($value, true)),
'object' => empty($value) ? new \stdClass() : (is_string($value) ? json_decode($value) : json_decode(json_encode($value, JSON_FORCE_OBJECT))),
'json' => $typeTransform(Json::class, $value, $this, $param),
'date' => $typeTransform(Date::class, $value, $this, $param),
'datetime' => $typeTransform(DateTime::class, $value, $this, $param),
'timestamp' => $typeTransform(DateTime::class, $value, $this, $param),
default => $typeTransform($type, $value, $this, $param),
};
}
/**
* 数据写入 类型转换.
*
* @param mixed $value 值
* @param string|array|null $type 要转换的类型
*
* @return mixed
*/
protected function writeTransform($value, string | array | null $type)
{
if (is_null($type) || is_null($value) || $value instanceof Raw || $value instanceof Express) {
return $value;
}
$param = '';
if (is_array($type)) {
[$type, $param] = $type;
} elseif (str_contains($type, ':')) {
[$type, $param] = explode(':', $type, 2);
}
$typeTransform = static function (string $type, $value, $model) {
if (class_exists($type)) {
if (is_subclass_of($type, Typeable::class)) {
$value = $value->value();
} elseif (is_subclass_of($type, FieldTypeTransform::class)) {
$value = $type::set($value, $model);
} elseif ($value instanceof BackedEnum) {
$value = $value->value;
} elseif ($value instanceof Stringable) {
$value = $value->__toString();
}
}
return $value;
};
return match ($type) {
'string','bigint' => (string) $value,
'int', 'integer' => (int) $value,
'float' => empty($param) ? (float) $value : (float) number_format($value, (int) $param, '.', ''),
'bool', 'boolean' => $value ? 1 : 0,
'object' => is_object($value) ? json_encode($value, JSON_FORCE_OBJECT) : $value,
'array' => json_encode((array) $value, JSON_UNESCAPED_UNICODE),
'json' => $typeTransform(Json::class, $value, $this),
'date' => $typeTransform(Date::class, $value, $this),
'datetime' => $typeTransform(DateTime::class, $value, $this),
'timestamp' => $typeTransform(DateTime::class, $value, $this),
default => $typeTransform($type, $value, $this),
};
}
/**
* 刷新对象原始数据(为当前数据).
*
* @return $this
*/
public function refreshOrigin()
{
return $this->setOption('origin', $this->getData());
}
/**
* 设置主键值
*
* @param int|string $value 值
* @return void
*/
public function setKey($value)
{
$pk = $this->getPk();
if (is_string($pk)) {
$this->set($pk, $value);
}
}
/**
* 获取主键值
*
* @return mixed
*/
public function getKey()
{
$pk = $this->getPk();
if (is_null($pk)) {
return;
}
if (is_string($pk)) {
return $this->get($pk);
}
foreach ($pk as $name) {
$data[$name] = $this->get($name);
}
return $data;
}
/**
* 重置模型数据.
*
* @param array $data
*
* @return $this
*/
public function data(array $data)
{
$this->initializeData($data);
return $this;
}
/**
* 获取模型实际数据.
*
* @param string|null $name 字段名
* @return mixed
*/
public function getData(?string $name = null)
{
if ($name) {
$name = $this->getRealFieldName($name);
return $this->getWeakData('data', $name);
}
return $this->getOption('data', []);
}
/**
* 判断模型是否存在数据字段.
*
* @param string $name 字段名
* @return bool
*/
public function hasData(string $name): bool
{
return $this->hasGetAttr($name) || array_key_exists($this->getMappingName($name), self::$weakMap[$this]['data']);
}
/**
* 设置数据对象的实际值
*
* @param string $name 名称
* @param mixed $value 值
*
* @return void
*/
protected function setData(string $name, $value)
{
$this->setWeakData('data', $name, $value);
if ($this->getWeakData('get', $name)) {
$this->setWeakData('get', $name, null);
}
}
/**
* 清空模型数据.
*
* @return $this
*/
public function clear()
{
$this->setOption('data', []);
$this->setOption('origin', []);
$this->setOption('get', []);
$this->setOption('relation', []);
return $this;
}
/**
* 获取原始数据.
*
* @param string|null $name 字段名
* @param bool $transform 是否自动类型转换
* @return mixed
*/
public function getOrigin(?string $name = null, bool $transfrom = false)
{
if ($name) {
$name = $this->getRealFieldName($name);
$result = $this->getWeakData('origin', $name);
return $transfrom ? $this->writeTransform($result, $this->getFields($name)) : $result;
}
return $this->getOption('origin');
}
/**
* 判断数据是否为空.
*
* @return bool
*/
public function isEmpty(): bool
{
return empty($this->getData());
}
/**
* 判断JSON数据是否为数组格式.
*
* @return bool|null
*/
public function isJsonAssoc(): bool|null
{
return $this->getOption('jsonAssoc', true);
}
/**
* 设置JSON数据格式.
*
* @return $this
*/
public function jsonAssoc(bool $assoc = true)
{
return $this->setOption('jsonAssoc', $assoc);
}
/**
* 设置数据对象的值 并进行类型自动转换
*
* @param string $name 名称
* @param mixed $value 值
*
* @return $this
*/
public function set(string $name, $value)
{
$name = $this->getMappingName($name);
$type = $this->getFields()[$name] ?? '';
if ($this->isExists() && in_array($name, $this->getOption('readonly'))) {
// 只读属性不能赋值
return $this;
}
if (is_null($value) && is_subclass_of($type, Model::class)) {
// 关联数据为空 设置一个空模型
$value = new $type();
} elseif (!($value instanceof Model || $value instanceof Collection || $value instanceof FieldTypeTransform) && $type && !$this->hasSetAttr($name)) {
// 类型自动转换
$value = $this->readTransform($value, $type);
}
$this->setData($name, $value);
return $this;
}
/**
* 字段是否定义修改器
*
* @param string $name 名称
*
* @return bool
*/
protected function hasSetAttr(string $name): bool
{
$attr = Str::studly($name);
$method = 'set' . $attr . 'Attr';
return method_exists($this, $method);
}
/**
* 字段是否定义获取器
*
* @param string $name 名称
*
* @return bool
*/
protected function hasGetAttr(string $name): bool
{
$attr = Str::studly($name);
$method = 'get' . $attr . 'Attr';
return method_exists($this, $method);
}
/**
* 使用修改器或类型自动转换处理数据(写入数据前自动调用)
*
* @param string $name 名称
* @param mixed $value 值
*
* @return mixed
*/
private function setWithAttr(string $name, $value)
{
$attr = Str::studly($name);
$method = 'set' . $attr . 'Attr';
if (method_exists($this, $method)) {
$value = $this->$method($value, $this->getData());
} else {
// 类型转换
$value = $this->writeTransform($value, $this->getFields($name));
}
if ($value instanceof Express) {
// 处理运算表达式
$step = $value->getStep();
$origin = $this->getOrigin($name);
$real = match ($value->getType()) {
'+' => $origin + $step,
'-' => $origin - $step,
'*' => $origin * $step,
'/' => $origin / $step,
default => $origin,
};
$this->set($name, $real);
} elseif (is_scalar($value)) {
// 同步写入修改器或类型自动转换结果
$this->set($name, $value);
}
return $value;
}
/**
* 获取数据对象的值(支持使用获取器)
*
* @param string $name 名称
* @param bool $attr 是否使用获取器
*
* @return mixed
*/
public function get(string $name, bool $attr = true)
{
$name = $this->getMappingName($name);
if ($attr && $value = $this->getWeakData('get', $name)) {
// 已经输出的数据直接返回
return $value;
}
if (!array_key_exists($name, $this->getData()) && !array_key_exists($name, $this->getFields())) {
// 动态获取关联数据
$value = $this->getRelationData($name) ?: null;
} else {
$value = $this->getData($name);
}
if ($attr) {
// 通过获取器输出
$value = $this->getWithAttr($name, $value, $this->getData());
$this->setWeakData('get', $name, $value);
}
return $value;
}
/**
* 获取映射字段
*
* @param string $name 名称
*
* @return string
*/
protected function getMappingName(string $name): string
{
$mapping = $this->getOption('mapping');
return array_search($name, $mapping) ?: $this->getRealFieldName($name);
}
/**
* 处理数据对象的值(经过获取器和类型转换)
*
* @param string $name 名称
* @param mixed $value 值
* @param array $data 所有数据
*
* @return mixed
*/
private function getWithAttr(string $name, $value, array $data = [])
{
$attr = Str::studly($name);
$method = 'get' . $attr . 'Attr';
$withAttr = $this->getWeakData('withAttr', $name);
if ($withAttr) {
// 动态获取器
$value = $withAttr($value, $data, $this);
} elseif (method_exists($this, $method)) {
// 获取器
$value = $this->$method($value, $data);
} elseif ($value instanceof Typeable || is_subclass_of($value, EnumTransform::class, false)) {
// 类型自动转换
if ($value instanceof Json) {
// JSON数据转换
$value = $this->readTransformJson($name, $value);
} else {
$value = $value->value();
}
} elseif (is_int($value) && $this->isTimeAttr($name) && false != $this->getDateFormat()) {
// 兼容数字类型时间字段的自动转换输出
$value = (new \DateTime())
->setTimestamp($value)
->format($this->getDateFormat());
}
return $value;
}
/**
* 处理JSON数据对象的值
*
* @param string $name 名称
* @param Json $value 值
*
* @return array|object
*/
protected function readTransformJson(string $name, Json $value)
{
// JSON数据转换
$value = $value->value();
if ($value) {
foreach ($value as $key => &$val) {
$type = $this->getFields($name . '->' . $key);
if ($type) {
// 定义了JSON属性类型自动转换
$val = $this->readTransform($val, $type);
}
}
}
return $value;
}
protected function isTimeAttr(string $name): bool
{
return in_array($name, [$this->getOption('createTime'), $this->getOption('updateTime'), $this->getOption('deleteTime')]) || in_array($name, $this->getOption('timestampField', []));
}
/**
* 使用获取器获取数据对象的值
*
* @param string $name 名称
*
* @return mixed
*/
public function getAttr(string $name)
{
return $this->get($name);
}
/**
* 设置数据对象的值 并进行类型自动转换
*
* @param string $name 名称
* @param mixed $value 值
*
* @return $this
*/
public function setAttr(string $name, $value)
{
return $this->set($name, $value);
}
/**
* 设置数据是否存在.
*
* @param bool $exists
*
* @return $this
*/
public function exists(bool $exists = true)
{
return $this->setOption('exists', $exists);
}
/**
* 判断数据是否存在数据库.
*
* @return bool
*/
public function isExists(): bool
{
return $this->getOption('exists', false);
}
/**
* 设置枚举类型自动读取数据方式
* true 表示使用name值返回
* 字符串 表示使用枚举类的方法返回
*
* @return $this
*/
public function withEnumRead(bool | string $method = true)
{
return $this->setOption('enumReadName', $method);
}
}

View File

@@ -0,0 +1,138 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\concern;
use Closure;
use Stringable;
use think\model\contract\Typeable;
use think\model\type\DateTime;
/**
* 自动写入数据.
*/
trait AutoWriteData
{
/**
* 字段自动写入.
*
* @param array $data 数据
* @param bool $update 是否更新
* @param array $allow 允许字段
* @return void
*/
protected function autoWriteData(array &$data, bool $update, array $allow = [])
{
// 数据写入前置检查
$this->checkData($data, $update);
// 自动时间戳处理
$this->autoDateTime($data, $update, $allow);
$auto = $this->getOption($update ? 'update' : 'insert', []);
foreach ($auto as $name => $val) {
$field = is_string($name) ? $name : $val;
if (!isset($data[$field])) {
if ($val instanceof Closure) {
$value = $val($this);
} else {
$value = is_string($name) ? $val : $this->setWithAttr($field, null, $data);
}
$data[$field] = $value;
$this->setData($field, $value);
}
}
}
/**
* 时间字段自动写入.
*
* @param array $data 数据
* @param bool $update 是否更新
* @param array $allow 允许字段
* @return void
*/
protected function autoDateTime(array &$data, bool $update, array $allow)
{
$autoDateTime = $this->getOption('autoWriteTimestamp', true);
if ($autoDateTime) {
$dateTimeFields = [$this->getOption('updateTime')];
if (!$update) {
array_unshift($dateTimeFields, $this->getOption('createTime'));
}
foreach ($dateTimeFields as $field) {
if (is_string($field) && (empty($allow) || in_array($field, $allow))) {
$data[$field] = $this->getDateTime($field);
$this->setData($field, $this->readTransform($data[$field], $this->getFields($field)));
}
}
}
}
public function getAutoTimeFields(): array
{
return [$this->getOption('createTime'), $this->getOption('updateTime')];
}
/**
* 获取当前时间.
*
* @param string $field 字段名
* @return mixed
*/
protected function getDateTime(string $field)
{
$type = $this->getFields($field) ?? 'string';
if (in_array($type, ['int', 'integer'])) {
return time();
} elseif (is_subclass_of($type, Typeable::class)) {
return $type::from('now', $this)->format('Y-m-d H:i:s.u');
} elseif (str_contains($type, '\\')) {
$obj = new $type();
if ($obj instanceof Stringable) {
return $obj->__toString();
} else {
return (string) $obj;
}
} else {
return DateTime::from('now', $this)->format('Y-m-d H:i:s.u');
}
}
public function getAutoWriteTimestamp()
{
return $this->getOption('autoWriteTimestamp');
}
public function isAutoWriteTimestamp(string | bool $auto)
{
return $this->setOption('autoWriteTimestamp', $auto);
}
public function getDateFormat()
{
return $this->getOption('dateFormat');
}
public function setDateFormat(string | bool $format)
{
return $this->setOption('dateFormat', $format);
}
public function setTimeField($createTime, $updateTime)
{
$this->setOption('createTime', $createTime);
$this->setOption('updateTime', $updateTime);
}
}

View File

@@ -0,0 +1,208 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\concern;
use Closure;
use think\helper\Str;
use think\model\Collection;
use think\model\contract\Modelable;
/**
* 模型数据转换处理.
*/
trait Conversion
{
/**
* 设置需要附加的输出属性.
*
* @param array $append 属性列表
* @param bool $merge 是否合并
*
* @return $this
*/
public function append(array $append, bool $merge = false)
{
return $this->setOption('append', $merge ? array_merge($this->getOption('append'), $append) : $append);
}
/**
* 设置需要隐藏的输出属性.
*
* @param array $hidden 属性列表
* @param bool $merge 是否合并
*
* @return $this
*/
public function hidden(array $hidden, bool $merge = false)
{
return $this->setOption('hidden', $merge ? array_merge($this->getOption('hidden'), $hidden) : $hidden);
}
/**
* 设置需要输出的属性.
*
* @param array $visible
* @param bool $merge 是否合并
*
* @return $this
*/
public function visible(array $visible, bool $merge = false)
{
return $this->setOption('visible', $merge ? array_merge($this->getOption('visible'), $visible) : $visible);
}
/**
* 设置属性的映射输出.
*
* @param array $map
*
* @return $this
*/
public function mapping(array $map)
{
return $this->setOption('mapping', $map);
}
/**
* 设置输出场景.
*
* @param string $scene
*
* @return $this
*/
public function scene(string $scene)
{
$method = 'scene' . Str::studly($scene);
if (method_exists($this, $method)) {
call_user_func([$this, $method]);
}
return $this;
}
/**
* 模型数据转数组.
*
* @return array
*/
public function toArray(): array
{
$mapping = $this->getOption('mapping');
foreach (['visible', 'hidden', 'append'] as $convert) {
${$convert} = $this->getOption($convert);
foreach (${$convert} as $key => $val) {
if (is_string($key)) {
$relation[$key][$convert] = $val;
unset(${$convert}[$key]);
} elseif (str_contains($val, '.')) {
[$relName, $name] = explode('.', $val);
$relation[$relName][$convert][] = $name;
unset(${$convert}[$key]);
} elseif ($item = array_search($val, $mapping)) {
${$convert}[$key] = $item;
}
}
}
$data = $this->getData();
$allow = array_diff($visible ?: array_keys($data), $hidden);
$item = [];
foreach ($data as $name => $val) {
if ($val instanceof Modelable || $val instanceof Collection) {
if (in_array($name, $hidden)) {
// 隐藏关联属性
unset($item[$name]);
continue;
}
if (!empty($relation[$name])) {
// 处理关联数据输出
foreach ($relation[$name] as $key => $attr) {
$val->$key($attr);
}
}
$item[$name] = $val->toArray();
} elseif (empty($allow) || in_array($name, $allow)) {
// 通过获取器输出
$item[$name] = $this->getWithAttr($name, $val, $data);
}
if (array_key_exists($name, $item) && isset($mapping[$name])) {
// 检查字段映射
$item[$mapping[$name]] = $item[$name];
unset($item[$name]);
}
}
// 输出额外属性 必须定义获取器
foreach ($this->getOption('append') as $key => $field) {
if (is_numeric($key)) {
$item[$field] = $this->get($field);
} else {
// 追加关联属性
$relation = $this->getRelationData($key, false);
foreach((array) $field as $key => $name) {
if (is_numeric($key)) {
$item[$name] = $relation?->get($name);
} else {
$item[$name] = $relation?->get($key);
}
}
}
}
if ($this->getOption('convertNameToCamel')) {
foreach ($item as $key => $val) {
$name = Str::camel($key);
if ($name !== $key) {
$item[$name] = $val;
unset($item[$key]);
}
}
}
return $item;
}
/**
* 模型数据转Json.
*
* @param int $options json参数
* @return string
*/
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $options);
}
/**
* 转换为数据集对象
*
* @param array|Collection $collection 数据集
* @param string|null $resultSetType 数据集类
*
* @return Collection
*/
public function toCollection(iterable $collection = [], ?string $resultSetType = null): Collection
{
$resultSetType = $resultSetType ?: $this->getOption('resultSetType');
if ($resultSetType && str_contains($resultSetType, '\\')) {
$collection = new $resultSetType($collection);
} else {
$collection = new Collection($collection);
}
return $collection;
}
}

View File

@@ -0,0 +1,202 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\concern;
use think\db\BaseQuery as Query;
use think\facade\Db;
/**
* 数据库连接.
*/
trait DbConnect
{
/**
* 设置Db对象实例.(用于兼容)
*/
public static function setDb($db)
{}
/**
* 获取Db对象实例.
* @return Query
*/
public function getQuery()
{
$db = $this->initDb()->newQuery($this->getOption('query'));
if ($this->getOption('cache')) {
[$key, $expire, $tag] = $this->getOption('cache');
$db->cache($key, $expire, $tag);
}
return $db->schema($this->getOption('schema'))
->pk($this->getPk())
->suffix($this->getOption('suffix'))
->setKey($this->getKey())
->replace($this->getOption('replace', false))
->model($this);
}
/**
* 初始化数据库连接对象.
* @return Query
*/
private function initDb()
{
$connection = $this->getOption('connection');
if ($this->getOption('db')) {
$db = $this->getOption('db')->connect($connection);
} else {
$db = Db::connect($connection);
}
$db = $db->name($this->getName());
if ($this->getOption('table')) {
$db->table($this->getOption('table'));
} else {
$db->suffix($this->getOption('suffix'));
}
return $db;
}
/**
* 获取数据表字段类型列表(或某个字段的类型).
*
* @param string|null $field 字段名
*
* @return array|string
*/
protected function getFields(?string $field = null)
{
$schema = $this->getOption('schema');
if (empty($schema)) {
// 获取数据表信息
$db = $this->initDb();
$fields = $db->getFieldsType();
$schema = array_merge($fields, $this->getOption('type', []));
// 获取主键
if (!$this->getOption('pk')) {
$this->setOption('pk', $db->getPk());
}
$this->setOption('schema', $schema);
}
if ($field) {
return $schema[$field] ?? null;
}
return $schema;
}
/**
* 新增数据是否使用Replace.
*
* @param bool $replace
*
* @return $this
*/
public function replace(bool $replace = true)
{
return $this->setOption('replace', $replace);
}
/**
* 获取当前模型的数据表后缀
*
* @return string
*/
public function getSuffix(): string
{
return $this->getOption('suffix', '');
}
/**
* 设置当前模型数据表的后缀
*
* @param string $suffix 数据表后缀
*
* @return $this
*/
public function setSuffix(string $suffix)
{
$this->setOption('suffix', $suffix);
return $this;
}
/**
* 构建实体模型查询.
*
* @param Query $query 查询对象
* @return void
*/
protected function query(Query $query) {}
/**
* 获取查询对象
*
* @param array|null $scope 设置不使用的全局查询范围
* @return Query
*/
public function db(array | null $scope = []): Query
{
$query = $this->getQuery();
// 全局查询范围
if (is_array($scope)) {
$globalScope = array_diff($this->getOption('globalScope', []), $scope);
$query->scope($globalScope);
}
// 执行扩展查询
$this->query($query);
return $query;
}
/**
* 设置不使用的全局查询范围.
*
* @param array $scope 不启用的全局查询范围
*
* @return Query
*/
public static function withoutGlobalScope(?array $scope = null): Query
{
$model = new static();
return $model->db($scope);
}
public static function __callStatic($method, $args)
{
$model = new static();
$db = $model->db();
if (!empty(self::$weakMap[$model]['autoRelation'])) {
// 自动获取关联数据
$db->with(self::$weakMap[$model]['autoRelation']);
}
return call_user_func_array([$db, $method], $args);
}
public function __call($method, $args)
{
if ($this->isExists() && strtolower($method) == 'withattr') {
return call_user_func_array([$this, 'withFieldAttr'], $args);
}
return call_user_func_array([$this->db(), $method], $args);
}
}

View File

@@ -0,0 +1,85 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\concern;
use ReflectionClass;
use think\db\exception\ModelEventException;
use think\helper\Str;
/**
* 模型事件处理.
*/
trait ModelEvent
{
/**
* 设置Event对象 (用于兼容)
*
* @param object $event Event对象
*
* @return void
*/
public static function setEvent($event)
{}
/**
* 当前操作的事件响应.
*
* @param bool $event 是否需要事件响应
*
* @return $this
*/
public function withEvent(bool $event)
{
return $this->setOption('withEvent', $event);
}
/**
* 触发事件.
*
* @param string $event 事件名
*
* @return bool
*/
protected function trigger(string $event): bool
{
if (!$this->getOption('withEvent', true)) {
return true;
}
$method = 'on' . Str::studly($event);
$obj = $this->getOption('event');
$obser = $this->getOption('eventObserver');
try {
if ($obser) {
$reflect = new ReflectionClass($obser);
$observer = $reflect->newinstance();
} else {
$observer = $this;
}
if (method_exists($observer, $method)) {
$result = $this->invoke([$observer, $method], [$this]);
} elseif (is_object($obj) && method_exists($obj, 'trigger')) {
$result = $obj->trigger(static::class . '.' . $event, $this);
$result = empty($result) ? true : end($result);
} else {
$result = true;
}
return false !== $result;
} catch (ModelEventException $e) {
return false;
}
}
}

View File

@@ -0,0 +1,87 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\model\concern;
use think\db\exception\DbException as Exception;
/**
* 乐观锁
*/
trait OptimLock
{
protected function getOptimLockField()
{
return $this->getOption('optimLock') ?? 'lock_version';
}
/**
* 数据检查.
* @param array $data 数据
* @param bool $isUpdate 是否更新
* @return void
*/
protected function checkData(array &$data, bool $isUpdate): void
{
$isUpdate ? $this->updateLockVersion($data) : $this->recordLockVersion($data);
}
/**
* 记录乐观锁
*
* @param array $data 数据
* @return void
*/
protected function recordLockVersion(array &$data): void
{
$optimLock = $this->getOptimLockField();
$this->setData($optimLock, 0);
$data[$optimLock] = 0;
}
/**
* 更新乐观锁
*
* @param array $data 数据
* @return void
*/
protected function updateLockVersion(array &$data): void
{
$optimLock = $this->getOptimLockField();
$lockVer = $this->getOrigin($optimLock);
$this->setData($optimLock, $lockVer + 1);
$data[$optimLock] = $lockVer + 1;
}
public function getDbWhere($where)
{
$db = $this->db();
// 检查条件
if (!empty($where)) {
$db->where($where);
}
$optimLock = $this->getOptimLockField();
$lockVer = $this->getOrigin($optimLock);
$pk = $this->getPk();
if (is_array($pk)) {
$db->where($this->getKey());
} else {
$db->where($pk, '=', $this->getKey());
}
$db->where($optimLock, '=', $lockVer);
return $db;
}
}

View File

@@ -0,0 +1,902 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\concern;
use Closure;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use think\db\exception\InvalidArgumentException;
use think\helper\Str;
use think\model\Collection;
use think\model\contract\Modelable as Model;
use think\model\Relation;
use think\model\relation\BelongsTo;
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;
use think\model\View;
/**
* 实体模型关联处理.
*/
trait RelationShip
{
/**
* 关联数据写入或删除.
*
* @param array $relation 关联
*
* @return $this
*/
public function together(array $relation)
{
return $this->setOption('together', $relation);
}
/**
* 设置关联JOIN数据.
*
* @param array $relations 关联数据
*
* @return void
*/
private function parseRelationData(array $relations)
{
foreach ($relations as $relation => $val) {
$relation = $this->getRealFieldName($relation);
$type = $this->getFields($relation);
$bind = $this->getBindAttr($this->getOption('bindAttr'), $relation);
if (!empty($bind)) {
// 绑定关联属性
$this->bindRelationAttr($val, $bind, $relation);
} elseif (is_subclass_of($type, Model::class)) {
// 明确类型直接设置关联属性
$this->setRelation($relation, new $type($val));
} else {
// 寄存关联数据
$this->setTempRelation($relation, $val);
}
}
}
/**
* 寄存关联数据.
*
* @param string $relation 关联属性
* @param array $data 关联数据
*
* @return void
*/
private function setTempRelation(string $relation, array $data)
{
$this->setWeakData('relation', $relation, $data);
}
/**
* 获取寄存的关联数据.
*
* @param string $relation 关联属性
*
* @return array
*/
public function getRelation(string $relation): array
{
return $this->getWeakData('relation', $relation, []);
}
/**
* 写入模型关联数据(一对一).
*
* @param array $relations 数据
* @param bool $isUpdate 是否更新
* @return void
*/
private function relationSave(array $relations = [], bool $isUpdate = true)
{
$together = $this->getOption('together');
foreach ($together as $key => $name) {
if (is_numeric($key) && isset($relations[$name])) {
// 支持关联写入或更新
$method = Str::camel($name);
$relation = $relations[$name];
$data = null;
if ($relation instanceof Model) {
if ($isUpdate) {
$relation->save();
} else {
$data = $this->$method()->save($relation);
}
} else {
// 数组或数据集
$relationModel = $this->$method();
if ($relationModel instanceof OneToOne) {
$data = $relationModel->save($relation);
} elseif ($relationModel instanceof HasMany || $relationModel instanceof MorphMany) {
$data = $relationModel->saveAll($relation);
if ($data) {
$data = $this->toCollection($data);
}
}
}
if ($data) {
// 重新赋值关联数据
$this->set($name, $data);
}
} elseif (is_array($name)) {
// 关联写入
$data = [];
if (array_is_list($name)) {
// 绑定关联属性
foreach($name as $field) {
if ($this->getData($field)) {
$data[$field] = $this->getData($field);
}
}
} else {
$data = $name;
}
$method = Str::camel($key);
$this->$method()->save($data);
}
}
}
/**
* 删除模型关联数据(一对一).
*
* @param array $relations 数据
* @return void
*/
private function relationDelete(array $relations = [])
{
foreach ($relations as $name => $relation) {
if ($relation && in_array($name, $this->getOption('together'))) {
$relation->delete();
}
}
}
/**
* 获取关联数据
*
* @param string $name 名称
* @param bool $set 是否设置为当前模型属性
*
* @return mixed
*/
protected function getRelationData(string $name, bool $set = true)
{
$method = Str::camel($name);
if (method_exists($this, $method) && !method_exists('think\Model', $method)) {
$modelRelation = $this->$method();
if ($modelRelation instanceof Relation) {
$value = $modelRelation->getRelation();
if ($set) {
$this->setData($name, $value);
}
return $value;
}
}
}
/**
* 判断是否存在关联
*
* @param string $name 名称
*
* @return bool
*/
public function hasRelation(string $name)
{
$method = Str::camel($name);
if (method_exists($this, $method)) {
$modelRelation = $this->$method();
if ($modelRelation instanceof Relation) {
return true;
}
}
return false;
}
protected function getBindAttr($bind, $name)
{
return $bind[$name] ?? [];
}
/**
* 设置关联绑定数据
*
* @param Model|array $model 关联对象
* @param array $bind 绑定属性
* @return void
*/
public function bindRelationAttr(Model | array $model, array $bind = [])
{
$data = is_array($model) ? $model : $model->toArray();
foreach ($data as $key => $val) {
if (isset($bind[$key])) {
$this->set($bind[$key], $val);
} elseif ($attr = array_search($key, $bind)) {
$this->set(is_numeric($attr) ? $key : $attr, $val);
} elseif (in_array($key, $bind)) {
$this->set($key, $val);
}
}
}
/**
* 设置关联数据.
*
* @param string $relation 关联属性
* @param Model|Collection $data 关联数据
*
* @return void
*/
public function setRelation(string $relation, $data)
{
$this->__set($relation, $data);
}
/**
* 查询存在关联数据的模型.
*
* @param string $relation 关联方法名
* @param mixed $operator 比较操作符
* @param int $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public static function has(string $relation, string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', ?Query $query = null): Query
{
return (new static())
->$relation()
->has($operator, $count, $id, $joinType, $query);
}
/**
* 查询不存在关联数据的模型.
*
* @param string $relation 关联方法名
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public static function hasNot(string $relation, string $id = '*', string $joinType = '', ?Query $query = null): Query
{
return (new static())
->$relation()
->has('=', 0, $id, $joinType, $query);
}
/**
* 根据关联条件查询当前模型.
*
* @param string|array $relation 关联方法名 或 ['关联方法名', '关联表别名']
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public static function hasWhere(string|array $relation, $where = [], string $fields = '*', string $joinType = '', ?Query $query = null): Query
{
if (is_array($relation)) {
[$relation, $alias] = $relation;
}
return (new static())
->$relation()
->hasWhere($where, $fields, $joinType, $query, '', $alias ?? '');
}
/**
* 根据关联条件查询当前模型.
*
* @param string|array $relation 关联方法名 或 ['关联方法名', '关联表别名']
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public static function hasWhereOr(string|array $relation, $where = [], string $fields = '*', string $joinType = '', ?Query $query = null): Query
{
if (is_array($relation)) {
[$relation, $alias] = $relation;
}
return (new static())
->$relation()
->hasWhere($where, $fields, $joinType, $query, 'OR', $alias ?? '');
}
/**
* 查询当前模型的关联数据.
*
* @param array $relations 关联名
* @param array $withRelationAttr 关联获取器
*
* @return void
*/
public function relationQuery(array $relations, array $withRelationAttr = []): void
{
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 (str_contains($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->setRelation($relation, $relationResult->getRelation((array) $subRelation, $closure));
}
}
/**
* 预载入关联查询 JOIN方式.
*
* @param Query $query Query对象
* @param string $relation 关联方法名
* @param mixed $field 字段
* @param string $joinType JOIN类型
* @param Closure $closure 闭包
* @param bool $first
*
* @return bool
*/
public function eagerly(Query $query, string $relation, $field, string $joinType = '', ?Closure $closure = null, bool $first = false): bool
{
$relation = Str::camel($relation);
$class = $this->$relation();
if ($class instanceof OneToOne) {
$class->eagerly($query, $relation, $field, $joinType, $closure, $first);
return true;
}
return false;
}
/**
* 预载入关联查询 返回数据集.
*
* @param array $resultSet 数据集
* @param array $relations 关联名
* @param array $withRelationAttr 关联获取器
* @param bool $join 是否为JOIN方式
* @param mixed $cache 关联缓存
*
* @return void
*/
public function eagerlyResultSet(array $resultSet, array $relations, array $withRelationAttr = [], bool $join = false, $cache = false): void
{
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 (str_contains($relation, '.')) {
[$relation, $subRelation] = explode('.', $relation, 2);
$subRelation = [$subRelation];
}
$relationName = $relation;
$relation = Str::camel($relation);
$relationResult = $this->$relation();
if (isset($withRelationAttr[$relationName])) {
$relationResult->withAttr($withRelationAttr[$relationName]);
}
if (is_scalar($cache)) {
$relationCache = [$cache];
} else {
$relationCache = $cache[$relationName] ?? $cache;
}
$relationResult->eagerlyResultSet($resultSet, $relationName, $subRelation, $closure, $relationCache, $join);
}
// 刷新视图模型数据
foreach ($resultSet as $result) {
if ($result instanceof View) {
$result->refresh();
}
}
}
/**
* 预载入关联查询 返回模型对象
*
* @param array $relations 关联
* @param array $withRelationAttr 关联获取器
* @param bool $join 是否为JOIN方式
* @param mixed $cache 关联缓存
*
* @return void
*/
public function eagerlyResult(Model $result, array $relations, array $withRelationAttr = [], bool $join = false, $cache = false): void
{
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 (str_contains($relation, '.')) {
[$relation, $subRelation] = explode('.', $relation, 2);
$subRelation = [$subRelation];
}
$relationName = $relation;
$relation = Str::camel($relation);
$relationResult = $this->$relation();
if (isset($withRelationAttr[$relationName])) {
$relationResult->withAttr($withRelationAttr[$relationName]);
}
if (is_scalar($cache)) {
$relationCache = [$cache];
} else {
$relationCache = $cache[$relationName] ?? [];
}
$relationResult->eagerlyResult($result, $relationName, $subRelation, $closure, $relationCache, $join);
}
if ($result instanceof View) {
// 刷新视图模型数据
$result->refresh();
}
}
/**
* 绑定(一对一)关联属性到当前模型.
*
* @param string $relation 关联名称
* @param array $attrs 绑定属性
*
* @throws Exception
*
* @return $this
*/
public function bindAttr(string $relation, array $attrs = [])
{
$relation = $this->__get($relation);
foreach ($attrs as $key => $attr) {
if (is_numeric($key)) {
if (!is_string($attr)) {
throw new InvalidArgumentException('bind attr must be string:' . $key);
}
$key = $attr;
}
if (null !== $this->getOrigin($key)) {
throw new Exception('bind attr has exists:' . $key);
}
if ($attr instanceof Closure) {
$value = $attr($relation, $key, $this);
} else {
$value = $relation?->get($attr);
}
$this->set($key, $value);
}
return $this;
}
/**
* 关联统计
*
* @param Query $query 查询对象
* @param array $relations 关联名
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param bool $useSubQuery 子查询
*
* @return void
*/
public function relationCount(Query $query, array $relations, string $aggregate = 'sum', string $field = 'id', bool $useSubQuery = true): void
{
foreach ($relations as $key => $relation) {
$closure = $name = null;
if ($relation instanceof Closure) {
$closure = $relation;
$relation = $key;
} elseif (is_string($key)) {
$name = $relation;
$relation = $key;
}
$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 (empty($name)) {
$name = Str::snake($relation) . '_' . $aggregate;
}
if ($useSubQuery) {
$query->field(['(' . $count . ')' => $name]);
} else {
$this->set($name, $count);
}
}
}
/**
* HAS ONE 关联定义.
*
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 当前主键
*
* @return HasOne
*/
public function hasOne(string $model, string $foreignKey = '', string $localKey = ''): HasOne
{
// 记录当前关联信息
$model = $this->parseRelationModel($model);
$localKey = $localKey ?: $this->getPk();
$foreignKey = $foreignKey ?: $this->getForeignKey($this->getName());
return new HasOne($this, $model, $foreignKey, $localKey);
}
/**
* BELONGS TO 关联定义.
*
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 关联主键
*
* @return BelongsTo
*/
public function belongsTo(string $model, string $foreignKey = '', string $localKey = ''): BelongsTo
{
// 记录当前关联信息
$model = $this->parseRelationModel($model);
$foreignKey = $foreignKey ?: $this->getForeignKey((new $model())->getName());
$localKey = $localKey ?: (new $model())->getPk();
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$relation = Str::snake($trace[1]['function']);
return new BelongsTo($this, $model, $foreignKey, $localKey, $relation);
}
/**
* HAS MANY 关联定义.
*
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 当前主键
*
* @return HasMany
*/
public function hasMany(string $model, string $foreignKey = '', string $localKey = ''): HasMany
{
// 记录当前关联信息
$model = $this->parseRelationModel($model);
$localKey = $localKey ?: $this->getPk();
$foreignKey = $foreignKey ?: $this->getForeignKey($this->getName());
return new HasMany($this, $model, $foreignKey, $localKey);
}
/**
* HAS MANY 远程关联定义.
*
* @param string $model 模型名
* @param string $through 中间模型名
* @param string $foreignKey 关联外键
* @param string $throughKey 关联外键
* @param string $localKey 当前主键
* @param string $throughPk 中间表主键
*
* @return HasManyThrough
*/
public function hasManyThrough(string $model, string $through, string $foreignKey = '', string $throughKey = '', string $localKey = '', string $throughPk = ''): HasManyThrough
{
// 记录当前关联信息
$model = $this->parseRelationModel($model);
$through = $this->parseRelationModel($through);
$localKey = $localKey ?: $this->getPk();
$foreignKey = $foreignKey ?: $this->getForeignKey($this->getName());
$throughKey = $throughKey ?: $this->getForeignKey((new $through())->getName());
$throughPk = $throughPk ?: (new $through())->getPk();
return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey, $throughPk);
}
/**
* HAS ONE 远程关联定义.
*
* @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->parseRelationModel($model);
$through = $this->parseRelationModel($through);
$localKey = $localKey ?: $this->getPk();
$foreignKey = $foreignKey ?: $this->getForeignKey($this->getName());
$throughKey = $throughKey ?: $this->getForeignKey((new $through())->getName());
$throughPk = $throughPk ?: (new $through())->getPk();
return new HasOneThrough($this, $model, $through, $foreignKey, $throughKey, $localKey, $throughPk);
}
/**
* BELONGS TO MANY 关联定义.
*
* @param string $model 模型名
* @param string $middle 中间表/模型名
* @param string $foreignKey 关联外键
* @param string $localKey 当前模型关联键
*
* @return BelongsToMany
*/
public function belongsToMany(string $model, string $middle = '', string $foreignKey = '', string $localKey = ''): BelongsToMany
{
// 记录当前关联信息
$model = $this->parseRelationModel($model);
$name = Str::snake(class_basename($model));
$middle = $middle ?: Str::snake($this->getName()) . '_' . $name;
$foreignKey = $foreignKey ?: $name . '_id';
$localKey = $localKey ?: $this->getForeignKey($this->getName());
return new BelongsToMany($this, $model, $middle, $foreignKey, $localKey);
}
/**
* MORPH One 关联定义.
*
* @param string $model 模型名
* @param string|array $morph 多态字段信息
* @param string $type 多态类型
*
* @return MorphOne
*/
public function morphOne(string $model, string | array | null $morph = null, string $type = ''): MorphOne
{
// 记录当前关联信息
$model = $this->parseRelationModel($model);
if (is_null($morph)) {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$morph = Str::snake($trace[1]['function']);
}
[$morphType, $foreignKey] = $this->parseMorph($morph);
$type = $type ?: get_class($this);
return new MorphOne($this, $model, $foreignKey, $morphType, $type);
}
/**
* MORPH MANY 关联定义.
*
* @param string $model 模型名
* @param string|array $morph 多态字段信息
* @param string $type 多态类型
*
* @return MorphMany
*/
public function morphMany(string $model, string | array | null $morph = null, string $type = ''): MorphMany
{
// 记录当前关联信息
$model = $this->parseRelationModel($model);
if (is_null($morph)) {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$morph = Str::snake($trace[1]['function']);
}
$type = $type ?: get_class($this);
[$morphType, $foreignKey] = $this->parseMorph($morph);
return new MorphMany($this, $model, $foreignKey, $morphType, $type);
}
/**
* MORPH TO 关联定义.
*
* @param string|array $morph 多态字段信息
* @param array $alias 多态别名定义
*
* @return MorphTo
*/
public function morphTo(string | array | null $morph = null, array $alias = []): MorphTo
{
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$relation = Str::snake($trace[1]['function']);
if (is_null($morph)) {
$morph = $relation;
}
[$morphType, $foreignKey] = $this->parseMorph($morph);
return new MorphTo($this, $morphType, $foreignKey, $alias, $relation);
}
/**
* MORPH TO MANY关联定义.
*
* @param string $model 模型名
* @param string $middle 中间表名/模型名
* @param string|array $morph 多态字段信息
* @param string $localKey 当前模型关联键
*
* @return MorphToMany
*/
public function morphToMany(string $model, string $middle, string | array | null $morph = null, ?string $localKey = null): MorphToMany
{
if (is_null($morph)) {
$morph = $middle;
}
[$morphType, $morphKey] = $this->parseMorph($morph);
$model = $this->parseRelationModel($model);
$name = Str::snake(class_basename($model));
$localKey = $localKey ?: $this->getForeignKey($name);
return new MorphToMany($this, $model, $middle, $morphType, $morphKey, $localKey);
}
/**
* MORPH BY MANY关联定义.
*
* @param string $model 模型名
* @param string $middle 中间表名/模型名
* @param string|array $morph 多态字段信息
* @param string $foreignKey 关联外键
*
* @return MorphToMany
*/
public function morphByMany(string $model, string $middle, string | array | null $morph = null, ?string $foreignKey = null): MorphToMany
{
if (is_null($morph)) {
$morph = $middle;
}
[$morphType, $morphKey] = $this->parseMorph($morph);
$model = $this->parseRelationModel($model);
$foreignKey = $foreignKey ?: $this->getForeignKey($this->getName());
return new MorphToMany($this, $model, $middle, $morphType, $morphKey, $foreignKey, true);
}
/**
* 解析多态
*
* @param string|array $morph
*
* @return array
*/
protected function parseMorph(string | array $morph): array
{
if (is_array($morph)) {
[$morphType, $foreignKey] = $morph;
} else {
$morphType = $morph . '_type';
$foreignKey = $morph . '_id';
}
return [$morphType, $foreignKey];
}
/**
* 解析模型的完整命名空间.
*
* @param string $model 模型名(或者完整类名)
*
* @return string
*/
protected function parseRelationModel(string $model): string
{
if (!str_contains($model, '\\')) {
$path = explode('\\', static::class);
array_pop($path);
array_push($path, Str::studly($model));
$model = implode('\\', $path);
}
return $model;
}
/**
* 获取模型的默认外键名.
*
* @param string $name 模型名
*
* @return string
*/
protected function getForeignKey(string $name): string
{
if (str_contains($name, '\\')) {
$name = class_basename($name);
}
return Str::snake($name) . '_id';
}
}

View File

@@ -0,0 +1,219 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\concern;
use Closure;
use think\db\BaseQuery as Query;
use think\Model;
/**
* 数据软删除
*
* @mixin Model
*
* @method $this withTrashed()
* @method $this onlyTrashed()
*/
trait SoftDelete
{
/**
* 获取查询对象
*
* @param array|null $scope 设置不使用的全局查询范围
* @return Query
*/
public function db(array | null $scope = []): Query
{
$query = parent::db($scope);
$this->withNoTrashed($query);
return $query;
}
/**
* 判断当前实例是否被软删除.
*
* @return bool
*/
public function trashed(): bool
{
$field = $this->getDeleteTimeField();
if ($field && !empty($this->getOrigin($field))) {
return true;
}
return false;
}
public function scopeWithTrashed(Query $query): void
{
$query->removeOption('soft_delete');
}
public function scopeOnlyTrashed(Query $query): void
{
$field = $this->getDeleteTimeField(true);
if ($field) {
$query->useSoftDelete($field, $this->getWithTrashedExp());
}
}
/**
* 获取软删除数据的查询条件.
*
* @return array
*/
protected function getWithTrashedExp(): array
{
return is_null($this->getOption('defaultSoftDelete')) ? ['notnull', ''] : ['<>', $this->getOption('defaultSoftDelete')];
}
/**
* 删除当前的记录.
*
* @return bool
*/
public function delete(): bool
{
if ($this->isEmpty() || false === $this->trigger('BeforeDelete')) {
return false;
}
$name = $this->getDeleteTimeField();
$force = $this->isForce();
if ($name && !$force) {
// 软删除
$this->exists()->withEvent(false)->save([$name => $this->getDateTime($name)]);
$this->withEvent(true);
$this->trigger('AfterDelete');
$this->exists(false);
$this->clear();
return true;
}
return parent::delete();
}
/**
* 删除记录.
*
* @param mixed $data 主键列表 支持闭包查询条件
* @param bool $force 是否强制删除
*
* @return bool
*/
public static function destroy($data, bool $force = false): bool
{
// 传入空值包括空字符串和空数组的时候不会做任何的数据删除操作但传入0则是有效的
if (empty($data) && 0 !== $data) {
return false;
}
$query = (new static())->db();
if ($force) {
$query->removeOption('soft_delete');
}
if (is_array($data) && key($data) !== 0) {
$query->where($data);
$data = [];
} elseif ($data instanceof Closure) {
call_user_func_array($data, [ &$query]);
$data = [];
}
$resultSet = $query->select((array) $data);
foreach ($resultSet as $result) {
/** @var Model $result */
$result->force($force)->delete();
}
return true;
}
/**
* 恢复被软删除的记录.
*
* @param array $where 更新条件
*
* @return bool
*/
public function restore(array $where = []): bool
{
$name = $this->getDeleteTimeField();
if (!$name || false === $this->trigger('BeforeRestore')) {
return false;
}
$db = $this->getDbWhere($where);
// 恢复删除
$db->useSoftDelete($name, $this->getWithTrashedExp())
->update([$name => $this->getOption('defaultSoftDelete')]);
$this->trigger('AfterRestore');
return true;
}
/**
* 获取软删除字段.
*
* @param bool $read 是否查询操作 写操作的时候会自动去掉表别名
*
* @return string|false
*/
public function getDeleteTimeField(bool $read = false): bool | string
{
$field = $this->getOption('deleteTime', 'delete_time');
if (false === $field) {
return false;
}
if (!str_contains($field, '.')) {
$field = '__TABLE__.' . $field;
}
if (!$read && str_contains($field, '.')) {
$array = explode('.', $field);
$field = array_pop($array);
}
return $field;
}
/**
* 查询的时候默认排除软删除数据.
*
* @param Query $query
*
* @return void
*/
protected function withNoTrashed(Query $query): void
{
$field = $this->getDeleteTimeField(true);
if ($field) {
$condition = is_null($this->getOption('defaultSoftDelete')) ? ['null', ''] : ['=', $this->getOption('defaultSoftDelete')];
$query->useSoftDelete($field, $condition);
}
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare (strict_types = 1);
namespace think\model\contract;
interface EnumTransform
{
public function value();
}

View File

@@ -0,0 +1,17 @@
<?php
declare (strict_types = 1);
namespace think\model\contract;
use think\model\contract\Modelable as Model;
interface FieldTypeTransform
{
public static function get(mixed $value, Model $model): ?static;
/**
* @return static|mixed
*/
public static function set($value, Model $model) : mixed;
}

View File

@@ -0,0 +1,9 @@
<?php
declare (strict_types = 1);
namespace think\model\contract;
interface Modelable
{
}

View File

@@ -0,0 +1,17 @@
<?php
declare (strict_types = 1);
namespace think\model\contract;
use think\model\contract\Modelable as Model;
interface Typeable
{
public static function from(mixed $value, Model $model);
/**
* @return mixed
*/
public function value();
}

View File

@@ -0,0 +1,350 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\relation;
use Closure;
use think\db\BaseQuery as Query;
use think\helper\Str;
use think\model\contract\Modelable as Model;
/**
* BelongsTo关联类.
*/
class BelongsTo extends OneToOne
{
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 关联主键
* @param string $relation 关联名
*/
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->query = (new $model())->db();
$this->relation = $relation;
if (get_class($parent) == $model) {
$this->selfRelation = true;
}
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Model
*/
public function getRelation(array $subRelation = [], ?Closure $closure = null)
{
if ($closure) {
$closure($this->query);
}
$foreignKey = $this->foreignKey;
$relationModel = $this->query
->removeWhereField($this->localKey)
->where($this->localKey, $this->parent->$foreignKey)
->relation($subRelation)
->find();
if ($relationModel) {
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->parent->bindRelationAttr($relationModel, $this->bindAttr);
}
} else {
$default = $this->query->getOption('default_model');
$relationModel = $this->getDefaultModel($default);
}
return $relationModel;
}
/**
* 创建关联统计子查询.
*
* @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 = 'id', &$name = ''): string
{
if ($closure) {
$closure($this->query, $name);
}
$alias = Str::snake(class_basename($this->model));
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
return $this->query
->alias($alias)
->whereExp($alias . '.' . $this->localKey, '=' . $this->parent->getTable(true) . '.' . $this->foreignKey)
->fetchSql()
->$aggregate($field);
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return int
*/
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null)
{
$foreignKey = $this->foreignKey;
if (!isset($result->$foreignKey)) {
return 0;
}
if ($closure) {
$closure($this->query, $name);
}
return $this->query
->where($this->localKey, '=', $result->$foreignKey)
->$aggregate($field);
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $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) : Query
{
$table = $this->query->getTable();
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
if ($this->isSelfRelation() && $alias == $relation) {
$relation .= '_';
}
return $query->alias($alias)
->whereExists(function ($query) use ($table, $alias, $relation) {
$query->table([$table => $relation])
->field($relation . '.' . $this->localKey)
->whereColumn($alias . '.' . $this->foreignKey, $relation . '.' . $this->localKey);
$this->getRelationSoftDelete($query, $relation);
});
}
/**
* 根据关联条件查询当前模型.
*
* @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, string $logic = '', string $relationAlias = ''): Query
{
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$table = $this->query->getTable();
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
$fields = $this->getRelationQueryFields($fields, $alias);
$relAlias = $relationAlias ?: $relation;
if ($this->isSelfRelation() && $alias == $relAlias) {
$relAlias .= '_';
}
$query->alias($alias)
->via($alias)
->field($fields)
->join([$table => $relAlias], $alias . '.' . $this->foreignKey . '=' . $relAlias . '.' . $this->localKey, $joinType);
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
}
/**
* 预载入关联查询(数据集).
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
protected function eagerlySet(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->$foreignKey)) {
$range[] = $result->$foreignKey;
}
}
if (!empty($range)) {
$this->query->removeWhereField($localKey);
$default = $this->query->getOption('default_model');
$defaultModel = $this->getDefaultModel($default);
$range = array_unique($range);
$data = $this->eagerlyWhere([
[$localKey, 'in', $range],
], $localKey, $subRelation, $closure, $cache, count($range) > 1 ? true : false);
// 动态绑定参数
$bindAttr = $this->query->getOption('bind_attr');
if ($bindAttr) {
$this->bind($bindAttr);
}
// 关联数据封装
foreach ($resultSet as $result) {
// 关联模型
if (!isset($data[$result->$foreignKey])) {
$relationModel = $defaultModel;
} else {
$relationModel = $data[$result->$foreignKey];
}
// 设置关联属性
if (!empty($this->bindAttr) && $relationModel) {
$result->bindRelationAttr($relationModel, $this->bindAttr, $relation);
} else {
$result->setRelation($relation, $relationModel);
}
}
}
}
/**
* 预载入关联查询(数据).
*
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
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, $subRelation, $closure, $cache);
// 关联模型
if (!isset($data[$result->$foreignKey])) {
$default = $this->query->getOption('default_model');
$relationModel = $this->getDefaultModel($default);
} else {
$relationModel = $data[$result->$foreignKey];
}
// 动态绑定参数
$bindAttr = $this->query->getOption('bind_attr');
if ($bindAttr) {
$this->bind($bindAttr);
}
// 设置关联属性
if (!empty($this->bindAttr) && $relationModel) {
$result->bindRelationAttr($relationModel, $this->bindAttr, $relation);
} else {
$result->setRelation($relation, $relationModel);
}
}
/**
* 添加关联数据.
*
* @param Model $model关联模型对象
*
* @return Model
*/
public function associate(Model $model): Model
{
$this->parent->set($this->foreignKey, $model->getKey());
$this->parent->save();
return $this->parent->setRelation($this->relation, $model);
}
/**
* 注销关联数据.
*
* @return Model
*/
public function dissociate(): Model
{
$foreignKey = $this->foreignKey;
$this->parent->set($foreignKey, null);
$this->parent->save();
return $this->parent->setRelation($this->relation, null);
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
if (empty($this->baseQuery)) {
if (isset($this->parent->{$this->foreignKey})) {
// 关联查询带入关联条件
$this->query->where($this->localKey, '=', $this->parent->{$this->foreignKey});
}
$this->baseQuery = true;
}
}
}

View File

@@ -0,0 +1,735 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\relation;
use Closure;
use think\Collection;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use think\db\Raw;
use think\helper\Str;
use think\model\contract\Modelable as 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';
/**
* 绑定的关联属性.
*
* @var array
*/
protected $bindAttr = [];
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $middle 中间表/模型名
* @param string $foreignKey 关联模型外键
* @param string $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 (str_contains($middle, '\\')) {
$this->pivotName = $middle;
$this->middle = Str::snake(class_basename($middle));
} else {
$this->middle = $middle;
}
$this->query = (new $model())->db();
$this->pivot = $this->newPivot();
}
/**
* 设置中间表模型.
*
* @param $pivot
*
* @return $this
*/
public function pivot(string $pivot)
{
$this->pivotName = $pivot;
return $this;
}
/**
* 设置中间表数据名称.
*
* @param string $name
*
* @return $this
*/
public function name(string $name)
{
$this->pivotDataName = $name;
return $this;
}
/**
* 绑定关联表的属性到父模型属性.
*
* @param array $attr 要绑定的属性列表
*
* @return $this
*/
public function bind(array $attr)
{
$this->bindAttr = $attr;
return $this;
}
/**
* 实例化中间表模型.
*
* @param $data
*
* @throws Exception
*
* @return Pivot
*/
protected function newPivot(array $data = []): Pivot
{
$class = $this->pivotName ?: Pivot::class;
$pivot = new $class($data, $this->parent, $this->middle);
if ($pivot instanceof Pivot) {
return $pivot;
} else {
throw new Exception('pivot model must extends: \think\model\Pivot');
}
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Collection
*/
public function getRelation(array $subRelation = [], ?Closure $closure = null): Collection
{
if ($closure) {
$closure($this->query);
}
return $this->relation($subRelation)->select();
}
/**
* 组装Pivot模型.
*
* @param Model $result 模型对象
*
* @return array
*/
protected function matchPivot(Model $result): array
{
$pivot = $result->getRelation('pivot');
$bindAttr = $this->query->getOption('bind_attr');
if (empty($bindAttr)) {
$bindAttr = $this->bindAttr;
}
foreach ($pivot as $attr => $val) {
$pos = array_search($attr, $bindAttr);
if (false !== $pos) {
// 中间表属性绑定
$key = !is_numeric($pos) ? $pos : $attr;
if (null !== $result->getOrigin($key)) {
throw new Exception('bind attr has exists:' . $attr);
}
$result->set($key, $val);
}
}
$result->setRelation($this->pivotDataName, $this->newPivot($pivot));
return $pivot;
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $operator 比较操作符
* @param integer $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query|null $query Query对象
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', ?Query $query = null): Query
{
$table = $this->query->getTable();
$pivot = $this->pivot->getTable();
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
if ('=' === $operator && 0 === $count) {
return $query->alias($alias)
->whereNotExists(function ($query) use ($pivot, $alias, $relation, $table) {
$query->table([$pivot => 'pivot'])
->field('pivot.' . $this->foreignKey)
->join($table . ' ' . $relation, $relation . '.' . $this->query->getPk() . '= pivot.' . $this->foreignKey)
->whereColumn($alias . '.' . $this->parent->getPk(), 'pivot.' . $this->localKey);
$this->getRelationSoftDelete($query, $relation);
});
}
$query->alias($alias)
->field($model . '.*')
->join([$pivot => 'pivot'], 'pivot.' . $this->localKey . '=' . $alias . '.' . $this->parent->getPk(), $joinType)
->join($table . ' ' . $relation, $relation . '.' . $this->query->getPk() . '= pivot.' . $this->foreignKey, $joinType)
->group($alias . '.' . $this->parent->getPk())
->having('count(' . $id . ')' . $operator . $count);
return $this->getRelationSoftDelete($query, $relation);
}
/**
* 根据关联条件查询当前模型
* @access public
* @param array|Closure $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query|null $query Query对象
* @return Query
*/
public function hasWhere($where = [], $fields = null, string $joinType = '', ?Query $query = null, string $logic = '', string $relationAlias = ''): Query
{
$table = $this->query->getTable();
$pivot = $this->pivot->getTable();
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
$fields = $this->getRelationQueryFields($fields, $alias);
$relAlias = $relationAlias ?: $relation;
$query->alias($alias)
->join([$pivot => 'pivot'], 'pivot.' . $this->localKey . '=' . $alias . '.' . $this->parent->getPk(), $joinType)
->join([$table => $relAlias], $relAlias . '.' . $this->query->getPk() . '= pivot.' . $this->foreignKey, $joinType)
->group($alias . '.' . $this->parent->getPk())
->field($fields);
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
}
/**
* 设置中间表的查询条件.
*
* @param string $field
* @param string $op
* @param mixed $condition
*
* @return $this
*/
public function wherePivot($field, $op = null, $condition = null)
{
$this->query->where('pivot.' . $field, $op, $condition);
return $this;
}
/**
* 预载入关联查询(数据集).
*
* @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;
$pk = $resultSet[0]->getPk();
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (isset($result->$pk)) {
$range[] = $result->$pk;
}
}
if (!empty($range)) {
// 查询关联数据
$range = array_unique($range);
$data = $this->eagerlyManyToMany([
['pivot.' . $localKey, 'in', $range],
], $subRelation, $closure, $cache, count($range) > 1 ? true : false);
// 关联数据封装
foreach ($resultSet as $result) {
if (!isset($data[$result->$pk])) {
$data[$result->$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$result->$pk]));
}
}
}
/**
* 预载入关联查询(单个数据).
*
* @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 (is_string($pk) && isset($result->$pk)) {
$pk = $result->$pk;
// 查询管理数据
$data = $this->eagerlyManyToMany([
['pivot.' . $this->localKey, '=', $pk],
], $subRelation, $closure, $cache);
// 关联数据封装
if (!isset($data[$pk])) {
$data[$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$pk]));
}
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return int
*/
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null)
{
$pk = $result->getPk();
if (!isset($result->$pk)) {
return 0;
}
$pk = $result->$pk;
if ($closure) {
$closure($this->query, $name);
}
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
['pivot.' . $this->localKey, '=', $pk],
])->$aggregate($field);
}
/**
* 获取关联统计子查询.
*
* @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 = 'id', ? string &$name = null) : string
{
if ($closure) {
$closure($this->query, $name);
}
$alias = Str::snake(class_basename($this->model));
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
if (!str_contains($field, '.')) {
$field = $alias . '.' . $field;
}
$this->query->alias($alias);
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
[
'pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->getTable(true) . '.' . $this->parent->getPk()),
],
])->fetchSql()->$aggregate($field);
}
/**
* 多对多 关联模型预查询.
*
* @param array $where 关联预查询条件
* @param array $subRelation 子关联
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @param bool $collection 是否数据集查询
*
* @return array
*/
protected function eagerlyManyToMany(array $where, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false) : array
{
if ($closure) {
$closure($this->query);
}
$withLimit = $this->query->getOption('limit');
if ($withLimit && $collection) {
$this->query->removeOption('limit');
}
if ($this->isOneofMany) {
// 仅获取一条关联数据
if (!$collection) {
$this->query->limit(1);
} else {
$withLimit = 1;
}
}
// 预载入关联查询 支持嵌套预载入
$list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)
->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->lazy();
// 组装模型数据
$data = [];
foreach ($list as $set) {
$pivot = $this->matchPivot($set);
$key = $pivot[$this->localKey];
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
continue;
}
$data[$key][] = $set;
}
return $data;
}
/**
* BELONGS TO MANY 关联查询.
*
* @param string $foreignKey 关联模型关联键
* @param string $localKey 当前模型关联键
* @param array $condition 关联查询条件
*
* @return Query
*/
protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query
{
// 关联查询封装
if (empty($this->baseQuery)) {
$tableName = $this->query->getTable(true);
$table = $this->pivot->db()->getTable();
$fields = $this->getQueryFields($tableName);
$this->query
->field($fields)
->tableField(true, $table, 'pivot', 'pivot__')
->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $this->query->getPk())
->where($condition);
}
return $this->query;
}
/**
* 保存(新增)当前关联数据对象
*
* @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
* @param array $pivot 中间表额外数据
*
* @return array|Pivot
*/
public function save($data, array $pivot = [])
{
// 保存关联表/中间表数据
return $this->attach($data, $pivot);
}
/**
* 批量保存当前关联数据对象
*
* @param iterable $dataSet 数据集
* @param array $pivot 中间表额外数据
* @param bool $samePivot 额外数据是否相同
*
* @return array|false
*/
public function saveAll(iterable $dataSet, array $pivot = [], bool $samePivot = false)
{
$result = [];
foreach ($dataSet as $key => $data) {
if (!$samePivot) {
$pivotData = $pivot[$key] ?? [];
} else {
$pivotData = $pivot;
}
$result[] = $this->attach($data, $pivotData);
}
return empty($result) ? false : $result;
}
/**
* 附加关联的一个中间表数据.
*
* @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
* @param array $pivot 中间表额外数据
*
* @throws Exception
*
* @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);
}
} elseif (is_numeric($data) || is_string($data)) {
// 根据关联表主键直接写入中间表
$id = $data;
} elseif ($data instanceof Model) {
// 根据关联表主键直接写入中间表
$id = $data->getKey();
}
if (!empty($id)) {
// 保存中间表数据
$pivot[$this->localKey] = $this->parent->getKey();
$ids = (array) $id;
foreach ($ids as $id) {
$pivot[$this->foreignKey] = $id;
$object = $this->newPivot();
$object->replace()->save($pivot);
$result[] = $object;
}
if (count($result) == 1) {
// 返回中间表模型对象
$result = $result[0];
}
return $result;
} else {
throw new Exception('miss relation data');
}
}
/**
* 判断是否存在关联数据.
*
* @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->foreignKey, $id)
->find();
return $pivot ?: false;
}
/**
* 解除关联的一个中间表数据.
*
* @param int|array $data 数据 可以使用关联对象的主键
* @param bool $relationDel 是否同时删除关联表数据
*
* @return int
*/
public function detach($data = null, bool $relationDel = false): int
{
if (is_array($data)) {
$id = $data;
} elseif (is_numeric($data) || is_string($data)) {
// 根据关联表主键直接写入中间表
$id = $data;
} elseif ($data instanceof Model) {
// 根据关联表主键直接写入中间表
$id = $data->getKey();
}
// 删除中间表数据
$pivot = [];
$pivot[] = [$this->localKey, '=', $this->parent->getKey()];
if (isset($id)) {
$pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id];
}
$result = $this->newPivot()->where($pivot)->delete();
// 删除关联表数据
if (isset($id) && $relationDel) {
$model = $this->model;
$model::destroy($id);
}
return $result;
}
/**
* 数据同步.
*
* @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())
->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;
} elseif (count($attributes) > 0) {
$this->detach($id);
$this->attach($id, $attributes);
$changes['updated'][] = $id;
}
}
return $changes;
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
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(true) . '.' . $this->parent->getPk())];
} else {
$condition = ['pivot.' . $localKey, '=', $this->parent->getKey()];
}
$this->belongsToManyQuery($foreignKey, $localKey, [$condition]);
$this->baseQuery = true;
}
}
}

View File

@@ -0,0 +1,386 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\relation;
use Closure;
use think\Collection;
use think\db\BaseQuery as Query;
use think\helper\Str;
use think\model\contract\Modelable as Model;
use think\model\Relation;
/**
* 一对多关联类.
*/
class HasMany extends Relation
{
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $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->query = (new $model())->db();
if (get_class($parent) == $model) {
$this->selfRelation = true;
}
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Collection
*/
public function getRelation(array $subRelation = [], ?Closure $closure = null): Collection
{
if ($closure) {
$closure($this->query);
}
return $this->query
->where($this->foreignKey, $this->parent->{$this->localKey})
->relation($subRelation)
->select();
}
/**
* 预载入关联查询.
*
* @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;
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (isset($result->$localKey)) {
$range[] = $result->$localKey;
}
}
if (!empty($range)) {
$range = array_unique($range);
$data = $this->eagerlyOneToMany([
[$this->foreignKey, 'in', $range],
], $subRelation, $closure, $cache, count($range) > 1 ? true : false);
// 关联数据封装
foreach ($resultSet as $result) {
$pk = $result->$localKey;
if (!isset($data[$pk])) {
$data[$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$pk]));
}
}
}
/**
* 预载入关联查询.
*
* @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;
if (isset($result->$localKey)) {
$pk = $result->$localKey;
$data = $this->eagerlyOneToMany([
[$this->foreignKey, '=', $pk],
], $subRelation, $closure, $cache);
// 关联数据封装
if (!isset($data[$pk])) {
$data[$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$pk]));
}
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return int
*/
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null)
{
$localKey = $this->localKey;
if (!isset($result->$localKey)) {
return 0;
}
if ($closure) {
$closure($this->query, $name);
}
return $this->query
->where($this->foreignKey, '=', $result->$localKey)
->$aggregate($field);
}
/**
* 创建关联统计子查询.
*
* @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 = 'id', ? string &$name = null) : string
{
if ($closure) {
$closure($this->query, $name);
}
$alias = Str::snake(class_basename($this->model));
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
return $this->query->alias($alias)
->whereExp($alias . '.' . $this->foreignKey, '=' . $this->parent->getTable(true) . '.' . $this->localKey)
->fetchSql()
->$aggregate($field);
}
/**
* 一对多 关联模型预查询.
*
* @param array $where 关联预查询条件
* @param array $subRelation 子关联
* @param Closure $closure
* @param array $cache 关联缓存
* @param bool $collection 是否数据集查询
*
* @return array
*/
protected function eagerlyOneToMany(array $where, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false) : array
{
$foreignKey = $this->foreignKey;
$this->query->removeWhereField($this->foreignKey);
// 预载入关联查询 支持嵌套预载入
if ($closure) {
$this->baseQuery = true;
$closure($this->query);
}
$withLimit = $this->query->getOption('limit');
if ($withLimit && $collection) {
$this->query->removeOption('limit');
}
if ($this->isOneofMany) {
if (!$collection) {
$this->query->limit(1);
} else {
$withLimit = 1;
}
}
$list = $this->query
->where($where)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->with($subRelation)
->lazy();
// 组装模型数据
$data = [];
foreach ($list as $set) {
$key = $set->$foreignKey;
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
continue;
}
$data[$key][] = $set;
}
return $data;
}
/**
* 保存(新增)当前关联数据对象
*
* @param array|Model $data 数据 可以使用数组 关联模型对象
* @param bool $replace 是否自动识别更新和写入
*
* @return Model|false
*/
public function save(array | Model $data, bool $replace = true)
{
$model = $this->make();
return $model->replace($replace)->save($data) ? $model : false;
}
/**
* 创建关联对象实例.
*
* @param array|Model $data
*
* @return Model
*/
public function make(array | Model $data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
}
// 保存关联表数据
$data[$this->foreignKey] = $this->parent->{$this->localKey};
return (new $this->model($data))->setSuffix($this->getModel()->getSuffix());
}
/**
* 批量保存当前关联数据对象
*
* @param iterable $dataSet 数据集
* @param bool $replace 是否自动识别更新和写入
*
* @return array|false
*/
public function saveAll(iterable $dataSet, bool $replace = true)
{
$result = [];
foreach ($dataSet as $key => $data) {
$result[] = $this->save($data, $replace);
}
return empty($result) ? false : $result;
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $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 = 'INNER', ?Query $query = null): Query
{
$table = $this->query->getTable();
$model = Str::snake(class_basename($this->parent));
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
return $query->alias($alias)
->whereExists(function ($query) use ($alias, $id, $table, $operator, $count) {
$table = $this->query->getTable();
$relation = Str::snake(class_basename($this->model));
if ($this->isSelfRelation() && $alias == $relation) {
$relation .= '_';
}
$query->table([$table => $relation])
->field('count(' . $id . ') AS count')
->whereColumn($relation . '.' . $this->foreignKey, $alias . '.' . $this->localKey)
->having('count ' . $operator . ' ' . $count);
$this->getRelationSoftDelete($query, $relation);
});
}
/**
* 根据关联条件查询当前模型.
*
* @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, string $logic = '', string $relationAlias = ''): Query
{
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$table = $this->query->getTable();
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
$fields = $this->getRelationQueryFields($fields, $alias);
$relAlias = $relationAlias ?: $relation;
if ($this->isSelfRelation() && $alias == $relAlias) {
$relAlias .= '_';
}
$query->alias($alias)
->via($alias)
->group($alias . '.' . $this->localKey)
->field($fields)
->join([$table => $relAlias], $alias . '.' . $this->localKey . '=' . $relAlias . '.' . $this->foreignKey, $joinType);
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
if (empty($this->baseQuery)) {
if (isset($this->parent->{$this->localKey})) {
// 关联查询带入关联条件
$this->query->where($this->foreignKey, '=', $this->parent->{$this->localKey});
}
$this->baseQuery = true;
}
}
}

View File

@@ -0,0 +1,391 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\relation;
use Closure;
use think\Collection;
use think\db\BaseQuery as Query;
use think\helper\Str;
use think\model\contract\Modelable as Model;
use think\model\Relation;
/**
* 远程一对多关联类.
*/
class HasManyThrough extends Relation
{
/**
* 中间关联表外键.
*
* @var string
*/
protected $throughKey;
/**
* 中间主键.
*
* @var string
*/
protected $throughPk;
/**
* 中间表查询对象
*
* @var Query
*/
protected $through;
/**
* 架构函数.
*
* @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, string $model, string $through, string $foreignKey, string $throughKey, string $localKey, string $throughPk)
{
$this->parent = $parent;
$this->model = $model;
$this->through = (new $through())->db();
$this->foreignKey = $foreignKey;
$this->throughKey = $throughKey;
$this->localKey = $localKey;
$this->throughPk = $throughPk;
$this->query = (new $model())->db();
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Collection
*/
public function getRelation(array $subRelation = [], ?Closure $closure = null)
{
if ($closure) {
$closure($this->query);
}
$this->baseQuery();
return $this->query->relation($subRelation)->select();
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $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 = 'INNER', ?Query $query = null): Query
{
// 子查询构建
$model = Str::snake(class_basename($this->parent));
$table = $this->through->getTable();
$relation = Str::snake(class_basename($this->model));
$relationTable = (new $this->model())->getTable();
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
// 统计子查询
$subQuery = $this->through
->field('COUNT(' . $id . ')')
->table($table)
->join([$relationTable => $relation], $relation . '.' . $this->throughKey . '=' . $table . '.' . $this->throughPk, $joinType)
->whereColumn($table . '.' . $this->throughPk, $model . '.' . $this->localKey);
$this->getRelationSoftDelete($subQuery, $relation);
return $query->alias($alias)->where('(' . $subQuery->buildSql() . ') ' . $operator . ' ' . $count);
}
/**
* 根据关联条件查询当前模型.
*
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function hasWhere($where = [], $fields = null, $joinType = '', ?Query $query = null, string $logic = '', string $relationAlias = ''): Query
{
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$table = $this->through->getTable();
$relationTable = (new $this->model())->getTable();
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
$relAlias = $relationAlias ?: $relation;
// EXISTS子查询
$subQuery = $this->through
->table($table)
->join([$relationTable => $relAlias], $relAlias . '.' . $this->throughKey . '=' . $table . '.' . $this->throughPk, $joinType)
->whereColumn($table . '.' . $this->throughPk, $alias . '.' . $this->localKey);
$this->getRelationSoftDelete($subQuery, $relAlias, $where, $logic);
return $query->alias($alias)->whereExists($subQuery->buildSql());
}
/**
* 预载入关联查询(数据集).
*
* @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);
$range = array_unique($range);
$data = $this->eagerlyWhere([
[$this->foreignKey, 'in', $range],
], $foreignKey, $subRelation, $closure, $cache, count($range) > 1 ? true : false);
// 关联数据封装
foreach ($resultSet as $result) {
$pk = $result->$localKey;
if (!isset($data[$pk])) {
$data[$pk] = [];
}
// 设置关联属性
$result->setRelation($relation, $this->resultSetBuild($data[$pk]));
}
}
}
/**
* 预载入关联查询(数据).
*
* @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;
$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]));
}
/**
* 关联模型预查询.
*
* @param array $where 关联预查询条件
* @param string $key 关联键名
* @param array $subRelation 子关联
* @param Closure $closure
* @param array $cache 关联缓存
* @param bool $collection 是否数据集查询
*
* @return array
*/
protected function eagerlyWhere(array $where, string $key, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false): array
{
// 预载入关联查询 支持嵌套预载入
$throughList = $this->through->where($where)->select();
$keys = $throughList->column($this->throughPk, $this->throughPk);
if ($closure) {
$this->baseQuery = true;
$closure($this->query);
}
$throughKey = $this->throughKey;
if ($this->baseQuery) {
$throughKey = Str::snake(class_basename($this->model)) . '.' . $this->throughKey;
}
$withLimit = $this->query->getOption('limit');
if ($withLimit && $collection) {
$this->query->removeOption('limit');
}
if ($this->isOneofMany) {
if (!$collection) {
$this->query->limit(1);
} else {
$withLimit = 1;
}
}
$list = $this->query
->where($throughKey, 'in', $keys)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->lazy();
// 组装模型数据
$data = [];
$keys = $throughList->column($this->foreignKey, $this->throughPk);
foreach ($list as $set) {
$key = $keys[$set->{$this->throughKey}];
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
continue;
}
$data[$key][] = $set;
}
return $data;
}
/**
* 关联统计
*
* @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 = 'id', ? string &$name = null)
{
$localKey = $this->localKey;
if (!isset($result->$localKey)) {
return 0;
}
if ($closure) {
$closure($this->query, $name);
}
$alias = Str::snake(class_basename($this->model));
$alias = $this->query->getAlias() ?: $alias;
$throughTable = $this->through->getTable();
$pk = $this->throughPk;
$throughKey = $this->throughKey;
$modelTable = $this->parent->getTable();
if (!str_contains($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);
}
/**
* 创建关联统计子查询.
*
* @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 = 'id', ? string &$name = null) : string
{
if ($closure) {
$closure($this->query, $name);
}
$alias = Str::snake(class_basename($this->model));
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
$throughTable = $this->through->getTable();
$pk = $this->throughPk;
$throughKey = $this->throughKey;
$modelTable = $this->parent->getTable();
if (!str_contains($field, '.')) {
$field = $alias . '.' . $field;
}
return $this->query
->alias($alias)
->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
->whereColumn($throughTable . '.' . $this->foreignKey, $this->parent->getTable() . '.' . $this->localKey)
->fetchSql()
->$aggregate($field);
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery() : void
{
if (empty($this->baseQuery) && $this->parent->getData()) {
$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);
$this->query
->field($fields)
->alias($alias)
->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey});
$this->baseQuery = true;
}
}
}

View File

@@ -0,0 +1,317 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\relation;
use Closure;
use think\db\BaseQuery as Query;
use think\helper\Str;
use think\model\contract\Modelable as Model;
/**
* HasOne 关联类.
*/
class HasOne extends OneToOne
{
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $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->query = (new $model())->db();
if (get_class($parent) == $model) {
$this->selfRelation = true;
}
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Model
*/
public function getRelation(array $subRelation = [], ?Closure $closure = null)
{
$localKey = $this->localKey;
if ($closure) {
$closure($this->query);
}
// 判断关联类型执行查询
$relationModel = $this->query
->removeWhereField($this->foreignKey)
->where($this->foreignKey, $this->parent->$localKey)
->relation($subRelation)
->find();
if ($relationModel) {
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->parent->bindRelationAttr($relationModel, $this->bindAttr);
}
} else {
$default = $this->query->getOption('default_model');
$relationModel = $this->getDefaultModel($default);
}
return $relationModel;
}
/**
* 创建关联统计子查询.
*
* @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 = 'id', ? string &$name = null) : string
{
if ($closure) {
$closure($this->query, $name);
}
$alias = Str::snake(class_basename($this->model));
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
return $this->query
->alias($alias)
->whereExp($alias . '.' . $this->foreignKey, '=' . $this->parent->getTable(true) . '.' . $this->localKey)
->fetchSql()
->$aggregate($field);
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return int
*/
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null)
{
$localKey = $this->localKey;
if (!isset($result->$localKey)) {
return 0;
}
if ($closure) {
$closure($this->query, $name);
}
return $this->query
->where($this->foreignKey, '=', $result->$localKey)
->$aggregate($field);
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $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) : Query
{
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$table = $this->query->getTable();
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
$method = (0 == $count && '=' == $operator) ? 'whereNotExists' : 'whereExists';
if ($this->isSelfRelation() && $alias == $relation) {
$relation .= '_';
}
return $query->alias($alias)->$method(function ($query) use ($table, $alias, $relation) {
$query->table([$table => $relation])
->field($relation . '.' . $this->foreignKey)
->whereColumn($alias . '.' . $this->localKey, $relation . '.' . $this->foreignKey);
$this->getRelationSoftDelete($query, $relation);
});
}
/**
* 根据关联条件查询当前模型.
*
* @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, string $logic = '', string $relationAlias = ''): Query
{
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$table = $this->query->getTable();
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
$fields = $this->getRelationQueryFields($fields, $alias);
$relAlias = $relationAlias ?: $relation;
if ($this->isSelfRelation() && $alias == $relAlias) {
$relAlias .= '_';
}
$query->alias($alias)
->via($alias)
->field($fields)
->join([$table => $relAlias], $alias . '.' . $this->localKey . '=' . $relAlias . '.' . $this->foreignKey, $joinType);
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
}
/**
* 预载入关联查询(数据集).
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
protected function eagerlySet(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);
$default = $this->query->getOption('default_model');
$defaultModel = $this->getDefaultModel($default);
$range = array_unique($range);
$data = $this->eagerlyWhere([
[$foreignKey, 'in', $range],
], $foreignKey, $subRelation, $closure, $cache, count($range) > 1 ? true : false);
// 动态绑定参数
$bindAttr = $this->query->getOption('bind_attr');
if ($bindAttr) {
$this->bind($bindAttr);
}
// 关联数据封装
foreach ($resultSet as $result) {
// 关联模型
if (!isset($data[$result->$localKey])) {
$relationModel = $defaultModel;
} else {
$relationModel = $data[$result->$localKey];
}
// 设置关联属性
if (!empty($this->bindAttr) && $relationModel) {
$result->bindRelationAttr($relationModel, $this->bindAttr, $relation);
} else {
$result->setRelation($relation, $relationModel);
}
}
}
}
/**
* 预载入关联查询(数据).
*
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
protected function eagerlyOne(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])) {
$default = $this->query->getOption('default_model');
$relationModel = $this->getDefaultModel($default);
} else {
$relationModel = $data[$result->$localKey];
}
// 动态绑定参数
$bindAttr = $this->query->getOption('bind_attr');
if ($bindAttr) {
$this->bind($bindAttr);
}
// 设置关联属性
if (!empty($this->bindAttr) && $relationModel) {
$result->bindRelationAttr($relationModel, $this->bindAttr, $relation);
} else {
$result->setRelation($relation, $relationModel);
}
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
if (empty($this->baseQuery)) {
if (isset($this->parent->{$this->localKey})) {
// 关联查询带入关联条件
$this->query->where($this->foreignKey, '=', $this->parent->{$this->localKey});
}
$this->baseQuery = true;
}
}
}

View File

@@ -0,0 +1,164 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\relation;
use Closure;
use think\model\contract\Modelable as Model;
/**
* 远程一对一关联类.
*/
class HasOneThrough extends HasManyThrough
{
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Model
*/
public function getRelation(array $subRelation = [], ?Closure $closure = null)
{
if ($closure) {
$closure($this->query);
}
$this->baseQuery();
$relationModel = $this->query->relation($subRelation)->find();
if ($relationModel) {
} else {
$default = $this->query->getOption('default_model');
$relationModel = $this->getDefaultModel($default);
}
return $relationModel;
}
/**
* 预载入关联查询(数据集).
*
* @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);
$default = $this->query->getOption('default_model');
$defaultModel = $this->getDefaultModel($default);
$data = $this->eagerlyWhere([
[$this->foreignKey, 'in', $range],
], $foreignKey, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
// 关联模型
if (!isset($data[$result->$localKey])) {
$relationModel = $defaultModel;
} else {
$relationModel = $data[$result->$localKey];
}
// 设置关联属性
$result->setRelation($relation, $relationModel);
}
}
}
/**
* 预载入关联查询(数据).
*
* @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])) {
$default = $this->query->getOption('default_model');
$relationModel = $this->getDefaultModel($default);
} else {
$relationModel = $data[$result->$localKey];
}
$result->setRelation($relation, $relationModel);
}
/**
* 关联模型预查询.
*
* @param array $where 关联预查询条件
* @param string $key 关联键名
* @param array $subRelation 子关联
* @param Closure $closure
* @param array $cache 关联缓存
* @param bool $collection 是否数据集查询
*
* @return array
*/
protected function eagerlyWhere(array $where, string $key, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false): array
{
// 预载入关联查询 支持嵌套预载入
$keys = $this->through->where($where)->column($this->throughPk, $this->foreignKey);
if ($closure) {
$closure($this->query);
}
$list = $this->query
->where($this->throughKey, 'in', $keys)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select();
// 组装模型数据
return array_map(function ($key) use ($list) {
$set = $list->where($this->throughKey, '=', $key)->first();
return $set ?: null;
}, $keys);
}
}

View File

@@ -0,0 +1,436 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\relation;
use Closure;
use think\Collection;
use think\db\BaseQuery as Query;
use think\helper\Str;
use think\model\contract\Modelable as Model;
use think\model\Relation;
/**
* 多态一对多关联.
*/
class MorphMany extends Relation
{
/**
* 多态关联外键.
*
* @var string
*/
protected $morphKey;
/**
* 多态字段名.
*
* @var string
*/
protected $morphType;
/**
* 多态类型.
*
* @var string
*/
protected $type;
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $morphKey 关联外键
* @param string $morphType 多态字段名
* @param string $type 多态类型
*/
public function __construct(Model $parent, string $model, string $morphKey, string $morphType, string $type)
{
$this->parent = $parent;
$this->model = $model;
$this->type = $type;
$this->morphKey = $morphKey;
$this->morphType = $morphType;
$this->query = (new $model())->db();
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Collection
*/
public function getRelation(array $subRelation = [], ?Closure $closure = null): Collection
{
if ($closure) {
$closure($this->query);
}
$this->baseQuery();
return $this->query->relation($subRelation)->select();
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $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)
{
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$table = $this->query->getTable();
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
$query->alias($alias)
->field($alias . '.*')
->join([$table => $relation], $alias . '.' . $this->parent->getPk() . '=' . $relation . '.' . $this->morphKey)
->where($relation . '.' . $this->morphType, '=', $this->type)
->group($relation . '.' . $this->morphKey)
->having('count(' . $id . ')' . $operator . $count);
return $this->getRelationSoftDelete($query, $relation);
}
/**
* 根据关联条件查询当前模型.
*
* @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, string $logic = '', string $relationAlias = '')
{
$table = $this->query->getTable();
$query = $query ?: $this->parent->db();
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$alias = $query->getAlias() ?: $model;
$fields = $this->getRelationQueryFields($fields, $alias);
$relAlias = $relationAlias ?: $relation;
$query->alias($alias)
->join([$table => $relAlias], $alias . '.' . $this->parent->getPk() . '=' . $relAlias . '.' . $this->morphKey, $joinType)
->where($relAlias . '.' . $this->morphType, '=', $this->type)
->group($relAlias . '.' . $this->morphKey)
->field($fields);
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
}
/**
* 预载入关联查询.
*
* @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
{
$morphType = $this->morphType;
$morphKey = $this->morphKey;
$type = $this->type;
$range = [];
foreach ($resultSet as $result) {
$pk = $result->getPk();
// 获取关联外键列表
if (isset($result->$pk)) {
$range[] = $result->$pk;
}
}
if (!empty($range)) {
$where = [
[$morphKey, 'in', array_unique($range)],
[$morphType, '=', $type],
];
$data = $this->eagerlyMorphToMany($where, $subRelation, $closure, $cache, true);
// 关联数据封装
foreach ($resultSet as $result) {
if (!isset($data[$result->$pk])) {
$data[$result->$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$result->$pk]));
}
}
}
/**
* 预载入关联查询.
*
* @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)) {
$key = $result->$pk;
$data = $this->eagerlyMorphToMany([
[$this->morphKey, '=', $key],
[$this->morphType, '=', $this->type],
], $subRelation, $closure, $cache);
if (!isset($data[$key])) {
$data[$key] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$key]));
}
}
/**
* 关联统计
*
* @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 = 'id', ? string &$name = null)
{
$pk = $result->getPk();
if (!isset($result->$pk)) {
return 0;
}
if ($closure) {
$closure($this->query, $name);
}
return $this->query
->where([
[$this->morphKey, '=', $result->$pk],
[$this->morphType, '=', $this->type],
])
->$aggregate($field);
}
/**
* 获取关联统计子查询.
*
* @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 = 'id', ? string &$name = null) : string
{
if ($closure) {
$closure($this->query, $name);
}
$alias = Str::snake(class_basename($this->model));
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
return $this->query
->alias($alias)
->whereColumn($alias . '.' . $this->morphKey, $this->parent->getTable(true) . '.' . $this->parent->getPk())
->where($alias . '.' . $this->morphType, '=', $this->type)
->fetchSql()
->$aggregate($field);
}
/**
* 多态一对多 关联模型预查询.
*
* @param array $where 关联预查询条件
* @param array $subRelation 子关联
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @param bool $collection 是否数据集查询
*
* @return array
*/
protected function eagerlyMorphToMany(array $where, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false) : array
{
// 预载入关联查询 支持嵌套预载入
$this->query->removeOption('where');
if ($closure) {
$this->baseQuery = true;
$closure($this->query);
}
$withLimit = $this->query->getOption('limit');
if ($withLimit && $collection) {
$this->query->removeOption('limit');
}
if ($this->isOneofMany) {
// 仅获取一条关联数据
if (!$collection) {
$this->query->limit(1);
} else {
$withLimit = 1;
}
}
$list = $this->query
->where($where)
->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->lazy();
// 组装模型数据
$data = [];
$morphKey = $this->morphKey;
foreach ($list as $set) {
$key = $set->$morphKey;
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
continue;
}
$data[$key][] = $set;
}
return $data;
}
/**
* 保存(新增)当前关联数据对象
*
* @param array|Model $data 数据 可以使用数组 关联模型对象
* @param bool $replace 是否自动识别更新和写入
*
* @return Model|false
*/
public function save(array | Model $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();
}
// 保存关联表数据
$pk = $this->parent->getPk();
$data[$this->morphKey] = $this->parent->$pk;
$data[$this->morphType] = $this->type;
return (new $this->model($data))->setSuffix($this->getModel()->getSuffix());
}
/**
* 批量保存当前关联数据对象
*
* @param iterable $dataSet 数据集
* @param bool $replace 是否自动识别更新和写入
*
* @return array|false
*/
public function saveAll(iterable $dataSet, bool $replace = true)
{
$result = [];
foreach ($dataSet as $key => $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;
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
if (empty($this->baseQuery) && $this->parent->getData()) {
$pk = $this->parent->getPk();
$this->query->where([
[$this->morphKey, '=', $this->parent->$pk],
[$this->morphType, '=', $this->type],
]);
$this->baseQuery = true;
}
}
}

View File

@@ -0,0 +1,393 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\relation;
use Closure;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use think\helper\Str;
use think\model\contract\Modelable as 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 = [];
/**
* 构造函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $morphKey 关联外键
* @param string $morphType 多态字段名
* @param string $type 多态类型
*/
public function __construct(Model $parent, string $model, string $morphKey, string $morphType, string $type)
{
$this->parent = $parent;
$this->model = $model;
$this->type = $type;
$this->morphKey = $morphKey;
$this->morphType = $morphType;
$this->query = (new $model())->db();
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Model
*/
public function getRelation(array $subRelation = [], ?Closure $closure = null)
{
if ($closure) {
$closure($this->query);
}
$this->baseQuery();
$relationModel = $this->query->relation($subRelation)->find();
if ($relationModel) {
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($this->parent, $relationModel);
}
} else {
$default = $this->query->getOption('default_model');
$relationModel = $this->getDefaultModel($default);
}
return $relationModel;
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $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)
{
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$table = $this->query->getTable();
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
$query->alias($alias)
->field($alias . '.*')
->join([$table => $relation], $alias . '.' . $this->parent->getPk() . '=' . $relation . '.' . $this->morphKey)
->where($relation . '.' . $this->morphType, '=', $this->type)
->group($relation . '.' . $this->morphKey)
->having('count(' . $id . ')' . $operator . $count);
return $this->getRelationSoftDelete($query, $relation);
}
/**
* 根据关联条件查询当前模型.
*
* @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, string $logic = '', string $relationAlias = '')
{
$table = $this->query->getTable();
$model = Str::snake(class_basename($this->parent));
$relation = Str::snake(class_basename($this->model));
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
$fields = $this->getRelationQueryFields($fields, $alias);
$relAlias = $relationAlias ?: $relation;
$query->alias($alias)
->join([$table => $relAlias], $alias . '.' . $this->parent->getPk() . '=' . $relAlias . '.' . $this->morphKey, $joinType)
->where($relAlias . '.' . $this->morphType, '=', $this->type)
->group($relAlias . '.' . $this->morphKey)
->field($fields);
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
}
/**
* 预载入关联查询.
*
* @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
{
$morphType = $this->morphType;
$morphKey = $this->morphKey;
$type = $this->type;
$range = [];
foreach ($resultSet as $result) {
$pk = $result->getPk();
// 获取关联外键列表
if (isset($result->$pk)) {
$range[] = $result->$pk;
}
}
if (!empty($range)) {
$data = $this->eagerlyMorphToOne([
[$morphKey, 'in', $range],
[$morphType, '=', $type],
], $subRelation, $closure, $cache);
$default = $this->query->getOption('default_model');
$defaultModel = $this->getDefaultModel($default);
// 关联数据封装
foreach ($resultSet as $result) {
if (!isset($data[$result->$pk])) {
$relationModel = $defaultModel;
} else {
$relationModel = $data[$result->$pk];
}
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($result, $relationModel);
} else {
// 设置关联属性
$result->setRelation($relation, $relationModel);
}
}
}
}
/**
* 预载入关联查询.
*
* @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->eagerlyMorphToOne([
[$this->morphKey, '=', $pk],
[$this->morphType, '=', $this->type],
], $subRelation, $closure, $cache);
if (isset($data[$pk])) {
$relationModel = $data[$pk];
} else {
$default = $this->query->getOption('default_model');
$relationModel = $this->getDefaultModel($default);
}
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($result, $relationModel);
} else {
// 设置关联属性
$result->setRelation($relation, $relationModel);
}
}
}
/**
* 多态一对一 关联模型预查询.
*
* @param array $where 关联预查询条件
* @param array $subRelation 子关联
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return array
*/
protected function eagerlyMorphToOne(array $where, array $subRelation = [], ?Closure $closure = null, array $cache = []): array
{
// 预载入关联查询 支持嵌套预载入
if ($closure) {
$this->baseQuery = true;
$closure($this->query);
}
$list = $this->query
->where($where)
->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->lazy();
// 组装模型数据
$data = [];
$morphKey = $this->morphKey;
foreach ($list as $set) {
$data[$set->$morphKey] = $set;
}
return $data;
}
/**
* 保存(新增)当前关联数据对象
*
* @param array|Model $data 数据 可以使用数组 关联模型对象
* @param bool $replace 是否自动识别更新和写入
*
* @return Model|false
*/
public function save(array | Model $data, bool $replace = true)
{
$model = $this->make();
return $model->replace($replace)->save($data) ? $model : false;
}
/**
* 创建关联对象实例.
*
* @param array|Model $data
*
* @return Model
*/
public function make(array | Model $data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
}
// 保存关联表数据
$pk = $this->parent->getPk();
$data[$this->morphKey] = $this->parent->$pk;
$data[$this->morphType] = $this->type;
return (new $this->model($data))->setSuffix($this->getModel()->getSuffix());
}
/**
* 执行基础查询(进执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
if (empty($this->baseQuery) && $this->parent->getData()) {
$pk = $this->parent->getPk();
$this->query->where([
[$this->morphKey, '=', $this->parent->$pk],
[$this->morphType, '=', $this->type],
]);
$this->baseQuery = true;
}
}
/**
* 绑定关联表的属性到父模型属性.
*
* @param array $attr 要绑定的属性列表
*
* @return $this
*/
public function bind(array $attr)
{
$this->bindAttr = $attr;
return $this;
}
/**
* 获取绑定属性.
*
* @return array
*/
public function getBindAttr(): array
{
return $this->bindAttr;
}
/**
* 绑定关联属性到父模型.
*
* @param Model $result 父模型对象
* @param Model $model 关联模型对象
*
* @throws Exception
*
* @return void
*/
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->set($key, $model?->get($attr));
}
}
}

View File

@@ -0,0 +1,392 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\relation;
use BackedEnum;
use Closure;
use think\db\exception\DbException as Exception;
use think\db\Query;
use think\helper\Str;
use think\model\contract\Modelable as 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 = [];
/**
* 架构函数.
*
* @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;
}
/**
* 获取当前的关联模型类的实例.
*
* @return Model
*/
public function getModel(): Model
{
$morphType = $this->morphType;
$model = $this->parseModel($this->parent->$morphType);
return new $model();
}
/**
* 延迟获取关联数据.
*
* @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;
return class_exists($model) ? $this->buildQuery((new $model())->relation($subRelation))->find($pk) : null;
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $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;
}
/**
* 根据关联条件查询当前模型.
*
* @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, string $logic = '')
{
$model = Str::snake(class_basename($this->parent));
$types = $this->parent->distinct()->column($this->morphType);
$query = $query ?: $this->parent->db();
$alias = $query->getAlias() ?: $model;
return $query->alias($alias)
->where(function (Query $query) use ($types, $where, $alias, $logic) {
foreach ($types as $type) {
if ($type) {
$query->whereExists(function (Query $query) use ($type, $where, $alias, $logic) {
$class = $this->parseModel($type);
/** @var Model $model */
$model = new $class();
$table = $model->getTable();
$logic = 'OR' == $logic ? 'whereOr' : 'where';
$query
->table($table)
->where($alias . '.' . $this->morphType, $type)
->whereColumn($alias . '.' . $this->morphKey, $table . '.' . $model->getPk())
->$logic($where);
}, 'OR');
}
}
});
}
/**
* 解析模型的完整命名空间.
*
* @param string $model 模型名(或者完整类名)
*
* @return Model
*/
protected function parseModel($model): string
{
if ($model instanceof BackedEnum) {
$model = $model->value;
}
if (isset($this->alias[$model])) {
$model = $this->alias[$model];
}
if (!str_contains($model, '\\')) {
$path = explode('\\', get_class($this->parent));
array_pop($path);
array_push($path, Str::studly($model));
$model = implode('\\', $path);
}
return $model;
}
/**
* 设置多态别名.
*
* @param array $alias 别名定义
*
* @return $this
*/
public function setAlias(array $alias)
{
$this->alias = $alias;
return $this;
}
/**
* 移除关联查询参数.
*
* @return $this
*/
public function removeOption(string $option = '')
{
return $this;
}
/**
* 预载入关联查询.
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param ?Closure $closure 闭包
* @param array $cache 关联缓存
*
* @throws Exception
*
* @return void
*/
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);
$data = [];
if (class_exists($model)) {
$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);
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];
}
$result->setRelation($relation, $relationModel);
}
}
}
}
}
/**
* 预载入关联查询.
*
* @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);
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param ?Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
*
* @return int
*/
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = '*')
{
}
/**
* 多态MorphTo 关联模型预查询.
*
* @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);
}
$result->setRelation($relation, $data ?: null);
}
/**
* 添加关联数据.
*
* @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->set($morphKey, $model->$pk);
$this->parent->set($morphType, $type ?: get_class($model));
$this->parent->save();
return $this->parent->setRelation($this->relation, $model);
}
/**
* 注销关联数据.
*
* @return Model
*/
public function dissociate(): Model
{
$morphKey = $this->morphKey;
$morphType = $this->morphType;
$this->parent->set($morphKey, null);
$this->parent->set($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;
}
}

View File

@@ -0,0 +1,497 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model\relation;
use Closure;
use Exception;
use think\db\BaseQuery as Query;
use think\db\Raw;
use think\model\contract\Modelable as 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;
/**
* 架构函数.
*
* @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);
}
/**
* 预载入关联查询(数据集).
*
* @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', array_unique($range)],
['pivot.' . $this->morphType, '=', $this->morphClass],
], $subRelation, $closure, $cache, true);
// 关联数据封装
foreach ($resultSet as $result) {
if (!isset($data[$result->$pk])) {
$data[$result->$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$result->$pk]));
}
}
}
/**
* 预载入关联查询(单个数据).
*
* @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]));
}
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return int
*/
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = '*', ?string &$name = null)
{
$pk = $result->getPk();
if (!isset($result->$pk)) {
return 0;
}
if ($closure) {
$closure($this->query, $name);
}
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
['pivot.' . $this->localKey, '=', $result->$pk],
['pivot.' . $this->morphType, '=', $this->morphClass],
])->$aggregate($field);
}
/**
* 获取关联统计子查询.
*
* @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->query, $name);
}
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
['pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->getTable(true) . '.' . $this->parent->getPk())],
['pivot.' . $this->morphType, '=', $this->morphClass],
])->fetchSql()->$aggregate($field);
}
/**
* BELONGS TO MANY 关联查询.
*
* @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();
$fields = $this->getQueryFields($tableName);
$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;
}
/**
* 多对多 关联模型预查询.
*
* @param array $where 关联预查询条件
* @param array $subRelation 子关联
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @param bool $collection 是否数据集查询
*
* @return array
*/
protected function eagerlyManyToMany(array $where, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false): array
{
if ($closure) {
$closure($this->query);
}
$withLimit = $this->query->getOption('limit');
if ($withLimit && $collection) {
$this->query->removeOption('limit');
}
if ($this->isOneofMany) {
// 仅获取一条关联数据
if (!$collection) {
$this->query->limit(1);
} else {
$withLimit = 1;
}
}
// 预载入关联查询 支持嵌套预载入
$list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)
->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->lazy();
// 组装模型数据
$data = [];
foreach ($list as $set) {
$pivot = $set->getRelation('pivot');
$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;
}
/**
* 附加关联的一个中间表数据.
*
* @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);
}
} elseif (is_numeric($data) || is_string($data)) {
// 根据关联表主键直接写入中间表
$id = $data;
} elseif ($data instanceof Model) {
// 根据关联表主键直接写入中间表
$id = $data->getKey();
}
if (!empty($id)) {
// 保存中间表数据
$pivot[$this->localKey] = $this->parent->getKey();
$pivot[$this->morphType] = $this->morphClass;
$result = [];
foreach ((array) $ids as $id) {
$pivot[$this->foreignKey] = $id;
$object = $this->newPivot();
$object->replace()->save($pivot);
$result[] = $object;
}
if (count($result) == 1) {
// 返回中间表模型对象
$result = $result[0];
}
return $result;
} else {
throw new Exception('miss relation data');
}
}
/**
* 判断是否存在关联数据.
*
* @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;
}
/**
* 解除关联的一个中间表数据.
*
* @param int|array $data 数据 可以使用关联对象的主键
* @param bool $relationDel 是否同时删除关联表数据
*
* @return int
*/
public function detach($data = null, bool $relationDel = false): int
{
if (is_array($data)) {
$id = $data;
} elseif (is_numeric($data) || is_string($data)) {
// 根据关联表主键直接写入中间表
$id = $data;
} elseif ($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->newPivot()->where($pivot)->delete();
// 删除关联表数据
if (isset($id) && $relationDel) {
$model = $this->model;
$model::destroy($id);
}
return $result;
}
/**
* 数据同步.
*
* @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;
} elseif (count($attributes) > 0) {
$this->detach($id);
$this->attach($id, $attributes);
$changes['updated'][] = $id;
}
}
return $changes;
}
/**
* 执行基础查询(仅执行一次).
*
* @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;
}
}

View File

@@ -0,0 +1,347 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace think\model\relation;
use Closure;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use think\db\exception\InvalidArgumentException;
use think\helper\Str;
use think\model\contract\Modelable as Model;
use think\model\Relation;
/**
* 一对一关联基础类.
*/
abstract class OneToOne extends Relation
{
/**
* JOIN类型.
*
* @var string
*/
protected $joinType = 'INNER';
/**
* 绑定的关联属性.
*
* @var array
*/
protected $bindAttr = [];
/**
* 关联名.
*
* @var string
*/
protected $relation;
/**
* 获取一对多关联的最新一条数据.
*
* @param string $field 排序字段
*
* @return $this
*/
public function firstOfMany(string $field = '')
{
return $this->first($field);
}
/**
* 获取一对多关联的最旧一条数据.
*
* @param string $field 排序字段
*
* @return $this
*/
public function lastOfMany(string $field = '')
{
return $this->last($field);
}
/**
* 设置join类型.
*
* @param string $type JOIN类型
*
* @return $this
*/
public function joinType(string $type)
{
$this->joinType = $type;
return $this;
}
/**
* 预载入关联查询JOIN方式.
*
* @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, string $relation, $field = true, string $joinType = '', ?Closure $closure = null, bool $first = false): void
{
$name = Str::snake(class_basename($this->parent));
if ($first) {
$table = $query->getTable();
$query->table([$table => $name]);
if ($query->getOption('field')) {
$masterField = $query->getOption('field');
$query->removeOption('field');
} else {
$masterField = true;
}
$query->tableField($masterField, $table, $name);
}
// 预载入封装
$joinTable = $this->query->getTable();
$joinAlias = Str::snake($relation);
$joinType = $joinType ?: $this->joinType;
if (true !== $field) {
$joinField = $field;
} elseif ($this->query->getOption('field')) {
$joinField = $this->query->getOption('field');
} else {
$joinField = $field;
}
$query->via($joinAlias);
if ($this instanceof BelongsTo) {
$foreignKeyExp = $this->foreignKey;
if (!str_contains($foreignKeyExp, '.')) {
$foreignKeyExp = $name . '.' . $this->foreignKey;
}
$joinOn = $foreignKeyExp . '=' . $joinAlias . '.' . $this->localKey;
} else {
$foreignKeyExp = $this->foreignKey;
if (!str_contains($foreignKeyExp, '.')) {
$foreignKeyExp = $joinAlias . '.' . $this->foreignKey;
}
$joinOn = $name . '.' . $this->localKey . '=' . $foreignKeyExp;
}
if ($closure) {
// 执行闭包查询
$closure($query);
// 使用field指定获取关联的字段
$withField = $query->getOption('field');
if ($withField) {
$joinField = $withField;
}
$query->removeOption('field');
}
$query->join([$joinTable => $joinAlias], $joinOn, $joinType)
->tableField($joinField, $joinTable, $joinAlias, $joinAlias . '__');
}
/**
* 预载入关联查询(数据集).
*
* @param array $resultSet
* @param string $relation
* @param array $subRelation
* @param Closure $closure
*
* @return mixed
*/
abstract protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], ?Closure $closure = null);
/**
* 预载入关联查询(数据).
*
* @param Model $result
* @param string $relation
* @param array $subRelation
* @param Closure $closure
*
* @return mixed
*/
abstract protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null);
/**
* 预载入关联查询(数据集).
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @param bool $join 是否为JOIN方式
*
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $join = false): void
{
if ($join) {
// 模型JOIN关联组装
foreach ($resultSet as $result) {
$this->match($this->model, $relation, $result);
}
} else {
// IN查询
$this->eagerlySet($resultSet, $relation, $subRelation, $closure, $cache);
}
}
/**
* 预载入关联查询(数据).
*
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @param bool $join 是否为JOIN方式
*
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $join = false): void
{
if ($join) {
// 模型JOIN关联组装
$this->match($this->model, $relation, $result);
} else {
// IN查询
$this->eagerlyOne($result, $relation, $subRelation, $closure, $cache);
}
}
/**
* 保存(新增)当前关联数据对象
*
* @param array|Model $data 数据 可以使用数组 关联模型对象
* @param bool $replace 是否自动识别更新和写入
*
* @return Model|false
*/
public function save(array | Model $data, bool $replace = true)
{
$model = $this->make();
return $model->replace($replace)->save($data) ? $model : false;
}
/**
* 创建关联对象实例.
*
* @param array|Model $data
*
* @return Model
*/
public function make(array | Model $data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
}
// 保存关联表数据
$data[$this->foreignKey] = $this->parent->{$this->localKey};
return (new $this->model($data))->setSuffix($this->getModel()->getSuffix());
}
/**
* 绑定关联表的属性到父模型属性.
*
* @param array $attr 要绑定的属性列表
*
* @return $this
*/
public function bind(array $attr)
{
$this->bindAttr = $attr;
return $this;
}
/**
* 一对一 关联模型预查询拼装.
*
* @param string $model 模型名称
* @param string $relation 关联名
* @param Model $result 模型对象实例
*
* @return void
*/
protected function match(string $model, string $relation, Model $result): void
{
$data = $result->getRelation($relation);
if (!empty($data)) {
if ($this->bindAttr) {
$result->bindRelationAttr($data, $this->bindAttr);
} else {
$relationModel = new $model($data);
$result->setRelation($relation, $relationModel);
}
}
}
/**
* 一对一 关联模型预查询IN方式.
*
* @param array $where 关联预查询条件
* @param string $key 关联键名
* @param array $subRelation 子关联
* @param Closure $closure
* @param array $cache 关联缓存
* @param bool $collection 是否数据集查询
* @return array
*/
protected function eagerlyWhere(array $where, string $key, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false)
{
// 预载入关联查询 支持嵌套预载入
if ($closure) {
$this->baseQuery = true;
$closure($this->query);
}
if ($collection) {
$this->query->removeOption('limit');
} else {
$this->query->limit(1);
}
$list = $this->query
->where($where)
->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->lazy();
// 组装模型数据
$data = [];
foreach ($list as $set) {
if (!isset($data[$set->$key])) {
$data[$set->$key] = $set;
}
}
return $data;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare (strict_types = 1);
namespace think\model\type;
use think\model\contract\Modelable;
class Date extends DateTime
{
protected $data;
public static function from(mixed $value, Modelable $model)
{
$static = new static();
$static->data($value, 'Y-m-d');
return $static;
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare (strict_types = 1);
namespace think\model\type;
use Stringable;
use think\model\contract\Modelable;
use think\model\contract\Typeable;
class DateTime implements Typeable
{
protected $data;
protected $format;
public static function from(mixed $value, Modelable $model)
{
$static = new static();
$static->data($value, $model->getDateFormat());
return $static;
}
public function data($time, $format)
{
if ($format) {
if (class_exists($format)) {
$time = $time instanceof $format ? $time : new $format($time);
$this->format = 'Y-m-d H:i:s.u';
} else {
if (is_object($time)) {
} elseif (is_numeric($time)) {
$time = (new \DateTime())->setTimestamp((int) $time);
} elseif (strpos('.', $time)) {
$time = \DateTime::createFromFormat('Y-m-d H:i:s.u', $time);
} else {
$time = $time ? (new \DateTime($time)) : null;
}
$this->format = $format;
}
}
$this->data = $time;
}
public function setFormat(string $format)
{
$this->format = $format;
}
public function format(string $format = '')
{
if ($this->data instanceof Stringable) {
return $this->data->__toString();
}
if (is_null($this->data)) {
return null;
}
return $this->data->format($format ?: $this->format);
}
public function value()
{
return $this->format();
}
/**
* @return string
*/
public function __toString()
{
return $this->value();
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare (strict_types = 1);
namespace think\model\type;
use think\model\contract\Modelable;
use think\model\contract\Typeable;
class Json implements Typeable
{
protected $data;
public static function from(mixed $value, Modelable $model)
{
$static = new static();
$static->data($value, $model->isJsonAssoc());
return $static;
}
public function data($data, ?bool $assoc)
{
if (is_string($data) && json_validate($data)) {
$data = json_decode($data, $assoc);
} elseif (empty($data)) {
$data = [];
}
$this->data = is_string($data) ? [$data] : $data;
}
public function value()
{
return $this->data;
}
/**
* @return string
*/
public function __toString()
{
return json_encode($this->data, JSON_UNESCAPED_UNICODE);
}
}

View File

@@ -0,0 +1,219 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: zhangyajun <448901948@qq.com>
// +----------------------------------------------------------------------
namespace think\paginator\driver;
use think\Paginator;
/**
* Bootstrap 分页驱动.
*/
class Bootstrap extends Paginator
{
/**
* 上一页按钮.
*
* @param string $text
*
* @return string
*/
protected function getPreviousButton(string $text = '&laquo;'): string
{
if ($this->currentPage() <= 1) {
return $this->getDisabledTextWrapper($text);
}
$url = $this->url(
$this->currentPage() - 1
);
return $this->getPageLinkWrapper($url, $text);
}
/**
* 下一页按钮.
*
* @param string $text
*
* @return string
*/
protected function getNextButton(string $text = '&raquo;'): string
{
if (!$this->hasMore) {
return $this->getDisabledTextWrapper($text);
}
$url = $this->url($this->currentPage() + 1);
return $this->getPageLinkWrapper($url, $text);
}
/**
* 页码按钮.
*
* @return string
*/
protected function getLinks(): string
{
if ($this->simple) {
return '';
}
$block = [
'first' => null,
'slider' => null,
'last' => null,
];
$side = 3;
$window = $side * 2;
if ($this->lastPage < $window + 6) {
$block['first'] = $this->getUrlRange(1, $this->lastPage);
} elseif ($this->currentPage <= $window) {
$block['first'] = $this->getUrlRange(1, $window + 2);
$block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
} elseif ($this->currentPage > ($this->lastPage - $window)) {
$block['first'] = $this->getUrlRange(1, 2);
$block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage);
} else {
$block['first'] = $this->getUrlRange(1, 2);
$block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side);
$block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
}
$html = '';
if (is_array($block['first'])) {
$html .= $this->getUrlLinks($block['first']);
}
if (is_array($block['slider'])) {
$html .= $this->getDots();
$html .= $this->getUrlLinks($block['slider']);
}
if (is_array($block['last'])) {
$html .= $this->getDots();
$html .= $this->getUrlLinks($block['last']);
}
return $html;
}
/**
* 渲染分页html.
*
* @return mixed
*/
public function render()
{
if ($this->hasPages()) {
if ($this->simple) {
return sprintf(
'<ul class="pager">%s %s</ul>',
$this->getPreviousButton(),
$this->getNextButton()
);
} else {
return sprintf(
'<ul class="pagination">%s %s %s</ul>',
$this->getPreviousButton(),
$this->getLinks(),
$this->getNextButton()
);
}
}
}
/**
* 生成一个可点击的按钮.
*
* @param string $url
* @param string $page
*
* @return string
*/
protected function getAvailablePageWrapper(string $url, string $page): string
{
return '<li><a href="' . htmlentities($url) . '">' . $page . '</a></li>';
}
/**
* 生成一个禁用的按钮.
*
* @param string $text
*
* @return string
*/
protected function getDisabledTextWrapper(string $text): string
{
return '<li class="disabled"><span>' . $text . '</span></li>';
}
/**
* 生成一个激活的按钮.
*
* @param string $text
*
* @return string
*/
protected function getActivePageWrapper(string $text): string
{
return '<li class="active"><span>' . $text . '</span></li>';
}
/**
* 生成省略号按钮.
*
* @return string
*/
protected function getDots(): string
{
return $this->getDisabledTextWrapper('...');
}
/**
* 批量生成页码按钮.
*
* @param array $urls
*
* @return string
*/
protected function getUrlLinks(array $urls): string
{
$html = '';
foreach ($urls as $page => $url) {
$html .= $this->getPageLinkWrapper($url, $page);
}
return $html;
}
/**
* 生成普通页码按钮.
*
* @param string $url
* @param string $page
*
* @return string
*/
protected function getPageLinkWrapper(string $url, string $page): string
{
if ($this->currentPage() == $page) {
return $this->getActivePageWrapper($page);
}
return $this->getAvailablePageWrapper($url, $page);
}
}

View File

@@ -0,0 +1,59 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2021 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think;
/**
* 异常基础类.
*/
class Exception extends \Exception
{
/**
* 保存异常页面显示的额外Debug数据.
*
* @var array
*/
protected $data = [];
/**
* 设置异常额外的Debug数据
* 数据将会显示为下面的格式.
*
* Exception Data
* --------------------------------------------------
* Label 1
* key1 value1
* key2 value2
* Label 2
* key1 value1
* key2 value2
*
* @param string $label 数据分类,用于异常页面显示
* @param array $data 需要显示的数据,必须为关联数组
*/
final protected function setData(string $label, array $data)
{
$this->data[$label] = $data;
}
/**
* 获取异常额外Debug数据
* 主要用于输出到异常页面便于调试.
*
* @return array 由setData设置的Debug数据
*/
final public function getData()
{
return $this->data;
}
}

View File

@@ -0,0 +1,69 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2021 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think;
class Facade
{
/**
* 始终创建新的对象实例.
*
* @var bool
*/
protected static $alwaysNewInstance;
protected static $instance;
/**
* 获取当前Facade对应类名.
*
* @return string
*/
protected static function getFacadeClass()
{
}
/**
* 创建Facade实例.
*
* @static
*
* @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);
}
}

View File

@@ -0,0 +1,9 @@
<?php
if (!\class_exists('think\Exception')) {
require __DIR__.'/Exception.php';
}
if (!\class_exists('think\Facade')) {
require __DIR__.'/Facade.php';
}