初始化代码

This commit is contained in:
2025-12-22 14:34:25 +08:00
parent c2c5ae2fdd
commit a77dbc743f
1510 changed files with 213008 additions and 0 deletions

3
vendor/topthink/think-orm/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea
composer.lock
vendor

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.

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

@@ -0,0 +1,27 @@
# ThinkORM
基于PHP7.1+ 和PDO实现的ORM支持多数据库2.0版本主要特性包括:
* 基于PDO和PHP强类型实现
* 支持原生查询和查询构造器
* 自动参数绑定和预查询
* 简洁易用的查询功能
* 强大灵活的模型用法
* 支持预载入关联查询和延迟关联查询
* 支持多数据库及动态切换
* 支持`MongoDb`
* 支持分布式及事务
* 支持断点重连
* 支持`JSON`查询
* 支持数据库日志
* 支持`PSR-16`缓存及`PSR-3`日志规范
## 安装
~~~
composer require topthink/think-orm
~~~
## 文档
详细参考 [ThinkORM开发指南](https://www.kancloud.cn/manual/think-orm/content)

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

@@ -0,0 +1,28 @@
{
"name": "topthink/think-orm",
"description": "think orm",
"keywords": [
"orm",
"database"
],
"license": "Apache-2.0",
"authors": [
{
"name": "liu21st",
"email": "liu21st@gmail.com"
}
],
"require": {
"php": ">=7.1.0",
"ext-json": "*",
"psr/simple-cache": "^1.0",
"psr/log": "~1.0",
"topthink/think-helper":"^3.1"
},
"autoload": {
"psr-4": {
"think\\": "src"
},
"files": []
}
}

View File

@@ -0,0 +1,406 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use think\db\BaseQuery;
use think\db\ConnectionInterface;
use think\db\Query;
use think\db\Raw;
/**
* Class DbManager
* @package think
* @mixin BaseQuery
* @mixin Query
*/
class DbManager
{
/**
* 数据库连接实例
* @var array
*/
protected $instance = [];
/**
* 数据库配置
* @var array
*/
protected $config = [];
/**
* Event对象或者数组
* @var array|object
*/
protected $event;
/**
* SQL监听
* @var array
*/
protected $listen = [];
/**
* SQL日志
* @var array
*/
protected $dbLog = [];
/**
* 查询次数
* @var int
*/
protected $queryTimes = 0;
/**
* 查询缓存对象
* @var CacheInterface
*/
protected $cache;
/**
* 查询日志对象
* @var LoggerInterface
*/
protected $log;
/**
* 架构函数
* @access public
*/
public function __construct()
{
$this->modelMaker();
}
/**
* 注入模型对象
* @access public
* @return void
*/
protected function modelMaker()
{
$this->triggerSql();
Model::setDb($this);
if (is_object($this->event)) {
Model::setEvent($this->event);
}
Model::maker(function (Model $model) {
$isAutoWriteTimestamp = $model->getAutoWriteTimestamp();
if (is_null($isAutoWriteTimestamp)) {
// 自动写入时间戳
$model->isAutoWriteTimestamp($this->getConfig('auto_timestamp', true));
}
$dateFormat = $model->getDateFormat();
if (is_null($dateFormat)) {
// 设置时间戳格式
$model->setDateFormat($this->getConfig('datetime_format', 'Y-m-d H:i:s'));
}
});
}
/**
* 监听SQL
* @access protected
* @return void
*/
protected function triggerSql(): void
{
// 监听SQL
$this->listen(function ($sql, $time, $master) {
if (0 === strpos($sql, 'CONNECT:')) {
$this->log($sql);
return;
}
// 记录SQL
if (is_bool($master)) {
// 分布式记录当前操作的主从
$master = $master ? 'master|' : 'slave|';
} else {
$master = '';
}
$this->log($sql . ' [ ' . $master . 'RunTime:' . $time . 's ]');
});
}
/**
* 初始化配置参数
* @access public
* @param array $config 连接配置
* @return void
*/
public function setConfig($config): void
{
$this->config = $config;
}
/**
* 设置缓存对象
* @access public
* @param CacheInterface $cache 缓存对象
* @return void
*/
public function setCache(CacheInterface $cache): void
{
$this->cache = $cache;
}
/**
* 设置日志对象
* @access public
* @param LoggerInterface $log 日志对象
* @return void
*/
public function setLog(LoggerInterface $log): void
{
$this->log = $log;
}
/**
* 记录SQL日志
* @access protected
* @param string $log SQL日志信息
* @param string $type 日志类型
* @return void
*/
public function log(string $log, string $type = 'sql')
{
if ($this->log) {
$this->log->log($type, $log);
} else {
$this->dbLog[$type][] = $log;
}
}
/**
* 获得查询日志(没有设置日志对象使用)
* @access public
* @param bool $clear 是否清空
* @return array
*/
public function getDbLog(bool $clear = false): array
{
$logs = $this->dbLog;
if ($clear) {
$this->dbLog = [];
}
return $logs;
}
/**
* 获取配置参数
* @access public
* @param string $name 配置参数
* @param mixed $default 默认值
* @return mixed
*/
public function getConfig(string $name = '', $default = null)
{
if ('' === $name) {
return $this->config;
}
return $this->config[$name] ?? $default;
}
/**
* 创建/切换数据库连接查询
* @access public
* @param string|null $name 连接配置标识
* @param bool $force 强制重新连接
* @return BaseQuery
*/
public function connect(string $name = null, bool $force = false): BaseQuery
{
$connection = $this->instance($name, $force);
$class = $connection->getQueryClass();
$query = new $class($connection);
$timeRule = $this->getConfig('time_query_rule');
if (!empty($timeRule)) {
$query->timeRule($timeRule);
}
return $query;
}
/**
* 创建数据库连接实例
* @access protected
* @param string|null $name 连接标识
* @param bool $force 强制重新连接
* @return ConnectionInterface
*/
protected function instance(string $name = null, bool $force = false): ConnectionInterface
{
if (empty($name)) {
$name = $this->getConfig('default', 'mysql');
}
if ($force || !isset($this->instance[$name])) {
$this->instance[$name] = $this->createConnection($name);
}
return $this->instance[$name];
}
/**
* 获取连接配置
* @param string $name
* @return array
*/
protected function getConnectionConfig(string $name): array
{
$connections = $this->getConfig('connections');
if (!isset($connections[$name])) {
throw new InvalidArgumentException('Undefined db config:' . $name);
}
return $connections[$name];
}
/**
* 创建连接
* @param $name
* @return ConnectionInterface
*/
protected function createConnection(string $name): ConnectionInterface
{
$config = $this->getConnectionConfig($name);
$type = !empty($config['type']) ? $config['type'] : 'mysql';
if (false !== strpos($type, '\\')) {
$class = $type;
} else {
$class = '\\think\\db\\connector\\' . ucfirst($type);
}
/** @var ConnectionInterface $connection */
$connection = new $class($config);
$connection->setDb($this);
if ($this->cache) {
$connection->setCache($this->cache);
}
return $connection;
}
/**
* 使用表达式设置数据
* @access public
* @param string $value 表达式
* @return Raw
*/
public function raw(string $value): Raw
{
return new Raw($value);
}
/**
* 更新查询次数
* @access public
* @return void
*/
public function updateQueryTimes(): void
{
$this->queryTimes++;
}
/**
* 重置查询次数
* @access public
* @return void
*/
public function clearQueryTimes(): void
{
$this->queryTimes = 0;
}
/**
* 获得查询次数
* @access public
* @return integer
*/
public function getQueryTimes(): int
{
return $this->queryTimes;
}
/**
* 监听SQL执行
* @access public
* @param callable $callback 回调方法
* @return void
*/
public function listen(callable $callback): void
{
$this->listen[] = $callback;
}
/**
* 获取监听SQL执行
* @access public
* @return array
*/
public function getListen(): array
{
return $this->listen;
}
/**
* 注册回调方法
* @access public
* @param string $event 事件名
* @param callable $callback 回调方法
* @return void
*/
public function event(string $event, callable $callback): void
{
$this->event[$event][] = $callback;
}
/**
* 触发事件
* @access public
* @param string $event 事件名
* @param mixed $params 传入参数
* @return mixed
*/
public function trigger(string $event, $params = null)
{
if (isset($this->event[$event])) {
foreach ($this->event[$event] as $callback) {
call_user_func_array($callback, [$this]);
}
}
}
public function __call($method, $args)
{
return call_user_func_array([$this->connect(), $method], $args);
}
}

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

@@ -0,0 +1,993 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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;
use ArrayAccess;
use Closure;
use JsonSerializable;
use think\contract\Arrayable;
use think\contract\Jsonable;
use think\db\BaseQuery as Query;
/**
* Class Model
* @package think
* @mixin Query
* @method void onAfterRead(Model $model) static after_read事件定义
* @method mixed onBeforeInsert(Model $model) static before_insert事件定义
* @method void onAfterInsert(Model $model) static after_insert事件定义
* @method mixed onBeforeUpdate(Model $model) static before_update事件定义
* @method void onAfterUpdate(Model $model) static after_update事件定义
* @method mixed onBeforeWrite(Model $model) static before_write事件定义
* @method void onAfterWrite(Model $model) static after_write事件定义
* @method mixed onBeforeDelete(Model $model) static before_write事件定义
* @method void onAfterDelete(Model $model) static after_delete事件定义
* @method void onBeforeRestore(Model $model) static before_restore事件定义
* @method void onAfterRestore(Model $model) static after_restore事件定义
*/
abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable
{
use model\concern\Attribute;
use model\concern\RelationShip;
use model\concern\ModelEvent;
use model\concern\TimeStamp;
use model\concern\Conversion;
/**
* 数据是否存在
* @var bool
*/
private $exists = false;
/**
* 是否强制更新所有数据
* @var bool
*/
private $force = false;
/**
* 是否Replace
* @var bool
*/
private $replace = false;
/**
* 数据表后缀
* @var string
*/
protected $suffix;
/**
* 更新条件
* @var array
*/
private $updateWhere;
/**
* 数据库配置
* @var string
*/
protected $connection;
/**
* 模型名称
* @var string
*/
protected $name;
/**
* 主键值
* @var string
*/
protected $key;
/**
* 数据表名称
* @var string
*/
protected $table;
/**
* 初始化过的模型.
* @var array
*/
protected static $initialized = [];
/**
* 软删除字段默认值
* @var mixed
*/
protected $defaultSoftDelete;
/**
* 全局查询范围
* @var array
*/
protected $globalScope = [];
/**
* 延迟保存信息
* @var bool
*/
private $lazySave = false;
/**
* Db对象
* @var DbManager
*/
protected static $db;
/**
* 容器对象的依赖注入方法
* @var callable
*/
protected static $invoker;
/**
* 服务注入
* @var Closure[]
*/
protected static $maker = [];
/**
* 设置服务注入
* @access public
* @param Closure $maker
* @return void
*/
public static function maker(Closure $maker)
{
static::$maker[] = $maker;
}
/**
* 设置Db对象
* @access public
* @param DbManager $db Db对象
* @return void
*/
public static function setDb(DbManager $db)
{
self::$db = $db;
}
/**
* 设置容器对象的依赖注入方法
* @access public
* @param callable $callable 依赖注入方法
* @return void
*/
public static function setInvoker(callable $callable): void
{
self::$invoker = $callable;
}
/**
* 调用反射执行模型方法 支持参数绑定
* @access public
* @param mixed $method
* @param array $vars 参数
* @return mixed
*/
public function invoke($method, array $vars = [])
{
if (self::$invoker) {
$call = self::$invoker;
return $call($method instanceof Closure ? $method : Closure::fromCallable([$this, $method]), $vars);
}
return call_user_func_array($method instanceof Closure ? $method : [$this, $method], $vars);
}
/**
* 架构函数
* @access public
* @param array $data 数据
*/
public function __construct(array $data = [])
{
$this->data = $data;
if (!empty($this->data)) {
// 废弃字段
foreach ((array) $this->disuse as $key) {
if (array_key_exists($key, $this->data)) {
unset($this->data[$key]);
}
}
}
// 记录原始数据
$this->origin = $this->data;
if (empty($this->name)) {
// 当前模型名
$name = str_replace('\\', '/', static::class);
$this->name = basename($name);
}
if (!empty(static::$maker)) {
foreach (static::$maker as $maker) {
call_user_func($maker, $this);
}
}
// 执行初始化操作
$this->initialize();
}
/**
* 获取当前模型名称
* @access public
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* 创建新的模型实例
* @access public
* @param array $data 数据
* @param mixed $where 更新条件
* @return Model
*/
public function newInstance(array $data = [], $where = null): Model
{
if (empty($data)) {
return new static();
}
$model = (new static($data))->exists(true);
$model->setUpdateWhere($where);
$model->trigger('AfterRead');
return $model;
}
/**
* 设置模型的更新条件
* @access protected
* @param mixed $where 更新条件
* @return void
*/
protected function setUpdateWhere($where): void
{
$this->updateWhere = $where;
}
/**
* 设置当前模型数据表的后缀
* @access public
* @param string $suffix 数据表后缀
* @return $this
*/
public function setSuffix(string $suffix)
{
$this->suffix = $suffix;
return $this;
}
/**
* 获取当前模型的数据表后缀
* @access public
* @return string
*/
public function getSuffix(): string
{
return $this->suffix ?: '';
}
/**
* 获取当前模型的数据库查询对象
* @access public
* @param array $scope 设置不使用的全局查询范围
* @return Query
*/
public function db($scope = []): Query
{
/** @var Query $query */
$query = self::$db->connect($this->connection)
->name($this->name . $this->suffix)
->pk($this->pk);
if (!empty($this->table)) {
$query->table($this->table . $this->suffix);
}
$query->model($this)
->json($this->json, $this->jsonAssoc)
->setFieldType(array_merge($this->schema, $this->jsonType));
// 软删除
if (property_exists($this, 'withTrashed') && !$this->withTrashed) {
$this->withNoTrashed($query);
}
// 全局作用域
if (is_array($scope)) {
$globalScope = array_diff($this->globalScope, $scope);
$query->scope($globalScope);
}
// 返回当前模型的数据库查询对象
return $query;
}
/**
* 初始化模型
* @access private
* @return void
*/
private function initialize(): void
{
if (!isset(static::$initialized[static::class])) {
static::$initialized[static::class] = true;
static::init();
}
}
/**
* 初始化处理
* @access protected
* @return void
*/
protected static function init()
{
}
protected function checkData(): void
{
}
protected function checkResult($result): void
{
}
/**
* 更新是否强制写入数据 而不做比较(亦可用于软删除的强制删除)
* @access public
* @param bool $force
* @return $this
*/
public function force(bool $force = true)
{
$this->force = $force;
return $this;
}
/**
* 判断force
* @access public
* @return bool
*/
public function isForce(): bool
{
return $this->force;
}
/**
* 新增数据是否使用Replace
* @access public
* @param bool $replace
* @return $this
*/
public function replace(bool $replace = true)
{
$this->replace = $replace;
return $this;
}
/**
* 刷新模型数据
* @access public
* @param bool $relation 是否刷新关联数据
* @return $this
*/
public function refresh(bool $relation = false)
{
if ($this->exists) {
$this->data = $this->db()->find($this->getKey())->getData();
$this->origin = $this->data;
if ($relation) {
$this->relation = [];
}
}
return $this;
}
/**
* 设置数据是否存在
* @access public
* @param bool $exists
* @return $this
*/
public function exists(bool $exists = true)
{
$this->exists = $exists;
return $this;
}
/**
* 判断数据是否存在数据库
* @access public
* @return bool
*/
public function isExists(): bool
{
return $this->exists;
}
/**
* 判断模型是否为空
* @access public
* @return bool
*/
public function isEmpty(): bool
{
return empty($this->data);
}
/**
* 延迟保存当前数据对象
* @access public
* @param array|bool $data 数据
* @return void
*/
public function lazySave($data = []): void
{
if (false === $data) {
$this->lazySave = false;
} else {
if (is_array($data)) {
$this->setAttrs($data);
}
$this->lazySave = true;
}
}
/**
* 保存当前数据对象
* @access public
* @param array $data 数据
* @param string $sequence 自增序列名
* @return bool
*/
public function save(array $data = [], string $sequence = null): bool
{
// 数据对象赋值
$this->setAttrs($data);
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
if (false === $result) {
return false;
}
// 写入回调
$this->trigger('AfterWrite');
// 重新记录原始数据
$this->origin = $this->data;
$this->set = [];
$this->lazySave = false;
return true;
}
/**
* 检查数据是否允许写入
* @access protected
* @return array
*/
protected function checkAllowFields(): array
{
// 检测字段
if (empty($this->field)) {
if (!empty($this->schema)) {
$this->field = array_keys(array_merge($this->schema, $this->jsonType));
} else {
$query = $this->db();
$table = $this->table ? $this->table . $this->suffix : $query->getTable();
$this->field = $query->getConnection()->getTableFields($table);
}
return $this->field;
}
$field = $this->field;
if ($this->autoWriteTimestamp) {
array_push($field, $this->createTime, $this->updateTime);
}
if (!empty($this->disuse)) {
// 废弃字段
$field = array_diff($field, $this->disuse);
}
return $field;
}
/**
* 保存写入数据
* @access protected
* @return bool
*/
protected function updateData(): bool
{
// 事件回调
if (false === $this->trigger('BeforeUpdate')) {
return false;
}
$this->checkData();
// 获取有更新的数据
$data = $this->getChangedData();
if (empty($data)) {
// 关联更新
if (!empty($this->relationWrite)) {
$this->autoRelationUpdate();
}
return true;
}
if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {
// 自动写入更新时间
$data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
$this->data[$this->updateTime] = $data[$this->updateTime];
}
// 检查允许字段
$allowFields = $this->checkAllowFields();
foreach ($this->relationWrite as $name => $val) {
if (!is_array($val)) {
continue;
}
foreach ($val as $key) {
if (isset($data[$key])) {
unset($data[$key]);
}
}
}
// 模型更新
$db = $this->db();
$db->startTrans();
try {
$this->key = null;
$where = $this->getWhere();
$result = $db->where($where)
->strict(false)
->cache(true)
->setOption('key', $this->key)
->field($allowFields)
->update($data);
$this->checkResult($result);
// 关联更新
if (!empty($this->relationWrite)) {
$this->autoRelationUpdate();
}
$db->commit();
// 更新回调
$this->trigger('AfterUpdate');
return true;
} catch (\Exception $e) {
$db->rollback();
throw $e;
}
}
/**
* 新增写入数据
* @access protected
* @param string $sequence 自增名
* @return bool
*/
protected function insertData(string $sequence = null): bool
{
// 时间戳自动写入
if ($this->autoWriteTimestamp) {
if ($this->createTime && !isset($this->data[$this->createTime])) {
$this->data[$this->createTime] = $this->autoWriteTimestamp($this->createTime);
}
if ($this->updateTime && !isset($this->data[$this->updateTime])) {
$this->data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
}
}
if (false === $this->trigger('BeforeInsert')) {
return false;
}
$this->checkData();
// 检查允许字段
$allowFields = $this->checkAllowFields();
$db = $this->db();
$db->startTrans();
try {
$result = $db->strict(false)
->field($allowFields)
->replace($this->replace)
->sequence($sequence)
->insert($this->data, true);
// 获取自动增长主键
if ($result) {
$pk = $this->getPk();
if (is_string($pk) && (!isset($this->data[$pk]) || '' == $this->data[$pk])) {
$this->data[$pk] = $result;
}
}
// 关联写入
if (!empty($this->relationWrite)) {
$this->autoRelationInsert();
}
$db->commit();
// 标记数据已经存在
$this->exists = true;
// 新增回调
$this->trigger('AfterInsert');
return true;
} catch (\Exception $e) {
$db->rollback();
throw $e;
}
}
/**
* 获取当前的更新条件
* @access public
* @return mixed
*/
public function getWhere()
{
$pk = $this->getPk();
if (is_string($pk) && isset($this->data[$pk])) {
$where = [[$pk, '=', $this->data[$pk]]];
$this->key = $this->data[$pk];
} elseif (is_array($pk)) {
foreach ($pk as $field) {
if (isset($this->data[$field])) {
$where[] = [$field, '=', $this->data[$field]];
}
}
}
if (empty($where)) {
$where = empty($this->updateWhere) ? null : $this->updateWhere;
}
return $where;
}
/**
* 保存多个数据到当前数据对象
* @access public
* @param iterable $dataSet 数据
* @param boolean $replace 是否自动识别更新和写入
* @return Collection
* @throws \Exception
*/
public function saveAll(iterable $dataSet, bool $replace = true): Collection
{
$db = $this->db();
$db->startTrans();
try {
$pk = $this->getPk();
if (is_string($pk) && $replace) {
$auto = true;
}
$result = [];
foreach ($dataSet as $key => $data) {
if ($this->exists || (!empty($auto) && isset($data[$pk]))) {
$result[$key] = self::update($data);
} else {
$result[$key] = self::create($data, $this->field, $this->replace);
}
}
$db->commit();
return $this->toCollection($result);
} catch (\Exception $e) {
$db->rollback();
throw $e;
}
}
/**
* 删除当前的记录
* @access public
* @return bool
*/
public function delete(): bool
{
if (!$this->exists || $this->isEmpty() || false === $this->trigger('BeforeDelete')) {
return false;
}
// 读取更新条件
$where = $this->getWhere();
$db = $this->db();
$db->startTrans();
try {
// 删除当前模型数据
$db->where($where)->delete();
// 关联删除
if (!empty($this->relationWrite)) {
$this->autoRelationDelete();
}
$db->commit();
$this->trigger('AfterDelete');
$this->exists = false;
$this->lazySave = false;
return true;
} catch (\Exception $e) {
$db->rollback();
throw $e;
}
}
/**
* 写入数据
* @access public
* @param array $data 数据数组
* @param array $allowField 允许字段
* @param bool $replace 使用Replace
* @return static
*/
public static function create(array $data, array $allowField = [], bool $replace = false): Model
{
$model = new static();
if (!empty($allowField)) {
$model->allowField($allowField);
}
$model->replace($replace)->save($data);
return $model;
}
/**
* 更新数据
* @access public
* @param array $data 数据数组
* @param mixed $where 更新条件
* @param array $allowField 允许字段
* @return static
*/
public static function update(array $data, $where = [], array $allowField = [])
{
$model = new static();
if (!empty($allowField)) {
$model->allowField($allowField);
}
if (!empty($where)) {
$model->setUpdateWhere($where);
}
$model->exists(true)->save($data);
return $model;
}
/**
* 删除记录
* @access public
* @param mixed $data 主键列表 支持闭包查询条件
* @param bool $force 是否强制删除
* @return bool
*/
public static function destroy($data, bool $force = false): bool
{
if (empty($data) && 0 !== $data) {
return false;
}
$model = new static();
$query = $model->db();
if (is_array($data) && key($data) !== 0) {
$query->where($data);
$data = null;
} elseif ($data instanceof \Closure) {
$data($query);
$data = null;
}
$resultSet = $query->select($data);
foreach ($resultSet as $result) {
$result->force($force)->delete();
}
return true;
}
/**
* 解序列化后处理
*/
public function __wakeup()
{
$this->initialize();
}
/**
* 修改器 设置数据对象的值
* @access public
* @param string $name 名称
* @param mixed $value 值
* @return void
*/
public function __set(string $name, $value): void
{
$this->setAttr($name, $value);
}
/**
* 获取器 获取数据对象的值
* @access public
* @param string $name 名称
* @return mixed
*/
public function __get(string $name)
{
return $this->getAttr($name);
}
/**
* 检测数据对象的值
* @access public
* @param string $name 名称
* @return bool
*/
public function __isset(string $name): bool
{
return !is_null($this->getAttr($name));
}
/**
* 销毁数据对象的值
* @access public
* @param string $name 名称
* @return void
*/
public function __unset(string $name): void
{
unset($this->data[$name], $this->relation[$name]);
}
// ArrayAccess
public function offsetSet($name, $value)
{
$this->setAttr($name, $value);
}
public function offsetExists($name): bool
{
return $this->__isset($name);
}
public function offsetUnset($name)
{
$this->__unset($name);
}
public function offsetGet($name)
{
return $this->getAttr($name);
}
/**
* 设置不使用的全局查询范围
* @access public
* @param array $scope 不启用的全局查询范围
* @return Query
*/
public static function withoutGlobalScope(array $scope = null)
{
$model = new static();
return $model->db($scope);
}
/**
* 切换后缀进行查询
* @access public
* @param string $suffix 切换的表后缀
* @return Model
*/
public static function suffix(string $suffix)
{
$model = new static();
$model->setSuffix($suffix);
return $model;
}
public function __call($method, $args)
{
if ('withattr' == strtolower($method)) {
return call_user_func_array([$this, 'withAttribute'], $args);
}
return call_user_func_array([$this->db(), $method], $args);
}
public static function __callStatic($method, $args)
{
$model = new static();
return call_user_func_array([$model->db(), $method], $args);
}
/**
* 析构方法
* @access public
*/
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}
}

View File

@@ -0,0 +1,497 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: zhangyajun <448901948@qq.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think;
use ArrayAccess;
use ArrayIterator;
use Closure;
use Countable;
use DomainException;
use IteratorAggregate;
use JsonSerializable;
use think\paginator\driver\Bootstrap;
use Traversable;
/**
* 分页基础类
* @mixin Collection
*/
abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
{
/**
* 是否简洁模式
* @var bool
*/
protected $simple = false;
/**
* 数据集
* @var Collection
*/
protected $items;
/**
* 当前页
* @var int
*/
protected $currentPage;
/**
* 最后一页
* @var int
*/
protected $lastPage;
/**
* 数据总数
* @var integer|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;
}
/**
* @access public
* @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;
}
/**
* 获取页码对应的链接
*
* @access protected
* @param int $page
* @return string
*/
protected function url(int $page): string
{
if ($page <= 0) {
$page = 1;
}
if (strpos($this->options['path'], '[PAGE]') === false) {
$parameters = [$this->options['var_page'] => $page];
$path = $this->options['path'];
} else {
$parameters = [];
$path = str_replace('[PAGE]', $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();
}
/**
* 自动获取当前页码
* @access public
* @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
* @access public
* @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;
}
public function total(): int
{
if ($this->simple) {
throw new DomainException('not support total');
}
return $this->total;
}
public function listRows(): int
{
return $this->listRows;
}
public function currentPage(): int
{
return $this->currentPage;
}
public function lastPage(): int
{
if ($this->simple) {
throw new DomainException('not support last');
}
return $this->lastPage;
}
/**
* 数据是否足够分页
* @access public
* @return bool
*/
public function hasPages(): bool
{
return !(1 == $this->currentPage && !$this->hasMore);
}
/**
* 创建一组分页链接
*
* @access public
* @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锚点
*
* @access public
* @param string|null $fragment
* @return $this
*/
public function fragment(string $fragment = null)
{
$this->options['fragment'] = $fragment;
return $this;
}
/**
* 添加URL参数
*
* @access public
* @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;
}
/**
* 构造锚点字符串
*
* @access public
* @return string
*/
protected function buildFragment(): string
{
return $this->options['fragment'] ? '#' . $this->options['fragment'] : '';
}
/**
* 渲染分页html
* @access public
* @return mixed
*/
abstract public function render();
public function items()
{
return $this->items->all();
}
/**
* 获取数据集
*
* @return Collection|\think\model\Collection
*/
public function getCollection()
{
return $this->items;
}
public function isEmpty(): bool
{
return $this->items->isEmpty();
}
/**
* 给每个元素执行个回调
*
* @access public
* @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
* @access public
* @return Traversable An instance of an object implementing <b>Iterator</b> or
* <b>Traversable</b>
*/
public function getIterator()
{
return new ArrayIterator($this->items->all());
}
/**
* Whether a offset exists
* @access public
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset)
{
return $this->items->offsetExists($offset);
}
/**
* Offset to retrieve
* @access public
* @param mixed $offset
* @return mixed
*/
public function offsetGet($offset)
{
return $this->items->offsetGet($offset);
}
/**
* Offset to set
* @access public
* @param mixed $offset
* @param mixed $value
*/
public function offsetSet($offset, $value)
{
$this->items->offsetSet($offset, $value);
}
/**
* Offset to unset
* @access public
* @param mixed $offset
* @return void
* @since 5.0.0
*/
public function offsetUnset($offset)
{
$this->items->offsetUnset($offset);
}
/**
* Count elements of an object
*/
public function count(): int
{
return $this->items->count();
}
public function __toString()
{
return (string) $this->render();
}
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(),
];
}
/**
* Specify data which should be serialized to JSON
*/
public function jsonSerialize()
{
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;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,209 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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;
use DateInterval;
use DateTime;
use DateTimeInterface;
use think\db\exception\InvalidArgumentException;
/**
* CacheItem实现类
*/
class CacheItem
{
/**
* 缓存Key
* @var string
*/
protected $key;
/**
* 缓存内容
* @var mixed
*/
protected $value;
/**
* 过期时间
* @var int|DateTimeInterface
*/
protected $expire;
/**
* 缓存tag
* @var string
*/
protected $tag;
/**
* 缓存是否命中
* @var bool
*/
protected $isHit = false;
public function __construct(string $key = null)
{
$this->key = $key;
}
/**
* 为此缓存项设置「键」
* @access public
* @param string $key
* @return $this
*/
public function setKey(string $key)
{
$this->key = $key;
return $this;
}
/**
* 返回当前缓存项的「键」
* @access public
* @return string
*/
public function getKey()
{
return $this->key;
}
/**
* 返回当前缓存项的有效期
* @access public
* @return DateTimeInterface|int|null
*/
public function getExpire()
{
if ($this->expire instanceof DateTimeInterface) {
return $this->expire;
}
return $this->expire ? $this->expire - time() : null;
}
/**
* 获取缓存Tag
* @access public
* @return string|array
*/
public function getTag()
{
return $this->tag;
}
/**
* 凭借此缓存项的「键」从缓存系统里面取出缓存项
* @access public
* @return mixed
*/
public function get()
{
return $this->value;
}
/**
* 确认缓存项的检查是否命中
* @access public
* @return bool
*/
public function isHit(): bool
{
return $this->isHit;
}
/**
* 为此缓存项设置「值」
* @access public
* @param mixed $value
* @return $this
*/
public function set($value)
{
$this->value = $value;
$this->isHit = true;
return $this;
}
/**
* 为此缓存项设置所属标签
* @access public
* @param string|array $tag
* @return $this
*/
public function tag($tag = null)
{
$this->tag = $tag;
return $this;
}
/**
* 设置缓存项的有效期
* @access public
* @param mixed $expire
* @return $this
*/
public function expire($expire)
{
if (is_null($expire)) {
$this->expire = null;
} elseif (is_numeric($expire) || $expire instanceof DateInterval) {
$this->expiresAfter($expire);
} elseif ($expire instanceof DateTimeInterface) {
$this->expire = $expire;
} else {
throw new InvalidArgumentException('not support datetime');
}
return $this;
}
/**
* 设置缓存项的准确过期时间点
* @access public
* @param DateTimeInterface $expiration
* @return $this
*/
public function expiresAt($expiration)
{
if ($expiration instanceof DateTimeInterface) {
$this->expire = $expiration;
} else {
throw new InvalidArgumentException('not support datetime');
}
return $this;
}
/**
* 设置缓存项的过期时间
* @access public
* @param int|DateInterval $timeInterval
* @return $this
* @throws InvalidArgumentException
*/
public function expiresAfter($timeInterval)
{
if ($timeInterval instanceof DateInterval) {
$this->expire = (int) DateTime::createFromFormat('U', (string) time())->add($timeInterval)->format('U');
} elseif (is_numeric($timeInterval)) {
$this->expire = $timeInterval + time();
} else {
throw new InvalidArgumentException('not support datetime');
}
return $this;
}
}

View File

@@ -0,0 +1,275 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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;
use Psr\SimpleCache\CacheInterface;
use think\DbManager;
use think\db\CacheItem;
/**
* 数据库连接基础类
*/
abstract class Connection
{
/**
* 当前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 Db
*/
protected $db;
/**
* 是否读取主库
* @var bool
*/
protected $readMaster = false;
/**
* 数据库连接参数配置
* @var array
*/
protected $config = [];
/**
* 缓存对象
* @var Cache
*/
protected $cache;
/**
* 获取当前的builder实例对象
* @access public
* @return Builder
*/
public function getBuilder()
{
return $this->builder;
}
/**
* 设置当前的数据库Db对象
* @access public
* @param DbManager $db
* @return void
*/
public function setDb(DbManager $db)
{
$this->db = $db;
}
/**
* 设置当前的缓存对象
* @access public
* @param CacheInterface $cache
* @return void
*/
public function setCache(CacheInterface $cache)
{
$this->cache = $cache;
}
/**
* 获取当前的缓存对象
* @access public
* @return CacheInterface|null
*/
public function getCache()
{
return $this->cache;
}
/**
* 获取数据库的配置参数
* @access public
* @param string $config 配置名称
* @return mixed
*/
public function getConfig(string $config = '')
{
if ('' === $config) {
return $this->config;
}
return $this->config[$config] ?? null;
}
/**
* 数据库SQL监控
* @access protected
* @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)) {
$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);
}
}
}
}
/**
* 缓存数据
* @access protected
* @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
* @access protected
* @param BaseQuery $query 查询对象
* @return string
*/
protected function getCacheKey(BaseQuery $query): string
{
if (!empty($query->getOptions('key'))) {
$key = 'think:' . $this->getConfig('database') . '.' . $query->getTable() . '|' . $query->getOptions('key');
} else {
$key = $query->getQueryGuid();
}
return $key;
}
/**
* 分析缓存
* @access protected
* @param BaseQuery $query 查询对象
* @param array $cache 缓存信息
* @return CacheItem
*/
protected function parseCache(BaseQuery $query, array $cache): CacheItem
{
[$key, $expire, $tag] = $cache;
if ($key instanceof CacheItem) {
$cacheItem = $key;
} else {
if (true === $key) {
$key = $this->getCacheKey($query);
}
$cacheItem = new CacheItem($key);
$cacheItem->expire($expire);
$cacheItem->tag($tag);
}
return $cacheItem;
}
/**
* 获取返回或者影响的记录数
* @access public
* @return integer
*/
public function getNumRows(): int
{
return $this->numRows;
}
/**
* 析构方法
* @access public
*/
public function __destruct()
{
// 关闭连接
$this->close();
}
}

View File

@@ -0,0 +1,196 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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;
use Psr\SimpleCache\CacheInterface;
use think\DbManager;
/**
* Connection interface
*/
interface ConnectionInterface
{
/**
* 获取当前连接器类对应的Query类
* @access public
* @return string
*/
public function getQueryClass(): string;
/**
* 连接数据库方法
* @access public
* @param array $config 接参数
* @param integer $linkNum 连接序号
* @return mixed
*/
public function connect(array $config = [], $linkNum = 0);
/**
* 设置当前的数据库Db对象
* @access public
* @param DbManager $db
* @return void
*/
public function setDb(DbManager $db);
/**
* 设置当前的缓存对象
* @access public
* @param CacheInterface $cache
* @return void
*/
public function setCache(CacheInterface $cache);
/**
* 获取数据库的配置参数
* @access public
* @param string $config 配置名称
* @return mixed
*/
public function getConfig(string $config = '');
/**
* 关闭数据库(或者重新连接)
* @access public
* @return $this
*/
public function close();
/**
* 查找单条记录
* @access public
* @param BaseQuery $query 查询对象
* @return array
* @throws DbException
* @throws ModelNotFoundException
* @throws DataNotFoundException
*/
public function find(BaseQuery $query): array;
/**
* 查找记录
* @access public
* @param BaseQuery $query 查询对象
* @return array
* @throws DbException
* @throws ModelNotFoundException
* @throws DataNotFoundException
*/
public function select(BaseQuery $query): array;
/**
* 插入记录
* @access public
* @param BaseQuery $query 查询对象
* @param boolean $getLastInsID 返回自增主键
* @return mixed
*/
public function insert(BaseQuery $query, bool $getLastInsID = false);
/**
* 批量插入记录
* @access public
* @param BaseQuery $query 查询对象
* @param mixed $dataSet 数据集
* @return integer
* @throws \Exception
* @throws \Throwable
*/
public function insertAll(BaseQuery $query, array $dataSet = []): int;
/**
* 更新记录
* @access public
* @param BaseQuery $query 查询对象
* @return integer
* @throws Exception
* @throws PDOException
*/
public function update(BaseQuery $query): int;
/**
* 删除记录
* @access public
* @param BaseQuery $query 查询对象
* @return int
* @throws Exception
* @throws PDOException
*/
public function delete(BaseQuery $query): int;
/**
* 得到某个字段的值
* @access public
* @param BaseQuery $query 查询对象
* @param string $field 字段名
* @param mixed $default 默认值
* @param bool $one 返回一个值
* @return mixed
*/
public function value(BaseQuery $query, string $field, $default = null);
/**
* 得到某个列的数组
* @access public
* @param BaseQuery $query 查询对象
* @param string $column 字段名 多个字段用逗号分隔
* @param string $key 索引
* @return array
*/
public function column(BaseQuery $query, string $column, string $key = ''): array;
/**
* 执行数据库事务
* @access public
* @param callable $callback 数据操作方法回调
* @return mixed
* @throws PDOException
* @throws \Exception
* @throws \Throwable
*/
public function transaction(callable $callback);
/**
* 启动事务
* @access public
* @return void
* @throws \PDOException
* @throws \Exception
*/
public function startTrans();
/**
* 用于非自动提交状态下面的查询提交
* @access public
* @return void
* @throws PDOException
*/
public function commit();
/**
* 事务回滚
* @access public
* @return void
* @throws PDOException
*/
public function rollback();
/**
* 获取最近一次查询的sql语句
* @access public
* @return string
*/
public function getLastSql(): string;
}

View File

@@ -0,0 +1,493 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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;
use think\db\exception\DbException as Exception;
use think\helper\Str;
/**
* SQL获取类
*/
class Fetch
{
/**
* 查询对象
* @var Query
*/
protected $query;
/**
* Connection对象
* @var Connection
*/
protected $connection;
/**
* Builder对象
* @var Builder
*/
protected $builder;
/**
* 创建一个查询SQL获取对象
*
* @param Query $query 查询对象
*/
public function __construct(Query $query)
{
$this->query = $query;
$this->connection = $query->getConnection();
$this->builder = $this->connection->getBuilder();
}
/**
* 聚合查询
* @access protected
* @param string $aggregate 聚合方法
* @param string $field 字段名
* @return string
*/
protected function aggregate(string $aggregate, string $field): string
{
$this->query->parseOptions();
$field = $aggregate . '(' . $this->builder->parseKey($this->query, $field) . ') AS think_' . strtolower($aggregate);
return $this->value($field, 0, false);
}
/**
* 得到某个字段的值
* @access public
* @param string $field 字段名
* @param mixed $default 默认值
* @return string
*/
public function value(string $field, $default = null, bool $one = true): string
{
$options = $this->query->parseOptions();
if (isset($options['field'])) {
$this->query->removeOption('field');
}
$this->query->setOption('field', (array) $field);
// 生成查询SQL
$sql = $this->builder->select($this->query, $one);
if (isset($options['field'])) {
$this->query->setOption('field', $options['field']);
} else {
$this->query->removeOption('field');
}
return $this->fetch($sql);
}
/**
* 得到某个列的数组
* @access public
* @param string $field 字段名 多个字段用逗号分隔
* @param string $key 索引
* @return string
*/
public function column(string $field, string $key = ''): string
{
$options = $this->query->parseOptions();
if (isset($options['field'])) {
$this->query->removeOption('field');
}
if ($key && '*' != $field) {
$field = $key . ',' . $field;
}
$field = array_map('trim', explode(',', $field));
$this->query->setOption('field', $field);
// 生成查询SQL
$sql = $this->builder->select($this->query);
if (isset($options['field'])) {
$this->query->setOption('field', $options['field']);
} else {
$this->query->removeOption('field');
}
return $this->fetch($sql);
}
/**
* 插入记录
* @access public
* @param array $data 数据
* @return string
*/
public function insert(array $data = []): string
{
$options = $this->query->parseOptions();
if (!empty($data)) {
$this->query->setOption('data', $data);
}
$sql = $this->builder->insert($this->query);
return $this->fetch($sql);
}
/**
* 插入记录并获取自增ID
* @access public
* @param array $data 数据
* @return string
*/
public function insertGetId(array $data = []): string
{
return $this->insert($data);
}
/**
* 保存数据 自动判断insert或者update
* @access public
* @param array $data 数据
* @param bool $forceInsert 是否强制insert
* @return string
*/
public function save(array $data = [], bool $forceInsert = false): string
{
if ($forceInsert) {
return $this->insert($data);
}
$data = array_merge($this->query->getOptions('data') ?: [], $data);
$this->query->setOption('data', $data);
if ($this->query->getOptions('where')) {
$isUpdate = true;
} else {
$isUpdate = $this->query->parseUpdateData($data);
}
return $isUpdate ? $this->update() : $this->insert();
}
/**
* 批量插入记录
* @access public
* @param array $dataSet 数据集
* @param integer $limit 每次写入数据限制
* @return string
*/
public function insertAll(array $dataSet = [], int $limit = null): string
{
$options = $this->query->parseOptions();
if (empty($dataSet)) {
$dataSet = $options['data'];
}
if (empty($limit) && !empty($options['limit'])) {
$limit = $options['limit'];
}
if ($limit) {
$array = array_chunk($dataSet, $limit, true);
$fetchSql = [];
foreach ($array as $item) {
$sql = $this->builder->insertAll($this->query, $item);
$bind = $this->query->getBind();
$fetchSql[] = $this->connection->getRealSql($sql, $bind);
}
return implode(';', $fetchSql);
}
$sql = $this->builder->insertAll($this->query, $dataSet);
return $this->fetch($sql);
}
/**
* 通过Select方式插入记录
* @access public
* @param array $fields 要插入的数据表字段名
* @param string $table 要插入的数据表名
* @return string
*/
public function selectInsert(array $fields, string $table): string
{
$this->query->parseOptions();
$sql = $this->builder->selectInsert($this->query, $fields, $table);
return $this->fetch($sql);
}
/**
* 更新记录
* @access public
* @param mixed $data 数据
* @return string
*/
public function update(array $data = []): string
{
$options = $this->query->parseOptions();
$data = !empty($data) ? $data : $options['data'];
$pk = $this->query->getPk();
if (empty($options['where'])) {
// 如果存在主键数据 则自动作为更新条件
if (is_string($pk) && isset($data[$pk])) {
$this->query->where($pk, '=', $data[$pk]);
unset($data[$pk]);
} elseif (is_array($pk)) {
// 增加复合主键支持
foreach ($pk as $field) {
if (isset($data[$field])) {
$this->query->where($field, '=', $data[$field]);
} else {
// 如果缺少复合主键数据则不执行
throw new Exception('miss complex primary data');
}
unset($data[$field]);
}
}
if (empty($this->query->getOptions('where'))) {
// 如果没有任何更新条件则不执行
throw new Exception('miss update condition');
}
}
// 更新数据
$this->query->setOption('data', $data);
// 生成UPDATE SQL语句
$sql = $this->builder->update($this->query);
return $this->fetch($sql);
}
/**
* 删除记录
* @access public
* @param mixed $data 表达式 true 表示强制删除
* @return string
*/
public function delete($data = null): string
{
$options = $this->query->parseOptions();
if (!is_null($data) && true !== $data) {
// AR模式分析主键条件
$this->query->parsePkWhere($data);
}
if (!empty($options['soft_delete'])) {
// 软删除
[$field, $condition] = $options['soft_delete'];
if ($condition) {
$this->query->setOption('soft_delete', null);
$this->query->setOption('data', [$field => $condition]);
// 生成删除SQL语句
$sql = $this->builder->delete($this->query);
return $this->fetch($sql);
}
}
// 生成删除SQL语句
$sql = $this->builder->delete($this->query);
return $this->fetch($sql);
}
/**
* 查找记录 返回SQL
* @access public
* @param mixed $data
* @return string
*/
public function select($data = null): string
{
$this->query->parseOptions();
if (!is_null($data)) {
// 主键条件分析
$this->query->parsePkWhere($data);
}
// 生成查询SQL
$sql = $this->builder->select($this->query);
return $this->fetch($sql);
}
/**
* 查找单条记录 返回SQL语句
* @access public
* @param mixed $data
* @return string
*/
public function find($data = null): string
{
$this->query->parseOptions();
if (!is_null($data)) {
// AR模式分析主键条件
$this->query->parsePkWhere($data);
}
// 生成查询SQL
$sql = $this->builder->select($this->query, true);
// 获取实际执行的SQL语句
return $this->fetch($sql);
}
/**
* 查找多条记录 如果不存在则抛出异常
* @access public
* @param mixed $data
* @return string
*/
public function selectOrFail($data = null): string
{
return $this->select($data);
}
/**
* 查找单条记录 如果不存在则抛出异常
* @access public
* @param mixed $data
* @return string
*/
public function findOrFail($data = null): string
{
return $this->find($data);
}
/**
* 查找单条记录 不存在返回空数据(或者空模型)
* @access public
* @param mixed $data 数据
* @return string
*/
public function findOrEmpty($data = null)
{
return $this->find($data);
}
/**
* 获取实际的SQL语句
* @access public
* @param string $sql
* @return string
*/
public function fetch(string $sql): string
{
$bind = $this->query->getBind();
return $this->connection->getRealSql($sql, $bind);
}
/**
* COUNT查询
* @access public
* @param string $field 字段名
* @return string
*/
public function count(string $field = '*'): string
{
$options = $this->query->parseOptions();
if (!empty($options['group'])) {
// 支持GROUP
$bind = $this->query->getBind();
$subSql = $this->query->options($options)->field('count(' . $field . ') AS think_count')->bind($bind)->buildSql();
$query = $this->query->newQuery()->table([$subSql => '_group_count_']);
return $query->fetchsql()->aggregate('COUNT', '*');
} else {
return $this->aggregate('COUNT', $field);
}
}
/**
* SUM查询
* @access public
* @param string $field 字段名
* @return string
*/
public function sum(string $field): string
{
return $this->aggregate('SUM', $field);
}
/**
* MIN查询
* @access public
* @param string $field 字段名
* @return string
*/
public function min(string $field): string
{
return $this->aggregate('MIN', $field);
}
/**
* MAX查询
* @access public
* @param string $field 字段名
* @return string
*/
public function max(string $field): string
{
return $this->aggregate('MAX', $field);
}
/**
* AVG查询
* @access public
* @param string $field 字段名
* @return string
*/
public function avg(string $field): string
{
return $this->aggregate('AVG', $field);
}
public function __call($method, $args)
{
if (strtolower(substr($method, 0, 5)) == 'getby') {
// 根据某个字段获取记录
$field = Str::snake(substr($method, 5));
return $this->where($field, '=', $args[0])->find();
} elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') {
// 根据某个字段获取记录的某个值
$name = Str::snake(substr($method, 10));
return $this->where($name, '=', $args[0])->value($args[1]);
}
$result = call_user_func_array([$this->query, $method], $args);
return $result === $this->query ? $this : $result;
}
}

View File

@@ -0,0 +1,741 @@
<?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;
use MongoDB\Driver\BulkWrite;
use MongoDB\Driver\Command;
use MongoDB\Driver\Cursor;
use MongoDB\Driver\Exception\AuthenticationException;
use MongoDB\Driver\Exception\BulkWriteException;
use MongoDB\Driver\Exception\ConnectionException;
use MongoDB\Driver\Exception\InvalidArgumentException;
use MongoDB\Driver\Exception\RuntimeException;
use MongoDB\Driver\Query as MongoQuery;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern;
use think\Collection;
use think\db\connector\Mongo as Connection;
use think\db\exception\DbException as Exception;
use think\Paginator;
class Mongo extends BaseQuery
{
/**
* 执行查询 返回数据集
* @access public
* @param MongoQuery $query 查询对象
* @return mixed
* @throws AuthenticationException
* @throws InvalidArgumentException
* @throws ConnectionException
* @throws RuntimeException
*/
public function query(MongoQuery $query)
{
return $this->connection->query($this, $query);
}
/**
* 执行指令 返回数据集
* @access public
* @param Command $command 指令
* @param string $dbName
* @param ReadPreference $readPreference readPreference
* @param string|array $typeMap 指定返回的typeMap
* @return mixed
* @throws AuthenticationException
* @throws InvalidArgumentException
* @throws ConnectionException
* @throws RuntimeException
*/
public function command(Command $command, string $dbName = '', ReadPreference $readPreference = null, $typeMap = null)
{
return $this->connection->command($command, $dbName, $readPreference, $typeMap);
}
/**
* 执行语句
* @access public
* @param BulkWrite $bulk
* @return int
* @throws AuthenticationException
* @throws InvalidArgumentException
* @throws ConnectionException
* @throws RuntimeException
* @throws BulkWriteException
*/
public function execute(BulkWrite $bulk)
{
return $this->connection->execute($this, $bulk);
}
/**
* 执行command
* @access public
* @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查询
* @access public
* @param string $field 字段名
* @return array
*/
public function getDistinct(string $field)
{
$result = $this->cmd('distinct', $field);
return $result[0]['values'];
}
/**
* 获取数据库的所有collection
* @access public
* @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查询
* @access public
* @param string $field 字段名
* @return integer
*/
public function count(string $field = null): int
{
$result = $this->cmd('count');
return $result[0]['n'];
}
/**
* 聚合查询
* @access public
* @param string $aggregate 聚合指令
* @param string $field 字段名
* @param bool $force 强制转为数字类型
* @return mixed
*/
public function aggregate(string $aggregate, $field, bool $force = 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;
}
/**
* 字段值增长
* @access public
* @param string $field 字段名
* @param float $step 增长值
* @return $this
*/
public function inc(string $field, float $step = 1)
{
$this->options['data'][$field] = ['$inc', $step];
return $this;
}
/**
* 字段值减少
* @access public
* @param string $field 字段名
* @param float $step 减少值
* @return $this
*/
public function dec(string $field, float $step = 1)
{
return $this->inc($field, -1 * $step);
}
/**
* 指定当前操作的Collection
* @access public
* @param string $table 表名
* @return $this
*/
public function table($table)
{
$this->options['table'] = $table;
return $this;
}
/**
* table方法的别名
* @access public
* @param string $collection
* @return $this
*/
public function collection(string $collection)
{
return $this->table($collection);
}
/**
* 设置typeMap
* @access public
* @param string|array $typeMap
* @return $this
*/
public function typeMap($typeMap)
{
$this->options['typeMap'] = $typeMap;
return $this;
}
/**
* awaitData
* @access public
* @param bool $awaitData
* @return $this
*/
public function awaitData(bool $awaitData)
{
$this->options['awaitData'] = $awaitData;
return $this;
}
/**
* batchSize
* @access public
* @param integer $batchSize
* @return $this
*/
public function batchSize(int $batchSize)
{
$this->options['batchSize'] = $batchSize;
return $this;
}
/**
* exhaust
* @access public
* @param bool $exhaust
* @return $this
*/
public function exhaust(bool $exhaust)
{
$this->options['exhaust'] = $exhaust;
return $this;
}
/**
* 设置modifiers
* @access public
* @param array $modifiers
* @return $this
*/
public function modifiers(array $modifiers)
{
$this->options['modifiers'] = $modifiers;
return $this;
}
/**
* 设置noCursorTimeout
* @access public
* @param bool $noCursorTimeout
* @return $this
*/
public function noCursorTimeout(bool $noCursorTimeout)
{
$this->options['noCursorTimeout'] = $noCursorTimeout;
return $this;
}
/**
* 设置oplogReplay
* @access public
* @param bool $oplogReplay
* @return $this
*/
public function oplogReplay(bool $oplogReplay)
{
$this->options['oplogReplay'] = $oplogReplay;
return $this;
}
/**
* 设置partial
* @access public
* @param bool $partial
* @return $this
*/
public function partial(bool $partial)
{
$this->options['partial'] = $partial;
return $this;
}
/**
* maxTimeMS
* @access public
* @param string $maxTimeMS
* @return $this
*/
public function maxTimeMS(string $maxTimeMS)
{
$this->options['maxTimeMS'] = $maxTimeMS;
return $this;
}
/**
* collation
* @access public
* @param array $collation
* @return $this
*/
public function collation(array $collation)
{
$this->options['collation'] = $collation;
return $this;
}
/**
* 设置是否REPLACE
* @access public
* @param bool $replace 是否使用REPLACE写入数据
* @return $this
*/
public function replace(bool $replace = true)
{
return $this;
}
/**
* 设置返回字段
* @access public
* @param 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;
}
/**
* 指定要排除的查询字段
* @access public
* @param array|string $field 要排除的字段
* @return $this
*/
public function withoutField($field)
{
if (empty($field) || '*' == $field) {
return $this;
}
if (is_string($field)) {
$field = array_map('trim', explode(',', $field));
}
$projection = [];
foreach ($field as $key => $val) {
if (is_numeric($key)) {
$projection[$val] = 0;
} else {
$projection[$key] = $val;
}
}
$this->options['projection'] = $projection;
return $this;
}
/**
* 设置skip
* @access public
* @param integer $skip
* @return $this
*/
public function skip(int $skip)
{
$this->options['skip'] = $skip;
return $this;
}
/**
* 设置slaveOk
* @access public
* @param bool $slaveOk
* @return $this
*/
public function slaveOk(bool $slaveOk)
{
$this->options['slaveOk'] = $slaveOk;
return $this;
}
/**
* 指定查询数量
* @access public
* @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
* @access public
* @param array|string $field
* @param string $order
* @return $this
*/
public function order($field, string $order = '')
{
if (is_array($field)) {
$this->options['sort'] = $field;
} else {
$this->options['sort'][$field] = 'asc' == strtolower($order) ? 1 : -1;
}
return $this;
}
/**
* 设置tailable
* @access public
* @param bool $tailable
* @return $this
*/
public function tailable(bool $tailable)
{
$this->options['tailable'] = $tailable;
return $this;
}
/**
* 设置writeConcern对象
* @access public
* @param WriteConcern $writeConcern
* @return $this
*/
public function writeConcern(WriteConcern $writeConcern)
{
$this->options['writeConcern'] = $writeConcern;
return $this;
}
/**
* 获取当前数据表的主键
* @access public
* @return string|array
*/
public function getPk()
{
return $this->pk ?: $this->connection->getConfig('pk');
}
/**
* 执行查询但只返回Cursor对象
* @access public
* @return Cursor
*/
public function getCursor(): Cursor
{
$this->parseOptions();
return $this->connection->getCursor($this);
}
/**
* 获取当前的查询标识
* @access public
* @param mixed $data 要序列化的数据
* @return string
*/
public function getQueryGuid($data = null): string
{
return md5($this->getConfig('database') . serialize(var_export($data ?: $this->options, true)));
}
/**
* 分页查询
* @access public
* @param int|array $listRows 每页数量 数组表示配置参数
* @param int|bool $simple 是否简洁模式或者总记录数
* @return Paginator
* @throws Exception
*/
public function paginate($listRows = null, $simple = false): Paginator
{
if (is_int($simple)) {
$total = $simple;
$simple = false;
}
$defaultConfig = [
'query' => [], //url额外参数
'fragment' => '', //url锚点
'var_page' => 'page', //分页变量
'list_rows' => 15, //每页数量
];
if (is_array($listRows)) {
$config = array_merge($defaultConfig, $listRows);
$listRows = intval($config['list_rows']);
} else {
$config = $defaultConfig;
$listRows = intval($listRows ?: $config['list_rows']);
}
$page = isset($config['page']) ? (int) $config['page'] : Paginator::getCurrentPage($config['var_page']);
$page = $page < 1 ? 1 : $page;
$config['path'] = $config['path'] ?? Paginator::getCurrentPath();
if (!isset($total) && !$simple) {
$options = $this->getOptions();
unset($this->options['order'], $this->options['limit'], $this->options['page'], $this->options['field']);
$total = $this->count();
$results = $this->options($options)->page($page, $listRows)->select();
} elseif ($simple) {
$results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select();
$total = null;
} else {
$results = $this->page($page, $listRows)->select();
}
$this->removeOption('limit');
$this->removeOption('page');
return Paginator::make($results, $listRows, $page, $total, $simple, $config);
}
/**
* 分批数据返回处理
* @access public
* @param integer $count 每次处理的数据数量
* @param callable $callback 处理回调方法
* @param string|array $column 分批处理的字段名
* @param string $order 字段排序
* @return bool
* @throws Exception
*/
public function chunk(int $count, callable $callback, $column = null, string $order = 'asc'): bool
{
$options = $this->getOptions();
$column = $column ?: $this->getPk();
if (isset($options['order'])) {
unset($options['order']);
}
if (is_array($column)) {
$times = 1;
$query = $this->options($options)->page($times, $count);
} else {
$query = $this->options($options)->limit($count);
if (strpos($column, '.')) {
[$alias, $key] = explode('.', $column);
} else {
$key = $column;
}
}
$resultSet = $query->order($column, $order)->select();
while (count($resultSet) > 0) {
if (false === call_user_func($callback, $resultSet)) {
return false;
}
if (isset($times)) {
$times++;
$query = $this->options($options)->page($times, $count);
} else {
$end = $resultSet->pop();
$lastId = is_array($end) ? $end[$key] : $end->getData($key);
$query = $this->options($options)
->limit($count)
->where($column, 'asc' == strtolower($order) ? '>' : '<', $lastId);
}
$resultSet = $query->order($column, $order)->select();
}
return true;
}
/**
* 分析表达式(可用于查询或者写入操作)
* @access public
* @return array
*/
public function parseOptions(): array
{
$options = $this->options;
// 获取数据表
if (empty($options['table'])) {
$options['table'] = $this->getTable();
}
foreach (['where', 'data'] 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['projection'])) {
$options['projection'] = [];
}
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
list($page, $listRows) = $options['page'];
$page = $page > 0 ? $page : 1;
$listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20);
$offset = $listRows * ($page - 1);
$options['skip'] = intval($offset);
$options['limit'] = intval($listRows);
}
$this->options = $options;
return $options;
}
/**
* 获取字段类型信息
* @access public
* @return array
*/
public function getFieldsType(): array
{
if (!empty($this->options['field_type'])) {
return $this->options['field_type'];
}
return [];
}
/**
* 获取字段类型信息
* @access public
* @param string $field 字段名
* @return string|null
*/
public function getFieldType(string $field)
{
$fieldType = $this->getFieldsType();
return $fieldType[$field] ?? null;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,493 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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;
use PDOStatement;
use think\helper\Str;
/**
* PDO数据查询类
*/
class Query extends BaseQuery
{
use concern\JoinAndViewQuery;
use concern\ParamsBind;
use concern\TableFieldInfo;
/**
* 表达式方式指定Field排序
* @access public
* @param string $field 排序字段
* @param array $bind 参数绑定
* @return $this
*/
public function orderRaw(string $field, array $bind = [])
{
if (!empty($bind)) {
$this->bindParams($field, $bind);
}
$this->options['order'][] = new Raw($field);
return $this;
}
/**
* 表达式方式指定查询字段
* @access public
* @param string $field 字段名
* @return $this
*/
public function fieldRaw(string $field)
{
$this->options['field'][] = new Raw($field);
return $this;
}
/**
* 指定Field排序 orderField('id',[1,2,3],'desc')
* @access public
* @param string $field 排序字段
* @param array $values 排序值
* @param string $order 排序 desc/asc
* @return $this
*/
public function orderField(string $field, array $values, string $order = '')
{
if (!empty($values)) {
$values['sort'] = $order;
$this->options['order'][$field] = $values;
}
return $this;
}
/**
* 随机排序
* @access public
* @return $this
*/
public function orderRand()
{
$this->options['order'][] = '[rand]';
return $this;
}
/**
* 使用表达式设置数据
* @access public
* @param string $field 字段名
* @param string $value 字段值
* @return $this
*/
public function exp(string $field, string $value)
{
$this->options['data'][$field] = new Raw($value);
return $this;
}
/**
* 表达式方式指定当前操作的数据表
* @access public
* @param mixed $table 表名
* @return $this
*/
public function tableRaw(string $table)
{
$this->options['table'] = new Raw($table);
return $this;
}
/**
* 执行查询 返回数据集
* @access public
* @param string $sql sql指令
* @param array $bind 参数绑定
* @return array
* @throws BindParamException
* @throws PDOException
*/
public function query(string $sql, array $bind = []): array
{
return $this->connection->query($this, $sql, $bind);
}
/**
* 执行语句
* @access public
* @param string $sql sql指令
* @param array $bind 参数绑定
* @return int
* @throws BindParamException
* @throws PDOException
*/
public function execute(string $sql, array $bind = []): int
{
return $this->connection->execute($this, $sql, $bind, true);
}
/**
* 获取执行的SQL语句而不进行实际的查询
* @access public
* @param bool $fetch 是否返回sql
* @return $this|Fetch
*/
public function fetchSql(bool $fetch = true)
{
$this->options['fetch_sql'] = $fetch;
if ($fetch) {
return new Fetch($this);
}
return $this;
}
/**
* 批处理执行SQL语句
* 批处理的指令都认为是execute操作
* @access public
* @param array $sql SQL批处理指令
* @return bool
*/
public function batchQuery(array $sql = []): bool
{
return $this->connection->batchQuery($this, $sql);
}
/**
* USING支持 用于多表删除
* @access public
* @param mixed $using USING
* @return $this
*/
public function using($using)
{
$this->options['using'] = $using;
return $this;
}
/**
* 存储过程调用
* @access public
* @param bool $procedure 是否为存储过程查询
* @return $this
*/
public function procedure(bool $procedure = true)
{
$this->options['procedure'] = $procedure;
return $this;
}
/**
* 指定group查询
* @access public
* @param string|array $group GROUP
* @return $this
*/
public function group($group)
{
$this->options['group'] = $group;
return $this;
}
/**
* 指定having查询
* @access public
* @param string $having having
* @return $this
*/
public function having(string $having)
{
$this->options['having'] = $having;
return $this;
}
/**
* 指定distinct查询
* @access public
* @param bool $distinct 是否唯一
* @return $this
*/
public function distinct(bool $distinct = true)
{
$this->options['distinct'] = $distinct;
return $this;
}
/**
* 设置自增序列名
* @access public
* @param string $sequence 自增序列名
* @return $this
*/
public function sequence(string $sequence = null)
{
$this->options['sequence'] = $sequence;
return $this;
}
/**
* 指定强制索引
* @access public
* @param string $force 索引名称
* @return $this
*/
public function force(string $force)
{
$this->options['force'] = $force;
return $this;
}
/**
* 查询注释
* @access public
* @param string $comment 注释
* @return $this
*/
public function comment(string $comment)
{
$this->options['comment'] = $comment;
return $this;
}
/**
* 设置是否REPLACE
* @access public
* @param bool $replace 是否使用REPLACE写入数据
* @return $this
*/
public function replace(bool $replace = true)
{
$this->options['replace'] = $replace;
return $this;
}
/**
* 设置当前查询所在的分区
* @access public
* @param string|array $partition 分区名称
* @return $this
*/
public function partition($partition)
{
$this->options['partition'] = $partition;
return $this;
}
/**
* 设置DUPLICATE
* @access public
* @param array|string|Raw $duplicate DUPLICATE信息
* @return $this
*/
public function duplicate($duplicate)
{
$this->options['duplicate'] = $duplicate;
return $this;
}
/**
* 设置查询的额外参数
* @access public
* @param string $extra 额外信息
* @return $this
*/
public function extra(string $extra)
{
$this->options['extra'] = $extra;
return $this;
}
/**
* 创建子查询SQL
* @access public
* @param bool $sub 是否添加括号
* @return string
* @throws Exception
*/
public function buildSql(bool $sub = true): string
{
return $sub ? '( ' . $this->fetchSql()->select() . ' )' : $this->fetchSql()->select();
}
/**
* 获取当前数据表的主键
* @access public
* @return string|array
*/
public function getPk()
{
if (empty($this->pk)) {
$this->pk = $this->connection->getPk($this->getTable());
}
return $this->pk;
}
/**
* 指定数据表自增主键
* @access public
* @param string $autoinc 自增键
* @return $this
*/
public function autoinc(string $autoinc)
{
$this->autoinc = $autoinc;
return $this;
}
/**
* 获取当前数据表的自增主键
* @access public
* @return string
*/
public function getAutoInc()
{
if (empty($this->autoinc)) {
$this->autoinc = $this->connection->getAutoInc($this->getTable());
}
return $this->autoinc;
}
/**
* 字段值增长
* @access public
* @param string $field 字段名
* @param float $step 增长值
* @return $this
*/
public function inc(string $field, float $step = 1)
{
$this->options['data'][$field] = ['INC', $step];
return $this;
}
/**
* 字段值减少
* @access public
* @param string $field 字段名
* @param float $step 增长值
* @return $this
*/
public function dec(string $field, float $step = 1)
{
$this->options['data'][$field] = ['DEC', $step];
return $this;
}
/**
* 获取当前的查询标识
* @access public
* @param mixed $data 要序列化的数据
* @return string
*/
public function getQueryGuid($data = null): string
{
return md5($this->getConfig('database') . serialize(var_export($data ?: $this->options, true)) . serialize($this->getBind(false)));
}
/**
* 执行查询但只返回PDOStatement对象
* @access public
* @return PDOStatement
*/
public function getPdo(): PDOStatement
{
return $this->connection->pdo($this);
}
/**
* 使用游标查找记录
* @access public
* @param mixed $data 数据
* @return \Generator
*/
public function cursor($data = null)
{
if (!is_null($data)) {
// 主键条件分析
$this->parsePkWhere($data);
}
$this->options['data'] = $data;
$connection = clone $this->connection;
return $connection->cursor($this);
}
/**
* 分批数据返回处理
* @access public
* @param integer $count 每次处理的数据数量
* @param callable $callback 处理回调方法
* @param string|array $column 分批处理的字段名
* @param string $order 字段排序
* @return bool
* @throws Exception
*/
public function chunk(int $count, callable $callback, $column = null, string $order = 'asc'): bool
{
$options = $this->getOptions();
$column = $column ?: $this->getPk();
if (isset($options['order'])) {
unset($options['order']);
}
$bind = $this->bind;
if (is_array($column)) {
$times = 1;
$query = $this->options($options)->page($times, $count);
} else {
$query = $this->options($options)->limit($count);
if (strpos($column, '.')) {
[$alias, $key] = explode('.', $column);
} else {
$key = $column;
}
}
$resultSet = $query->order($column, $order)->select();
while (count($resultSet) > 0) {
if (false === call_user_func($callback, $resultSet)) {
return false;
}
if (isset($times)) {
$times++;
$query = $this->options($options)->page($times, $count);
} else {
$end = $resultSet->pop();
$lastId = is_array($end) ? $end[$key] : $end->getData($key);
$query = $this->options($options)
->limit($count)
->where($column, 'asc' == strtolower($order) ? '>' : '<', $lastId);
}
$resultSet = $query->bind($bind)->order($column, $order)->select();
}
return true;
}
}

View File

@@ -0,0 +1,52 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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;
/**
* SQL Raw
*/
class Raw
{
/**
* 查询表达式
*
* @var string
*/
protected $value;
/**
* 创建一个查询表达式
*
* @param string $value
* @return void
*/
public function __construct(string $value)
{
$this->value = $value;
}
/**
* 获取表达式
*
* @return string
*/
public function getValue(): string
{
return $this->value;
}
public function __toString()
{
return (string) $this->value;
}
}

View File

@@ -0,0 +1,182 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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;
use ArrayAccess;
/**
* 数组查询对象
*/
class Where implements ArrayAccess
{
/**
* 查询表达式
* @var array
*/
protected $where = [];
/**
* 是否需要把查询条件两边增加括号
* @var bool
*/
protected $enclose = false;
/**
* 创建一个查询表达式
*
* @param array $where 查询条件数组
* @param bool $enclose 是否增加括号
*/
public function __construct(array $where = [], bool $enclose = false)
{
$this->where = $where;
$this->enclose = $enclose;
}
/**
* 设置是否添加括号
* @access public
* @param bool $enclose
* @return $this
*/
public function enclose(bool $enclose = true)
{
$this->enclose = $enclose;
return $this;
}
/**
* 解析为Query对象可识别的查询条件数组
* @access public
* @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;
}
/**
* 分析查询表达式
* @access protected
* @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;
}
/**
* 修改器 设置数据对象的值
* @access public
* @param string $name 名称
* @param mixed $value 值
* @return void
*/
public function __set($name, $value)
{
$this->where[$name] = $value;
}
/**
* 获取器 获取数据对象的值
* @access public
* @param string $name 名称
* @return mixed
*/
public function __get($name)
{
return $this->where[$name] ?? null;
}
/**
* 检测数据对象的值
* @access public
* @param string $name 名称
* @return bool
*/
public function __isset($name)
{
return isset($this->where[$name]);
}
/**
* 销毁数据对象的值
* @access public
* @param string $name 名称
* @return void
*/
public function __unset($name)
{
unset($this->where[$name]);
}
// ArrayAccess
public function offsetSet($name, $value)
{
$this->__set($name, $value);
}
public function offsetExists($name)
{
return $this->__isset($name);
}
public function offsetUnset($name)
{
$this->__unset($name);
}
public function offsetGet($name)
{
return $this->__get($name);
}
}

View File

@@ -0,0 +1,675 @@
<?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 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'];
/**
* 架构函数
* @access public
* @param Connection $connection 数据库连接对象实例
*/
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
/**
* 获取当前的连接对象实例
* @access public
* @return Connection
*/
public function getConnection(): Connection
{
return $this->connection;
}
/**
* key分析
* @access protected
* @param string $key
* @return string
*/
protected function parseKey(Query $query, string $key): string
{
if (0 === strpos($key, '__TABLE__.')) {
[$collection, $key] = explode('.', $key, 2);
}
if ('id' == $key && $this->connection->getConfig('pk_convert_id')) {
$key = '_id';
}
return trim($key);
}
/**
* value分析
* @access protected
* @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数据分析
* @access protected
* @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];
} elseif (is_null($val)) {
$result[$item] = 'NULL';
} else {
$result[$item] = $this->parseValue($query, $val, $key);
}
}
return $result;
}
/**
* Set数据分析
* @access protected
* @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]) && 0 === strpos($val[0], '$')) {
$result[$val[0]][$item] = $this->parseValue($query, $val[1], $key);
} else {
$result['$set'][$item] = $this->parseValue($query, $val, $key);
}
}
return $result;
}
/**
* 生成查询过滤条件
* @access public
* @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->getOptions('where'));
} else {
if (strpos($field, '|')) {
// 不同字段使用相同查询条件OR
$array = explode('|', $field);
foreach ($array as $k) {
$filter['$or'][] = $this->parseWhereItem($query, $k, $value);
}
} elseif (strpos($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;
}
/**
* 日期时间条件解析
* @access protected
* @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
* @access public
* @return mixed
*/
public function getLastInsID()
{
return $this->insertId;
}
/**
* 生成insert BulkWrite对象
* @access public
* @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对象
* @access public
* @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对象
* @access public
* @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对象
* @access public
* @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查询对象
* @access public
* @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命令
* @access public
* @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;
}
/**
* 聚合查询命令
* @access public
* @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命令
* @access public
* @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
* @access public
* @return Command
*/
public function listcollections(): Command
{
$cmd = ['listCollections' => 1];
$command = new Command($cmd);
$this->log('cmd', 'listCollections', $cmd);
return $command;
}
/**
* 查询数据表的状态信息
* @access public
* @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,421 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\builder;
use think\db\Builder;
use think\db\exception\DbException as Exception;
use think\db\Query;
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
* @access public
* @param Query $query 查询对象
* @param bool $one 是否仅获取一个记录
* @return string
*/
public function select(Query $query, bool $one = false): string
{
$options = $query->getOptions();
return str_replace(
['%TABLE%', '%PARTITION%', '%DISTINCT%', '%EXTRA%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
[
$this->parseTable($query, $options['table']),
$this->parsePartition($query, $options['partition']),
$this->parseDistinct($query, $options['distinct']),
$this->parseExtra($query, $options['extra']),
$this->parseField($query, $options['field']),
$this->parseJoin($query, $options['join']),
$this->parseWhere($query, $options['where']),
$this->parseGroup($query, $options['group']),
$this->parseHaving($query, $options['having']),
$this->parseOrder($query, $options['order']),
$this->parseLimit($query, $one ? '1' : $options['limit']),
$this->parseUnion($query, $options['union']),
$this->parseLock($query, $options['lock']),
$this->parseComment($query, $options['comment']),
$this->parseForce($query, $options['force']),
],
$this->selectSql);
}
/**
* 生成Insert SQL
* @access public
* @param Query $query 查询对象
* @return string
*/
public function insert(Query $query): string
{
$options = $query->getOptions();
// 分析并处理数据
$data = $this->parseData($query, $options['data']);
if (empty($data)) {
return '';
}
$set = [];
foreach ($data as $key => $val) {
$set[] = $key . ' = ' . $val;
}
return str_replace(
['%INSERT%', '%EXTRA%', '%TABLE%', '%PARTITION%', '%SET%', '%DUPLICATE%', '%COMMENT%'],
[
!empty($options['replace']) ? 'REPLACE' : 'INSERT',
$this->parseExtra($query, $options['extra']),
$this->parseTable($query, $options['table']),
$this->parsePartition($query, $options['partition']),
implode(' , ', $set),
$this->parseDuplicate($query, $options['duplicate']),
$this->parseComment($query, $options['comment']),
],
$this->insertSql);
}
/**
* 生成insertall SQL
* @access public
* @param Query $query 查询对象
* @param array $dataSet 数据集
* @param bool $replace 是否replace
* @return string
*/
public function insertAll(Query $query, array $dataSet, bool $replace = false): string
{
$options = $query->getOptions();
// 获取绑定信息
$bind = $query->getFieldsBindType();
// 获取合法的字段
if ('*' == $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%'],
[
$replace ? 'REPLACE' : 'INSERT',
$this->parseExtra($query, $options['extra']),
$this->parseTable($query, $options['table']),
$this->parsePartition($query, $options['partition']),
implode(' , ', $fields),
implode(' , ', $values),
$this->parseDuplicate($query, $options['duplicate']),
$this->parseComment($query, $options['comment']),
],
$this->insertAllSql);
}
/**
* 生成update SQL
* @access public
* @param Query $query 查询对象
* @return string
*/
public function update(Query $query): string
{
$options = $query->getOptions();
$data = $this->parseData($query, $options['data']);
if (empty($data)) {
return '';
}
$set = [];
foreach ($data as $key => $val) {
$set[] = $key . ' = ' . $val;
}
return str_replace(
['%TABLE%', '%PARTITION%', '%EXTRA%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
[
$this->parseTable($query, $options['table']),
$this->parsePartition($query, $options['partition']),
$this->parseExtra($query, $options['extra']),
implode(' , ', $set),
$this->parseJoin($query, $options['join']),
$this->parseWhere($query, $options['where']),
$this->parseOrder($query, $options['order']),
$this->parseLimit($query, $options['limit']),
$this->parseLock($query, $options['lock']),
$this->parseComment($query, $options['comment']),
],
$this->updateSql);
}
/**
* 生成delete SQL
* @access public
* @param Query $query 查询对象
* @return string
*/
public function delete(Query $query): string
{
$options = $query->getOptions();
return str_replace(
['%TABLE%', '%PARTITION%', '%EXTRA%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
[
$this->parseTable($query, $options['table']),
$this->parsePartition($query, $options['partition']),
$this->parseExtra($query, $options['extra']),
!empty($options['using']) ? ' USING ' . $this->parseTable($query, $options['using']) . ' ' : '',
$this->parseJoin($query, $options['join']),
$this->parseWhere($query, $options['where']),
$this->parseOrder($query, $options['order']),
$this->parseLimit($query, $options['limit']),
$this->parseLock($query, $options['lock']),
$this->parseComment($query, $options['comment']),
],
$this->deleteSql);
}
/**
* 正则查询
* @access protected
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param 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 = $value->getValue();
}
return $key . ' ' . $exp . ' ' . $value;
}
/**
* FIND_IN_SET 查询
* @access protected
* @param Query $query 查询对象
* @param string $key
* @param string $exp
* @param mixed $value
* @param string $field
* @return string
*/
protected function parseFindInSet(Query $query, string $key, string $exp, $value, string $field): string
{
if ($value instanceof Raw) {
$value = $value->getValue();
}
return 'FIND_IN_SET(' . $value . ', ' . $key . ')';
}
/**
* 字段和表名处理
* @access public
* @param Query $query 查询对象
* @param mixed $key 字段名
* @param bool $strict 严格检测
* @return string
*/
public function parseKey(Query $query, $key, bool $strict = false): string
{
if (is_int($key)) {
return (string) $key;
} elseif ($key instanceof Raw) {
return $key->getValue();
}
$key = trim($key);
if (strpos($key, '->') && false === strpos($key, '(')) {
// JSON字段支持
[$field, $name] = explode('->', $key, 2);
return 'json_extract(' . $this->parseKey($query, $field) . ', \'$' . (strpos($name, '[') === 0 ? '' : '.') . str_replace('->', '.', $name) . '\')';
} elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {
[$table, $key] = explode('.', $key, 2);
$alias = $query->getOptions('alias');
if ('__TABLE__' == $table) {
$table = $query->getOptions('table');
$table = is_array($table) ? array_shift($table) : $table;
}
if (isset($alias[$table])) {
$table = $alias[$table];
}
}
if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) {
throw new Exception('not support data:' . $key);
}
if ('*' != $key && !preg_match('/[,\'\"\*\(\)`.\s]/', $key)) {
$key = '`' . $key . '`';
}
if (isset($table)) {
if (strpos($table, '.')) {
$table = str_replace('.', '`.`', $table);
}
$key = '`' . $table . '`.' . $key;
}
return $key;
}
/**
* 随机排序
* @access protected
* @param Query $query 查询对象
* @return string
*/
protected function parseRand(Query $query): string
{
return 'rand()';
}
/**
* Partition 分析
* @access protected
* @param Query $query 查询对象
* @param string|array $partition 分区
* @return string
*/
protected function parsePartition(Query $query, $partition): string
{
if ('' == $partition) {
return '';
}
if (is_string($partition)) {
$partition = explode(',', $partition);
}
return ' PARTITION (' . implode(' , ', $partition) . ') ';
}
/**
* ON DUPLICATE KEY UPDATE 分析
* @access protected
* @param Query $query 查询对象
* @param mixed $duplicate
* @return string
*/
protected function parseDuplicate(Query $query, $duplicate): string
{
if ('' == $duplicate) {
return '';
}
if ($duplicate instanceof Raw) {
return ' ON DUPLICATE KEY UPDATE ' . $duplicate->getValue() . ' ';
}
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) . " = " . $val->getValue();
} 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,95 @@
<?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\Query;
/**
* Oracle数据库驱动
*/
class Oracle extends Builder
{
protected $selectSql = 'SELECT * FROM (SELECT thinkphp.*, rownum AS numrow FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%) thinkphp ) %LIMIT%%COMMENT%';
/**
* limit分析
* @access protected
* @param Query $query 查询对象
* @param mixed $limit
* @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 : '';
}
/**
* 设置锁机制
* @access protected
* @param Query $query 查询对象
* @param bool|false $lock
* @return string
*/
protected function parseLock(Query $query, $lock = false): string
{
if (!$lock) {
return '';
}
return ' FOR UPDATE NOWAIT ';
}
/**
* 字段和表名处理
* @access public
* @param Query $query 查询对象
* @param string $key
* @param string $strict
* @return string
*/
public function parseKey(Query $query, $key, bool $strict = false): string
{
$key = trim($key);
if (strpos($key, '->') && false === strpos($key, '(')) {
// JSON字段支持
[$field, $name] = explode($key, '->');
$key = $field . '."' . $name . '"';
}
return $key;
}
/**
* 随机排序
* @access protected
* @param Query $query 查询对象
* @return string
*/
protected function parseRand(Query $query): string
{
return 'DBMS_RANDOM.value';
}
}

View File

@@ -0,0 +1,118 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\builder;
use think\db\Builder;
use think\db\Query;
use think\db\Raw;
/**
* Pgsql数据库驱动
*/
class Pgsql extends Builder
{
/**
* INSERT SQL表达式
* @var string
*/
protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
/**
* INSERT ALL SQL表达式
* @var string
*/
protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
/**
* limit分析
* @access protected
* @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;
}
/**
* 字段和表名处理
* @access public
* @param Query $query 查询对象
* @param mixed $key 字段名
* @param bool $strict 严格检测
* @return string
*/
public function parseKey(Query $query, $key, bool $strict = false): string
{
if (is_int($key)) {
return (string) $key;
} elseif ($key instanceof Raw) {
return $key->getValue();
}
$key = trim($key);
if (strpos($key, '->') && false === strpos($key, '(')) {
// JSON字段支持
[$field, $name] = explode('->', $key);
$key = '"' . $field . '"' . '->>\'' . $name . '\'';
} elseif (strpos($key, '.')) {
[$table, $key] = explode('.', $key, 2);
$alias = $query->getOptions('alias');
if ('__TABLE__' == $table) {
$table = $query->getOptions('table');
$table = is_array($table) ? array_shift($table) : $table;
}
if (isset($alias[$table])) {
$table = $alias[$table];
}
if ('*' != $key && !preg_match('/[,\"\*\(\).\s]/', $key)) {
$key = '"' . $key . '"';
}
}
if (isset($table)) {
$key = $table . '.' . $key;
}
return $key;
}
/**
* 随机排序
* @access protected
* @param Query $query 查询对象
* @return string
*/
protected function parseRand(Query $query): string
{
return 'RANDOM()';
}
}

View File

@@ -0,0 +1,97 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\builder;
use think\db\Builder;
use think\db\Query;
use think\db\Raw;
/**
* Sqlite数据库驱动
*/
class Sqlite extends Builder
{
/**
* limit
* @access public
* @param Query $query 查询对象
* @param mixed $limit
* @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;
}
/**
* 随机排序
* @access protected
* @param Query $query 查询对象
* @return string
*/
protected function parseRand(Query $query): string
{
return 'RANDOM()';
}
/**
* 字段和表名处理
* @access public
* @param Query $query 查询对象
* @param mixed $key 字段名
* @param bool $strict 严格检测
* @return string
*/
public function parseKey(Query $query, $key, bool $strict = false): string
{
if (is_int($key)) {
return (string) $key;
} elseif ($key instanceof Raw) {
return $key->getValue();
}
$key = trim($key);
if (strpos($key, '.')) {
[$table, $key] = explode('.', $key, 2);
$alias = $query->getOptions('alias');
if ('__TABLE__' == $table) {
$table = $query->getOptions('table');
$table = is_array($table) ? array_shift($table) : $table;
}
if (isset($alias[$table])) {
$table = $alias[$table];
}
}
if (isset($table)) {
$key = $table . '.' . $key;
}
return $key;
}
}

View File

@@ -0,0 +1,184 @@
<?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\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分析
* @access protected
* @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[] = $val->getValue();
} elseif ('[rand]' == $val) {
$array[] = $this->parseRand($query);
} else {
if (is_numeric($key)) {
[$key, $sort] = explode(' ', strpos($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);
}
/**
* 随机排序
* @access protected
* @param Query $query 查询对象
* @return string
*/
protected function parseRand(Query $query): string
{
return 'rand()';
}
/**
* 字段和表名处理
* @access public
* @param Query $query 查询对象
* @param mixed $key 字段名
* @param bool $strict 严格检测
* @return string
*/
public function parseKey(Query $query, $key, bool $strict = false): string
{
if (is_int($key)) {
return (string) $key;
} elseif ($key instanceof Raw) {
return $key->getValue();
}
$key = trim($key);
if (strpos($key, '.') && !preg_match('/[,\'\"\(\)\[\s]/', $key)) {
[$table, $key] = explode('.', $key, 2);
$alias = $query->getOptions('alias');
if ('__TABLE__' == $table) {
$table = $query->getOptions('table');
$table = is_array($table) ? array_shift($table) : $table;
}
if (isset($alias[$table])) {
$table = $alias[$table];
}
}
if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) {
throw new Exception('not support data:' . $key);
}
if ('*' != $key && !preg_match('/[,\'\"\*\(\)\[.\s]/', $key)) {
$key = '[' . $key . ']';
}
if (isset($table)) {
$key = '[' . $table . '].' . $key;
}
return $key;
}
/**
* limit
* @access protected
* @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,107 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\concern;
use think\db\Raw;
/**
* 聚合查询
*/
trait AggregateQuery
{
/**
* 聚合查询
* @access protected
* @param string $aggregate 聚合方法
* @param string|Raw $field 字段名
* @param bool $force 强制转为数字类型
* @return mixed
*/
protected function aggregate(string $aggregate, $field, bool $force = false)
{
return $this->connection->aggregate($this, $aggregate, $field, $force);
}
/**
* COUNT查询
* @access public
* @param string|Raw $field 字段名
* @return int
*/
public function count(string $field = '*'): int
{
if (!empty($this->options['group'])) {
// 支持GROUP
$options = $this->getOptions();
$subSql = $this->options($options)
->field('count(' . $field . ') AS think_count')
->bind($this->bind)
->buildSql();
$query = $this->newQuery()->table([$subSql => '_group_count_']);
$count = $query->aggregate('COUNT', '*');
} else {
$count = $this->aggregate('COUNT', $field);
}
return (int) $count;
}
/**
* SUM查询
* @access public
* @param string|Raw $field 字段名
* @return float
*/
public function sum($field): float
{
return $this->aggregate('SUM', $field, true);
}
/**
* MIN查询
* @access public
* @param string|Raw $field 字段名
* @param bool $force 强制转为数字类型
* @return mixed
*/
public function min($field, bool $force = true)
{
return $this->aggregate('MIN', $field, $force);
}
/**
* MAX查询
* @access public
* @param string|Raw $field 字段名
* @param bool $force 强制转为数字类型
* @return mixed
*/
public function max($field, bool $force = true)
{
return $this->aggregate('MAX', $field, $force);
}
/**
* AVG查询
* @access public
* @param string|Raw $field 字段名
* @return float
*/
public function avg($field): float
{
return $this->aggregate('AVG', $field, true);
}
}

View File

@@ -0,0 +1,229 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\concern;
use think\db\Raw;
use think\helper\Str;
/**
* JOIN和VIEW查询
*/
trait JoinAndViewQuery
{
/**
* 查询SQL组装 join
* @access public
* @param mixed $join 关联的表名
* @param mixed $condition 条件
* @param string $type JOIN类型
* @param array $bind 参数绑定
* @return $this
*/
public function join($join, string $condition = null, string $type = 'INNER', array $bind = [])
{
$table = $this->getJoinTable($join);
if (!empty($bind) && $condition) {
$this->bindParams($condition, $bind);
}
$this->options['join'][] = [$table, strtoupper($type), $condition];
return $this;
}
/**
* LEFT JOIN
* @access public
* @param mixed $join 关联的表名
* @param mixed $condition 条件
* @param array $bind 参数绑定
* @return $this
*/
public function leftJoin($join, string $condition = null, array $bind = [])
{
return $this->join($join, $condition, 'LEFT', $bind);
}
/**
* RIGHT JOIN
* @access public
* @param mixed $join 关联的表名
* @param mixed $condition 条件
* @param array $bind 参数绑定
* @return $this
*/
public function rightJoin($join, string $condition = null, array $bind = [])
{
return $this->join($join, $condition, 'RIGHT', $bind);
}
/**
* FULL JOIN
* @access public
* @param mixed $join 关联的表名
* @param mixed $condition 条件
* @param array $bind 参数绑定
* @return $this
*/
public function fullJoin($join, string $condition = null, array $bind = [])
{
return $this->join($join, $condition, 'FULL');
}
/**
* 获取Join表名及别名 支持
* ['prefix_table或者子查询'=>'alias'] 'table alias'
* @access protected
* @param array|string|Raw $join JION表名
* @param string $alias 别名
* @return string|array
*/
protected function getJoinTable($join, &$alias = null)
{
if (is_array($join)) {
$table = $join;
$alias = array_shift($join);
return $table;
} elseif ($join instanceof Raw) {
return $join;
}
$join = trim($join);
if (false !== strpos($join, '(')) {
// 使用子查询
$table = $join;
} else {
// 使用别名
if (strpos($join, ' ')) {
// 使用别名
[$table, $alias] = explode(' ', $join);
} else {
$table = $join;
if (false === strpos($join, '.')) {
$alias = $join;
}
}
if ($this->prefix && false === strpos($table, '.') && 0 !== strpos($table, $this->prefix)) {
$table = $this->getTable($table);
}
}
if (!empty($alias) && $table != $alias) {
$table = [$table => $alias];
}
return $table;
}
/**
* 指定JOIN查询字段
* @access public
* @param string|array $join 数据表
* @param string|array $field 查询字段
* @param string $on JOIN条件
* @param string $type JOIN类型
* @param array $bind 参数绑定
* @return $this
*/
public function view($join, $field = true, $on = null, string $type = 'INNER', array $bind = [])
{
$this->options['view'] = true;
$fields = [];
$table = $this->getJoinTable($join, $alias);
if (true === $field) {
$fields = $alias . '.*';
} else {
if (is_string($field)) {
$field = explode(',', $field);
}
foreach ($field as $key => $val) {
if (is_numeric($key)) {
$fields[] = $alias . '.' . $val;
$this->options['map'][$val] = $alias . '.' . $val;
} else {
if (preg_match('/[,=\.\'\"\(\s]/', $key)) {
$name = $key;
} else {
$name = $alias . '.' . $key;
}
$fields[] = $name . ' AS ' . $val;
$this->options['map'][$val] = $name;
}
}
}
$this->field($fields);
if ($on) {
$this->join($table, $on, $type, $bind);
} else {
$this->table($table);
}
return $this;
}
/**
* 视图查询处理
* @access protected
* @param array $options 查询参数
* @return void
*/
protected function parseView(array &$options): void
{
foreach (['AND', 'OR'] as $logic) {
if (isset($options['where'][$logic])) {
foreach ($options['where'][$logic] as $key => $val) {
if (array_key_exists($key, $options['map'])) {
array_shift($val);
array_unshift($val, $options['map'][$key]);
$options['where'][$logic][$options['map'][$key]] = $val;
unset($options['where'][$logic][$key]);
}
}
}
}
if (isset($options['order'])) {
// 视图查询排序处理
foreach ($options['order'] as $key => $val) {
if (is_numeric($key) && is_string($val)) {
if (strpos($val, ' ')) {
[$field, $sort] = explode(' ', $val);
if (array_key_exists($field, $options['map'])) {
$options['order'][$options['map'][$field]] = $sort;
unset($options['order'][$key]);
}
} elseif (array_key_exists($val, $options['map'])) {
$options['order'][$options['map'][$val]] = 'asc';
unset($options['order'][$key]);
}
} elseif (array_key_exists($key, $options['map'])) {
$options['order'][$options['map'][$key]] = $val;
unset($options['order'][$key]);
}
}
}
}
}

View File

@@ -0,0 +1,516 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\concern;
use Closure;
use think\helper\Str;
use think\Model;
use think\model\Collection as ModelCollection;
/**
* 模型及关联查询
*/
trait ModelRelationQuery
{
/**
* 当前模型对象
* @var Model
*/
protected $model;
/**
* 指定模型
* @access public
* @param Model $model 模型对象实例
* @return $this
*/
public function model(Model $model)
{
$this->model = $model;
return $this;
}
/**
* 获取当前的模型对象
* @access public
* @return Model|null
*/
public function getModel()
{
return $this->model;
}
/**
* 设置需要隐藏的输出属性
* @access public
* @param array $hidden 需要隐藏的字段名
* @return $this
*/
public function hidden(array $hidden)
{
$this->options['hidden'] = $hidden;
return $this;
}
/**
* 设置需要输出的属性
* @access public
* @param array $visible 需要输出的属性
* @return $this
*/
public function visible(array $visible)
{
$this->options['visible'] = $visible;
return $this;
}
/**
* 设置需要追加输出的属性
* @access public
* @param array $append 需要追加的属性
* @return $this
*/
public function append(array $append)
{
$this->options['append'] = $append;
return $this;
}
/**
* 添加查询范围
* @access public
* @param array|string|Closure $scope 查询范围定义
* @param array $args 参数
* @return $this
*/
public function scope($scope, ...$args)
{
// 查询范围的第一个参数始终是当前查询对象
array_unshift($args, $this);
if ($scope instanceof Closure) {
call_user_func_array($scope, $args);
return $this;
}
if (is_string($scope)) {
$scope = explode(',', $scope);
}
if ($this->model) {
// 检查模型类的查询范围方法
foreach ($scope as $name) {
$method = 'scope' . trim($name);
if (method_exists($this->model, $method)) {
call_user_func_array([$this->model, $method], $args);
}
}
}
return $this;
}
/**
* 设置关联查询
* @access public
* @param array $relation 关联名称
* @return $this
*/
public function relation(array $relation)
{
if (!empty($relation)) {
$this->options['relation'] = $relation;
}
return $this;
}
/**
* 使用搜索器条件搜索字段
* @access public
* @param array $fields 搜索字段
* @param array $data 搜索数据
* @param string $prefix 字段前缀标识
* @return $this
*/
public function withSearch(array $fields, array $data = [], string $prefix = '')
{
foreach ($fields as $key => $field) {
if ($field instanceof Closure) {
$field($this, $data[$key] ?? null, $data, $prefix);
} elseif ($this->model) {
// 检测搜索器
$fieldName = is_numeric($key) ? $field : $key;
$method = 'search' . Str::studly($fieldName) . 'Attr';
if (method_exists($this->model, $method)) {
$this->model->$method($this, $data[$field] ?? null, $data, $prefix);
}
}
}
return $this;
}
/**
* 设置数据字段获取器
* @access public
* @param string|array $name 字段名
* @param callable $callback 闭包获取器
* @return $this
*/
public function withAttr($name, callable $callback = null)
{
if (is_array($name)) {
$this->options['with_attr'] = $name;
} else {
$this->options['with_attr'][$name] = $callback;
}
return $this;
}
/**
* 关联预载入 In方式
* @access public
* @param array|string $with 关联方法名称
* @return $this
*/
public function with($with)
{
if (!empty($with)) {
$this->options['with'] = (array) $with;
}
return $this;
}
/**
* 关联预载入 JOIN方式
* @access protected
* @param array|string $with 关联方法名
* @param string $joinType JOIN方式
* @return $this
*/
public function withJoin($with, string $joinType = '')
{
if (empty($with)) {
return $this;
}
$with = (array) $with;
$first = true;
foreach ($with as $key => $relation) {
$closure = null;
$field = true;
if ($relation instanceof Closure) {
// 支持闭包查询过滤关联条件
$closure = $relation;
$relation = $key;
} elseif (is_array($relation)) {
$field = $relation;
$relation = $key;
} elseif (is_string($relation) && strpos($relation, '.')) {
$relation = strstr($relation, '.', true);
}
$result = $this->model->eagerly($this, $relation, $field, $joinType, $closure, $first);
if (!$result) {
unset($with[$key]);
} else {
$first = false;
}
}
$this->via();
$this->options['with_join'] = $with;
return $this;
}
/**
* 关联统计
* @access protected
* @param array|string $relations 关联方法名
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param bool $subQuery 是否使用子查询
* @return $this
*/
protected function withAggregate($relations, string $aggregate = 'count', $field = '*', bool $subQuery = true)
{
if (!$subQuery) {
$this->options['with_count'][] = [$relations, $aggregate, $field];
} else {
if (!isset($this->options['field'])) {
$this->field('*');
}
$this->model->relationCount($this, (array) $relations, $aggregate, $field, true);
}
return $this;
}
/**
* 关联缓存
* @access public
* @param string|array|bool $relation 关联方法名
* @param mixed $key 缓存key
* @param integer|\DateTime $expire 缓存有效期
* @param string $tag 缓存标签
* @return $this
*/
public function withCache($relation = true, $key = true, $expire = null, string $tag = null)
{
if (false === $relation || false === $key || !$this->getConnection()->getCache()) {
return $this;
}
if ($key instanceof \DateTimeInterface || $key instanceof \DateInterval || (is_int($key) && is_null($expire))) {
$expire = $key;
$key = true;
}
if (true === $relation || is_numeric($relation)) {
$this->options['with_cache'] = $relation;
return $this;
}
$relations = (array) $relation;
foreach ($relations as $name => $relation) {
if (!is_numeric($name)) {
$this->options['with_cache'][$name] = is_array($relation) ? $relation : [$key, $relation, $tag];
} else {
$this->options['with_cache'][$relation] = [$key, $expire, $tag];
}
}
return $this;
}
/**
* 关联统计
* @access public
* @param string|array $relation 关联方法名
* @param bool $subQuery 是否使用子查询
* @return $this
*/
public function withCount($relation, bool $subQuery = true)
{
return $this->withAggregate($relation, 'count', '*', $subQuery);
}
/**
* 关联统计Sum
* @access public
* @param string|array $relation 关联方法名
* @param string $field 字段
* @param bool $subQuery 是否使用子查询
* @return $this
*/
public function withSum($relation, string $field, bool $subQuery = true)
{
return $this->withAggregate($relation, 'sum', $field, $subQuery);
}
/**
* 关联统计Max
* @access public
* @param string|array $relation 关联方法名
* @param string $field 字段
* @param bool $subQuery 是否使用子查询
* @return $this
*/
public function withMax($relation, string $field, bool $subQuery = true)
{
return $this->withAggregate($relation, 'max', $field, $subQuery);
}
/**
* 关联统计Min
* @access public
* @param string|array $relation 关联方法名
* @param string $field 字段
* @param bool $subQuery 是否使用子查询
* @return $this
*/
public function withMin($relation, string $field, bool $subQuery = true)
{
return $this->withAggregate($relation, 'min', $field, $subQuery);
}
/**
* 关联统计Avg
* @access public
* @param string|array $relation 关联方法名
* @param string $field 字段
* @param bool $subQuery 是否使用子查询
* @return $this
*/
public function withAvg($relation, string $field, bool $subQuery = true)
{
return $this->withAggregate($relation, 'avg', $field, $subQuery);
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $relation 关联方法名
* @param mixed $operator 比较操作符
* @param integer $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @return $this
*/
public function has(string $relation, string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '')
{
return $this->model->has($relation, $operator, $count, $id, $joinType, $this);
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $relation 关联方法名
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @return $this
*/
public function hasWhere(string $relation, $where = [], string $fields = '*', string $joinType = '')
{
return $this->model->hasWhere($relation, $where, $fields, $joinType, $this);
}
/**
* 查询数据转换为模型数据集对象
* @access protected
* @param array $resultSet 数据集
* @return ModelCollection
*/
protected function resultSetToModelCollection(array $resultSet): ModelCollection
{
if (empty($resultSet)) {
return $this->model->toCollection();
}
// 检查动态获取器
if (!empty($this->options['with_attr'])) {
foreach ($this->options['with_attr'] as $name => $val) {
if (strpos($name, '.')) {
[$relation, $field] = explode('.', $name);
$withRelationAttr[$relation][$field] = $val;
unset($this->options['with_attr'][$name]);
}
}
}
$withRelationAttr = $withRelationAttr ?? [];
foreach ($resultSet as $key => &$result) {
// 数据转换为模型对象
$this->resultToModel($result, $this->options, true, $withRelationAttr);
}
if (!empty($this->options['with'])) {
// 预载入
$result->eagerlyResultSet($resultSet, $this->options['with'], $withRelationAttr, false, $this->options['with_cache'] ?? false);
}
if (!empty($this->options['with_join'])) {
// 预载入
$result->eagerlyResultSet($resultSet, $this->options['with_join'], $withRelationAttr, true, $this->options['with_cache'] ?? false);
}
// 模型数据集转换
return $this->model->toCollection($resultSet);
}
/**
* 查询数据转换为模型对象
* @access protected
* @param array $result 查询数据
* @param array $options 查询参数
* @param bool $resultSet 是否为数据集查询
* @param array $withRelationAttr 关联字段获取器
* @return void
*/
protected function resultToModel(array &$result, array $options = [], bool $resultSet = false, array $withRelationAttr = []): void
{
// 动态获取器
if (!empty($options['with_attr']) && empty($withRelationAttr)) {
foreach ($options['with_attr'] as $name => $val) {
if (strpos($name, '.')) {
[$relation, $field] = explode('.', $name);
$withRelationAttr[$relation][$field] = $val;
unset($options['with_attr'][$name]);
}
}
}
// JSON 数据处理
if (!empty($options['json'])) {
$this->jsonResult($result, $options['json'], $options['json_assoc'], $withRelationAttr);
}
$result = $this->model
->newInstance($result, $resultSet ? null : $this->getModelUpdateCondition($options));
// 动态获取器
if (!empty($options['with_attr'])) {
$result->withAttribute($options['with_attr']);
}
// 输出属性控制
if (!empty($options['visible'])) {
$result->visible($options['visible']);
} elseif (!empty($options['hidden'])) {
$result->hidden($options['hidden']);
}
if (!empty($options['append'])) {
$result->append($options['append']);
}
// 关联查询
if (!empty($options['relation'])) {
$result->relationQuery($options['relation'], $withRelationAttr);
}
// 预载入查询
if (!$resultSet && !empty($options['with'])) {
$result->eagerlyResult($result, $options['with'], $withRelationAttr, false, $options['with_cache'] ?? false);
}
// JOIN预载入查询
if (!$resultSet && !empty($options['with_join'])) {
$result->eagerlyResult($result, $options['with_join'], $withRelationAttr, true, $options['with_cache'] ?? false);
}
// 关联统计
if (!empty($options['with_count'])) {
foreach ($options['with_count'] as $val) {
$result->relationCount($this, (array) $val[0], $val[1], $val[2], false);
}
}
}
}

View File

@@ -0,0 +1,106 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\concern;
use PDO;
/**
* 参数绑定支持
*/
trait ParamsBind
{
/**
* 当前参数绑定
* @var array
*/
protected $bind = [];
/**
* 批量参数绑定
* @access public
* @param array $value 绑定变量值
* @return $this
*/
public function bind(array $value)
{
$this->bind = array_merge($this->bind, $value);
return $this;
}
/**
* 单个参数绑定
* @access public
* @param mixed $value 绑定变量值
* @param integer $type 绑定类型
* @param string $name 绑定标识
* @return string
*/
public function bindValue($value, int $type = null, string $name = null)
{
$name = $name ?: 'ThinkBind_' . (count($this->bind) + 1) . '_' . mt_rand() . '_';
$this->bind[$name] = [$value, $type ?: PDO::PARAM_STR];
return $name;
}
/**
* 检测参数是否已经绑定
* @access public
* @param string $key 参数名
* @return bool
*/
public function isBind($key)
{
return isset($this->bind[$key]);
}
/**
* 参数绑定
* @access public
* @param string $sql 绑定的sql表达式
* @param array $bind 参数绑定
* @return void
*/
protected function bindParams(string &$sql, array $bind = []): void
{
foreach ($bind as $key => $value) {
if (is_array($value)) {
$name = $this->bindValue($value[0], $value[1], $value[2] ?? null);
} else {
$name = $this->bindValue($value);
}
if (is_numeric($key)) {
$sql = substr_replace($sql, ':' . $name, strpos($sql, '?'), 1);
} else {
$sql = str_replace(':' . $key, ':' . $name, $sql);
}
}
}
/**
* 获取绑定的参数 并清空
* @access public
* @param bool $clear 是否清空绑定数据
* @return array
*/
public function getBind(bool $clear = true): array
{
$bind = $this->bind;
if ($clear) {
$this->bind = [];
}
return $bind;
}
}

View File

@@ -0,0 +1,248 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\concern;
use think\Collection;
use think\db\exception\DataNotFoundException;
use think\db\exception\ModelNotFoundException;
use think\helper\Str;
/**
* 查询数据处理
*/
trait ResultOperation
{
/**
* 是否允许返回空数据(或空模型)
* @access public
* @param bool $allowEmpty 是否允许为空
* @return $this
*/
public function allowEmpty(bool $allowEmpty = true)
{
$this->options['allow_empty'] = $allowEmpty;
return $this;
}
/**
* 设置查询数据不存在是否抛出异常
* @access public
* @param bool $fail 数据不存在是否抛出异常
* @return $this
*/
public function failException(bool $fail = true)
{
$this->options['fail'] = $fail;
return $this;
}
/**
* 处理数据
* @access protected
* @param array $result 查询数据
* @return void
*/
protected function result(array &$result): void
{
if (!empty($this->options['json'])) {
$this->jsonResult($result, $this->options['json'], true);
}
if (!empty($this->options['with_attr'])) {
$this->getResultAttr($result, $this->options['with_attr']);
}
$this->filterResult($result);
}
/**
* 处理数据集
* @access public
* @param array $resultSet 数据集
* @return void
*/
protected function resultSet(array &$resultSet): void
{
if (!empty($this->options['json'])) {
foreach ($resultSet as &$result) {
$this->jsonResult($result, $this->options['json'], true);
}
}
if (!empty($this->options['with_attr'])) {
foreach ($resultSet as &$result) {
$this->getResultAttr($result, $this->options['with_attr']);
}
}
if (!empty($this->options['visible']) || !empty($this->options['hidden'])) {
foreach ($resultSet as &$result) {
$this->filterResult($result);
}
}
// 返回Collection对象
$resultSet = new Collection($resultSet);
}
/**
* 处理数据的可见和隐藏
* @access protected
* @param array $result 查询数据
* @return void
*/
protected function filterResult(&$result): void
{
if (!empty($this->options['visible'])) {
foreach ($this->options['visible'] as $key) {
$array[] = $key;
}
$result = array_intersect_key($result, array_flip($array));
} elseif (!empty($this->options['hidden'])) {
foreach ($this->options['hidden'] as $key) {
$array[] = $key;
}
$result = array_diff_key($result, array_flip($array));
}
}
/**
* 使用获取器处理数据
* @access protected
* @param array $result 查询数据
* @param array $withAttr 字段获取器
* @return void
*/
protected function getResultAttr(array &$result, array $withAttr = []): void
{
foreach ($withAttr as $name => $closure) {
$name = Str::snake($name);
if (strpos($name, '.')) {
// 支持JSON字段 获取器定义
[$key, $field] = explode('.', $name);
if (isset($result[$key])) {
$result[$key][$field] = $closure($result[$key][$field] ?? null, $result[$key]);
}
} else {
$result[$name] = $closure($result[$name] ?? null, $result);
}
}
}
/**
* 处理空数据
* @access protected
* @return array|Model|null
* @throws DbException
* @throws ModelNotFoundException
* @throws DataNotFoundException
*/
protected function resultToEmpty()
{
if (!empty($this->options['fail'])) {
$this->throwNotFound();
} elseif (!empty($this->options['allow_empty'])) {
return !empty($this->model) ? $this->model->newInstance() : [];
}
}
/**
* 查找单条记录 不存在返回空数据(或者空模型)
* @access public
* @param mixed $data 数据
* @return array|Model
*/
public function findOrEmpty($data = null)
{
return $this->allowEmpty(true)->find($data);
}
/**
* JSON字段数据转换
* @access protected
* @param array $result 查询数据
* @param array $json JSON字段
* @param bool $assoc 是否转换为数组
* @param array $withRelationAttr 关联获取器
* @return void
*/
protected function jsonResult(array &$result, array $json = [], bool $assoc = false, array $withRelationAttr = []): void
{
foreach ($json as $name) {
if (!isset($result[$name])) {
continue;
}
$result[$name] = json_decode($result[$name], true);
if (isset($withRelationAttr[$name])) {
foreach ($withRelationAttr[$name] as $key => $closure) {
$result[$name][$key] = $closure($result[$name][$key] ?? null, $result[$name]);
}
}
if (!$assoc) {
$result[$name] = (object) $result[$name];
}
}
}
/**
* 查询失败 抛出异常
* @access protected
* @return void
* @throws ModelNotFoundException
* @throws DataNotFoundException
*/
protected function throwNotFound(): void
{
if (!empty($this->model)) {
$class = get_class($this->model);
throw new ModelNotFoundException('model data Not Found:' . $class, $class, $this->options);
}
$table = $this->getTable();
throw new DataNotFoundException('table data not Found:' . $table, $table, $this->options);
}
/**
* 查找多条记录 如果不存在则抛出异常
* @access public
* @param array|string|Query|Closure $data 数据
* @return array|Model
* @throws DbException
* @throws ModelNotFoundException
* @throws DataNotFoundException
*/
public function selectOrFail($data = null)
{
return $this->failException(true)->select($data);
}
/**
* 查找单条记录 如果不存在则抛出异常
* @access public
* @param array|string|Query|Closure $data 数据
* @return array|Model
* @throws DbException
* @throws ModelNotFoundException
* @throws DataNotFoundException
*/
public function findOrFail($data = null)
{
return $this->failException(true)->find($data);
}
}

View File

@@ -0,0 +1,99 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\concern;
/**
* 数据字段信息
*/
trait TableFieldInfo
{
/**
* 获取数据表字段信息
* @access public
* @param string $tableName 数据表名
* @return array
*/
public function getTableFields($tableName = ''): array
{
if ('' == $tableName) {
$tableName = $this->getTable();
}
return $this->connection->getTableFields($tableName);
}
/**
* 获取详细字段类型信息
* @access public
* @param string $tableName 数据表名称
* @return array
*/
public function getFields(string $tableName = ''): array
{
return $this->connection->getFields($tableName ?: $this->getTable());
}
/**
* 获取字段类型信息
* @access public
* @return array
*/
public function getFieldsType(): array
{
if (!empty($this->options['field_type'])) {
return $this->options['field_type'];
}
return $this->connection->getFieldsType($this->getTable());
}
/**
* 获取字段类型信息
* @access public
* @param string $field 字段名
* @return string|null
*/
public function getFieldType(string $field)
{
$fieldType = $this->getFieldsType();
return $fieldType[$field] ?? null;
}
/**
* 获取字段类型信息
* @access public
* @return array
*/
public function getFieldsBindType(): array
{
$fieldType = $this->getFieldsType();
return array_map([$this->connection, 'getFieldBindType'], $fieldType);
}
/**
* 获取字段类型信息
* @access public
* @param string $field 字段名
* @return int
*/
public function getFieldBindType(string $field): int
{
$fieldType = $this->getFieldType($field);
return $this->connection->getFieldBindType($fieldType ?: '');
}
}

View File

@@ -0,0 +1,218 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\concern;
/**
* 时间查询支持
*/
trait TimeFieldQuery
{
/**
* 日期查询表达式
* @var array
*/
protected $timeRule = [
'today' => ['today', 'tomorrow -1second'],
'yesterday' => ['yesterday', 'today -1second'],
'week' => ['this week 00:00:00', 'next week 00:00:00 -1second'],
'last week' => ['last week 00:00:00', 'this week 00:00:00 -1second'],
'month' => ['first Day of this month 00:00:00', 'first Day of next month 00:00:00 -1second'],
'last month' => ['first Day of last month 00:00:00', 'first Day of this month 00:00:00 -1second'],
'year' => ['this year 1/1', 'next year 1/1 -1second'],
'last year' => ['last year 1/1', 'this year 1/1 -1second'],
];
/**
* 添加日期或者时间查询规则
* @access public
* @param array $rule 时间表达式
* @return $this
*/
public function timeRule(array $rule)
{
$this->timeRule = array_merge($this->timeRule, $rule);
return $this;
}
/**
* 查询日期或者时间
* @access public
* @param string $field 日期字段名
* @param string $op 比较运算符或者表达式
* @param string|array $range 比较范围
* @param string $logic AND OR
* @return $this
*/
public function whereTime(string $field, string $op, $range = null, string $logic = 'AND')
{
if (is_null($range)) {
if (isset($this->timeRule[$op])) {
$range = $this->timeRule[$op];
} else {
$range = $op;
}
$op = is_array($range) ? 'between' : '>=';
}
return $this->parseWhereExp($logic, $field, strtolower($op) . ' time', $range, [], true);
}
/**
* 查询某个时间间隔数据
* @access public
* @param string $field 日期字段名
* @param string $start 开始时间
* @param string $interval 时间间隔单位 day/month/year/week/hour/minute/second
* @param int $step 间隔
* @param string $logic AND OR
* @return $this
*/
public function whereTimeInterval(string $field, string $start, string $interval = 'day', int $step = 1, string $logic = 'AND')
{
$startTime = strtotime($start);
$endTime = strtotime(($step > 0 ? '+' : '-') . abs($step) . ' ' . $interval . (abs($step) > 1 ? 's' : ''), $startTime);
return $this->whereTime($field, 'between', $step > 0 ? [$startTime, $endTime - 1] : [$endTime, $startTime - 1], $logic);
}
/**
* 查询月数据 whereMonth('time_field', '2018-1')
* @access public
* @param string $field 日期字段名
* @param string $month 月份信息
* @param int $step 间隔
* @param string $logic AND OR
* @return $this
*/
public function whereMonth(string $field, string $month = 'this month', int $step = 1, string $logic = 'AND')
{
if (in_array($month, ['this month', 'last month'])) {
$month = date('Y-m', strtotime($month));
}
return $this->whereTimeInterval($field, $month, 'month', $step, $logic);
}
/**
* 查询周数据 whereWeek('time_field', '2018-1-1') 从2018-1-1开始的一周数据
* @access public
* @param string $field 日期字段名
* @param string $week 周信息
* @param int $step 间隔
* @param string $logic AND OR
* @return $this
*/
public function whereWeek(string $field, string $week = 'this week', int $step = 1, string $logic = 'AND')
{
if (in_array($week, ['this week', 'last week'])) {
$week = date('Y-m-d', strtotime($week));
}
return $this->whereTimeInterval($field, $week, 'week', $step, $logic);
}
/**
* 查询年数据 whereYear('time_field', '2018')
* @access public
* @param string $field 日期字段名
* @param string $year 年份信息
* @param int $step 间隔
* @param string $logic AND OR
* @return $this
*/
public function whereYear(string $field, string $year = 'this year', int $step = 1, string $logic = 'AND')
{
if (in_array($year, ['this year', 'last year'])) {
$year = date('Y', strtotime($year));
}
return $this->whereTimeInterval($field, $year . '-1-1', 'year', $step, $logic);
}
/**
* 查询日数据 whereDay('time_field', '2018-1-1')
* @access public
* @param string $field 日期字段名
* @param string $day 日期信息
* @param int $step 间隔
* @param string $logic AND OR
* @return $this
*/
public function whereDay(string $field, string $day = 'today', int $step = 1, string $logic = 'AND')
{
if (in_array($day, ['today', 'yesterday'])) {
$day = date('Y-m-d', strtotime($day));
}
return $this->whereTimeInterval($field, $day, 'day', $step, $logic);
}
/**
* 查询日期或者时间范围 whereBetweenTime('time_field', '2018-1-1','2018-1-15')
* @access public
* @param string $field 日期字段名
* @param string|int $startTime 开始时间
* @param string|int $endTime 结束时间
* @param string $logic AND OR
* @return $this
*/
public function whereBetweenTime(string $field, $startTime, $endTime, string $logic = 'AND')
{
return $this->whereTime($field, 'between', [$startTime, $endTime], $logic);
}
/**
* 查询日期或者时间范围 whereNotBetweenTime('time_field', '2018-1-1','2018-1-15')
* @access public
* @param string $field 日期字段名
* @param string|int $startTime 开始时间
* @param string|int $endTime 结束时间
* @return $this
*/
public function whereNotBetweenTime(string $field, $startTime, $endTime)
{
return $this->whereTime($field, '<', $startTime)
->whereTime($field, '>', $endTime);
}
/**
* 查询当前时间在两个时间字段范围 whereBetweenTimeField('start_time', 'end_time')
* @access public
* @param string $startField 开始时间字段
* @param string $endField 结束时间字段
* @return $this
*/
public function whereBetweenTimeField(string $startField, string $endField)
{
return $this->whereTime($startField, '<=', time())
->whereTime($endField, '>=', time());
}
/**
* 查询当前时间不在两个时间字段范围 whereNotBetweenTimeField('start_time', 'end_time')
* @access public
* @param string $startField 开始时间字段
* @param string $endField 结束时间字段
* @return $this
*/
public function whereNotBetweenTimeField(string $startField, string $endField)
{
return $this->whereTime($startField, '>', time())
->whereTime($endField, '<', time(), 'OR');
}
}

View File

@@ -0,0 +1,117 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\concern;
use think\db\BaseQuery;
/**
* 事务支持
*/
trait Transaction
{
/**
* 执行数据库Xa事务
* @access public
* @param callable $callback 数据操作方法回调
* @param array $dbs 多个查询对象或者连接对象
* @return mixed
* @throws PDOException
* @throws \Exception
* @throws \Throwable
*/
public function transactionXa($callback, array $dbs = [])
{
$xid = uniqid('xa');
if (empty($dbs)) {
$dbs[] = $this->getConnection();
}
foreach ($dbs as $key => $db) {
if ($db instanceof BaseQuery) {
$db = $db->getConnection();
$dbs[$key] = $db;
}
$db->startTransXa($xid);
}
try {
$result = null;
if (is_callable($callback)) {
$result = call_user_func_array($callback, [$this]);
}
foreach ($dbs as $db) {
$db->prepareXa($xid);
}
foreach ($dbs as $db) {
$db->commitXa($xid);
}
return $result;
} catch (\Exception | \Throwable $e) {
foreach ($dbs as $db) {
$db->rollbackXa($xid);
}
throw $e;
}
}
/**
* 执行数据库事务
* @access public
* @param callable $callback 数据操作方法回调
* @return mixed
*/
public function transaction(callable $callback)
{
return $this->connection->transaction($callback);
}
/**
* 启动事务
* @access public
* @return void
*/
public function startTrans(): void
{
$this->connection->startTrans();
}
/**
* 用于非自动提交状态下面的查询提交
* @access public
* @return void
* @throws PDOException
*/
public function commit(): void
{
$this->connection->commit();
}
/**
* 事务回滚
* @access public
* @return void
* @throws PDOException
*/
public function rollback(): void
{
$this->connection->rollback();
}
}

View File

@@ -0,0 +1,540 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\concern;
use Closure;
use think\db\BaseQuery;
use think\db\Raw;
trait WhereQuery
{
/**
* 指定AND查询条件
* @access public
* @param mixed $field 查询字段
* @param mixed $op 查询表达式
* @param mixed $condition 查询条件
* @return $this
*/
public function where($field, $op = null, $condition = null)
{
if ($field instanceof $this) {
$this->parseQueryWhere($field);
return $this;
} elseif (true === $field || 1 === $field) {
$this->options['where']['AND'][] = true;
return $this;
}
$param = func_get_args();
array_shift($param);
return $this->parseWhereExp('AND', $field, $op, $condition, $param);
}
/**
* 解析Query对象查询条件
* @access public
* @param BaseQuery $query 查询对象
* @return void
*/
protected function parseQueryWhere(BaseQuery $query): void
{
$this->options['where'] = $query->getOptions('where');
if ($query->getOptions('via')) {
$via = $query->getOptions('via');
foreach ($this->options['where'] as $logic => &$where) {
foreach ($where as $key => &$val) {
if (is_array($val) && !strpos($val[0], '.')) {
$val[0] = $via . '.' . $val[0];
}
}
}
}
$this->bind($query->getBind(false));
}
/**
* 指定OR查询条件
* @access public
* @param mixed $field 查询字段
* @param mixed $op 查询表达式
* @param mixed $condition 查询条件
* @return $this
*/
public function whereOr($field, $op = null, $condition = null)
{
$param = func_get_args();
array_shift($param);
return $this->parseWhereExp('OR', $field, $op, $condition, $param);
}
/**
* 指定XOR查询条件
* @access public
* @param mixed $field 查询字段
* @param mixed $op 查询表达式
* @param mixed $condition 查询条件
* @return $this
*/
public function whereXor($field, $op = null, $condition = null)
{
$param = func_get_args();
array_shift($param);
return $this->parseWhereExp('XOR', $field, $op, $condition, $param);
}
/**
* 指定Null查询条件
* @access public
* @param mixed $field 查询字段
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereNull(string $field, string $logic = 'AND')
{
return $this->parseWhereExp($logic, $field, 'NULL', null, [], true);
}
/**
* 指定NotNull查询条件
* @access public
* @param mixed $field 查询字段
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereNotNull(string $field, string $logic = 'AND')
{
return $this->parseWhereExp($logic, $field, 'NOTNULL', null, [], true);
}
/**
* 指定Exists查询条件
* @access public
* @param mixed $condition 查询条件
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereExists($condition, string $logic = 'AND')
{
if (is_string($condition)) {
$condition = new Raw($condition);
}
$this->options['where'][strtoupper($logic)][] = ['', 'EXISTS', $condition];
return $this;
}
/**
* 指定NotExists查询条件
* @access public
* @param mixed $condition 查询条件
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereNotExists($condition, string $logic = 'AND')
{
if (is_string($condition)) {
$condition = new Raw($condition);
}
$this->options['where'][strtoupper($logic)][] = ['', 'NOT EXISTS', $condition];
return $this;
}
/**
* 指定In查询条件
* @access public
* @param mixed $field 查询字段
* @param mixed $condition 查询条件
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereIn(string $field, $condition, string $logic = 'AND')
{
return $this->parseWhereExp($logic, $field, 'IN', $condition, [], true);
}
/**
* 指定NotIn查询条件
* @access public
* @param mixed $field 查询字段
* @param mixed $condition 查询条件
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereNotIn(string $field, $condition, string $logic = 'AND')
{
return $this->parseWhereExp($logic, $field, 'NOT IN', $condition, [], true);
}
/**
* 指定Like查询条件
* @access public
* @param mixed $field 查询字段
* @param mixed $condition 查询条件
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereLike(string $field, $condition, string $logic = 'AND')
{
return $this->parseWhereExp($logic, $field, 'LIKE', $condition, [], true);
}
/**
* 指定NotLike查询条件
* @access public
* @param mixed $field 查询字段
* @param mixed $condition 查询条件
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereNotLike(string $field, $condition, string $logic = 'AND')
{
return $this->parseWhereExp($logic, $field, 'NOT LIKE', $condition, [], true);
}
/**
* 指定Between查询条件
* @access public
* @param mixed $field 查询字段
* @param mixed $condition 查询条件
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereBetween(string $field, $condition, string $logic = 'AND')
{
return $this->parseWhereExp($logic, $field, 'BETWEEN', $condition, [], true);
}
/**
* 指定NotBetween查询条件
* @access public
* @param mixed $field 查询字段
* @param mixed $condition 查询条件
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereNotBetween(string $field, $condition, string $logic = 'AND')
{
return $this->parseWhereExp($logic, $field, 'NOT BETWEEN', $condition, [], true);
}
/**
* 指定FIND_IN_SET查询条件
* @access public
* @param mixed $field 查询字段
* @param mixed $condition 查询条件
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereFindInSet(string $field, $condition, string $logic = 'AND')
{
return $this->parseWhereExp($logic, $field, 'FIND IN SET', $condition, [], true);
}
/**
* 比较两个字段
* @access public
* @param string $field1 查询字段
* @param string $operator 比较操作符
* @param string $field2 比较字段
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereColumn(string $field1, string $operator, string $field2 = null, string $logic = 'AND')
{
if (is_null($field2)) {
$field2 = $operator;
$operator = '=';
}
return $this->parseWhereExp($logic, $field1, 'COLUMN', [$operator, $field2], [], true);
}
/**
* 设置软删除字段及条件
* @access public
* @param string $field 查询字段
* @param mixed $condition 查询条件
* @return $this
*/
public function useSoftDelete(string $field, $condition = null)
{
if ($field) {
$this->options['soft_delete'] = [$field, $condition];
}
return $this;
}
/**
* 指定Exp查询条件
* @access public
* @param mixed $field 查询字段
* @param string $where 查询条件
* @param array $bind 参数绑定
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereExp(string $field, string $where, array $bind = [], string $logic = 'AND')
{
if (!empty($bind)) {
$this->bindParams($where, $bind);
}
$this->options['where'][$logic][] = [$field, 'EXP', new Raw($where)];
return $this;
}
/**
* 指定字段Raw查询
* @access public
* @param string $field 查询字段表达式
* @param mixed $op 查询表达式
* @param string $condition 查询条件
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereFieldRaw(string $field, $op, $condition = null, string $logic = 'AND')
{
if (is_null($condition)) {
$condition = $op;
$op = '=';
}
$this->options['where'][$logic][] = [new Raw($field), $op, $condition];
return $this;
}
/**
* 指定表达式查询条件
* @access public
* @param string $where 查询条件
* @param array $bind 参数绑定
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function whereRaw(string $where, array $bind = [], string $logic = 'AND')
{
if (!empty($bind)) {
$this->bindParams($where, $bind);
}
$this->options['where'][$logic][] = new Raw($where);
return $this;
}
/**
* 指定表达式查询条件 OR
* @access public
* @param string $where 查询条件
* @param array $bind 参数绑定
* @return $this
*/
public function whereOrRaw(string $where, array $bind = [])
{
return $this->whereRaw($where, $bind, 'OR');
}
/**
* 分析查询表达式
* @access protected
* @param string $logic 查询逻辑 and or xor
* @param mixed $field 查询字段
* @param mixed $op 查询表达式
* @param mixed $condition 查询条件
* @param array $param 查询参数
* @param bool $strict 严格模式
* @return $this
*/
protected function parseWhereExp(string $logic, $field, $op, $condition, array $param = [], bool $strict = false)
{
$logic = strtoupper($logic);
if (is_string($field) && !empty($this->options['via']) && false === strpos($field, '.')) {
$field = $this->options['via'] . '.' . $field;
}
if ($field instanceof Raw) {
return $this->whereRaw($field, is_array($op) ? $op : [], $logic);
} elseif ($strict) {
// 使用严格模式查询
if ('=' == $op) {
$where = $this->whereEq($field, $condition);
} else {
$where = [$field, $op, $condition, $logic];
}
} elseif (is_array($field)) {
// 解析数组批量查询
return $this->parseArrayWhereItems($field, $logic);
} elseif ($field instanceof Closure) {
$where = $field;
} elseif (is_string($field)) {
if (preg_match('/[,=\<\'\"\(\s]/', $field)) {
return $this->whereRaw($field, is_array($op) ? $op : [], $logic);
} elseif (is_string($op) && strtolower($op) == 'exp') {
$bind = isset($param[2]) && is_array($param[2]) ? $param[2] : [];
return $this->whereExp($field, $condition, $bind, $logic);
}
$where = $this->parseWhereItem($logic, $field, $op, $condition, $param);
}
if (!empty($where)) {
$this->options['where'][$logic][] = $where;
}
return $this;
}
/**
* 分析查询表达式
* @access protected
* @param string $logic 查询逻辑 and or xor
* @param mixed $field 查询字段
* @param mixed $op 查询表达式
* @param mixed $condition 查询条件
* @param array $param 查询参数
* @return array
*/
protected function parseWhereItem(string $logic, $field, $op, $condition, array $param = []): array
{
if (is_array($op)) {
// 同一字段多条件查询
array_unshift($param, $field);
$where = $param;
} elseif ($field && is_null($condition)) {
if (is_string($op) && in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) {
// null查询
$where = [$field, $op, ''];
} elseif ('=' === $op || is_null($op)) {
$where = [$field, 'NULL', ''];
} elseif ('<>' === $op) {
$where = [$field, 'NOTNULL', ''];
} else {
// 字段相等查询
$where = $this->whereEq($field, $op);
}
} elseif (is_string($op) && in_array(strtoupper($op), ['EXISTS', 'NOT EXISTS', 'NOTEXISTS'], true)) {
$where = [$field, $op, is_string($condition) ? new Raw($condition) : $condition];
} else {
$where = $field ? [$field, $op, $condition, $param[2] ?? null] : [];
}
return $where;
}
/**
* 相等查询的主键处理
* @access protected
* @param string $field 字段名
* @param mixed $value 字段值
* @return array
*/
protected function whereEq(string $field, $value): array
{
if ($this->getPk() == $field) {
$this->options['key'] = $value;
}
return [$field, '=', $value];
}
/**
* 数组批量查询
* @access protected
* @param array $field 批量查询
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
protected function parseArrayWhereItems(array $field, string $logic)
{
if (key($field) !== 0) {
$where = [];
foreach ($field as $key => $val) {
if ($val instanceof Raw) {
$where[] = [$key, 'exp', $val];
} else {
$where[] = is_null($val) ? [$key, 'NULL', ''] : [$key, is_array($val) ? 'IN' : '=', $val];
}
}
} else {
// 数组批量查询
$where = $field;
}
if (!empty($where)) {
$this->options['where'][$logic] = isset($this->options['where'][$logic]) ?
array_merge($this->options['where'][$logic], $where) : $where;
}
return $this;
}
/**
* 去除某个查询条件
* @access public
* @param string $field 查询字段
* @param string $logic 查询逻辑 and or xor
* @return $this
*/
public function removeWhereField(string $field, string $logic = 'AND')
{
$logic = strtoupper($logic);
if (isset($this->options['where'][$logic])) {
foreach ($this->options['where'][$logic] as $key => $val) {
if (is_array($val) && $val[0] == $field) {
unset($this->options['where'][$logic][$key]);
}
}
}
return $this;
}
/**
* 条件查询
* @access public
* @param mixed $condition 满足条件(支持闭包)
* @param Closure|array $query 满足条件后执行的查询表达式(闭包或数组)
* @param Closure|array $otherwise 不满足条件后执行
* @return $this
*/
public function when($condition, $query, $otherwise = null)
{
if ($condition instanceof Closure) {
$condition = $condition($this);
}
if ($condition) {
if ($query instanceof Closure) {
$query($this, $condition);
} elseif (is_array($query)) {
$this->where($query);
}
} elseif ($otherwise) {
if ($otherwise instanceof Closure) {
$otherwise($this, $condition);
} elseif (is_array($otherwise)) {
$this->where($otherwise);
}
}
return $this;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,162 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\connector;
use PDO;
use think\db\PDOConnection;
/**
* mysql数据库驱动
*/
class Mysql extends PDOConnection
{
/**
* 解析pdo连接的dsn信息
* @access protected
* @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;
}
/**
* 取得数据表的字段信息
* @access public
* @param string $tableName
* @return array
*/
public function getFields(string $tableName): array
{
[$tableName] = explode(' ', $tableName);
if (false === strpos($tableName, '`')) {
if (strpos($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' => (bool) ('' === $val['null']), // not null is empty, null is yes
'default' => $val['default'],
'primary' => (strtolower($val['key']) == 'pri'),
'autoinc' => (strtolower($val['extra']) == 'auto_increment'),
'comment' => $val['comment'],
];
}
}
return $this->fieldCase($info);
}
/**
* 取得数据库的表信息
* @access public
* @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事务
* @access public
* @param string $xid XA事务id
* @return void
*/
public function startTransXa(string $xid)
{
$this->initConnect(true);
$this->linkID->execute("XA START '$xid'");
}
/**
* 预编译XA事务
* @access public
* @param string $xid XA事务id
* @return void
*/
public function prepareXa(string $xid)
{
$this->initConnect(true);
$this->linkID->execute("XA END '$xid'");
$this->linkID->execute("XA PREPARE '$xid'");
}
/**
* 提交XA事务
* @access public
* @param string $xid XA事务id
* @return void
*/
public function commitXa(string $xid)
{
$this->initConnect(true);
$this->linkID->execute("XA COMMIT '$xid'");
}
/**
* 回滚XA事务
* @access public
* @param string $xid XA事务id
* @return void
*/
public function rollbackXa(string $xid)
{
$this->initConnect(true);
$this->linkID->execute("XA ROLLBACK '$xid'");
}
}

View File

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

View File

@@ -0,0 +1,108 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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;
/**
* 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信息
* @access protected
* @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;
}
/**
* 取得数据表的字段信息
* @access public
* @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" 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' => (0 === strpos($val['extra'], 'nextval(')),
];
}
}
return $this->fieldCase($info);
}
/**
* 取得数据库的表信息
* @access public
* @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,96 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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信息
* @access protected
* @param array $config 连接信息
* @return string
*/
protected function parseDsn(array $config): string
{
$dsn = 'sqlite:' . $config['database'];
return $dsn;
}
/**
* 取得数据表的字段信息
* @access public
* @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);
}
/**
* 取得数据库的表信息
* @access public
* @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,122 @@
<?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\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信息
* @access protected
* @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'];
}
return $dsn;
}
/**
* 取得数据表的字段信息
* @access public
* @param string $tableName
* @return array
*/
public function getFields(string $tableName): array
{
[$tableName] = explode(' ', $tableName);
$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);
}
/**
* 取得数据表的字段信息
* @access public
* @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,35 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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.
* @access public
* @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,43 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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.
* @access public
* @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);
}
/**
* 获取数据表名
* @access public
* @return string
*/
public function getTable()
{
return $this->table;
}
}

View File

@@ -0,0 +1,81 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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 Exception;
/**
* Database相关异常处理类
*/
class DbException extends Exception
{
/**
* DbException constructor.
* @access public
* @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);
}
/**
* 保存异常页面显示的额外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($label, array $data)
{
$this->data[$label] = $data;
}
/**
* 获取异常额外Debug数据
* 主要用于输出到异常页面便于调试
* @return array 由setData设置的Debug数据
*/
final public function getData()
{
return $this->data;
}
}

View File

@@ -0,0 +1,21 @@
<?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,19 @@
<?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>
// +----------------------------------------------------------------------
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~2019 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;
/**
* 构造方法
* @access public
* @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);
}
/**
* 获取模型类名
* @access public
* @return string
*/
public function getModel()
{
return $this->model;
}
}

View File

@@ -0,0 +1,41 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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.
* @access public
* @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;
$this->setData('PDO Error Info', [
'SQLSTATE' => $error[0],
'Driver Error Code' => isset($error[1]) ? $error[1] : 0,
'Driver Error Message' => isset($error[2]) ? $error[2] : '',
]);
parent::__construct($exception->getMessage(), $config, $sql, $code);
}
}

View File

@@ -0,0 +1,86 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace think\facade;
if (class_exists('think\Facade')) {
class Facade extends \think\Facade
{}
} else {
class Facade
{
/**
* 始终创建新的对象实例
* @var bool
*/
protected static $alwaysNewInstance;
protected static $instance;
/**
* 获取当前Facade对应类名
* @access protected
* @return string
*/
protected static function getFacadeClass()
{}
/**
* 创建Facade实例
* @static
* @access protected
* @param bool $newInstance 是否每次创建新的实例
* @return object
*/
protected static function createFacade(bool $newInstance = false)
{
$class = static::getFacadeClass() ?: 'think\DbManager';
if (static::$alwaysNewInstance) {
$newInstance = true;
}
if ($newInstance) {
return new $class();
}
if (!self::$instance) {
self::$instance = new $class();
}
return self::$instance;
}
// 调用实际类的方法
public static function __callStatic($method, $params)
{
return call_user_func_array([static::createFacade(), $method], $params);
}
}
}
/**
* @see \think\DbManager
* @mixin \think\DbManager
*/
class Db extends Facade
{
/**
* 获取当前Facade对应类名或者已经绑定的容器对象标识
* @access protected
* @return string
*/
protected static function getFacadeClass()
{
return 'think\DbManager';
}
}

View File

@@ -0,0 +1,250 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: zhangyajun <448901948@qq.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\model;
use think\Collection as BaseCollection;
use think\Model;
use think\Paginator;
/**
* 模型数据集类
*/
class Collection extends BaseCollection
{
/**
* 延迟预载入关联查询
* @access public
* @param array|string $relation 关联
* @param mixed $cache 关联缓存
* @return $this
*/
public function load($relation, $cache = false)
{
if (!$this->isEmpty()) {
$item = current($this->items);
$item->eagerlyResultSet($this->items, (array) $relation, [], false, $cache);
}
return $this;
}
/**
* 删除数据集的数据
* @access public
* @return bool
*/
public function delete(): bool
{
$this->each(function (Model $model) {
$model->delete();
});
return true;
}
/**
* 更新数据
* @access public
* @param array $data 数据数组
* @param array $allowField 允许字段
* @return bool
*/
public function update(array $data, array $allowField = []): bool
{
$this->each(function (Model $model) use ($data, $allowField) {
if (!empty($allowField)) {
$model->allowField($allowField);
}
$model->save($data);
});
return true;
}
/**
* 设置需要隐藏的输出属性
* @access public
* @param array $hidden 属性列表
* @return $this
*/
public function hidden(array $hidden)
{
$this->each(function (Model $model) use ($hidden) {
$model->hidden($hidden);
});
return $this;
}
/**
* 设置需要输出的属性
* @access public
* @param array $visible
* @return $this
*/
public function visible(array $visible)
{
$this->each(function (Model $model) use ($visible) {
$model->visible($visible);
});
return $this;
}
/**
* 设置需要追加的输出属性
* @access public
* @param array $append 属性列表
* @return $this
*/
public function append(array $append)
{
$this->each(function (Model $model) use ($append) {
$model->append($append);
});
return $this;
}
/**
* 设置父模型
* @access public
* @param Model $parent 父模型
* @return $this
*/
public function setParent(Model $parent)
{
$this->each(function (Model $model) use ($parent) {
$model->setParent($parent);
});
return $this;
}
/**
* 设置数据字段获取器
* @access public
* @param string|array $name 字段名
* @param callable $callback 闭包获取器
* @return $this
*/
public function withAttr($name, $callback = null)
{
$this->each(function (Model $model) use ($name, $callback) {
$model->withAttribute($name, $callback);
});
return $this;
}
/**
* 绑定(一对一)关联属性到当前模型
* @access protected
* @param string $relation 关联名称
* @param array $attrs 绑定属性
* @return $this
* @throws Exception
*/
public function bindAttr(string $relation, array $attrs = [])
{
$this->each(function (Model $model) use ($relation, $attrs) {
$model->bindAttr($relation, $attrs);
});
return $this;
}
/**
* 按指定键整理数据
*
* @access public
* @param mixed $items 数据
* @param string $indexKey 键名
* @return array
*/
public function dictionary($items = null, string &$indexKey = null)
{
if ($items instanceof self || $items instanceof Paginator) {
$items = $items->all();
}
$items = is_null($items) ? $this->items : $items;
if ($items && empty($indexKey)) {
$indexKey = $items[0]->getPk();
}
if (isset($indexKey) && is_string($indexKey)) {
return array_column($items, null, $indexKey);
}
return $items;
}
/**
* 比较数据集,返回差集
*
* @access public
* @param mixed $items 数据
* @param string $indexKey 指定比较的键名
* @return static
*/
public function diff($items, string $indexKey = null)
{
if ($this->isEmpty()) {
return new static($items);
}
$diff = [];
$dictionary = $this->dictionary($items, $indexKey);
if (is_string($indexKey)) {
foreach ($this->items as $item) {
if (!isset($dictionary[$item[$indexKey]])) {
$diff[] = $item;
}
}
}
return new static($diff);
}
/**
* 比较数据集,返回交集
*
* @access public
* @param mixed $items 数据
* @param string $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,53 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model;
use think\Model;
/**
* 多对多中间表模型类
*/
class Pivot extends Model
{
/**
* 父模型
* @var Model
*/
public $parent;
/**
* 是否时间自动写入
* @var bool
*/
protected $autoWriteTimestamp = false;
/**
* 架构函数
* @access public
* @param array $data 数据
* @param Model $parent 上级模型
* @param string $table 中间数据表名
*/
public function __construct(array $data = [], Model $parent = null, string $table = '')
{
$this->parent = $parent;
if (is_null($this->name)) {
$this->name = $table;
}
parent::__construct($data);
}
}

View File

@@ -0,0 +1,258 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model;
use Closure;
use ReflectionFunction;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use think\Model;
/**
* 模型关联基础类
* @package think\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 int
*/
protected $withLimit;
/**
* 关联数据字段限制
* @var array
*/
protected $withField;
/**
* 获取关联的所属模型
* @access public
* @return Model
*/
public function getParent(): Model
{
return $this->parent;
}
/**
* 获取当前的关联模型类的Query实例
* @access public
* @return Query
*/
public function getQuery()
{
return $this->query;
}
/**
* 获取当前的关联模型类的实例
* @access public
* @return Model
*/
public function getModel(): Model
{
return $this->query->getModel();
}
/**
* 当前关联是否为自关联
* @access public
* @return bool
*/
public function isSelfRelation(): bool
{
return $this->selfRelation;
}
/**
* 封装关联数据集
* @access public
* @param array $resultSet 数据集
* @param Model $parent 父模型
* @return mixed
*/
protected function resultSetBuild(array $resultSet, Model $parent = null)
{
return (new $this->model)->toCollection($resultSet)->setParent($parent);
}
protected function getQueryFields(string $model)
{
$fields = $this->query->getOptions('field');
return $this->getRelationQueryFields($fields, $model);
}
protected function getRelationQueryFields($fields, string $model)
{
if (empty($fields) || '*' == $fields) {
return $model . '.*';
}
if (is_string($fields)) {
$fields = explode(',', $fields);
}
foreach ($fields as &$field) {
if (false === strpos($field, '.')) {
$field = $model . '.' . $field;
}
}
return $fields;
}
protected function getQueryWhere(array &$where, string $relation): void
{
foreach ($where as $key => &$val) {
if (is_string($key)) {
$where[] = [false === strpos($key, '.') ? $relation . '.' . $key : $key, '=', $val];
unset($where[$key]);
} elseif (isset($val[0]) && false === strpos($val[0], '.')) {
$val[0] = $relation . '.' . $val[0];
}
}
}
/**
* 更新数据
* @access public
* @param array $data 更新数据
* @return integer
*/
public function update(array $data = []): int
{
return $this->query->update($data);
}
/**
* 删除记录
* @access public
* @param mixed $data 表达式 true 表示强制删除
* @return int
* @throws Exception
* @throws PDOException
*/
public function delete($data = null): int
{
return $this->query->delete($data);
}
/**
* 限制关联数据的数量
* @access public
* @param int $limit 关联数量限制
* @return $this
*/
public function withLimit(int $limit)
{
$this->withLimit = $limit;
return $this;
}
/**
* 限制关联数据的字段
* @access public
* @param array $field 关联字段限制
* @return $this
*/
public function withField(array $field)
{
$this->withField = $field;
return $this;
}
/**
* 判断闭包的参数类型
* @access protected
* @return mixed
*/
protected function getClosureType(Closure $closure)
{
$reflect = new ReflectionFunction($closure);
$params = $reflect->getParameters();
if (!empty($params)) {
$type = $params[0]->getType();
return Relation::class == $type || is_null($type) ? $this : $this->query;
}
return $this;
}
/**
* 执行基础查询(仅执行一次)
* @access protected
* @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,651 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model\concern;
use InvalidArgumentException;
use think\db\Raw;
use think\helper\Str;
use think\model\Relation;
/**
* 模型数据处理
*/
trait Attribute
{
/**
* 数据表主键 复合主键使用数组定义
* @var string|array
*/
protected $pk = 'id';
/**
* 数据表字段信息 留空则自动获取
* @var array
*/
protected $schema = [];
/**
* 当前允许写入的字段
* @var array
*/
protected $field = [];
/**
* 字段自动类型转换
* @var array
*/
protected $type = [];
/**
* 数据表废弃字段
* @var array
*/
protected $disuse = [];
/**
* 数据表只读字段
* @var array
*/
protected $readonly = [];
/**
* 当前模型数据
* @var array
*/
private $data = [];
/**
* 原始数据
* @var array
*/
private $origin = [];
/**
* JSON数据表字段
* @var array
*/
protected $json = [];
/**
* JSON数据表字段类型
* @var array
*/
protected $jsonType = [];
/**
* JSON数据取出是否需要转换为数组
* @var bool
*/
protected $jsonAssoc = false;
/**
* 是否严格字段大小写
* @var bool
*/
protected $strict = true;
/**
* 修改器执行记录
* @var array
*/
private $set = [];
/**
* 动态获取器
* @var array
*/
private $withAttr = [];
/**
* 获取模型对象的主键
* @access public
* @return string|array
*/
public function getPk()
{
return $this->pk;
}
/**
* 判断一个字段名是否为主键字段
* @access public
* @param string $key 名称
* @return bool
*/
protected function isPk(string $key): bool
{
$pk = $this->getPk();
if (is_string($pk) && $pk == $key) {
return true;
} elseif (is_array($pk) && in_array($key, $pk)) {
return true;
}
return false;
}
/**
* 获取模型对象的主键值
* @access public
* @return mixed
*/
public function getKey()
{
$pk = $this->getPk();
if (is_string($pk) && array_key_exists($pk, $this->data)) {
return $this->data[$pk];
}
return;
}
/**
* 设置允许写入的字段
* @access public
* @param array $field 允许写入的字段
* @return $this
*/
public function allowField(array $field)
{
$this->field = $field;
return $this;
}
/**
* 设置只读字段
* @access public
* @param array $field 只读字段
* @return $this
*/
public function readOnly(array $field)
{
$this->readonly = $field;
return $this;
}
/**
* 获取实际的字段名
* @access protected
* @param string $name 字段名
* @return string
*/
protected function getRealFieldName(string $name): string
{
return $this->strict ? $name : Str::snake($name);
}
/**
* 设置数据对象值
* @access public
* @param array $data 数据
* @param bool $set 是否调用修改器
* @param array $allow 允许的字段名
* @return $this
*/
public function data(array $data, bool $set = false, array $allow = [])
{
// 清空数据
$this->data = [];
// 废弃字段
foreach ($this->disuse as $key) {
if (array_key_exists($key, $data)) {
unset($data[$key]);
}
}
if (!empty($allow)) {
$result = [];
foreach ($allow as $name) {
if (isset($data[$name])) {
$result[$name] = $data[$name];
}
}
$data = $result;
}
if ($set) {
// 数据对象赋值
$this->setAttrs($data);
} else {
$this->data = $data;
}
return $this;
}
/**
* 批量追加数据对象值
* @access public
* @param array $data 数据
* @param bool $set 是否需要进行数据处理
* @return $this
*/
public function appendData(array $data, bool $set = false)
{
if ($set) {
$this->setAttrs($data);
} else {
$this->data = array_merge($this->data, $data);
}
return $this;
}
/**
* 获取对象原始数据 如果不存在指定字段返回null
* @access public
* @param string $name 字段名 留空获取全部
* @return mixed
*/
public function getOrigin(string $name = null)
{
if (is_null($name)) {
return $this->origin;
}
return array_key_exists($name, $this->origin) ? $this->origin[$name] : null;
}
/**
* 获取对象原始数据 如果不存在指定字段返回false
* @access public
* @param string $name 字段名 留空获取全部
* @return mixed
* @throws InvalidArgumentException
*/
public function getData(string $name = null)
{
if (is_null($name)) {
return $this->data;
}
$fieldName = $this->getRealFieldName($name);
if (array_key_exists($fieldName, $this->data)) {
return $this->data[$fieldName];
} elseif (array_key_exists($fieldName, $this->relation)) {
return $this->relation[$fieldName];
}
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}
/**
* 获取变化的数据 并排除只读数据
* @access public
* @return array
*/
public function getChangedData(): array
{
$data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
if ((empty($a) || empty($b)) && $a !== $b) {
return 1;
}
return is_object($a) || $a != $b ? 1 : 0;
});
// 只读字段不允许更新
foreach ($this->readonly as $key => $field) {
if (isset($data[$field])) {
unset($data[$field]);
}
}
return $data;
}
/**
* 直接设置数据对象值
* @access public
* @param string $name 属性名
* @param mixed $value 值
* @return void
*/
public function set(string $name, $value): void
{
$name = $this->getRealFieldName($name);
$this->data[$name] = $value;
}
/**
* 通过修改器 批量设置数据对象值
* @access public
* @param array $data 数据
* @return void
*/
public function setAttrs(array $data): void
{
// 进行数据处理
foreach ($data as $key => $value) {
$this->setAttr($key, $value, $data);
}
}
/**
* 通过修改器 设置数据对象值
* @access public
* @param string $name 属性名
* @param mixed $value 属性值
* @param array $data 数据
* @return void
*/
public function setAttr(string $name, $value, array $data = []): void
{
$name = $this->getRealFieldName($name);
if (isset($this->set[$name])) {
return;
}
if (is_null($value) && $this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
// 自动写入的时间戳字段
$value = $this->autoWriteTimestamp();
} else {
// 检测修改器
$method = 'set' . Str::studly($name) . 'Attr';
if (method_exists($this, $method)) {
$array = $this->data;
$value = $this->$method($value, array_merge($this->data, $data));
$this->set[$name] = true;
if (is_null($value) && $array !== $this->data) {
return;
}
} elseif (isset($this->type[$name])) {
// 类型转换
$value = $this->writeTransform($value, $this->type[$name]);
}
}
// 设置数据对象属性
$this->data[$name] = $value;
}
/**
* 数据写入 类型转换
* @access protected
* @param mixed $value 值
* @param string|array $type 要转换的类型
* @return mixed
*/
protected function writeTransform($value, $type)
{
if (is_null($value)) {
return;
}
if ($value instanceof Raw) {
return $value;
}
if (is_array($type)) {
[$type, $param] = $type;
} elseif (strpos($type, ':')) {
[$type, $param] = explode(':', $type, 2);
}
switch ($type) {
case 'integer':
$value = (int) $value;
break;
case 'float':
if (empty($param)) {
$value = (float) $value;
} else {
$value = (float) number_format($value, $param, '.', '');
}
break;
case 'boolean':
$value = (bool) $value;
break;
case 'timestamp':
if (!is_numeric($value)) {
$value = strtotime($value);
}
break;
case 'datetime':
$value = is_numeric($value) ? $value : strtotime($value);
$value = $this->formatDateTime('Y-m-d H:i:s.u', $value);
break;
case 'object':
if (is_object($value)) {
$value = json_encode($value, JSON_FORCE_OBJECT);
}
break;
case 'array':
$value = (array) $value;
case 'json':
$option = !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE;
$value = json_encode($value, $option);
break;
case 'serialize':
$value = serialize($value);
break;
default:
if (is_object($value) && false !== strpos($type, '\\') && method_exists($value, '__toString')) {
// 对象类型
$value = $value->__toString();
}
}
return $value;
}
/**
* 获取器 获取数据对象的值
* @access public
* @param string $name 名称
* @return mixed
* @throws InvalidArgumentException
*/
public function getAttr(string $name)
{
try {
$relation = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$relation = $this->isRelationAttr($name);
$value = null;
}
return $this->getValue($name, $value, $relation);
}
/**
* 获取经过获取器处理后的数据对象的值
* @access protected
* @param string $name 字段名称
* @param mixed $value 字段值
* @param bool|string $relation 是否为关联属性或者关联名
* @return mixed
* @throws InvalidArgumentException
*/
protected function getValue(string $name, $value, $relation = false)
{
// 检测属性获取器
$fieldName = $this->getRealFieldName($name);
$method = 'get' . Str::studly($name) . 'Attr';
if (isset($this->withAttr[$fieldName])) {
if ($relation) {
$value = $this->getRelationValue($relation);
}
if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
$value = $this->getJsonValue($fieldName, $value);
} else {
$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
}
} elseif (method_exists($this, $method)) {
if ($relation) {
$value = $this->getRelationValue($relation);
}
$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$fieldName])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$fieldName]);
} elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
$value = $this->getTimestampValue($value);
} elseif ($relation) {
$value = $this->getRelationValue($relation);
// 保存关联对象值
$this->relation[$name] = $value;
}
return $value;
}
/**
* 获取JSON字段属性值
* @access protected
* @param string $name 属性名
* @param mixed $value JSON数据
* @return mixed
*/
protected function getJsonValue($name, $value)
{
foreach ($this->withAttr[$name] as $key => $closure) {
if ($this->jsonAssoc) {
$value[$key] = $closure($value[$key], $value);
} else {
$value->$key = $closure($value->$key, $value);
}
}
return $value;
}
/**
* 获取关联属性值
* @access protected
* @param string $relation 关联名
* @return mixed
*/
protected function getRelationValue(string $relation)
{
$modelRelation = $this->$relation();
return $modelRelation instanceof Relation ? $this->getRelationData($modelRelation) : null;
}
/**
* 数据读取 类型转换
* @access protected
* @param mixed $value 值
* @param string|array $type 要转换的类型
* @return mixed
*/
protected function readTransform($value, $type)
{
if (is_null($value)) {
return;
}
if (is_array($type)) {
[$type, $param] = $type;
} elseif (strpos($type, ':')) {
[$type, $param] = explode(':', $type, 2);
}
switch ($type) {
case 'integer':
$value = (int) $value;
break;
case 'float':
if (empty($param)) {
$value = (float) $value;
} else {
$value = (float) number_format($value, $param, '.', '');
}
break;
case 'boolean':
$value = (bool) $value;
break;
case 'timestamp':
if (!is_null($value)) {
$format = !empty($param) ? $param : $this->dateFormat;
$value = $this->formatDateTime($format, $value, true);
}
break;
case 'datetime':
if (!is_null($value)) {
$format = !empty($param) ? $param : $this->dateFormat;
$value = $this->formatDateTime($format, $value);
}
break;
case 'json':
$value = json_decode($value, true);
break;
case 'array':
$value = empty($value) ? [] : json_decode($value, true);
break;
case 'object':
$value = empty($value) ? new \stdClass() : json_decode($value);
break;
case 'serialize':
try {
$value = unserialize($value);
} catch (\Exception $e) {
$value = null;
}
break;
default:
if (false !== strpos($type, '\\')) {
// 对象类型
$value = new $type($value);
}
}
return $value;
}
/**
* 设置数据字段获取器
* @access public
* @param string|array $name 字段名
* @param callable $callback 闭包获取器
* @return $this
*/
public function withAttribute($name, callable $callback = null)
{
if (is_array($name)) {
foreach ($name as $key => $val) {
$this->withAttribute($key, $val);
}
} else {
$name = $this->getRealFieldName($name);
if (strpos($name, '.')) {
[$name, $key] = explode('.', $name);
$this->withAttr[$name][$key] = $callback;
} else {
$this->withAttr[$name] = $callback;
}
}
return $this;
}
}

View File

@@ -0,0 +1,285 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model\concern;
use think\Collection;
use think\db\exception\DbException as Exception;
use think\helper\Str;
use think\Model;
use think\model\Collection as ModelCollection;
use think\model\relation\OneToOne;
/**
* 模型数据转换处理
*/
trait Conversion
{
/**
* 数据输出显示的属性
* @var array
*/
protected $visible = [];
/**
* 数据输出隐藏的属性
* @var array
*/
protected $hidden = [];
/**
* 数据输出需要追加的属性
* @var array
*/
protected $append = [];
/**
* 数据集对象名
* @var string
*/
protected $resultSetType;
/**
* 设置需要附加的输出属性
* @access public
* @param array $append 属性列表
* @return $this
*/
public function append(array $append = [])
{
$this->append = $append;
return $this;
}
/**
* 设置附加关联对象的属性
* @access public
* @param string $attr 关联属性
* @param string|array $append 追加属性名
* @return $this
* @throws Exception
*/
public function appendRelationAttr(string $attr, array $append)
{
$relation = Str::camel($attr);
if (isset($this->relation[$relation])) {
$model = $this->relation[$relation];
} else {
$model = $this->getRelationData($this->$relation());
}
if ($model instanceof Model) {
foreach ($append as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
throw new Exception('bind attr has exists:' . $key);
}
$this->data[$key] = $model->$attr;
}
}
return $this;
}
/**
* 设置需要隐藏的输出属性
* @access public
* @param array $hidden 属性列表
* @return $this
*/
public function hidden(array $hidden = [])
{
$this->hidden = $hidden;
return $this;
}
/**
* 设置需要输出的属性
* @access public
* @param array $visible
* @return $this
*/
public function visible(array $visible = [])
{
$this->visible = $visible;
return $this;
}
/**
* 转换当前模型对象为数组
* @access public
* @return array
*/
public function toArray(): array
{
$item = [];
$hasVisible = false;
foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$this->visible[$relation][] = $name;
} else {
$this->visible[$val] = true;
$hasVisible = true;
}
unset($this->visible[$key]);
}
}
foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$this->hidden[$relation][] = $name;
} else {
$this->hidden[$val] = true;
}
unset($this->hidden[$key]);
}
}
// 合并关联数据
$data = array_merge($this->data, $this->relation);
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
}
// 追加属性(必须定义获取器)
foreach ($this->append as $key => $name) {
$this->appendAttrToArray($item, $key, $name);
}
return $item;
}
protected function appendAttrToArray(array &$item, $key, $name)
{
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key, true);
$item[$key] = $relation ? $relation->append($name)
->toArray() : [];
} elseif (strpos($name, '.')) {
[$key, $attr] = explode('.', $name);
// 追加关联对象属性
$relation = $this->getRelation($key, true);
$item[$key] = $relation ? $relation->append([$attr])
->toArray() : [];
} else {
$value = $this->getAttr($name);
$item[$name] = $value;
$this->getBindAttr($name, $value, $item);
}
}
protected function getBindAttr(string $name, $value, array &$item = [])
{
$relation = $this->isRelationAttr($name);
if (!$relation) {
return false;
}
$modelRelation = $this->$relation();
if ($modelRelation instanceof OneToOne) {
$bindAttr = $modelRelation->getBindAttr();
if (!empty($bindAttr)) {
unset($item[$name]);
}
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($item[$key])) {
throw new Exception('bind attr has exists:' . $key);
}
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
}
/**
* 转换当前模型对象为JSON字符串
* @access public
* @param integer $options json参数
* @return string
*/
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $options);
}
public function __toString()
{
return $this->toJson();
}
// JsonSerializable
public function jsonSerialize()
{
return $this->toArray();
}
/**
* 转换数据集为数据集对象
* @access public
* @param array|Collection $collection 数据集
* @param string $resultSetType 数据集类
* @return Collection
*/
public function toCollection(iterable $collection = [], string $resultSetType = null): Collection
{
$resultSetType = $resultSetType ?: $this->resultSetType;
if ($resultSetType && false !== strpos($resultSetType, '\\')) {
$collection = new $resultSetType($collection);
} else {
$collection = new ModelCollection($collection);
}
return $collection;
}
}

View File

@@ -0,0 +1,88 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model\concern;
use think\db\exception\ModelEventException;
use think\helper\Str;
/**
* 模型事件处理
*/
trait ModelEvent
{
/**
* Event对象
* @var object
*/
protected static $event;
/**
* 是否需要事件响应
* @var bool
*/
protected $withEvent = true;
/**
* 设置Event对象
* @access public
* @param object $event Event对象
* @return void
*/
public static function setEvent($event)
{
self::$event = $event;
}
/**
* 当前操作的事件响应
* @access protected
* @param bool $event 是否需要事件响应
* @return $this
*/
public function withEvent(bool $event)
{
$this->withEvent = $event;
return $this;
}
/**
* 触发事件
* @access protected
* @param string $event 事件名
* @return bool
*/
protected function trigger(string $event): bool
{
if (!$this->withEvent) {
return true;
}
$call = 'on' . Str::studly($event);
try {
if (method_exists(static::class, $call)) {
$result = call_user_func([static::class, $call], $this);
} elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) {
$result = self::$event->trigger(static::class . '.' . $event, $this);
$result = empty($result) ? true : end($result);
} else {
$result = true;
}
return false === $result ? false : true;
} catch (ModelEventException $e) {
return false;
}
}
}

View File

@@ -0,0 +1,85 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model\concern;
use think\db\exception\DbException as Exception;
/**
* 乐观锁
*/
trait OptimLock
{
protected function getOptimLockField()
{
return property_exists($this, 'optimLock') && isset($this->optimLock) ? $this->optimLock : 'lock_version';
}
/**
* 数据检查
* @access protected
* @return void
*/
protected function checkData(): void
{
$this->isExists() ? $this->updateLockVersion() : $this->recordLockVersion();
}
/**
* 记录乐观锁
* @access protected
* @return void
*/
protected function recordLockVersion(): void
{
$optimLock = $this->getOptimLockField();
if ($optimLock) {
$this->set($optimLock, 0);
}
}
/**
* 更新乐观锁
* @access protected
* @return void
*/
protected function updateLockVersion(): void
{
$optimLock = $this->getOptimLockField();
if ($optimLock && $lockVer = $this->getOrigin($optimLock)) {
// 更新乐观锁
$this->set($optimLock, $lockVer + 1);
}
}
public function getWhere()
{
$where = parent::getWhere();
$optimLock = $this->getOptimLockField();
if ($optimLock && $lockVer = $this->getOrigin($optimLock)) {
$where[] = [$optimLock, '=', $lockVer];
}
return $where;
}
protected function checkResult($result): void
{
if (!$result) {
throw new Exception('record has update');
}
}
}

View File

@@ -0,0 +1,781 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model\concern;
use Closure;
use think\Collection;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use think\helper\Str;
use think\Model;
use think\model\Relation;
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\OneToOne;
/**
* 模型关联处理
*/
trait RelationShip
{
/**
* 父关联模型对象
* @var object
*/
private $parent;
/**
* 模型关联数据
* @var array
*/
private $relation = [];
/**
* 关联写入定义信息
* @var array
*/
private $together = [];
/**
* 关联自动写入信息
* @var array
*/
protected $relationWrite = [];
/**
* 设置父关联对象
* @access public
* @param Model $model 模型对象
* @return $this
*/
public function setParent(Model $model)
{
$this->parent = $model;
return $this;
}
/**
* 获取父关联对象
* @access public
* @return Model
*/
public function getParent(): Model
{
return $this->parent;
}
/**
* 获取当前模型的关联模型数据
* @access public
* @param string $name 关联方法名
* @param bool $auto 不存在是否自动获取
* @return mixed
*/
public function getRelation(string $name = null, bool $auto = false)
{
if (is_null($name)) {
return $this->relation;
}
if (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
} elseif ($auto) {
$relation = Str::camel($name);
return $this->getRelationValue($relation);
}
}
/**
* 设置关联数据对象值
* @access public
* @param string $name 属性名
* @param mixed $value 属性值
* @param array $data 数据
* @return $this
*/
public function setRelation(string $name, $value, array $data = [])
{
// 检测修改器
$method = 'set' . Str::studly($name) . 'Attr';
if (method_exists($this, $method)) {
$value = $this->$method($value, array_merge($this->data, $data));
}
$this->relation[$this->getRealFieldName($name)] = $value;
return $this;
}
/**
* 查询当前模型的关联数据
* @access public
* @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 (strpos($relation, '.')) {
[$relation, $subRelation] = explode('.', $relation, 2);
}
$method = Str::camel($relation);
$relationName = Str::snake($relation);
$relationResult = $this->$method();
if (isset($withRelationAttr[$relationName])) {
$relationResult->withAttr($withRelationAttr[$relationName]);
}
$this->relation[$relation] = $relationResult->getRelation($subRelation, $closure);
}
}
/**
* 关联数据写入
* @access public
* @param array $relation 关联
* @return $this
*/
public function together(array $relation)
{
$this->together = $relation;
$this->checkAutoRelationWrite();
return $this;
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $relation 关联方法名
* @param mixed $operator 比较操作符
* @param integer $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);
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $relation 关联方法名
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public static function hasWhere(string $relation, $where = [], string $fields = '*', string $joinType = '', Query $query = null): Query
{
return (new static())
->$relation()
->hasWhere($where, $fields, $joinType, $query);
}
/**
* 预载入关联查询 JOIN方式
* @access public
* @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;
} else {
return false;
}
}
/**
* 预载入关联查询 返回数据集
* @access public
* @param array $resultSet 数据集
* @param string $relation 关联名
* @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 (strpos($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);
}
}
/**
* 预载入关联查询 返回模型对象
* @access public
* @param Model $result 数据对象
* @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 (strpos($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);
}
}
/**
* 绑定(一对一)关联属性到当前模型
* @access protected
* @param string $relation 关联名称
* @param array $attrs 绑定属性
* @return $this
* @throws Exception
*/
public function bindAttr(string $relation, array $attrs = [])
{
$relation = $this->getRelation($relation);
foreach ($attrs as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
$value = $this->getOrigin($key);
if (!is_null($value)) {
throw new Exception('bind attr has exists:' . $key);
}
$this->set($key, $relation ? $relation->$attr : null);
}
return $this;
}
/**
* 关联统计
* @access public
* @param 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 = '*', 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->setAttr($name, $count);
}
}
}
/**
* HAS ONE 关联定义
* @access public
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 当前主键
* @return HasOne
*/
public function hasOne(string $model, string $foreignKey = '', string $localKey = ''): HasOne
{
// 记录当前关联信息
$model = $this->parseModel($model);
$localKey = $localKey ?: $this->getPk();
$foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
return new HasOne($this, $model, $foreignKey, $localKey);
}
/**
* BELONGS TO 关联定义
* @access public
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 关联主键
* @return BelongsTo
*/
public function belongsTo(string $model, string $foreignKey = '', string $localKey = ''): BelongsTo
{
// 记录当前关联信息
$model = $this->parseModel($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 关联定义
* @access public
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 当前主键
* @return HasMany
*/
public function hasMany(string $model, string $foreignKey = '', string $localKey = ''): HasMany
{
// 记录当前关联信息
$model = $this->parseModel($model);
$localKey = $localKey ?: $this->getPk();
$foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
return new HasMany($this, $model, $foreignKey, $localKey);
}
/**
* HAS MANY 远程关联定义
* @access public
* @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->parseModel($model);
$through = $this->parseModel($through);
$localKey = $localKey ?: $this->getPk();
$foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
$throughKey = $throughKey ?: $this->getForeignKey((new $through)->getName());
$throughPk = $throughPk ?: (new $through)->getPk();
return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey, $throughPk);
}
/**
* HAS ONE 远程关联定义
* @access public
* @param string $model 模型名
* @param string $through 中间模型名
* @param string $foreignKey 关联外键
* @param string $throughKey 关联外键
* @param string $localKey 当前主键
* @param string $throughPk 中间表主键
* @return HasOneThrough
*/
public function hasOneThrough(string $model, string $through, string $foreignKey = '', string $throughKey = '', string $localKey = '', string $throughPk = ''): HasOneThrough
{
// 记录当前关联信息
$model = $this->parseModel($model);
$through = $this->parseModel($through);
$localKey = $localKey ?: $this->getPk();
$foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
$throughKey = $throughKey ?: $this->getForeignKey((new $through)->getName());
$throughPk = $throughPk ?: (new $through)->getPk();
return new HasOneThrough($this, $model, $through, $foreignKey, $throughKey, $localKey, $throughPk);
}
/**
* BELONGS TO MANY 关联定义
* @access public
* @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->parseModel($model);
$name = Str::snake(class_basename($model));
$middle = $middle ?: Str::snake($this->name) . '_' . $name;
$foreignKey = $foreignKey ?: $name . '_id';
$localKey = $localKey ?: $this->getForeignKey($this->name);
return new BelongsToMany($this, $model, $middle, $foreignKey, $localKey);
}
/**
* MORPH One 关联定义
* @access public
* @param string $model 模型名
* @param string|array $morph 多态字段信息
* @param string $type 多态类型
* @return MorphOne
*/
public function morphOne(string $model, $morph = null, string $type = ''): MorphOne
{
// 记录当前关联信息
$model = $this->parseModel($model);
if (is_null($morph)) {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$morph = Str::snake($trace[1]['function']);
}
if (is_array($morph)) {
[$morphType, $foreignKey] = $morph;
} else {
$morphType = $morph . '_type';
$foreignKey = $morph . '_id';
}
$type = $type ?: get_class($this);
return new MorphOne($this, $model, $foreignKey, $morphType, $type);
}
/**
* MORPH MANY 关联定义
* @access public
* @param string $model 模型名
* @param string|array $morph 多态字段信息
* @param string $type 多态类型
* @return MorphMany
*/
public function morphMany(string $model, $morph = null, string $type = ''): MorphMany
{
// 记录当前关联信息
$model = $this->parseModel($model);
if (is_null($morph)) {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$morph = Str::snake($trace[1]['function']);
}
$type = $type ?: get_class($this);
if (is_array($morph)) {
[$morphType, $foreignKey] = $morph;
} else {
$morphType = $morph . '_type';
$foreignKey = $morph . '_id';
}
return new MorphMany($this, $model, $foreignKey, $morphType, $type);
}
/**
* MORPH TO 关联定义
* @access public
* @param string|array $morph 多态字段信息
* @param array $alias 多态别名定义
* @return MorphTo
*/
public function morphTo($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;
}
// 记录当前关联信息
if (is_array($morph)) {
[$morphType, $foreignKey] = $morph;
} else {
$morphType = $morph . '_type';
$foreignKey = $morph . '_id';
}
return new MorphTo($this, $morphType, $foreignKey, $alias, $relation);
}
/**
* 解析模型的完整命名空间
* @access protected
* @param string $model 模型名(或者完整类名)
* @return string
*/
protected function parseModel(string $model): string
{
if (false === strpos($model, '\\')) {
$path = explode('\\', static::class);
array_pop($path);
array_push($path, Str::studly($model));
$model = implode('\\', $path);
}
return $model;
}
/**
* 获取模型的默认外键名
* @access protected
* @param string $name 模型名
* @return string
*/
protected function getForeignKey(string $name): string
{
if (strpos($name, '\\')) {
$name = class_basename($name);
}
return Str::snake($name) . '_id';
}
/**
* 检查属性是否为关联属性 如果是则返回关联方法名
* @access protected
* @param string $attr 关联属性名
* @return string|false
*/
protected function isRelationAttr(string $attr)
{
$relation = Str::camel($attr);
if (method_exists($this, $relation) && !method_exists('think\Model', $relation)) {
return $relation;
}
return false;
}
/**
* 智能获取关联模型数据
* @access protected
* @param Relation $modelRelation 模型关联对象
* @return mixed
*/
protected function getRelationData(Relation $modelRelation)
{
if ($this->parent && !$modelRelation->isSelfRelation()
&& get_class($this->parent) == get_class($modelRelation->getModel())) {
return $this->parent;
}
// 获取关联数据
return $modelRelation->getRelation();
}
/**
* 关联数据自动写入检查
* @access protected
* @return void
*/
protected function checkAutoRelationWrite(): void
{
foreach ($this->together as $key => $name) {
if (is_array($name)) {
if (key($name) === 0) {
$this->relationWrite[$key] = [];
// 绑定关联属性
foreach ($name as $val) {
if (isset($this->data[$val])) {
$this->relationWrite[$key][$val] = $this->data[$val];
}
}
} else {
// 直接传入关联数据
$this->relationWrite[$key] = $name;
}
} elseif (isset($this->relation[$name])) {
$this->relationWrite[$name] = $this->relation[$name];
} elseif (isset($this->data[$name])) {
$this->relationWrite[$name] = $this->data[$name];
unset($this->data[$name]);
}
}
}
/**
* 自动关联数据更新(针对一对一关联)
* @access protected
* @return void
*/
protected function autoRelationUpdate(): void
{
foreach ($this->relationWrite as $name => $val) {
if ($val instanceof Model) {
$val->exists(true)->save();
} else {
$model = $this->getRelation($name, true);
if ($model instanceof Model) {
$model->exists(true)->save($val);
}
}
}
}
/**
* 自动关联数据写入(针对一对一关联)
* @access protected
* @return void
*/
protected function autoRelationInsert(): void
{
foreach ($this->relationWrite as $name => $val) {
$method = Str::camel($name);
$this->$method()->save($val);
}
}
/**
* 自动关联数据删除(支持一对一及一对多关联)
* @access protected
* @return void
*/
protected function autoRelationDelete(): void
{
foreach ($this->relationWrite as $key => $name) {
$name = is_numeric($key) ? $name : $key;
$result = $this->getRelation($name, true);
if ($result instanceof Model) {
$result->delete();
} elseif ($result instanceof Collection) {
foreach ($result as $model) {
$model->delete();
}
}
}
}
/**
* 移除当前模型的关联属性
* @access public
* @return $this
*/
public function removeRelation()
{
$this->relation = [];
return $this;
}
}

View File

@@ -0,0 +1,246 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model\concern;
use think\db\BaseQuery as Query;
/**
* 数据软删除
*/
trait SoftDelete
{
/**
* 是否包含软删除数据
* @var bool
*/
protected $withTrashed = false;
/**
* 判断当前实例是否被软删除
* @access public
* @return bool
*/
public function trashed(): bool
{
$field = $this->getDeleteTimeField();
if ($field && !empty($this->getOrigin($field))) {
return true;
}
return false;
}
/**
* 查询软删除数据
* @access public
* @return Query
*/
public static function withTrashed(): Query
{
$model = new static();
return $model->withTrashedData(true)->db();
}
/**
* 是否包含软删除数据
* @access protected
* @param bool $withTrashed 是否包含软删除数据
* @return $this
*/
protected function withTrashedData(bool $withTrashed)
{
$this->withTrashed = $withTrashed;
return $this;
}
/**
* 只查询软删除数据
* @access public
* @return Query
*/
public static function onlyTrashed(): Query
{
$model = new static();
$field = $model->getDeleteTimeField(true);
if ($field) {
return $model
->db()
->useSoftDelete($field, $model->getWithTrashedExp());
}
return $model->db();
}
/**
* 获取软删除数据的查询条件
* @access protected
* @return array
*/
protected function getWithTrashedExp(): array
{
return is_null($this->defaultSoftDelete) ? ['notnull', ''] : ['<>', $this->defaultSoftDelete];
}
/**
* 删除当前的记录
* @access public
* @return bool
*/
public function delete(): bool
{
if (!$this->isExists() || $this->isEmpty() || false === $this->trigger('BeforeDelete')) {
return false;
}
$name = $this->getDeleteTimeField();
if ($name && !$this->isForce()) {
// 软删除
$this->set($name, $this->autoWriteTimestamp($name));
$result = $this->exists()->withEvent(false)->save();
$this->withEvent(true);
} else {
// 读取更新条件
$where = $this->getWhere();
// 删除当前模型数据
$result = $this->db()
->where($where)
->removeOption('soft_delete')
->delete();
$this->lazySave(false);
}
// 关联删除
if (!empty($this->relationWrite)) {
$this->autoRelationDelete();
}
$this->trigger('AfterDelete');
$this->exists(false);
return true;
}
/**
* 删除记录
* @access public
* @param mixed $data 主键列表 支持闭包查询条件
* @param bool $force 是否强制删除
* @return bool
*/
public static function destroy($data, bool $force = false): bool
{
// 包含软删除数据
$query = (new static())->db(false);
if (is_array($data) && key($data) !== 0) {
$query->where($data);
$data = null;
} elseif ($data instanceof \Closure) {
call_user_func_array($data, [ & $query]);
$data = null;
} elseif (is_null($data)) {
return false;
}
$resultSet = $query->select($data);
foreach ($resultSet as $result) {
$result->force($force)->delete();
}
return true;
}
/**
* 恢复被软删除的记录
* @access public
* @param array $where 更新条件
* @return bool
*/
public function restore($where = []): bool
{
$name = $this->getDeleteTimeField();
if (!$name || false === $this->trigger('BeforeRestore')) {
return false;
}
if (empty($where)) {
$pk = $this->getPk();
if (is_string($pk)) {
$where[] = [$pk, '=', $this->getData($pk)];
}
}
// 恢复删除
$this->db(false)
->where($where)
->useSoftDelete($name, $this->getWithTrashedExp())
->update([$name => $this->defaultSoftDelete]);
$this->trigger('AfterRestore');
return true;
}
/**
* 获取软删除字段
* @access protected
* @param bool $read 是否查询操作 写操作的时候会自动去掉表别名
* @return string|false
*/
protected function getDeleteTimeField(bool $read = false)
{
$field = property_exists($this, 'deleteTime') && isset($this->deleteTime) ? $this->deleteTime : 'delete_time';
if (false === $field) {
return false;
}
if (false === strpos($field, '.')) {
$field = '__TABLE__.' . $field;
}
if (!$read && strpos($field, '.')) {
$array = explode('.', $field);
$field = array_pop($array);
}
return $field;
}
/**
* 查询的时候默认排除软删除数据
* @access protected
* @param Query $query
* @return void
*/
protected function withNoTrashed(Query $query): void
{
$field = $this->getDeleteTimeField(true);
if ($field) {
$condition = is_null($this->defaultSoftDelete) ? ['null', ''] : ['=', $this->defaultSoftDelete];
$query->useSoftDelete($field, $condition);
}
}
}

View File

@@ -0,0 +1,208 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model\concern;
use DateTime;
/**
* 自动时间戳
*/
trait TimeStamp
{
/**
* 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型
* @var bool|string
*/
protected $autoWriteTimestamp;
/**
* 创建时间字段 false表示关闭
* @var false|string
*/
protected $createTime = 'create_time';
/**
* 更新时间字段 false表示关闭
* @var false|string
*/
protected $updateTime = 'update_time';
/**
* 时间字段显示格式
* @var string
*/
protected $dateFormat;
/**
* 是否需要自动写入时间字段
* @access public
* @param bool|string $auto
* @return $this
*/
public function isAutoWriteTimestamp($auto)
{
$this->autoWriteTimestamp = $this->checkTimeFieldType($auto);
return $this;
}
/**
* 检测时间字段的实际类型
* @access public
* @param bool|string $type
* @return mixed
*/
protected function checkTimeFieldType($type)
{
if (true === $type) {
if (isset($this->type[$this->createTime])) {
$type = $this->type[$this->createTime];
} elseif (isset($this->schema[$this->createTime]) && in_array($this->schema[$this->createTime], ['datetime', 'date', 'timestamp', 'int'])) {
$type = $this->schema[$this->createTime];
} else {
$type = $this->getFieldType($this->createTime);
}
}
return $type;
}
/**
* 获取自动写入时间字段
* @access public
* @return bool|string
*/
public function getAutoWriteTimestamp()
{
return $this->autoWriteTimestamp;
}
/**
* 设置时间字段格式化
* @access public
* @param string|false $format
* @return $this
*/
public function setDateFormat($format)
{
$this->dateFormat = $format;
return $this;
}
/**
* 获取自动写入时间字段
* @access public
* @return string|false
*/
public function getDateFormat()
{
return $this->dateFormat;
}
/**
* 自动写入时间戳
* @access protected
* @return mixed
*/
protected function autoWriteTimestamp()
{
// 检测时间字段类型
$type = $this->checkTimeFieldType($this->autoWriteTimestamp);
return is_string($type) ? $this->getTimeTypeValue($type) : time();
}
/**
* 获取指定类型的时间字段值
* @access protected
* @param string $type 时间字段类型
* @return mixed
*/
protected function getTimeTypeValue(string $type)
{
$value = time();
switch ($type) {
case 'datetime':
case 'date':
case 'timestamp':
$value = $this->formatDateTime('Y-m-d H:i:s.u');
break;
default:
if (false !== strpos($type, '\\')) {
// 对象数据写入
$obj = new $type();
if (method_exists($obj, '__toString')) {
// 对象数据写入
$value = $obj->__toString();
}
}
}
return $value;
}
/**
* 时间日期字段格式化处理
* @access protected
* @param mixed $format 日期格式
* @param mixed $time 时间日期表达式
* @param bool $timestamp 时间表达式是否为时间戳
* @return mixed
*/
protected function formatDateTime($format, $time = 'now', bool $timestamp = false)
{
if (empty($time)) {
return;
}
if (false === $format) {
return $time;
} elseif (false !== strpos($format, '\\')) {
return new $format($time);
}
if ($time instanceof DateTime) {
$dateTime = $time;
} elseif ($timestamp) {
$dateTime = new DateTime();
$dateTime->setTimestamp((int) $time);
} else {
$dateTime = new DateTime($time);
}
return $dateTime->format($format);
}
/**
* 获取时间字段值
* @access protected
* @param mixed $value
* @return mixed
*/
protected function getTimestampValue($value)
{
$type = $this->checkTimeFieldType($this->autoWriteTimestamp);
if (is_string($type) && in_array(strtolower($type), [
'datetime', 'date', 'timestamp',
])) {
$value = $this->formatDateTime($this->dateFormat, $value);
} else {
$value = $this->formatDateTime($this->dateFormat, $value, true);
}
return $value;
}
}

View File

@@ -0,0 +1,331 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model\relation;
use Closure;
use think\db\BaseQuery as Query;
use think\helper\Str;
use think\Model;
/**
* BelongsTo关联类
*/
class BelongsTo extends OneToOne
{
/**
* 架构函数
* @access public
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 关联主键
* @param string $relation 关联名
*/
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;
}
}
/**
* 延迟获取关联数据
* @access public
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
* @return Model
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
if ($closure) {
$closure($this->getClosureType($closure));
}
$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->bindAttr($relationModel, $this->parent);
}
$relationModel->setParent(clone $this->parent);
}
return $relationModel;
}
/**
* 创建关联统计子查询
* @access public
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 聚合字段别名
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', &$name = ''): string
{
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
return $this->query
->whereExp($this->localKey, '=' . $this->parent->getTable() . '.' . $this->foreignKey)
->fetchSql()
->$aggregate($field);
}
/**
* 关联统计
* @access public
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
* @return integer
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$foreignKey = $this->foreignKey;
if (!isset($result->$foreignKey)) {
return 0;
}
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
return $this->query
->where($this->localKey, '=', $result->$foreignKey)
->$aggregate($field);
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $operator 比较操作符
* @param integer $count 个数
* @param string $id 关联表的统计字段
* @param string $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 = class_basename($this->parent);
$relation = class_basename($this->model);
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$softDelete = $this->query->getOptions('soft_delete');
$query = $query ?: $this->parent->db()->alias($model);
return $query->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey, $softDelete) {
$query->table([$table => $relation])
->field($relation . '.' . $localKey)
->whereExp($model . '.' . $foreignKey, '=' . $relation . '.' . $localKey)
->when($softDelete, function ($query) use ($softDelete, $relation) {
$query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
});
});
}
/**
* 根据关联条件查询当前模型
* @access public
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null): Query
{
$table = $this->query->getTable();
$model = class_basename($this->parent);
$relation = class_basename($this->model);
if (is_array($where)) {
$this->getQueryWhere($where, $relation);
} elseif ($where instanceof Query) {
$where->via($relation);
} elseif ($where instanceof Closure) {
$where($this->query->via($relation));
$where = $this->query;
}
$fields = $this->getRelationQueryFields($fields, $model);
$softDelete = $this->query->getOptions('soft_delete');
$query = $query ?: $this->parent->db()->alias($model);
return $query->field($fields)
->join([$table => $relation], $model . '.' . $this->foreignKey . '=' . $relation . '.' . $this->localKey, $joinType ?: $this->joinType)
->when($softDelete, function ($query) use ($softDelete, $relation) {
$query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
})
->where($where);
}
/**
* 预载入关联查询(数据集)
* @access protected
* @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);
$data = $this->eagerlyWhere([
[$localKey, 'in', $range],
], $localKey, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
// 关联模型
if (!isset($data[$result->$foreignKey])) {
$relationModel = null;
} else {
$relationModel = $data[$result->$foreignKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
if ($relationModel && !empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($relationModel, $result);
} else {
// 设置关联属性
$result->setRelation($relation, $relationModel);
}
}
}
}
/**
* 预载入关联查询(数据)
* @access protected
* @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])) {
$relationModel = null;
} else {
$relationModel = $data[$result->$foreignKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
if ($relationModel && !empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($relationModel, $result);
} else {
// 设置关联属性
$result->setRelation($relation, $relationModel);
}
}
/**
* 添加关联数据
* @access public
* @param Model $model关联模型对象
* @return Model
*/
public function associate(Model $model): Model
{
$this->parent->setAttr($this->foreignKey, $model->getKey());
$this->parent->save();
return $this->parent->setRelation($this->relation, $model);
}
/**
* 注销关联数据
* @access public
* @return Model
*/
public function dissociate(): Model
{
$foreignKey = $this->foreignKey;
$this->parent->setAttr($foreignKey, null);
$this->parent->save();
return $this->parent->setRelation($this->relation, null);
}
/**
* 执行基础查询(仅执行一次)
* @access protected
* @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,707 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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\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;
use think\model\Pivot;
use think\model\Relation;
use think\Paginator;
/**
* 多对多关联类
*/
class BelongsToMany extends Relation
{
/**
* 中间表表名
* @var string
*/
protected $middle;
/**
* 中间表模型名称
* @var string
*/
protected $pivotName;
/**
* 中间表模型对象
* @var Pivot
*/
protected $pivot;
/**
* 中间表数据名称
* @var string
*/
protected $pivotDataName = 'pivot';
/**
* 架构函数
* @access public
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $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 (false !== strpos($middle, '\\')) {
$this->pivotName = $middle;
$this->middle = class_basename($middle);
} else {
$this->middle = $middle;
}
$this->query = (new $model)->db();
$this->pivot = $this->newPivot();
}
/**
* 设置中间表模型
* @access public
* @param $pivot
* @return $this
*/
public function pivot(string $pivot)
{
$this->pivotName = $pivot;
return $this;
}
/**
* 设置中间表数据名称
* @access public
* @param string $name
* @return $this
*/
public function name(string $name)
{
$this->pivotDataName = $name;
return $this;
}
/**
* 实例化中间表模型
* @access public
* @param $data
* @return Pivot
* @throws Exception
*/
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');
}
}
/**
* 合成中间表模型
* @access protected
* @param array|Collection|Paginator $models
*/
protected function hydratePivot(iterable $models)
{
foreach ($models as $model) {
$pivot = [];
foreach ($model->getData() as $key => $val) {
if (strpos($key, '__')) {
[$name, $attr] = explode('__', $key, 2);
if ('pivot' == $name) {
$pivot[$attr] = $val;
unset($model->$key);
}
}
}
$model->setRelation($this->pivotDataName, $this->newPivot($pivot));
}
}
/**
* 创建关联查询Query对象
* @access protected
* @return Query
*/
protected function buildQuery(): Query
{
$foreignKey = $this->foreignKey;
$localKey = $this->localKey;
// 关联查询
$pk = $this->parent->getPk();
$condition = ['pivot.' . $localKey, '=', $this->parent->$pk];
return $this->belongsToManyQuery($foreignKey, $localKey, [$condition]);
}
/**
* 延迟获取关联数据
* @access public
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
* @return Collection
*/
public function getRelation(array $subRelation = [], Closure $closure = null): Collection
{
if ($closure) {
$closure($this->getClosureType($closure));
}
$result = $this->buildQuery()
->relation($subRelation)
->select()
->setParent(clone $this->parent);
$this->hydratePivot($result);
return $result;
}
/**
* 重载select方法
* @access public
* @param mixed $data
* @return Collection
*/
public function select($data = null): Collection
{
$result = $this->buildQuery()->select($data);
$this->hydratePivot($result);
return $result;
}
/**
* 重载paginate方法
* @access public
* @param int|array $listRows
* @param int|bool $simple
* @return Paginator
*/
public function paginate($listRows = null, $simple = false): Paginator
{
$result = $this->buildQuery()->paginate($listRows, $simple);
$this->hydratePivot($result);
return $result;
}
/**
* 重载find方法
* @access public
* @param mixed $data
* @return Model
*/
public function find($data = null)
{
$result = $this->buildQuery()->find($data);
if (!$result->isEmpty()) {
$this->hydratePivot([$result]);
}
return $result;
}
/**
* 查找多条记录 如果不存在则抛出异常
* @access public
* @param array|string|Query|\Closure $data
* @return Collection
*/
public function selectOrFail($data = null): Collection
{
return $this->buildQuery()->failException(true)->select($data);
}
/**
* 查找单条记录 如果不存在则抛出异常
* @access public
* @param array|string|Query|\Closure $data
* @return Model
*/
public function findOrFail($data = null): Model
{
return $this->buildQuery()->failException(true)->find($data);
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $operator 比较操作符
* @param integer $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Model
*/
public function has(string $operator = '>=', $count = 1, $id = '*', string $joinType = 'INNER', Query $query = null)
{
return $this->parent;
}
/**
* 根据关联条件查询当前模型
* @access public
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
* @throws Exception
*/
public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: hasWhere');
}
/**
* 设置中间表的查询条件
* @access public
* @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;
}
/**
* 预载入关联查询(数据集)
* @access public
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$pk = $resultSet[0]->getPk();
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (isset($result->$pk)) {
$range[] = $result->$pk;
}
}
if (!empty($range)) {
// 查询关联数据
$data = $this->eagerlyManyToMany([
['pivot.' . $localKey, 'in', $range],
], $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
if (!isset($data[$result->$pk])) {
$data[$result->$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$result->$pk], clone $this->parent));
}
}
}
/**
* 预载入关联查询(单个数据)
* @access public
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$pk = $result->getPk();
if (isset($result->$pk)) {
$pk = $result->$pk;
// 查询管理数据
$data = $this->eagerlyManyToMany([
['pivot.' . $this->localKey, '=', $pk],
], $subRelation, $closure, $cache);
// 关联数据封装
if (!isset($data[$pk])) {
$data[$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
}
}
/**
* 关联统计
* @access public
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
* @return integer
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): float
{
$pk = $result->getPk();
if (!isset($result->$pk)) {
return 0;
}
$pk = $result->$pk;
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
['pivot.' . $this->localKey, '=', $pk],
])->$aggregate($field);
}
/**
* 获取关联统计子查询
* @access public
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
[
'pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->db(false)->getTable() . '.' . $this->parent->getPk()),
],
])->fetchSql()->$aggregate($field);
}
/**
* 多对多 关联模型预查询
* @access protected
* @param array $where 关联预查询条件
* @param array $subRelation 子关联
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return array
*/
protected function eagerlyManyToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array
{
if ($closure) {
$closure($this->getClosureType($closure));
}
// 预载入关联查询 支持嵌套预载入
$list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)
->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select();
// 组装模型数据
$data = [];
foreach ($list as $set) {
$pivot = [];
foreach ($set->getData() as $key => $val) {
if (strpos($key, '__')) {
[$name, $attr] = explode('__', $key, 2);
if ('pivot' == $name) {
$pivot[$attr] = $val;
unset($set->$key);
}
}
}
$key = $pivot[$this->localKey];
if ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->withLimit) {
continue;
}
$set->setRelation($this->pivotDataName, $this->newPivot($pivot));
$data[$key][] = $set;
}
return $data;
}
/**
* BELONGS TO MANY 关联查询
* @access protected
* @param string $foreignKey 关联模型关联键
* @param string $localKey 当前模型关联键
* @param array $condition 关联查询条件
* @return Query
*/
protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query
{
// 关联查询封装
$tableName = $this->query->getTable();
$table = $this->pivot->db()->getTable();
$fields = $this->getQueryFields($tableName);
if ($this->withLimit) {
$this->query->limit($this->withLimit);
}
$query = $this->query
->field($fields)
->tableField(true, $table, 'pivot', 'pivot__');
if (empty($this->baseQuery)) {
$relationFk = $this->query->getPk();
$query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk)
->where($condition);
}
return $query;
}
/**
* 保存(新增)当前关联数据对象
* @access public
* @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
* @param array $pivot 中间表额外数据
* @return array|Pivot
*/
public function save($data, array $pivot = [])
{
// 保存关联表/中间表数据
return $this->attach($data, $pivot);
}
/**
* 批量保存当前关联数据对象
* @access public
* @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;
}
/**
* 附加关联的一个中间表数据
* @access public
* @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
* @param array $pivot 中间表额外数据
* @return array|Pivot
* @throws Exception
*/
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) {
// 根据关联表主键直接写入中间表
$relationFk = $data->getPk();
$id = $data->$relationFk;
}
if (!empty($id)) {
// 保存中间表数据
$pk = $this->parent->getPk();
$pivot[$this->localKey] = $this->parent->$pk;
$ids = (array) $id;
foreach ($ids as $id) {
$pivot[$this->foreignKey] = $id;
$this->pivot->replace()
->exists(false)
->data([])
->save($pivot);
$result[] = $this->newPivot($pivot);
}
if (count($result) == 1) {
// 返回中间表模型对象
$result = $result[0];
}
return $result;
} else {
throw new Exception('miss relation data');
}
}
/**
* 判断是否存在关联数据
* @access public
* @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键
* @return Pivot|false
*/
public function attached($data)
{
if ($data instanceof Model) {
$id = $data->getKey();
} else {
$id = $data;
}
$pivot = $this->pivot
->where($this->localKey, $this->parent->getKey())
->where($this->foreignKey, $id)
->find();
return $pivot ?: false;
}
/**
* 解除关联的一个中间表数据
* @access public
* @param integer|array $data 数据 可以使用关联对象的主键
* @param bool $relationDel 是否同时删除关联表数据
* @return integer
*/
public function detach($data = null, bool $relationDel = false): int
{
if (is_array($data)) {
$id = $data;
} elseif (is_numeric($data) || is_string($data)) {
// 根据关联表主键直接写入中间表
$id = $data;
} elseif ($data instanceof Model) {
// 根据关联表主键直接写入中间表
$relationFk = $data->getPk();
$id = $data->$relationFk;
}
// 删除中间表数据
$pk = $this->parent->getPk();
$pivot = [];
$pivot[] = [$this->localKey, '=', $this->parent->$pk];
if (isset($id)) {
$pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id];
}
$result = $this->pivot->where($pivot)->delete();
// 删除关联表数据
if (isset($id) && $relationDel) {
$model = $this->model;
$model::destroy($id);
}
return $result;
}
/**
* 数据同步
* @access public
* @param array $ids
* @param bool $detaching
* @return array
*/
public function sync(array $ids, bool $detaching = true): array
{
$changes = [
'attached' => [],
'detached' => [],
'updated' => [],
];
$pk = $this->parent->getPk();
$current = $this->pivot
->where($this->localKey, $this->parent->$pk)
->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->attach($id, $attributes)) {
$changes['updated'][] = $id;
}
}
return $changes;
}
}

View File

@@ -0,0 +1,367 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model\relation;
use Closure;
use think\Collection;
use think\db\BaseQuery as Query;
use think\helper\Str;
use think\Model;
use think\model\Relation;
/**
* 一对多关联类
*/
class HasMany extends Relation
{
/**
* 架构函数
* @access public
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 当前模型主键
*/
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;
}
}
/**
* 延迟获取关联数据
* @access public
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
* @return Collection
*/
public function getRelation(array $subRelation = [], Closure $closure = null): Collection
{
if ($closure) {
$closure($this->getClosureType($closure));
}
if ($this->withLimit) {
$this->query->limit($this->withLimit);
}
return $this->query
->where($this->foreignKey, $this->parent->{$this->localKey})
->relation($subRelation)
->select()
->setParent(clone $this->parent);
}
/**
* 预载入关联查询
* @access public
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param 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)) {
$data = $this->eagerlyOneToMany([
[$this->foreignKey, 'in', $range],
], $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
$pk = $result->$localKey;
if (!isset($data[$pk])) {
$data[$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
}
}
}
/**
* 预载入关联查询
* @access public
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param 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], clone $this->parent));
}
}
/**
* 关联统计
* @access public
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
* @return integer
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$localKey = $this->localKey;
if (!isset($result->$localKey)) {
return 0;
}
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
return $this->query
->where($this->foreignKey, '=', $result->$localKey)
->$aggregate($field);
}
/**
* 创建关联统计子查询
* @access public
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
return $this->query->alias($aggregate . '_table')
->whereExp($aggregate . '_table.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey)
->fetchSql()
->$aggregate($field);
}
/**
* 一对多 关联模型预查询
* @access public
* @param array $where 关联预查询条件
* @param array $subRelation 子关联
* @param Closure $closure
* @param array $cache 关联缓存
* @return array
*/
protected function eagerlyOneToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array
{
$foreignKey = $this->foreignKey;
$this->query->removeWhereField($this->foreignKey);
// 预载入关联查询 支持嵌套预载入
if ($closure) {
$this->baseQuery = true;
$closure($this->getClosureType($closure));
}
$list = $this->query
->where($where)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->with($subRelation)
->select();
// 组装模型数据
$data = [];
foreach ($list as $set) {
$key = $set->$foreignKey;
if ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->withLimit) {
continue;
}
$data[$key][] = $set;
}
return $data;
}
/**
* 保存(新增)当前关联数据对象
* @access public
* @param mixed $data 数据 可以使用数组 关联模型对象
* @param boolean $replace 是否自动识别更新和写入
* @return Model|false
*/
public function save($data, bool $replace = true)
{
$model = $this->make();
return $model->replace($replace)->save($data) ? $model : false;
}
/**
* 创建关联对象实例
* @param array|Model $data
* @return Model
*/
public function make($data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
}
// 保存关联表数据
$data[$this->foreignKey] = $this->parent->{$this->localKey};
return new $this->model($data);
}
/**
* 批量保存当前关联数据对象
* @access public
* @param iterable $dataSet 数据集
* @param boolean $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;
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $operator 比较操作符
* @param integer $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = 'INNER', Query $query = null): Query
{
$table = $this->query->getTable();
$model = class_basename($this->parent);
$relation = class_basename($this->model);
if ('*' != $id) {
$id = $relation . '.' . (new $this->model)->getPk();
}
$softDelete = $this->query->getOptions('soft_delete');
$query = $query ?: $this->parent->db()->alias($model);
return $query->field($model . '.*')
->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType)
->when($softDelete, function ($query) use ($softDelete, $relation) {
$query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
})
->group($relation . '.' . $this->foreignKey)
->having('count(' . $id . ')' . $operator . $count);
}
/**
* 根据关联条件查询当前模型
* @access public
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null): Query
{
$table = $this->query->getTable();
$model = class_basename($this->parent);
$relation = class_basename($this->model);
if (is_array($where)) {
$this->getQueryWhere($where, $relation);
} elseif ($where instanceof Query) {
$where->via($relation);
} elseif ($where instanceof Closure) {
$where($this->query->via($relation));
$where = $this->query;
}
$fields = $this->getRelationQueryFields($fields, $model);
$softDelete = $this->query->getOptions('soft_delete');
$query = $query ?: $this->parent->db()->alias($model);
return $query->group($model . '.' . $this->localKey)
->field($fields)
->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey)
->when($softDelete, function ($query) use ($softDelete, $relation) {
$query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
})
->where($where);
}
/**
* 执行基础查询(仅执行一次)
* @access protected
* @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,382 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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\Collection;
use think\db\BaseQuery as Query;
use think\helper\Str;
use think\Model;
use think\model\Relation;
/**
* 远程一对多关联类
*/
class HasManyThrough extends Relation
{
/**
* 中间关联表外键
* @var string
*/
protected $throughKey;
/**
* 中间主键
* @var string
*/
protected $throughPk;
/**
* 中间表查询对象
* @var Query
*/
protected $through;
/**
* 架构函数
* @access public
* @param Model $parent 上级模型对象
* @param string $model 关联模型名
* @param string $through 中间模型名
* @param string $foreignKey 关联外键
* @param string $throughKey 中间关联外键
* @param string $localKey 当前模型主键
* @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();
}
/**
* 延迟获取关联数据
* @access public
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
* @return Collection
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
if ($closure) {
$closure($this->getClosureType($closure));
}
$this->baseQuery();
if ($this->withLimit) {
$this->query->limit($this->withLimit);
}
return $this->query->relation($subRelation)
->select()
->setParent(clone $this->parent);
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $operator 比较操作符
* @param integer $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param 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));
$throughTable = $this->through->getTable();
$pk = $this->throughPk;
$throughKey = $this->throughKey;
$relation = new $this->model;
$relationTable = $relation->getTable();
$softDelete = $this->query->getOptions('soft_delete');
if ('*' != $id) {
$id = $relationTable . '.' . $relation->getPk();
}
$query = $query ?: $this->parent->db()->alias($model);
return $query->field($model . '.*')
->join($throughTable, $throughTable . '.' . $this->foreignKey . '=' . $model . '.' . $this->localKey)
->join($relationTable, $relationTable . '.' . $throughKey . '=' . $throughTable . '.' . $this->throughPk)
->when($softDelete, function ($query) use ($softDelete, $relationTable) {
$query->where($relationTable . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
})
->group($relationTable . '.' . $this->throughKey)
->having('count(' . $id . ')' . $operator . $count);
}
/**
* 根据关联条件查询当前模型
* @access public
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function hasWhere($where = [], $fields = null, $joinType = '', Query $query = null): Query
{
$model = Str::snake(class_basename($this->parent));
$throughTable = $this->through->getTable();
$pk = $this->throughPk;
$throughKey = $this->throughKey;
$modelTable = (new $this->model)->getTable();
if (is_array($where)) {
$this->getQueryWhere($where, $modelTable);
} elseif ($where instanceof Query) {
$where->via($modelTable);
} elseif ($where instanceof Closure) {
$where($this->query->via($modelTable));
$where = $this->query;
}
$fields = $this->getRelationQueryFields($fields, $model);
$softDelete = $this->query->getOptions('soft_delete');
$query = $query ?: $this->parent->db()->alias($model);
return $query->join($throughTable, $throughTable . '.' . $this->foreignKey . '=' . $model . '.' . $this->localKey)
->join($modelTable, $modelTable . '.' . $throughKey . '=' . $throughTable . '.' . $this->throughPk)
->when($softDelete, function ($query) use ($softDelete, $modelTable) {
$query->where($modelTable . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
})
->group($modelTable . '.' . $this->throughKey)
->where($where)
->field($fields);
}
/**
* 预载入关联查询(数据集)
* @access protected
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (isset($result->$localKey)) {
$range[] = $result->$localKey;
}
}
if (!empty($range)) {
$this->query->removeWhereField($foreignKey);
$data = $this->eagerlyWhere([
[$this->foreignKey, 'in', $range],
], $foreignKey, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
$pk = $result->$localKey;
if (!isset($data[$pk])) {
$data[$pk] = [];
}
// 设置关联属性
$result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
}
}
}
/**
* 预载入关联查询(数据)
* @access protected
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$pk = $result->$localKey;
$this->query->removeWhereField($foreignKey);
$data = $this->eagerlyWhere([
[$foreignKey, '=', $pk],
], $foreignKey, $subRelation, $closure, $cache);
// 关联数据封装
if (!isset($data[$pk])) {
$data[$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
}
/**
* 关联模型预查询
* @access public
* @param array $where 关联预查询条件
* @param string $key 关联键名
* @param array $subRelation 子关联
* @param Closure $closure
* @param array $cache 关联缓存
* @return array
*/
protected function eagerlyWhere(array $where, string $key, array $subRelation = [], Closure $closure = null, array $cache = []): array
{
// 预载入关联查询 支持嵌套预载入
$throughList = $this->through->where($where)->select();
$keys = $throughList->column($this->throughPk, $this->throughPk);
if ($closure) {
$this->baseQuery = true;
$closure($this->getClosureType($closure));
}
$list = $this->query
->where($this->throughKey, 'in', $keys)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select();
// 组装模型数据
$data = [];
$keys = $throughList->column($this->foreignKey, $this->throughPk);
foreach ($list as $set) {
$key = $keys[$set->{$this->throughKey}];
if ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->withLimit) {
continue;
}
$data[$key][] = $set;
}
return $data;
}
/**
* 关联统计
* @access public
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
* @return mixed
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$localKey = $this->localKey;
if (!isset($result->$localKey)) {
return 0;
}
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
$alias = Str::snake(class_basename($this->model));
$throughTable = $this->through->getTable();
$pk = $this->throughPk;
$throughKey = $this->throughKey;
$modelTable = $this->parent->getTable();
if (false === strpos($field, '.')) {
$field = $alias . '.' . $field;
}
return $this->query
->alias($alias)
->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
->where($throughTable . '.' . $this->foreignKey, $result->$localKey)
->$aggregate($field);
}
/**
* 创建关联统计子查询
* @access public
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
$alias = Str::snake(class_basename($this->model));
$throughTable = $this->through->getTable();
$pk = $this->throughPk;
$throughKey = $this->throughKey;
$modelTable = $this->parent->getTable();
if (false === strpos($field, '.')) {
$field = $alias . '.' . $field;
}
return $this->query
->alias($alias)
->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
->whereExp($throughTable . '.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey)
->fetchSql()
->$aggregate($field);
}
/**
* 执行基础查询(仅执行一次)
* @access protected
* @return void
*/
protected function baseQuery(): 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)
->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey});
$this->baseQuery = true;
}
}
}

View File

@@ -0,0 +1,300 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | 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\model\relation;
use Closure;
use think\db\BaseQuery as Query;
use think\helper\Str;
use think\Model;
/**
* HasOne 关联类
*/
class HasOne extends OneToOne
{
/**
* 架构函数
* @access public
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 当前模型主键
*/
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;
}
}
/**
* 延迟获取关联数据
* @access public
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
* @return Model
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
$localKey = $this->localKey;
if ($closure) {
$closure($this->getClosureType($closure));
}
// 判断关联类型执行查询
$relationModel = $this->query
->removeWhereField($this->foreignKey)
->where($this->foreignKey, $this->parent->$localKey)
->relation($subRelation)
->find();
if ($relationModel) {
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($relationModel, $this->parent);
}
$relationModel->setParent(clone $this->parent);
}
return $relationModel;
}
/**
* 创建关联统计子查询
* @access public
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
return $this->query
->whereExp($this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey)
->fetchSql()
->$aggregate($field);
}
/**
* 关联统计
* @access public
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
* @return integer
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$localKey = $this->localKey;
if (!isset($result->$localKey)) {
return 0;
}
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
return $this->query
->where($this->foreignKey, '=', $result->$localKey)
->$aggregate($field);
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $operator 比较操作符
* @param integer $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query
{
$table = $this->query->getTable();
$model = class_basename($this->parent);
$relation = class_basename($this->model);
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$softDelete = $this->query->getOptions('soft_delete');
$query = $query ?: $this->parent->db()->alias($model);
return $query->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey, $softDelete) {
$query->table([$table => $relation])
->field($relation . '.' . $foreignKey)
->whereExp($model . '.' . $localKey, '=' . $relation . '.' . $foreignKey)
->when($softDelete, function ($query) use ($softDelete, $relation) {
$query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
});
});
}
/**
* 根据关联条件查询当前模型
* @access public
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null): Query
{
$table = $this->query->getTable();
$model = class_basename($this->parent);
$relation = class_basename($this->model);
if (is_array($where)) {
$this->getQueryWhere($where, $relation);
} elseif ($where instanceof Query) {
$where->via($relation);
} elseif ($where instanceof Closure) {
$where($this->query->via($relation));
$where = $this->query;
}
$fields = $this->getRelationQueryFields($fields, $model);
$softDelete = $this->query->getOptions('soft_delete');
$query = $query ?: $this->parent->db()->alias($model);
return $query->field($fields)
->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType ?: $this->joinType)
->when($softDelete, function ($query) use ($softDelete, $relation) {
$query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
})
->where($where);
}
/**
* 预载入关联查询(数据集)
* @access protected
* @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);
$data = $this->eagerlyWhere([
[$foreignKey, 'in', $range],
], $foreignKey, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
// 关联模型
if (!isset($data[$result->$localKey])) {
$relationModel = null;
} else {
$relationModel = $data[$result->$localKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
if ($relationModel && !empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($relationModel, $result);
} else {
// 设置关联属性
$result->setRelation($relation, $relationModel);
}
}
}
}
/**
* 预载入关联查询(数据)
* @access protected
* @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])) {
$relationModel = null;
} else {
$relationModel = $data[$result->$localKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
if ($relationModel && !empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($relationModel, $result);
} else {
$result->setRelation($relation, $relationModel);
}
}
/**
* 执行基础查询(仅执行一次)
* @access protected
* @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,163 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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\helper\Str;
use think\Model;
/**
* 远程一对一关联类
*/
class HasOneThrough extends HasManyThrough
{
/**
* 延迟获取关联数据
* @access public
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
* @return Model
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
if ($closure) {
$closure($this->getClosureType($closure));
}
$this->baseQuery();
$relationModel = $this->query->relation($subRelation)->find();
if ($relationModel) {
$relationModel->setParent(clone $this->parent);
}
return $relationModel;
}
/**
* 预载入关联查询(数据集)
* @access protected
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (isset($result->$localKey)) {
$range[] = $result->$localKey;
}
}
if (!empty($range)) {
$this->query->removeWhereField($foreignKey);
$data = $this->eagerlyWhere([
[$this->foreignKey, 'in', $range],
], $foreignKey, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
// 关联模型
if (!isset($data[$result->$localKey])) {
$relationModel = null;
} else {
$relationModel = $data[$result->$localKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
// 设置关联属性
$result->setRelation($relation, $relationModel);
}
}
}
/**
* 预载入关联查询(数据)
* @access protected
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$this->query->removeWhereField($foreignKey);
$data = $this->eagerlyWhere([
[$foreignKey, '=', $result->$localKey],
], $foreignKey, $subRelation, $closure, $cache);
// 关联模型
if (!isset($data[$result->$localKey])) {
$relationModel = null;
} else {
$relationModel = $data[$result->$localKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
$result->setRelation($relation, $relationModel);
}
/**
* 关联模型预查询
* @access public
* @param array $where 关联预查询条件
* @param string $key 关联键名
* @param array $subRelation 子关联
* @param Closure $closure
* @param array $cache 关联缓存
* @return array
*/
protected function eagerlyWhere(array $where, string $key, array $subRelation = [], Closure $closure = null, array $cache = []): array
{
// 预载入关联查询 支持嵌套预载入
$keys = $this->through->where($where)->column($this->throughPk, $this->foreignKey);
if ($closure) {
$closure($this->getClosureType($closure));
}
$list = $this->query
->where($this->throughKey, 'in', $keys)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select();
// 组装模型数据
$data = [];
$keys = array_flip($keys);
foreach ($list as $set) {
$data[$keys[$set->{$this->throughKey}]] = $set;
}
return $data;
}
}

View File

@@ -0,0 +1,353 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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\Collection;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use think\helper\Str;
use think\Model;
use think\model\Relation;
/**
* 多态一对多关联
*/
class MorphMany extends Relation
{
/**
* 多态关联外键
* @var string
*/
protected $morphKey;
/**
* 多态字段名
* @var string
*/
protected $morphType;
/**
* 多态类型
* @var string
*/
protected $type;
/**
* 架构函数
* @access public
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $morphKey 关联外键
* @param string $morphType 多态字段名
* @param string $type 多态类型
*/
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();
}
/**
* 延迟获取关联数据
* @access public
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
* @return Collection
*/
public function getRelation(array $subRelation = [], Closure $closure = null): Collection
{
if ($closure) {
$closure($this->getClosureType($closure));
}
$this->baseQuery();
if ($this->withLimit) {
$this->query->limit($this->withLimit);
}
return $this->query->relation($subRelation)
->select()
->setParent(clone $this->parent);
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $operator 比较操作符
* @param integer $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: has');
}
/**
* 根据关联条件查询当前模型
* @access public
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: hasWhere');
}
/**
* 预载入关联查询
* @access public
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$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', $range],
[$morphType, '=', $type],
];
$data = $this->eagerlyMorphToMany($where, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
if (!isset($data[$result->$pk])) {
$data[$result->$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$result->$pk], clone $this->parent));
}
}
}
/**
* 预载入关联查询
* @access public
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$pk = $result->getPk();
if (isset($result->$pk)) {
$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], clone $this->parent));
}
}
/**
* 关联统计
* @access public
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
* @return mixed
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$pk = $result->getPk();
if (!isset($result->$pk)) {
return 0;
}
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
return $this->query
->where([
[$this->morphKey, '=', $result->$pk],
[$this->morphType, '=', $this->type],
])
->$aggregate($field);
}
/**
* 获取关联统计子查询
* @access public
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
$closure($this->getClosureType($closure), $name);
}
return $this->query
->whereExp($this->morphKey, '=' . $this->parent->getTable() . '.' . $this->parent->getPk())
->where($this->morphType, '=', $this->type)
->fetchSql()
->$aggregate($field);
}
/**
* 多态一对多 关联模型预查询
* @access protected
* @param array $where 关联预查询条件
* @param array $subRelation 子关联
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return array
*/
protected function eagerlyMorphToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array
{
// 预载入关联查询 支持嵌套预载入
$this->query->removeOption('where');
if ($closure) {
$this->baseQuery = true;
$closure($this->getClosureType($closure));
}
$list = $this->query
->where($where)
->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select();
$morphKey = $this->morphKey;
// 组装模型数据
$data = [];
foreach ($list as $set) {
$key = $set->$morphKey;
if ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->withLimit) {
continue;
}
$data[$key][] = $set;
}
return $data;
}
/**
* 保存(新增)当前关联数据对象
* @access public
* @param mixed $data 数据 可以使用数组 关联模型对象
* @param bool $replace 是否自动识别更新和写入
* @return Model|false
*/
public function save($data, bool $replace = true)
{
$model = $this->make();
return $model->replace($replace)->save($data) ? $model : false;
}
/**
* 创建关联对象实例
* @param array|Model $data
* @return Model
*/
public function make($data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
}
// 保存关联表数据
$pk = $this->parent->getPk();
$data[$this->morphKey] = $this->parent->$pk;
$data[$this->morphType] = $this->type;
return new $this->model($data);
}
/**
* 批量保存当前关联数据对象
* @access public
* @param iterable $dataSet 数据集
* @param boolean $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;
}
/**
* 执行基础查询(仅执行一次)
* @access protected
* @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,280 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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\helper\Str;
use think\Model;
use think\model\Relation;
/**
* 多态一对一关联类
*/
class MorphOne extends Relation
{
/**
* 多态关联外键
* @var string
*/
protected $morphKey;
/**
* 多态字段
* @var string
*/
protected $morphType;
/**
* 多态类型
* @var string
*/
protected $type;
/**
* 构造函数
* @access public
* @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();
}
/**
* 延迟获取关联数据
* @access public
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
* @return Model
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
if ($closure) {
$closure($this->getClosureType($closure));
}
$this->baseQuery();
$relationModel = $this->query->relation($subRelation)->find();
if ($relationModel) {
$relationModel->setParent(clone $this->parent);
}
return $relationModel;
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $operator 比较操作符
* @param integer $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null)
{
return $this->parent;
}
/**
* 根据关联条件查询当前模型
* @access public
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: hasWhere');
}
/**
* 预载入关联查询
* @access public
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$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);
// 关联数据封装
foreach ($resultSet as $result) {
if (!isset($data[$result->$pk])) {
$relationModel = null;
} else {
$relationModel = $data[$result->$pk];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
$result->setRelation($relation, $relationModel);
}
}
}
/**
* 预载入关联查询
* @access public
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$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];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
} else {
$relationModel = null;
}
$result->setRelation($relation, $relationModel);
}
}
/**
* 多态一对一 关联模型预查询
* @access protected
* @param array $where 关联预查询条件
* @param array $subRelation 子关联
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return array
*/
protected function eagerlyMorphToOne(array $where, array $subRelation = [], $closure = null, array $cache = []): array
{
// 预载入关联查询 支持嵌套预载入
if ($closure) {
$this->baseQuery = true;
$closure($this->getClosureType($closure));
}
$list = $this->query
->where($where)
->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select();
$morphKey = $this->morphKey;
// 组装模型数据
$data = [];
foreach ($list as $set) {
$data[$set->$morphKey] = $set;
}
return $data;
}
/**
* 保存(新增)当前关联数据对象
* @access public
* @param mixed $data 数据 可以使用数组 关联模型对象
* @param boolean $replace 是否自动识别更新和写入
* @return Model|false
*/
public function save($data, bool $replace = true)
{
$model = $this->make();
return $model->replace($replace)->save($data) ? $model : false;
}
/**
* 创建关联对象实例
* @param array|Model $data
* @return Model
*/
public function make($data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
}
// 保存关联表数据
$pk = $this->parent->getPk();
$data[$this->morphKey] = $this->parent->$pk;
$data[$this->morphType] = $this->type;
return new $this->model($data);
}
/**
* 执行基础查询(进执行一次)
* @access protected
* @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,333 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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\exception\DbException as Exception;
use think\helper\Str;
use think\Model;
use think\model\Relation;
/**
* 多态关联类
*/
class MorphTo extends Relation
{
/**
* 多态关联外键
* @var string
*/
protected $morphKey;
/**
* 多态字段
* @var string
*/
protected $morphType;
/**
* 多态别名
* @var array
*/
protected $alias = [];
/**
* 关联名
* @var string
*/
protected $relation;
/**
* 架构函数
* @access public
* @param Model $parent 上级模型对象
* @param string $morphType 多态字段名
* @param string $morphKey 外键名
* @param array $alias 多态别名定义
* @param string $relation 关联名
*/
public function __construct(Model $parent, string $morphType, string $morphKey, array $alias = [], string $relation = null)
{
$this->parent = $parent;
$this->morphType = $morphType;
$this->morphKey = $morphKey;
$this->alias = $alias;
$this->relation = $relation;
}
/**
* 获取当前的关联模型类的实例
* @access public
* @return Model
*/
public function getModel(): Model
{
$morphType = $this->morphType;
$model = $this->parseModel($this->parent->$morphType);
return (new $model);
}
/**
* 延迟获取关联数据
* @access public
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
* @return Model
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
$morphKey = $this->morphKey;
$morphType = $this->morphType;
// 多态模型
$model = $this->parseModel($this->parent->$morphType);
// 主键数据
$pk = $this->parent->$morphKey;
$relationModel = (new $model)->relation($subRelation)->find($pk);
if ($relationModel) {
$relationModel->setParent(clone $this->parent);
}
return $relationModel;
}
/**
* 根据关联条件查询当前模型
* @access public
* @param string $operator 比较操作符
* @param integer $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null)
{
return $this->parent;
}
/**
* 根据关联条件查询当前模型
* @access public
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
* @return Query
*/
public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: hasWhere');
}
/**
* 解析模型的完整命名空间
* @access protected
* @param string $model 模型名(或者完整类名)
* @return string
*/
protected function parseModel(string $model): string
{
if (isset($this->alias[$model])) {
$model = $this->alias[$model];
}
if (false === strpos($model, '\\')) {
$path = explode('\\', get_class($this->parent));
array_pop($path);
array_push($path, Str::studly($model));
$model = implode('\\', $path);
}
return $model;
}
/**
* 设置多态别名
* @access public
* @param array $alias 别名定义
* @return $this
*/
public function setAlias(array $alias)
{
$this->alias = $alias;
return $this;
}
/**
* 移除关联查询参数
* @access public
* @return $this
*/
public function removeOption()
{
return $this;
}
/**
* 预载入关联查询
* @access public
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
* @throws Exception
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$morphKey = $this->morphKey;
$morphType = $this->morphType;
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (!empty($result->$morphKey)) {
$range[$result->$morphType][] = $result->$morphKey;
}
}
if (!empty($range)) {
foreach ($range as $key => $val) {
// 多态类型映射
$model = $this->parseModel($key);
$obj = new $model;
$pk = $obj->getPk();
$list = $obj->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select($val);
$data = [];
foreach ($list as $k => $vo) {
$data[$vo->$pk] = $vo;
}
foreach ($resultSet as $result) {
if ($key == $result->$morphType) {
// 关联模型
if (!isset($data[$result->$morphKey])) {
$relationModel = null;
throw new Exception('relation data not exists :' . $this->model);
} else {
$relationModel = $data[$result->$morphKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
$result->setRelation($relation, $relationModel);
}
}
}
}
}
/**
* 预载入关联查询
* @access public
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
// 多态类型映射
$model = $this->parseModel($result->{$this->morphType});
$this->eagerlyMorphToOne($model, $relation, $result, $subRelation, $cache);
}
/**
* 关联统计
* @access public
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @return integer
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*')
{}
/**
* 多态MorphTo 关联模型预查询
* @access protected
* @param string $model 关联模型对象
* @param string $relation 关联名
* @param Model $result
* @param array $subRelation 子关联
* @param array $cache 关联缓存
* @return void
*/
protected function eagerlyMorphToOne(string $model, string $relation, Model $result, array $subRelation = [], array $cache = []): void
{
// 预载入关联查询 支持嵌套预载入
$pk = $this->parent->{$this->morphKey};
$data = (new $model)->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->find($pk);
if ($data) {
$data->setParent(clone $result);
$data->exists(true);
}
$result->setRelation($relation, $data ?: null);
}
/**
* 添加关联数据
* @access public
* @param Model $model 关联模型对象
* @param string $type 多态类型
* @return Model
*/
public function associate(Model $model, string $type = ''): Model
{
$morphKey = $this->morphKey;
$morphType = $this->morphType;
$pk = $model->getPk();
$this->parent->setAttr($morphKey, $model->$pk);
$this->parent->setAttr($morphType, $type ?: get_class($model));
$this->parent->save();
return $this->parent->setRelation($this->relation, $model);
}
/**
* 注销关联数据
* @access public
* @return Model
*/
public function dissociate(): Model
{
$morphKey = $this->morphKey;
$morphType = $this->morphType;
$this->parent->setAttr($morphKey, null);
$this->parent->setAttr($morphType, null);
$this->parent->save();
return $this->parent->setRelation($this->relation, null);
}
}

View File

@@ -0,0 +1,332 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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\helper\Str;
use think\Model;
use think\model\Relation;
/**
* 一对一关联基础类
* @package think\model\relation
*/
abstract class OneToOne extends Relation
{
/**
* JOIN类型
* @var string
*/
protected $joinType = 'INNER';
/**
* 绑定的关联属性
* @var array
*/
protected $bindAttr = [];
/**
* 关联名
* @var string
*/
protected $relation;
/**
* 设置join类型
* @access public
* @param string $type JOIN类型
* @return $this
*/
public function joinType(string $type)
{
$this->joinType = $type;
return $this;
}
/**
* 预载入关联查询JOIN方式
* @access public
* @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->getOptions('field')) {
$masterField = $query->getOptions('field');
$query->removeOption('field');
} else {
$masterField = true;
}
$query->tableField($masterField, $table, $name);
}
// 预载入封装
$joinTable = $this->query->getTable();
$joinAlias = $relation;
$joinType = $joinType ?: $this->joinType;
$query->via($joinAlias);
if ($this instanceof BelongsTo) {
$joinOn = $name . '.' . $this->foreignKey . '=' . $joinAlias . '.' . $this->localKey;
} else {
$joinOn = $name . '.' . $this->localKey . '=' . $joinAlias . '.' . $this->foreignKey;
}
if ($closure) {
// 执行闭包查询
$closure($this->getClosureType($closure));
// 使用withField指定获取关联的字段
if ($this->withField) {
$field = $this->withField;
}
}
$query->join([$joinTable => $joinAlias], $joinOn, $joinType)
->tableField($field, $joinTable, $joinAlias, $relation . '__');
}
/**
* 预载入关联查询(数据集)
* @access protected
* @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);
/**
* 预载入关联查询(数据)
* @access protected
* @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);
/**
* 预载入关联查询(数据集)
* @access public
* @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);
}
}
/**
* 预载入关联查询(数据)
* @access public
* @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);
}
}
/**
* 保存(新增)当前关联数据对象
* @access public
* @param mixed $data 数据 可以使用数组 关联模型对象
* @param boolean $replace 是否自动识别更新和写入
* @return Model|false
*/
public function save($data, bool $replace = true)
{
if ($data instanceof Model) {
$data = $data->getData();
}
$model = new $this->model;
// 保存关联表数据
$data[$this->foreignKey] = $this->parent->{$this->localKey};
return $model->replace($replace)->save($data) ? $model : false;
}
/**
* 绑定关联表的属性到父模型属性
* @access public
* @param array $attr 要绑定的属性列表
* @return $this
*/
public function bind(array $attr)
{
$this->bindAttr = $attr;
return $this;
}
/**
* 获取绑定属性
* @access public
* @return array
*/
public function getBindAttr(): array
{
return $this->bindAttr;
}
/**
* 一对一 关联模型预查询拼装
* @access public
* @param string $model 模型名称
* @param string $relation 关联名
* @param Model $result 模型对象实例
* @return void
*/
protected function match(string $model, string $relation, Model $result): void
{
// 重新组装模型数据
foreach ($result->getData() as $key => $val) {
if (strpos($key, '__')) {
[$name, $attr] = explode('__', $key, 2);
if ($name == $relation) {
$list[$name][$attr] = $val;
unset($result->$key);
}
}
}
if (isset($list[$relation])) {
$array = array_unique($list[$relation]);
if (count($array) == 1 && null === current($array)) {
$relationModel = null;
} else {
$relationModel = new $model($list[$relation]);
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
if ($relationModel && !empty($this->bindAttr)) {
$this->bindAttr($relationModel, $result);
}
} else {
$relationModel = null;
}
$result->setRelation($relation, $relationModel);
}
/**
* 绑定关联属性到父模型
* @access protected
* @param Model $model 关联模型对象
* @param Model $result 父模型对象
* @return void
* @throws Exception
*/
protected function bindAttr(Model $model, Model $result): void
{
foreach ($this->bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
$value = $result->getOrigin($key);
if (!is_null($value)) {
throw new Exception('bind attr has exists:' . $key);
}
$result->setAttr($key, $model ? $model->$attr : null);
}
}
/**
* 一对一 关联模型预查询IN方式
* @access public
* @param array $where 关联预查询条件
* @param string $key 关联键名
* @param array $subRelation 子关联
* @param Closure $closure
* @param array $cache 关联缓存
* @return array
*/
protected function eagerlyWhere(array $where, string $key, array $subRelation = [], Closure $closure = null, array $cache = [])
{
// 预载入关联查询 支持嵌套预载入
if ($closure) {
$this->baseQuery = true;
$closure($this->getClosureType($closure));
}
if ($this->withField) {
$this->query->field($this->withField);
}
if ($this->query->getOptions('order')) {
$this->query->group($key);
}
$list = $this->query
->where($where)
->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select();
// 组装模型数据
$data = [];
foreach ($list as $set) {
if (!isset($data[$set->$key])) {
$data[$set->$key] = $set;
}
}
return $data;
}
}

View File

@@ -0,0 +1,209 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 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);
}
}