This commit is contained in:
Your Name
2026-03-01 14:17:59 +08:00
parent 65aa12f146
commit a07e844c47
6071 changed files with 777651 additions and 0 deletions
@@ -0,0 +1,284 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: zhangyajun <448901948@qq.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\model;
use think\Collection as BaseCollection;
use think\Model;
use think\Paginator;
/**
* 模型数据集类.
*
* @template TKey of array-key
* @template TModel of \think\Model
*
* @extends BaseCollection<TKey, TModel>
*/
class Collection extends BaseCollection
{
/**
* 延迟预载入关联查询.
*
* @param array $relation 关联
* @param mixed $cache 关联缓存
*
* @return $this
*/
public function load(array $relation, $cache = false)
{
if (!$this->isEmpty()) {
$item = current($this->items);
$item->eagerlyResultSet($this->items, $relation, [], false, $cache);
}
return $this;
}
/**
* 删除数据集的数据.
*
* @return bool
*/
public function delete(): bool
{
$this->each(function (Model $model) {
$model->delete();
});
return true;
}
/**
* 更新数据.
*
* @param array $data 数据数组
* @param array $allowField 允许字段
*
* @return bool
*/
public function update(array $data, array $allowField = []): bool
{
$this->each(function (Model $model) use ($data, $allowField) {
if (!empty($allowField)) {
$model->allowField($allowField);
}
$model->save($data);
});
return true;
}
/**
* 设置需要隐藏的输出属性.
*
* @param array $hidden 属性列表
* @param bool $merge 是否合并
*
* @return $this
*/
public function hidden(array $hidden, bool $merge = false)
{
$this->each(function (Model $model) use ($hidden, $merge) {
$model->hidden($hidden, $merge);
});
return $this;
}
/**
* 设置需要输出的属性.
*
* @param array $visible
* @param bool $merge 是否合并
*
* @return $this
*/
public function visible(array $visible, bool $merge = false)
{
$this->each(function (Model $model) use ($visible, $merge) {
$model->visible($visible, $merge);
});
return $this;
}
/**
* 设置需要追加的输出属性.
*
* @param array $append 属性列表
* @param bool $merge 是否合并
*
* @return $this
*/
public function append(array $append, bool $merge = false)
{
$this->each(function (Model $model) use ($append, $merge) {
$model->append($append, $merge);
});
return $this;
}
/**
* 设置模型输出场景.
*
* @param string $scene 场景名称
*
* @return $this
*/
public function scene(string $scene)
{
$this->each(function (Model $model) use ($scene) {
$model->scene($scene);
});
return $this;
}
/**
* 设置父模型.
*
* @param Model $parent 父模型
*
* @return $this
*/
public function setParent(Model $parent)
{
$this->each(function (Model $model) use ($parent) {
$model->setParent($parent);
});
return $this;
}
/**
* 设置数据字段获取器.
*
* @param string|array $name 字段名
* @param callable $callback 闭包获取器
*
* @return $this
*/
public function withAttr(string|array $name, callable $callback = null)
{
$this->each(function (Model $model) use ($name, $callback) {
$model->withAttr($name, $callback);
});
return $this;
}
/**
* 绑定(一对一)关联属性到当前模型.
*
* @param string $relation 关联名称
* @param array $attrs 绑定属性
*
* @throws Exception
*
* @return $this
*/
public function bindAttr(string $relation, array $attrs = [])
{
$this->each(function (Model $model) use ($relation, $attrs) {
$model->bindAttr($relation, $attrs);
});
return $this;
}
/**
* 按指定键整理数据.
*
* @param mixed $items 数据
* @param string|null $indexKey 键名
*
* @return array
*/
public function dictionary($items = null, string &$indexKey = null)
{
if ($items instanceof self || $items instanceof Paginator) {
$items = $items->all();
}
$items = is_null($items) ? $this->items : $items;
if ($items && empty($indexKey)) {
$indexKey = $items[0]->getPk();
}
if (isset($indexKey) && is_string($indexKey)) {
return array_column($items, null, $indexKey);
}
return $items;
}
/**
* 比较数据集,返回差集.
*
* @param mixed $items 数据
* @param string|null $indexKey 指定比较的键名
*
* @return static
*/
public function diff($items, string $indexKey = null)
{
if ($this->isEmpty()) {
return new static($items);
}
$diff = [];
$dictionary = $this->dictionary($items, $indexKey);
if (is_string($indexKey)) {
foreach ($this->items as $item) {
if (!isset($dictionary[$item[$indexKey]])) {
$diff[] = $item;
}
}
}
return new static($diff);
}
/**
* 比较数据集,返回交集.
*
* @param mixed $items 数据
* @param string|null $indexKey 指定比较的键名
*
* @return static
*/
public function intersect($items, string $indexKey = null)
{
if ($this->isEmpty()) {
return new static([]);
}
$intersect = [];
$dictionary = $this->dictionary($items, $indexKey);
if (is_string($indexKey)) {
foreach ($this->items as $item) {
if (isset($dictionary[$item[$indexKey]])) {
$intersect[] = $item;
}
}
}
return new static($intersect);
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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;
/**
* 架构函数.
*
* @param array $data 数据
* @param Model|null $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);
}
/**
* 创建新的模型实例.
*
* @param array $data 数据
* @param mixed $where 更新条件
* @param array $options 参数
*
* @return Model
*/
public function newInstance(array $data = [], $where = null, array $options = []): Model
{
$model = parent::newInstance($data, $where, $options);
$model->parent = $this->parent;
$model->name = $this->name;
return $model;
}
}
+252
View File
@@ -0,0 +1,252 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\model;
use Closure;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use think\Model;
/**
* 模型关联基础类.
*
* @mixin Query
*/
abstract class Relation
{
/**
* 父模型对象
*
* @var Model
*/
protected $parent;
/**
* 当前关联的模型类名.
*
* @var string
*/
protected $model;
/**
* 关联模型查询对象
*
* @var Query
*/
protected $query;
/**
* 关联表外键.
*
* @var string
*/
protected $foreignKey;
/**
* 关联表主键.
*
* @var string
*/
protected $localKey;
/**
* 是否执行关联基础查询.
*
* @var bool
*/
protected $baseQuery;
/**
* 是否为自关联.
*
* @var bool
*/
protected $selfRelation = false;
/**
* 关联数据字段限制.
*
* @var array
*/
protected $withField;
/**
* 排除关联数据字段.
*
* @var array
*/
protected $withoutField;
/**
* 默认数据.
*
* @var mixed
*/
protected $default;
/**
* 获取关联的所属模型.
*
* @return Model
*/
public function getParent(): Model
{
return $this->parent;
}
/**
* 获取当前的关联模型类的Query实例.
*
* @return Query
*/
public function getQuery()
{
return $this->query;
}
/**
* 获取关联表外键.
*
* @return string
*/
public function getForeignKey(): string
{
return $this->foreignKey;
}
/**
* 获取关联表主键.
*
* @return string
*/
public function getLocalKey(): string
{
return $this->localKey;
}
/**
* 获取当前的关联模型类的实例.
*
* @return Model
*/
public function getModel(): Model
{
return $this->query->getModel();
}
/**
* 当前关联是否为自关联.
*
* @return bool
*/
public function isSelfRelation(): bool
{
return $this->selfRelation;
}
/**
* 封装关联数据集.
*
* @param array $resultSet 数据集
* @param 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 (!str_contains($field, '.')) {
$field = $model . '.' . $field;
}
}
return $fields;
}
protected function getQueryWhere(array &$where, string $relation): void
{
foreach ($where as $key => &$val) {
if (is_string($key)) {
$where[] = [!str_contains($key, '.') ? $relation . '.' . $key : $key, '=', $val];
unset($where[$key]);
} elseif (isset($val[0]) && !str_contains($val[0], '.')) {
$val[0] = $relation . '.' . $val[0];
}
}
}
/**
* 获取关联数据默认值
*
* @param mixed $data 模型数据
*
* @return mixed
*/
protected function getDefaultModel($data)
{
if (is_array($data)) {
$model = (new $this->model())->data($data);
} elseif ($data instanceof Closure) {
$model = new $this->model();
$data($model);
} else {
$model = $data;
}
return $model;
}
/**
* 执行基础查询(仅执行一次).
*
* @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);
}
}
@@ -0,0 +1,661 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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 InvalidArgumentException;
use Stringable;
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 $get = [];
/**
* 动态获取器.
*
* @var array
*/
private $withAttr = [];
/**
* 数据表延迟写入的字段
*
* @var array
*/
protected $lazyFields = [];
/**
* 获取模型对象的主键.
*
* @return string|array
*/
public function getPk()
{
return $this->pk;
}
/**
* 判断一个字段名是否为主键字段.
*
* @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;
}
/**
* 获取模型对象的主键值
*
* @return mixed
*/
public function getKey()
{
$pk = $this->getPk();
if (is_string($pk) && array_key_exists($pk, $this->data)) {
return $this->data[$pk];
}
}
/**
* 设置允许写入的字段.
*
* @param array $field 允许写入的字段
*
* @return $this
*/
public function allowField(array $field)
{
$this->field = $field;
return $this;
}
/**
* 设置只读字段.
*
* @param array $field 只读字段
*
* @return $this
*/
public function readOnly(array $field)
{
$this->readonly = $field;
return $this;
}
/**
* 获取实际的字段名.
*
* @param string $name 字段名
*
* @return string
*/
protected function getRealFieldName(string $name): string
{
if ($this->convertNameToCamel || !$this->strict) {
return Str::snake($name);
}
return $name;
}
/**
* 设置数据对象值
*
* @param array|object $data 数据
* @param bool $set 是否调用修改器
* @param array $allow 允许的字段名
*
* @return $this
*/
public function data(array|object $data, bool $set = false, array $allow = [])
{
if ($data instanceof Model) {
$data = $data->getData();
} elseif (is_object($data)) {
$data = get_object_vars($data);
}
// 清空数据
$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;
}
$this->appendData($data, $set);
return $this;
}
/**
* 批量追加数据对象值
*
* @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;
}
/**
* 刷新对象原始数据(为当前数据).
*
* @return $this
*/
public function refreshOrigin()
{
$this->origin = $this->data;
return $this;
}
/**
* 获取对象原始数据 如果不存在指定字段返回null.
*
* @param string $name 字段名 留空获取全部
*
* @return mixed
*/
public function getOrigin(string $name = null)
{
if (is_null($name)) {
return $this->origin;
}
$fieldName = $this->getRealFieldName($name);
return array_key_exists($fieldName, $this->origin) ? $this->origin[$fieldName] : null;
}
/**
* 获取当前对象数据 如果不存在指定字段返回false.
*
* @param string $name 字段名 留空获取全部
*
* @throws InvalidArgumentException
*
* @return mixed
*/
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];
}
if (array_key_exists($fieldName, $this->relation)) {
return $this->relation[$fieldName];
}
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}
/**
* 获取变化的数据 并排除只读数据.
*
* @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 (array_key_exists($field, $data)) {
unset($data[$field]);
}
}
return $data;
}
/**
* 直接设置数据对象值
*
* @param string $name 属性名
* @param mixed $value 值
*
* @return void
*/
public function set(string $name, $value): void
{
$name = $this->getRealFieldName($name);
$this->data[$name] = $value;
unset($this->get[$name]);
}
/**
* 通过修改器 批量设置数据对象值
*
* @param array $data 数据
*
* @return void
*/
public function setAttrs(array $data): void
{
// 进行数据处理
foreach ($data as $key => $value) {
$this->setAttr($key, $value, $data);
}
}
/**
* 通过修改器 设置数据对象值
*
* @param string $name 属性名
* @param mixed $value 属性值
* @param array $data 数据
*
* @return void
*/
public function setAttr(string $name, $value, array $data = []): void
{
$name = $this->getRealFieldName($name);
// 检测修改器
$method = 'set' . Str::studly($name) . 'Attr';
if (method_exists($this, $method)) {
$array = $this->data;
$value = $this->$method($value, array_merge($this->data, $data));
if (is_null($value) && $array !== $this->data) {
return;
}
} elseif (isset($this->type[$name])) {
// 类型转换
$value = $this->writeTransform($value, $this->type[$name]);
} elseif ($this->isRelationAttr($name)) {
// 关联属性
$this->relation[$name] = $value;
} elseif ((array_key_exists($name, $this->origin) || empty($this->origin)) && $value instanceof Stringable) {
// 对象类型
$value = $value->__toString();
}
// 设置数据对象属性
$this->data[$name] = $value;
unset($this->get[$name]);
}
/**
* 数据写入 类型转换.
*
* @param mixed $value 值
* @param string|array $type 要转换的类型
*
* @return mixed
*/
protected function writeTransform($value, string|array $type)
{
if (is_null($value)) {
return;
}
if ($value instanceof Raw) {
return $value;
}
if (is_array($type)) {
[$type, $param] = $type;
} elseif (str_contains($type, ':')) {
[$type, $param] = explode(':', $type, 2);
}
return match ($type) {
'integer' => (int) $value,
'float' => empty($param) ? (float) $value : (float) number_format($value, (int) $param, '.', ''),
'boolean' => (bool) $value,
'timestamp' => !is_numeric($value) ? strtotime($value) : $value,
'datetime' => $this->formatDateTime('Y-m-d H:i:s.u', $value, true),
'object' => is_object($value) ? json_encode($value, JSON_FORCE_OBJECT) : $value,
'array' => json_encode((array) $value, !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE),
'json' => json_encode($value, !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE),
'serialize' => serialize($value),
default => $value instanceof Stringable && str_contains($type, '\\') ? $value->__toString() : $value,
};
}
/**
* 获取器 获取数据对象的值
*
* @param string $name 名称
*
* @throws InvalidArgumentException
*
* @return mixed
*/
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);
}
/**
* 获取经过获取器处理后的数据对象的值
*
* @param string $name 字段名称
* @param mixed $value 字段值
* @param bool|string $relation 是否为关联属性或者关联名
*
* @throws InvalidArgumentException
*
* @return mixed
*/
protected function getValue(string $name, $value, bool|string $relation = false)
{
// 检测属性获取器
$fieldName = $this->getRealFieldName($name);
if (array_key_exists($fieldName, $this->get)) {
return $this->get[$fieldName];
}
$method = 'get' . Str::studly($name) . 'Attr';
if (isset($this->withAttr[$fieldName])) {
if ($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];
if ($closure instanceof \Closure) {
$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;
}
$this->get[$fieldName] = $value;
return $value;
}
/**
* 获取JSON字段属性值
*
* @param string $name 属性名
* @param mixed $value JSON数据
*
* @return mixed
*/
protected function getJsonValue(string $name, $value)
{
if (is_null($value)) {
return $value;
}
foreach ($this->withAttr[$name] as $key => $closure) {
if ($this->jsonAssoc) {
$value[$key] = $closure($value[$key], $value);
} else {
$value->$key = $closure($value->$key, $value);
}
}
return $value;
}
/**
* 获取关联属性值
*
* @param string $relation 关联名
*
* @return mixed
*/
protected function getRelationValue(string $relation)
{
$modelRelation = $this->$relation();
return $modelRelation instanceof Relation ? $this->getRelationData($modelRelation) : null;
}
/**
* 数据读取 类型转换.
*
* @param mixed $value 值
* @param string|array $type 要转换的类型
*
* @return mixed
*/
protected function readTransform($value, string|array $type)
{
if (is_null($value)) {
return;
}
if (is_array($type)) {
[$type, $param] = $type;
} elseif (str_contains($type, ':')) {
[$type, $param] = explode(':', $type, 2);
}
$call = function ($value) {
try {
$value = unserialize($value);
} catch (\Exception $e) {
$value = null;
}
return $value;
};
return match ($type) {
'integer' => (int) $value,
'float' => empty($param) ? (float) $value : (float) number_format($value, (int) $param, '.', ''),
'boolean' => (bool) $value,
'timestamp' => !is_null($value) ? $this->formatDateTime(!empty($param) ? $param : $this->dateFormat, $value, true) : null,
'datetime' => !is_null($value) ? $this->formatDateTime(!empty($param) ? $param : $this->dateFormat, $value) : null,
'json' => json_decode($value, true),
'array' => empty($value) ? [] : json_decode($value, true),
'object' => empty($value) ? new \stdClass() : json_decode($value),
'serialize' => $call($value),
default => str_contains($type, '\\') ? new $type($value) : $value,
};
}
/**
* 设置数据字段获取器.
*
* @param string|array $name 字段名
* @param Closure $callback 闭包获取器
*
* @return $this
*/
public function withAttr(string|array $name, Closure $callback = null)
{
if (is_array($name)) {
foreach ($name as $key => $val) {
$this->withAttr($key, $val);
}
} else {
$name = $this->getRealFieldName($name);
if (str_contains($name, '.')) {
[$name, $key] = explode('.', $name);
$this->withAttr[$name][$key] = $callback;
} else {
$this->withAttr[$name] = $callback;
}
}
return $this;
}
}
@@ -0,0 +1,400 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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 array
*/
protected $scene = [];
/**
* 数据输出字段映射.
*
* @var array
*/
protected $mapping = [];
/**
* 数据集对象名.
*
* @var string
*/
protected $resultSetType;
/**
* 数据命名是否自动转为驼峰.
*
* @var bool
*/
protected $convertNameToCamel;
/**
* 转换数据为驼峰命名(用于输出).
*
* @param bool $toCamel 是否自动驼峰命名
*
* @return $this
*/
public function convertNameToCamel(bool $toCamel = true)
{
$this->convertNameToCamel = $toCamel;
return $this;
}
/**
* 设置输出层场景.
*
* @param string $scene 场景名称
*
* @return $this
*/
public function scene(string $scene)
{
if (isset($this->scene[$scene])) {
$data = $this->scene[$scene];
foreach (['append', 'hidden', 'visible'] as $name) {
if (isset($data[$name])) {
$this->$name($data[$name]);
}
}
}
return $this;
}
/**
* 设置附加关联对象的属性.
*
* @param string $attr 关联属性
* @param string|array $append 追加属性名
*
* @throws Exception
*
* @return $this
*/
public function appendRelationAttr(string $attr, array $append)
{
$relation = Str::camel($attr);
$model = $this->relation[$relation] ?? $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;
}
/**
* 设置需要附加的输出属性.
*
* @param array $append 属性列表
* @param bool $merge 是否合并
*
* @return $this
*/
public function append(array $append = [], bool $merge = false)
{
if ($merge) {
$this->append = array_merge($this->append, $append);
} else {
$this->append = $append;
}
return $this;
}
/**
* 设置需要隐藏的输出属性.
*
* @param array $hidden 属性列表
* @param bool $merge 是否合并
*
* @return $this
*/
public function hidden(array $hidden = [], bool $merge = false)
{
if ($merge) {
$this->hidden = array_merge($this->hidden, $hidden);
} else {
$this->hidden = $hidden;
}
return $this;
}
/**
* 设置需要输出的属性.
*
* @param array $visible
* @param bool $merge 是否合并
*
* @return $this
*/
public function visible(array $visible = [], bool $merge = false)
{
if ($merge) {
$this->visible = array_merge($this->visible, $visible);
} else {
$this->visible = $visible;
}
return $this;
}
/**
* 设置属性的映射输出.
*
* @param array $map
*
* @return $this
*/
public function mapping(array $map)
{
$this->mapping = $map;
return $this;
}
/**
* 转换当前模型对象为数组.
*
* @return array
*/
public function toArray(): array
{
$item = $visible = $hidden = [];
$hasVisible = false;
foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (str_contains($val, '.')) {
[$relation, $name] = explode('.', $val);
$visible[$relation][] = $name;
} else {
$visible[$val] = true;
$hasVisible = true;
}
} else {
$visible[$key] = $val;
}
}
foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (str_contains($val, '.')) {
[$relation, $name] = explode('.', $val);
$hidden[$relation][] = $name;
} else {
$hidden[$val] = true;
}
} else {
$hidden[$key] = $val;
}
}
// 追加属性(必须定义获取器)
foreach ($this->append as $key => $name) {
$this->appendAttrToArray($item, $key, $name, $visible, $hidden);
}
// 合并关联数据
$data = array_merge($this->data, $this->relation);
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($visible[$key]) && is_array($visible[$key])) {
$val->visible($visible[$key]);
} elseif (isset($hidden[$key]) && is_array($hidden[$key])) {
$val->hidden($hidden[$key], true);
}
// 关联模型对象
if (!isset($hidden[$key]) || true !== $hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
if (isset($this->mapping[$key])) {
// 检查字段映射
$mapName = $this->mapping[$key];
$item[$mapName] = $item[$key];
unset($item[$key]);
}
}
if ($this->convertNameToCamel) {
foreach ($item as $key => $val) {
$name = Str::camel($key);
if ($name !== $key) {
$item[$name] = $val;
unset($item[$key]);
}
}
}
return $item;
}
protected function appendAttrToArray(array &$item, $key, array|string $name, array $visible, array $hidden): void
{
if (is_array($name)) {
// 批量追加关联对象属性
$relation = $this->getRelationWith($key, $hidden, $visible);
$item[$key] = $relation ? $relation->append($name)->toArray() : [];
} elseif (str_contains($name, '.')) {
// 追加单个关联对象属性
[$key, $attr] = explode('.', $name);
$relation = $this->getRelationWith($key, $hidden, $visible);
$item[$key] = $relation ? $relation->append([$attr])->toArray() : [];
} else {
$value = $this->getAttr($name);
$item[$name] = $value;
$this->getBindAttrValue($name, $value, $item);
}
}
protected function getRelationWith(string $key, array $hidden, array $visible)
{
$relation = $this->getRelation($key, true);
if ($relation) {
if (isset($visible[$key])) {
$relation->visible($visible[$key]);
} elseif (isset($hidden[$key])) {
$relation->hidden($hidden[$key]);
}
}
return $relation;
}
protected function getBindAttrValue(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字符串.
*
* @param int $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(): array
{
return $this->toArray();
}
/**
* 转换数据集为数据集对象
*
* @param array|Collection $collection 数据集
* @param string $resultSetType 数据集类
*
* @return Collection
*/
public function toCollection(iterable $collection = [], string $resultSetType = null): Collection
{
$resultSetType = $resultSetType ?: $this->resultSetType;
if ($resultSetType && str_contains($resultSetType, '\\')) {
$collection = new $resultSetType($collection);
} else {
$collection = new ModelCollection($collection);
}
return $collection;
}
}
@@ -0,0 +1,94 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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对象
*
* @param object $event Event对象
*
* @return void
*/
public static function setEvent($event)
{
self::$event = $event;
}
/**
* 当前操作的事件响应.
*
* @param bool $event 是否需要事件响应
*
* @return $this
*/
public function withEvent(bool $event)
{
$this->withEvent = $event;
return $this;
}
/**
* 触发事件.
*
* @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('model.' . static::class . '.' . $event, $this);
$result = empty($result) ? true : end($result);
} else {
$result = true;
}
return !(false === $result);
} catch (ModelEventException $e) {
return false;
}
}
}
@@ -0,0 +1,85 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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';
}
/**
* 数据检查.
*
* @return void
*/
protected function checkData(): void
{
$this->isExists() ? $this->updateLockVersion() : $this->recordLockVersion();
}
/**
* 记录乐观锁
*
* @return void
*/
protected function recordLockVersion(): void
{
$optimLock = $this->getOptimLockField();
if ($optimLock) {
$this->set($optimLock, 0);
}
}
/**
* 更新乐观锁
*
* @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');
}
}
}
@@ -0,0 +1,870 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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\MorphToMany;
use think\model\relation\OneToOne;
/**
* 模型关联处理.
*/
trait RelationShip
{
/**
* 父关联模型对象
*
* @var object
*/
private $parent;
/**
* 模型关联数据.
*
* @var array
*/
private $relation = [];
/**
* 关联写入定义信息.
*
* @var array
*/
private $together = [];
/**
* 关联自动写入信息.
*
* @var array
*/
protected $relationWrite = [];
/**
* 设置父关联对象
*
* @param Model $model 模型对象
*
* @return $this
*/
public function setParent(Model $model)
{
$this->parent = $model;
return $this;
}
/**
* 获取父关联对象
*
* @return Model
*/
public function getParent(): Model
{
return $this->parent;
}
/**
* 获取当前模型的关联模型数据.
*
* @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);
}
}
/**
* 设置关联数据对象值
*
* @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;
}
/**
* 查询当前模型的关联数据.
*
* @param array $relations 关联名
* @param array $withRelationAttr 关联获取器
*
* @return void
*/
public function relationQuery(array $relations, array $withRelationAttr = []): void
{
foreach ($relations as $key => $relation) {
$subRelation = [];
$closure = null;
if ($relation instanceof Closure) {
// 支持闭包查询过滤关联条件
$closure = $relation;
$relation = $key;
}
if (is_array($relation)) {
$subRelation = $relation;
$relation = $key;
} elseif (str_contains($relation, '.')) {
[$relation, $subRelation] = explode('.', $relation, 2);
}
$method = Str::camel($relation);
$relationName = Str::snake($relation);
$relationResult = $this->$method();
if (isset($withRelationAttr[$relationName])) {
$relationResult->withAttr($withRelationAttr[$relationName]);
}
$this->relation[$relation] = $relationResult->getRelation((array) $subRelation, $closure);
}
}
/**
* 关联数据写入.
*
* @param array $relation 关联
*
* @return $this
*/
public function together(array $relation)
{
$this->together = $relation;
$this->checkAutoRelationWrite();
return $this;
}
/**
* 根据关联条件查询当前模型.
*
* @param string $relation 关联方法名
* @param mixed $operator 比较操作符
* @param int $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public static function has(string $relation, string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query
{
return (new static())
->$relation()
->has($operator, $count, $id, $joinType, $query);
}
/**
* 根据关联条件查询当前模型.
*
* @param string $relation 关联方法名
* @param 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方式.
*
* @param Query $query Query对象
* @param string $relation 关联方法名
* @param mixed $field 字段
* @param string $joinType JOIN类型
* @param Closure $closure 闭包
* @param bool $first
*
* @return bool
*/
public function eagerly(Query $query, string $relation, $field, string $joinType = '', Closure $closure = null, bool $first = false): bool
{
$relation = Str::camel($relation);
$class = $this->$relation();
if ($class instanceof OneToOne) {
$class->eagerly($query, $relation, $field, $joinType, $closure, $first);
return true;
}
return false;
}
/**
* 预载入关联查询 返回数据集.
*
* @param array $resultSet 数据集
* @param 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 (str_contains($relation, '.')) {
[$relation, $subRelation] = explode('.', $relation, 2);
$subRelation = [$subRelation];
}
$relationName = $relation;
$relation = Str::camel($relation);
$relationResult = $this->$relation();
if (isset($withRelationAttr[$relationName])) {
$relationResult->withAttr($withRelationAttr[$relationName]);
}
if (is_scalar($cache)) {
$relationCache = [$cache];
} else {
$relationCache = $cache[$relationName] ?? $cache;
}
$relationResult->eagerlyResultSet($resultSet, $relationName, $subRelation, $closure, $relationCache, $join);
}
}
/**
* 预载入关联查询 返回模型对象
*
* @param array $relations 关联
* @param array $withRelationAttr 关联获取器
* @param bool $join 是否为JOIN方式
* @param mixed $cache 关联缓存
*
* @return void
*/
public function eagerlyResult(array $relations, array $withRelationAttr = [], bool $join = false, $cache = false): void
{
foreach ($relations as $key => $relation) {
$subRelation = [];
$closure = null;
if ($relation instanceof Closure) {
$closure = $relation;
$relation = $key;
}
if (is_array($relation)) {
$subRelation = $relation;
$relation = $key;
} elseif (str_contains($relation, '.')) {
[$relation, $subRelation] = explode('.', $relation, 2);
$subRelation = [$subRelation];
}
$relationName = $relation;
$relation = Str::camel($relation);
$relationResult = $this->$relation();
if (isset($withRelationAttr[$relationName])) {
$relationResult->withAttr($withRelationAttr[$relationName]);
}
if (is_scalar($cache)) {
$relationCache = [$cache];
} else {
$relationCache = $cache[$relationName] ?? [];
}
$relationResult->eagerlyResult($this, $relationName, $subRelation, $closure, $relationCache, $join);
}
}
/**
* 绑定(一对一)关联属性到当前模型.
*
* @param string $relation 关联名称
* @param array $attrs 绑定属性
*
* @throws Exception
*
* @return $this
*/
public function bindAttr(string $relation, array $attrs = [])
{
$relation = $this->getRelation($relation, true);
foreach ($attrs as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
$value = $this->getOrigin($key);
if (!is_null($value)) {
throw new Exception('bind attr has exists:' . $key);
}
$this->set($key, $relation ? $relation->$attr : null);
}
return $this;
}
/**
* 关联统计
*
* @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 关联定义.
*
* @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 关联定义.
*
* @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 关联定义.
*
* @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 远程关联定义.
*
* @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 远程关联定义.
*
* @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 关联定义.
*
* @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 关联定义.
*
* @param string $model 模型名
* @param string|array $morph 多态字段信息
* @param string $type 多态类型
*
* @return MorphOne
*/
public function morphOne(string $model, string|array $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']);
}
[$morphType, $foreignKey] = $this->parseMorph($morph);
$type = $type ?: get_class($this);
return new MorphOne($this, $model, $foreignKey, $morphType, $type);
}
/**
* MORPH MANY 关联定义.
*
* @param string $model 模型名
* @param string|array $morph 多态字段信息
* @param string $type 多态类型
*
* @return MorphMany
*/
public function morphMany(string $model, string|array $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);
[$morphType, $foreignKey] = $this->parseMorph($morph);
return new MorphMany($this, $model, $foreignKey, $morphType, $type);
}
/**
* MORPH TO 关联定义.
*
* @param string|array $morph 多态字段信息
* @param array $alias 多态别名定义
*
* @return MorphTo
*/
public function morphTo(string|array $morph = null, array $alias = []): MorphTo
{
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$relation = Str::snake($trace[1]['function']);
if (is_null($morph)) {
$morph = $relation;
}
[$morphType, $foreignKey] = $this->parseMorph($morph);
return new MorphTo($this, $morphType, $foreignKey, $alias, $relation);
}
/**
* MORPH TO MANY关联定义.
*
* @param string $model 模型名
* @param string $middle 中间表名/模型名
* @param string|array $morph 多态字段信息
* @param string $localKey 当前模型关联键
*
* @return MorphToMany
*/
public function morphToMany(string $model, string $middle, string|array $morph = null, string $localKey = null): MorphToMany
{
if (is_null($morph)) {
$morph = $middle;
}
[$morphType, $morphKey] = $this->parseMorph($morph);
$model = $this->parseModel($model);
$name = Str::snake(class_basename($model));
$localKey = $localKey ?: $this->getForeignKey($name);
return new MorphToMany($this, $model, $middle, $morphType, $morphKey, $localKey);
}
/**
* MORPH BY MANY关联定义.
*
* @param string $model 模型名
* @param string $middle 中间表名/模型名
* @param string|array $morph 多态字段信息
* @param string $foreignKey 关联外键
*
* @return MorphToMany
*/
public function morphByMany(string $model, string $middle, string|array $morph = null, string $foreignKey = null): MorphToMany
{
if (is_null($morph)) {
$morph = $middle;
}
[$morphType, $morphKey] = $this->parseMorph($morph);
$model = $this->parseModel($model);
$foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
return new MorphToMany($this, $model, $middle, $morphType, $morphKey, $foreignKey, true);
}
/**
* 解析多态
*
* @param string|array $morph
*
* @return array
*/
protected function parseMorph(string|array $morph): array
{
if (is_array($morph)) {
[$morphType, $foreignKey] = $morph;
} else {
$morphType = $morph . '_type';
$foreignKey = $morph . '_id';
}
return [$morphType, $foreignKey];
}
/**
* 解析模型的完整命名空间.
*
* @param string $model 模型名(或者完整类名)
*
* @return string
*/
protected function parseModel(string $model): string
{
if (!str_contains($model, '\\')) {
$path = explode('\\', static::class);
array_pop($path);
array_push($path, Str::studly($model));
$model = implode('\\', $path);
}
return $model;
}
/**
* 获取模型的默认外键名.
*
* @param string $name 模型名
*
* @return string
*/
protected function getForeignKey(string $name): string
{
if (str_contains($name, '\\')) {
$name = class_basename($name);
}
return Str::snake($name) . '_id';
}
/**
* 检查属性是否为关联属性 如果是则返回关联方法名.
*
* @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)) || isset(static::$macro[static::class][$relation])) {
return $relation;
}
return false;
}
/**
* 智能获取关联模型数据.
*
* @param Relation $modelRelation 模型关联对象
*
* @return mixed
*/
protected function getRelationData(Relation $modelRelation)
{
if (
$this->parent && !$modelRelation->isSelfRelation()
&& get_class($this->parent) == get_class($modelRelation->getModel())
&& ($modelRelation instanceof OneToOne || $modelRelation instanceof HasOneThrough || $modelRelation instanceof MorphTo || $modelRelation instanceof MorphOne)
) {
if(empty($this->parent->parent)) $this->parent->parent = $this;
return $this->parent;
}
// 获取关联数据
return $modelRelation->getRelation();
}
/**
* 关联数据自动写入检查.
*
* @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]);
}
}
}
/**
* 自动关联数据更新(针对一对一关联).
*
* @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);
}
}
}
}
/**
* 自动关联数据写入(针对一对一关联).
*
* @return void
*/
protected function autoRelationInsert(): void
{
foreach ($this->relationWrite as $name => $val) {
$method = Str::camel($name);
$this->$method()->save($val);
}
}
/**
* 自动关联数据删除(支持一对一及一对多关联).
*
* @param bool $force 强制删除
*
* @return void
*/
protected function autoRelationDelete($force = false): void
{
foreach ($this->relationWrite as $key => $name) {
$name = is_numeric($key) ? $name : $key;
$result = $this->getRelation($name, true);
if ($result instanceof Model) {
$result->force($force)->delete();
} elseif ($result instanceof Collection) {
foreach ($result as $model) {
$model->force($force)->delete();
}
}
}
}
/**
* 移除当前模型的关联属性.
*
* @return $this
*/
public function removeRelation()
{
$this->relation = [];
return $this;
}
}
@@ -0,0 +1,238 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\model\concern;
use think\db\BaseQuery as Query;
use think\Model;
/**
* 数据软删除
*
* @mixin Model
*
* @method $this withTrashed()
* @method $this onlyTrashed()
*/
trait SoftDelete
{
public function db($scope = []): Query
{
$query = parent::db($scope);
$this->withNoTrashed($query);
return $query;
}
/**
* 判断当前实例是否被软删除.
*
* @return bool
*/
public function trashed(): bool
{
$field = $this->getDeleteTimeField();
if ($field && !empty($this->getOrigin($field))) {
return true;
}
return false;
}
public function scopeWithTrashed(Query $query): void
{
$query->removeOption('soft_delete');
}
public function scopeOnlyTrashed(Query $query): void
{
$field = $this->getDeleteTimeField(true);
if ($field) {
$query->useSoftDelete($field, $this->getWithTrashedExp());
}
}
/**
* 获取软删除数据的查询条件.
*
* @return array
*/
protected function getWithTrashedExp(): array
{
return is_null($this->defaultSoftDelete) ? ['notnull', ''] : ['<>', $this->defaultSoftDelete];
}
/**
* 删除当前的记录.
*
* @return bool
*/
public function delete(): bool
{
if (!$this->isExists() || $this->isEmpty() || false === $this->trigger('BeforeDelete')) {
return false;
}
$name = $this->getDeleteTimeField();
$force = $this->isForce();
if ($name && !$force) {
// 软删除
$this->set($name, $this->autoWriteTimestamp());
$this->exists()->withEvent(false)->save();
$this->withEvent(true);
} else {
// 读取更新条件
$where = $this->getWhere();
// 删除当前模型数据
$this->db()
->where($where)
->removeOption('soft_delete')
->delete();
}
// 关联删除
if (!empty($this->relationWrite)) {
$this->autoRelationDelete($force);
}
$this->trigger('AfterDelete');
$this->exists(false);
return true;
}
/**
* 删除记录.
*
* @param mixed $data 主键列表 支持闭包查询条件
* @param bool $force 是否强制删除
*
* @return bool
*/
public static function destroy($data, bool $force = false): bool
{
// 传入空值(包括空字符串和空数组)的时候不会做任何的数据删除操作,但传入0则是有效的
if (empty($data) && 0 !== $data) {
return false;
}
$model = (new static());
$query = $model->db(false);
// 仅当强制删除时包含软删除数据
if ($force) {
$query->removeOption('soft_delete');
}
if (is_array($data) && key($data) !== 0) {
$query->where($data);
$data = [];
} elseif ($data instanceof \Closure) {
call_user_func_array($data, [&$query]);
$data = [];
}
$resultSet = $query->select((array) $data);
foreach ($resultSet as $result) {
/** @var Model $result */
$result->force($force)->delete();
}
return true;
}
/**
* 恢复被软删除的记录.
*
* @param array $where 更新条件
*
* @return bool
*/
public function restore(array $where = []): bool
{
$name = $this->getDeleteTimeField();
if (!$name || false === $this->trigger('BeforeRestore')) {
return false;
}
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;
}
/**
* 获取软删除字段.
*
* @param bool $read 是否查询操作 写操作的时候会自动去掉表别名
*
* @return string|false
*/
public function getDeleteTimeField(bool $read = false): bool|string
{
$field = property_exists($this, 'deleteTime') && isset($this->deleteTime) ? $this->deleteTime : 'delete_time';
if (false === $field) {
return false;
}
if (!str_contains($field, '.')) {
$field = '__TABLE__.' . $field;
}
if (!$read && str_contains($field, '.')) {
$array = explode('.', $field);
$field = array_pop($array);
}
return $field;
}
/**
* 查询的时候默认排除软删除数据.
*
* @param Query $query
*
* @return void
*/
protected function withNoTrashed(Query $query): void
{
$field = $this->getDeleteTimeField(true);
if ($field) {
$condition = is_null($this->defaultSoftDelete) ? ['null', ''] : ['=', $this->defaultSoftDelete];
$query->useSoftDelete($field, $condition);
}
}
}
@@ -0,0 +1,237 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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;
use DateTimeInterface;
use Stringable;
/**
* 自动时间戳.
*/
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;
/**
* 是否需要自动写入时间字段.
*
* @param bool|string $auto
*
* @return $this
*/
public function isAutoWriteTimestamp($auto)
{
$this->autoWriteTimestamp = $this->checkTimeFieldType($auto);
return $this;
}
/**
* 检测时间字段的实际类型.
*
* @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;
}
/**
* 设置时间字段名称.
*
* @param string $createTime
* @param string $updateTime
*
* @return $this
*/
public function setTimeField(string $createTime, string $updateTime)
{
$this->createTime = $createTime;
$this->updateTime = $updateTime;
return $this;
}
/**
* 获取自动写入时间字段.
*
* @return bool|string
*/
public function getAutoWriteTimestamp()
{
return $this->autoWriteTimestamp;
}
/**
* 设置时间字段格式化.
*
* @param string|false $format
*
* @return $this
*/
public function setDateFormat($format)
{
$this->dateFormat = $format;
return $this;
}
/**
* 获取自动写入时间字段.
*
* @return string|false
*/
public function getDateFormat()
{
return $this->dateFormat;
}
/**
* 自动写入时间戳.
*
* @return mixed
*/
protected function autoWriteTimestamp()
{
// 检测时间字段类型
$type = $this->checkTimeFieldType($this->autoWriteTimestamp);
return is_string($type) ? $this->getTimeTypeValue($type) : time();
}
/**
* 获取指定类型的时间字段值
*
* @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 (str_contains($type, '\\')) {
// 对象数据写入
$obj = new $type();
if ($obj instanceof Stringable) {
// 对象数据写入
$value = $obj->__toString();
}
}
}
return $value;
}
/**
* 时间日期字段格式化处理.
*
* @param mixed $format 日期格式
* @param mixed $time 时间日期表达式
* @param bool $timestamp 时间表达式是否为时间戳
*
* @return mixed
*/
protected function formatDateTime($format, $time = 'now', bool $timestamp = false)
{
if (empty($time)) {
return $time;
}
if (false === $format) {
return $time;
} elseif (str_contains($format, '\\')) {
return new $format($time);
}
if ($time instanceof DateTimeInterface) {
$dateTime = $time;
} elseif ($timestamp) {
$dateTime = new DateTime();
$dateTime->setTimestamp(is_numeric($time) ? (int) $time : strtotime($time));
} else {
$dateTime = new DateTime($time);
}
return $dateTime->format($format);
}
/**
* 获取时间字段值
*
* @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;
}
}
@@ -0,0 +1,101 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace think\model\concern;
use think\db\BaseQuery as Query;
use think\db\exception\DbException as Exception;
use think\Model;
/**
* 虚拟模型.
*/
trait Virtual
{
/**
* 获取当前模型的数据库查询对象
*
* @param array $scope 设置不使用的全局查询范围
*
* @return Query
*/
public function db($scope = []): Query
{
throw new Exception('virtual model not support db query');
}
/**
* 获取字段类型信息.
*
* @param string $field 字段名
*
* @return string|null
*/
public function getFieldType(string $field)
{
}
/**
* 保存当前数据对象
*
* @param array|object $data 数据
* @param string $sequence 自增序列名
*
* @return bool
*/
public function save(array|object $data = [], string $sequence = null): bool
{
if ($data instanceof Model) {
$data = $data->getData();
} elseif (is_object($data)) {
$data = get_object_vars($data);
}
// 数据对象赋值
$this->setAttrs($data);
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}
// 写入回调
$this->trigger('AfterWrite');
$this->exists(true);
return true;
}
/**
* 删除当前的记录.
*
* @return bool
*/
public function delete(): bool
{
if (!$this->isExists() || $this->isEmpty() || false === $this->trigger('BeforeDelete')) {
return false;
}
// 关联删除
if (!empty($this->relationWrite)) {
$this->autoRelationDelete();
}
$this->trigger('AfterDelete');
$this->exists(false);
return true;
}
}
@@ -0,0 +1,346 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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\Model;
/**
* BelongsTo关联类.
*/
class BelongsTo extends OneToOne
{
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 关联主键
* @param string $relation 关联名
*/
public function __construct(Model $parent, string $model, string $foreignKey, string $localKey, string $relation = null)
{
$this->parent = $parent;
$this->model = $model;
$this->foreignKey = $foreignKey;
$this->localKey = $localKey;
$this->query = (new $model())->db();
$this->relation = $relation;
if (get_class($parent) == $model) {
$this->selfRelation = true;
}
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Model
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
if ($closure) {
$closure($this->query);
}
$foreignKey = $this->foreignKey;
$relationModel = $this->query
->removeWhereField($this->localKey)
->where($this->localKey, $this->parent->$foreignKey)
->relation($subRelation)
->find();
if ($relationModel) {
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($this->parent, $relationModel);
}
$relationModel->setParent(clone $this->parent);
} else {
$default = $this->query->getOptions('default_model');
$relationModel = $this->getDefaultModel($default);
}
return $relationModel;
}
/**
* 创建关联统计子查询.
*
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 聚合字段别名
*
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', &$name = ''): string
{
if ($closure) {
$closure($this->query, $name);
}
return $this->query
->whereExp($this->localKey, '=' . $this->parent->getTable() . '.' . $this->foreignKey)
->fetchSql()
->$aggregate($field);
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return int
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$foreignKey = $this->foreignKey;
if (!isset($result->$foreignKey)) {
return 0;
}
if ($closure) {
$closure($this->query, $name);
}
return $this->query
->where($this->localKey, '=', $result->$foreignKey)
->$aggregate($field);
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query
{
$table = $this->query->getTable();
$model = 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);
});
});
}
/**
* 根据关联条件查询当前模型.
*
* @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();
return $query->alias($model)
->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);
}
/**
* 预载入关联查询(数据集).
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (isset($result->$foreignKey)) {
$range[] = $result->$foreignKey;
}
}
if (!empty($range)) {
$this->query->removeWhereField($localKey);
$default = $this->query->getOptions('default_model');
$defaultModel = $this->getDefaultModel($default);
$data = $this->eagerlyWhere([
[$localKey, 'in', $range],
], $localKey, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
// 关联模型
if (!isset($data[$result->$foreignKey])) {
$relationModel = $defaultModel;
} else {
$relationModel = $data[$result->$foreignKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
// 设置关联属性
$result->setRelation($relation, $relationModel);
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($result, $relationModel);
$result->hidden([$relation], true);
}
}
}
}
/**
* 预载入关联查询(数据).
*
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$this->query->removeWhereField($localKey);
$data = $this->eagerlyWhere([
[$localKey, '=', $result->$foreignKey],
], $localKey, $subRelation, $closure, $cache);
// 关联模型
if (!isset($data[$result->$foreignKey])) {
$default = $this->query->getOptions('default_model');
$relationModel = $this->getDefaultModel($default);
} else {
$relationModel = $data[$result->$foreignKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
// 设置关联属性
$result->setRelation($relation, $relationModel);
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($result, $relationModel);
$result->hidden([$relation], true);
}
}
/**
* 添加关联数据.
*
* @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);
}
/**
* 注销关联数据.
*
* @return Model
*/
public function dissociate(): Model
{
$foreignKey = $this->foreignKey;
$this->parent->setAttr($foreignKey, null);
$this->parent->save();
return $this->parent->setRelation($this->relation, null);
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
if (empty($this->baseQuery)) {
if (isset($this->parent->{$this->foreignKey})) {
// 关联查询带入关联条件
$this->query->where($this->localKey, '=', $this->parent->{$this->foreignKey});
}
$this->baseQuery = true;
}
}
}
@@ -0,0 +1,658 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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\Model;
use think\model\Pivot;
use think\model\Relation;
/**
* 多对多关联类.
*/
class BelongsToMany extends Relation
{
/**
* 中间表表名.
*
* @var string
*/
protected $middle;
/**
* 中间表模型名称.
*
* @var string
*/
protected $pivotName;
/**
* 中间表模型对象
*
* @var Pivot
*/
protected $pivot;
/**
* 中间表数据名称.
*
* @var string
*/
protected $pivotDataName = 'pivot';
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $middle 中间表/模型名
* @param string $foreignKey 关联模型外键
* @param string $localKey 当前模型关联键
*/
public function __construct(Model $parent, string $model, string $middle, string $foreignKey, string $localKey)
{
$this->parent = $parent;
$this->model = $model;
$this->foreignKey = $foreignKey;
$this->localKey = $localKey;
if (str_contains($middle, '\\')) {
$this->pivotName = $middle;
$this->middle = class_basename($middle);
} else {
$this->middle = $middle;
}
$this->query = (new $model())->db();
$this->pivot = $this->newPivot();
}
/**
* 设置中间表模型.
*
* @param $pivot
*
* @return $this
*/
public function pivot(string $pivot)
{
$this->pivotName = $pivot;
return $this;
}
/**
* 设置中间表数据名称.
*
* @param string $name
*
* @return $this
*/
public function name(string $name)
{
$this->pivotDataName = $name;
return $this;
}
/**
* 实例化中间表模型.
*
* @param $data
*
* @throws Exception
*
* @return Pivot
*/
protected function newPivot(array $data = []): Pivot
{
$class = $this->pivotName ?: Pivot::class;
$pivot = new $class($data, $this->parent, $this->middle);
if ($pivot instanceof Pivot) {
return $pivot;
} else {
throw new Exception('pivot model must extends: \think\model\Pivot');
}
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Collection
*/
public function getRelation(array $subRelation = [], Closure $closure = null): Collection
{
if ($closure) {
$closure($this->query);
}
return $this->relation($subRelation)
->select()
->setParent(clone $this->parent);
}
/**
* 组装Pivot模型.
*
* @param Model $result 模型对象
*
* @return array
*/
protected function matchPivot(Model $result): array
{
$pivot = [];
foreach ($result->getData() as $key => $val) {
if (str_contains($key, '__')) {
[$name, $attr] = explode('__', $key, 2);
if ('pivot' == $name) {
$pivot[$attr] = $val;
unset($result->$key);
}
}
}
$pivotData = $this->pivot->newInstance($pivot, [
[$this->localKey, '=', $this->parent->getKey(), null],
[$this->foreignKey, '=', $result->getKey(), null],
]);
$result->setRelation($this->pivotDataName, $pivotData);
return $pivot;
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $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;
}
/**
* 根据关联条件查询当前模型.
*
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @throws Exception
*
* @return Query
*/
public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: hasWhere');
}
/**
* 设置中间表的查询条件.
*
* @param string $field
* @param string $op
* @param mixed $condition
*
* @return $this
*/
public function wherePivot($field, $op = null, $condition = null)
{
$this->query->where('pivot.' . $field, $op, $condition);
return $this;
}
/**
* 预载入关联查询(数据集).
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$pk = $resultSet[0]->getPk();
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (isset($result->$pk)) {
$range[] = $result->$pk;
}
}
if (!empty($range)) {
// 查询关联数据
$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));
}
}
}
/**
* 预载入关联查询(单个数据).
*
* @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));
}
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return int
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$pk = $result->getPk();
if (!isset($result->$pk)) {
return 0;
}
$pk = $result->$pk;
if ($closure) {
$closure($this->query, $name);
}
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
['pivot.' . $this->localKey, '=', $pk],
])->$aggregate($field);
}
/**
* 获取关联统计子查询.
*
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
$closure($this->query, $name);
}
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
[
'pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->db(false)->getTable() . '.' . $this->parent->getPk()),
],
])->fetchSql()->$aggregate($field);
}
/**
* 多对多 关联模型预查询.
*
* @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->query);
}
$withLimit = $this->query->getOptions('limit');
if ($withLimit) {
$this->query->removeOption('limit');
}
// 预载入关联查询 支持嵌套预载入
$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 = $this->matchPivot($set);
$key = $pivot[$this->localKey];
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
continue;
}
$data[$key][] = $set;
}
return $data;
}
/**
* BELONGS TO MANY 关联查询.
*
* @param string $foreignKey 关联模型关联键
* @param string $localKey 当前模型关联键
* @param array $condition 关联查询条件
*
* @return Query
*/
protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query
{
// 关联查询封装
if (empty($this->baseQuery)) {
$tableName = $this->query->getTable();
$table = $this->pivot->db()->getTable();
$fields = $this->getQueryFields($tableName);
$this->query
->field($fields)
->tableField(true, $table, 'pivot', 'pivot__')
->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $this->query->getPk())
->where($condition);
}
return $this->query;
}
/**
* 保存(新增)当前关联数据对象
*
* @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
* @param array $pivot 中间表额外数据
*
* @return array|Pivot
*/
public function save($data, array $pivot = [])
{
// 保存关联表/中间表数据
return $this->attach($data, $pivot);
}
/**
* 批量保存当前关联数据对象
*
* @param iterable $dataSet 数据集
* @param array $pivot 中间表额外数据
* @param bool $samePivot 额外数据是否相同
*
* @return array|false
*/
public function saveAll(iterable $dataSet, array $pivot = [], bool $samePivot = false)
{
$result = [];
foreach ($dataSet as $key => $data) {
if (!$samePivot) {
$pivotData = $pivot[$key] ?? [];
} else {
$pivotData = $pivot;
}
$result[] = $this->attach($data, $pivotData);
}
return empty($result) ? false : $result;
}
/**
* 附加关联的一个中间表数据.
*
* @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
* @param array $pivot 中间表额外数据
*
* @throws Exception
*
* @return array|Pivot
*/
public function attach($data, array $pivot = [])
{
if (is_array($data)) {
if (key($data) === 0) {
$id = $data;
} else {
// 保存关联表数据
$model = new $this->model();
$id = $model->insertGetId($data);
}
} elseif (is_numeric($data) || is_string($data)) {
// 根据关联表主键直接写入中间表
$id = $data;
} elseif ($data instanceof Model) {
// 根据关联表主键直接写入中间表
$id = $data->getKey();
}
if (!empty($id)) {
// 保存中间表数据
$pivot[$this->localKey] = $this->parent->getKey();
$ids = (array) $id;
foreach ($ids as $id) {
$pivot[$this->foreignKey] = $id;
$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');
}
}
/**
* 判断是否存在关联数据.
*
* @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键
*
* @return Pivot|false
*/
public function attached($data)
{
if ($data instanceof Model) {
$id = $data->getKey();
} else {
$id = $data;
}
$pivot = $this->pivot
->where($this->localKey, $this->parent->getKey())
->where($this->foreignKey, $id)
->find();
return $pivot ?: false;
}
/**
* 解除关联的一个中间表数据.
*
* @param int|array $data 数据 可以使用关联对象的主键
* @param bool $relationDel 是否同时删除关联表数据
*
* @return int
*/
public function detach($data = null, bool $relationDel = false): int
{
if (is_array($data)) {
$id = $data;
} elseif (is_numeric($data) || is_string($data)) {
// 根据关联表主键直接写入中间表
$id = $data;
} elseif ($data instanceof Model) {
// 根据关联表主键直接写入中间表
$id = $data->getKey();
}
// 删除中间表数据
$pivot = [];
$pivot[] = [$this->localKey, '=', $this->parent->getKey()];
if (isset($id)) {
$pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id];
}
$result = $this->pivot->where($pivot)->delete();
// 删除关联表数据
if (isset($id) && $relationDel) {
$model = $this->model;
$model::destroy($id);
}
return $result;
}
/**
* 数据同步.
*
* @param array $ids
* @param bool $detaching
*
* @return array
*/
public function sync(array $ids, bool $detaching = true): array
{
$changes = [
'attached' => [],
'detached' => [],
'updated' => [],
];
$current = $this->pivot
->where($this->localKey, $this->parent->getKey())
->column($this->foreignKey);
$records = [];
foreach ($ids as $key => $value) {
if (!is_array($value)) {
$records[$value] = [];
} else {
$records[$key] = $value;
}
}
$detach = array_diff($current, array_keys($records));
if ($detaching && count($detach) > 0) {
$this->detach($detach);
$changes['detached'] = $detach;
}
foreach ($records as $id => $attributes) {
if (!in_array($id, $current)) {
$this->attach($id, $attributes);
$changes['attached'][] = $id;
} elseif (count($attributes) > 0 && $this->attach($id, $attributes)) {
$changes['updated'][] = $id;
}
}
return $changes;
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
if (empty($this->baseQuery)) {
$foreignKey = $this->foreignKey;
$localKey = $this->localKey;
$this->query->filter(function ($result, $options) {
$this->matchPivot($result);
});
// 关联查询
if (null === $this->parent->getKey()) {
$condition = ['pivot.' . $localKey, 'exp', new Raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk())];
} else {
$condition = ['pivot.' . $localKey, '=', $this->parent->getKey()];
}
$this->belongsToManyQuery($foreignKey, $localKey, [$condition]);
$this->baseQuery = true;
}
}
}
@@ -0,0 +1,378 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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\Model;
use think\model\Relation;
/**
* 一对多关联类.
*/
class HasMany extends Relation
{
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 当前模型主键
*/
public function __construct(Model $parent, string $model, string $foreignKey, string $localKey)
{
$this->parent = $parent;
$this->model = $model;
$this->foreignKey = $foreignKey;
$this->localKey = $localKey;
$this->query = (new $model())->db();
if (get_class($parent) == $model) {
$this->selfRelation = true;
}
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Collection
*/
public function getRelation(array $subRelation = [], Closure $closure = null): Collection
{
if ($closure) {
$closure($this->query);
}
return $this->query
->where($this->foreignKey, $this->parent->{$this->localKey})
->relation($subRelation)
->select()
->setParent(clone $this->parent);
}
/**
* 预载入关联查询.
*
* @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));
}
}
}
/**
* 预载入关联查询.
*
* @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));
}
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return int
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$localKey = $this->localKey;
if (!isset($result->$localKey)) {
return 0;
}
if ($closure) {
$closure($this->query, $name);
}
return $this->query
->where($this->foreignKey, '=', $result->$localKey)
->$aggregate($field);
}
/**
* 创建关联统计子查询.
*
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
$closure($this->query, $name);
}
return $this->query->alias($aggregate . '_table')
->whereExp($aggregate . '_table.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey)
->fetchSql()
->$aggregate($field);
}
/**
* 一对多 关联模型预查询.
*
* @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->query);
}
$withLimit = $this->query->getOptions('limit');
if ($withLimit) {
$this->query->removeOption('limit');
}
$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 ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
continue;
}
$data[$key][] = $set;
}
return $data;
}
/**
* 保存(新增)当前关联数据对象
*
* @param array|Model $data 数据 可以使用数组 关联模型对象
* @param bool $replace 是否自动识别更新和写入
*
* @return Model|false
*/
public function save(array|Model $data, bool $replace = true)
{
$model = $this->make();
return $model->replace($replace)->save($data) ? $model : false;
}
/**
* 创建关联对象实例.
*
* @param array|Model $data
*
* @return Model
*/
public function make(array|Model $data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
}
// 保存关联表数据
$data[$this->foreignKey] = $this->parent->{$this->localKey};
return new $this->model($data);
}
/**
* 批量保存当前关联数据对象
*
* @param iterable $dataSet 数据集
* @param bool $replace 是否自动识别更新和写入
*
* @return array|false
*/
public function saveAll(iterable $dataSet, bool $replace = true)
{
$result = [];
foreach ($dataSet as $key => $data) {
$result[] = $this->save($data, $replace);
}
return empty($result) ? false : $result;
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = 'INNER', Query $query = null): Query
{
$table = $this->query->getTable();
$model = 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);
}
/**
* 根据关联条件查询当前模型.
*
* @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();
return $query->alias($model)
->group($model . '.' . $this->localKey)
->field($fields)
->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType)
->when($softDelete, function ($query) use ($softDelete, $relation) {
$query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
})
->where($where);
}
/**
* 执行基础查询(仅执行一次).
*
* @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;
}
}
}
@@ -0,0 +1,401 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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;
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 关联模型名
* @param string $through 中间模型名
* @param string $foreignKey 关联外键
* @param string $throughKey 中间关联外键
* @param string $localKey 当前模型主键
* @param string $throughPk 中间模型主键
*/
public function __construct(Model $parent, string $model, string $through, string $foreignKey, string $throughKey, string $localKey, string $throughPk)
{
$this->parent = $parent;
$this->model = $model;
$this->through = (new $through())->db();
$this->foreignKey = $foreignKey;
$this->throughKey = $throughKey;
$this->localKey = $localKey;
$this->throughPk = $throughPk;
$this->query = (new $model())->db();
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Collection
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
if ($closure) {
$closure($this->query);
}
$this->baseQuery();
return $this->query->relation($subRelation)
->select()
->setParent(clone $this->parent);
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query
{
$model = Str::snake(class_basename($this->parent));
$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);
}
/**
* 根据关联条件查询当前模型.
*
* @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();
return $query->alias($model)
->join($throughTable, $throughTable . '.' . $this->foreignKey . '=' . $model . '.' . $this->localKey)
->join($modelTable, $modelTable . '.' . $throughKey . '=' . $throughTable . '.' . $this->throughPk, $joinType)
->when($softDelete, function ($query) use ($softDelete, $modelTable) {
$query->where($modelTable . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null);
})
->group($modelTable . '.' . $this->throughKey)
->where($where)
->field($fields);
}
/**
* 预载入关联查询(数据集).
*
* @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));
}
}
}
/**
* 预载入关联查询(数据).
*
* @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));
}
/**
* 关联模型预查询.
*
* @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->query);
}
$throughKey = $this->throughKey;
if ($this->baseQuery) {
$throughKey = Str::snake(class_basename($this->model)) . '.' . $this->throughKey;
}
$withLimit = $this->query->getOptions('limit');
if ($withLimit) {
$this->query->removeOption('limit');
}
$list = $this->query
->where($throughKey, 'in', $keys)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select();
// 组装模型数据
$data = [];
$keys = $throughList->column($this->foreignKey, $this->throughPk);
foreach ($list as $set) {
$key = $keys[$set->{$this->throughKey}];
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
continue;
}
$data[$key][] = $set;
}
return $data;
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return mixed
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$localKey = $this->localKey;
if (!isset($result->$localKey)) {
return 0;
}
if ($closure) {
$closure($this->query, $name);
}
$alias = Str::snake(class_basename($this->model));
$throughTable = $this->through->getTable();
$pk = $this->throughPk;
$throughKey = $this->throughKey;
$modelTable = $this->parent->getTable();
if (!str_contains($field, '.')) {
$field = $alias . '.' . $field;
}
return $this->query
->alias($alias)
->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
->where($throughTable . '.' . $this->foreignKey, $result->$localKey)
->$aggregate($field);
}
/**
* 创建关联统计子查询.
*
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
$closure($this->query, $name);
}
$alias = Str::snake(class_basename($this->model));
$throughTable = $this->through->getTable();
$pk = $this->throughPk;
$throughKey = $this->throughKey;
$modelTable = $this->parent->getTable();
if (!str_contains($field, '.')) {
$field = $alias . '.' . $field;
}
return $this->query
->alias($alias)
->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
->whereExp($throughTable . '.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey)
->fetchSql()
->$aggregate($field);
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
if (empty($this->baseQuery) && $this->parent->getData()) {
$alias = Str::snake(class_basename($this->model));
$throughTable = $this->through->getTable();
$pk = $this->throughPk;
$throughKey = $this->throughKey;
$modelTable = $this->parent->getTable();
$fields = $this->getQueryFields($alias);
$this->query
->field($fields)
->alias($alias)
->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey});
$this->baseQuery = true;
}
}
}
@@ -0,0 +1,316 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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\Model;
/**
* HasOne 关联类.
*/
class HasOne extends OneToOne
{
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $foreignKey 关联外键
* @param string $localKey 当前模型主键
*/
public function __construct(Model $parent, string $model, string $foreignKey, string $localKey)
{
$this->parent = $parent;
$this->model = $model;
$this->foreignKey = $foreignKey;
$this->localKey = $localKey;
$this->query = (new $model())->db();
if (get_class($parent) == $model) {
$this->selfRelation = true;
}
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Model
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
$localKey = $this->localKey;
if ($closure) {
$closure($this->query);
}
// 判断关联类型执行查询
$relationModel = $this->query
->removeWhereField($this->foreignKey)
->where($this->foreignKey, $this->parent->$localKey)
->relation($subRelation)
->find();
if ($relationModel) {
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($this->parent, $relationModel);
}
$relationModel->setParent(clone $this->parent);
} else {
$default = $this->query->getOptions('default_model');
$relationModel = $this->getDefaultModel($default);
}
return $relationModel;
}
/**
* 创建关联统计子查询.
*
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
$closure($this->query, $name);
}
return $this->query
->whereExp($this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey)
->fetchSql()
->$aggregate($field);
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return int
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$localKey = $this->localKey;
if (!isset($result->$localKey)) {
return 0;
}
if ($closure) {
$closure($this->query, $name);
}
return $this->query
->where($this->foreignKey, '=', $result->$localKey)
->$aggregate($field);
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query
{
$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);
});
});
}
/**
* 根据关联条件查询当前模型.
*
* @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();
return $query->alias($model)
->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);
}
/**
* 预载入关联查询(数据集).
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (isset($result->$localKey)) {
$range[] = $result->$localKey;
}
}
if (!empty($range)) {
$this->query->removeWhereField($foreignKey);
$default = $this->query->getOptions('default_model');
$defaultModel = $this->getDefaultModel($default);
$data = $this->eagerlyWhere([
[$foreignKey, 'in', $range],
], $foreignKey, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
// 关联模型
if (!isset($data[$result->$localKey])) {
$relationModel = $defaultModel;
} else {
$relationModel = $data[$result->$localKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
// 设置关联属性
$result->setRelation($relation, $relationModel);
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($result, $relationModel);
$result->hidden([$relation], true);
}
}
}
}
/**
* 预载入关联查询(数据).
*
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$this->query->removeWhereField($foreignKey);
$data = $this->eagerlyWhere([
[$foreignKey, '=', $result->$localKey],
], $foreignKey, $subRelation, $closure, $cache);
// 关联模型
if (!isset($data[$result->$localKey])) {
$default = $this->query->getOptions('default_model');
$relationModel = $this->getDefaultModel($default);
} else {
$relationModel = $data[$result->$localKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
// 设置关联属性
$result->setRelation($relation, $relationModel);
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($result, $relationModel);
$result->hidden([$relation], true);
}
}
/**
* 执行基础查询(仅执行一次).
*
* @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;
}
}
}
@@ -0,0 +1,171 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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\Model;
/**
* 远程一对一关联类.
*/
class HasOneThrough extends HasManyThrough
{
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Model
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
if ($closure) {
$closure($this->query);
}
$this->baseQuery();
$relationModel = $this->query->relation($subRelation)->find();
if ($relationModel) {
$relationModel->setParent(clone $this->parent);
} else {
$default = $this->query->getOptions('default_model');
$relationModel = $this->getDefaultModel($default);
}
return $relationModel;
}
/**
* 预载入关联查询(数据集).
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (isset($result->$localKey)) {
$range[] = $result->$localKey;
}
}
if (!empty($range)) {
$this->query->removeWhereField($foreignKey);
$default = $this->query->getOptions('default_model');
$defaultModel = $this->getDefaultModel($default);
$data = $this->eagerlyWhere([
[$this->foreignKey, 'in', $range],
], $foreignKey, $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
// 关联模型
if (!isset($data[$result->$localKey])) {
$relationModel = $defaultModel;
} else {
$relationModel = $data[$result->$localKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
// 设置关联属性
$result->setRelation($relation, $relationModel);
}
}
}
/**
* 预载入关联查询(数据).
*
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$localKey = $this->localKey;
$foreignKey = $this->foreignKey;
$this->query->removeWhereField($foreignKey);
$data = $this->eagerlyWhere([
[$foreignKey, '=', $result->$localKey],
], $foreignKey, $subRelation, $closure, $cache);
// 关联模型
if (!isset($data[$result->$localKey])) {
$default = $this->query->getOptions('default_model');
$relationModel = $this->getDefaultModel($default);
} else {
$relationModel = $data[$result->$localKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
$result->setRelation($relation, $relationModel);
}
/**
* 关联模型预查询.
*
* @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->query);
}
$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;
}
}
@@ -0,0 +1,398 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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\Model;
use think\model\Relation;
/**
* 多态一对多关联.
*/
class MorphMany extends Relation
{
/**
* 多态关联外键.
*
* @var string
*/
protected $morphKey;
/**
* 多态字段名.
*
* @var string
*/
protected $morphType;
/**
* 多态类型.
*
* @var string
*/
protected $type;
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $morphKey 关联外键
* @param string $morphType 多态字段名
* @param string $type 多态类型
*/
public function __construct(Model $parent, string $model, string $morphKey, string $morphType, string $type)
{
$this->parent = $parent;
$this->model = $model;
$this->type = $type;
$this->morphKey = $morphKey;
$this->morphType = $morphType;
$this->query = (new $model())->db();
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Collection
*/
public function getRelation(array $subRelation = [], Closure $closure = null): Collection
{
if ($closure) {
$closure($this->query);
}
$this->baseQuery();
return $this->query->relation($subRelation)
->select()
->setParent(clone $this->parent);
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: has');
}
/**
* 根据关联条件查询当前模型.
*
* @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');
}
/**
* 预载入关联查询.
*
* @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));
}
}
}
/**
* 预载入关联查询.
*
* @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));
}
}
/**
* 关联统计
*
* @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->query, $name);
}
return $this->query
->where([
[$this->morphKey, '=', $result->$pk],
[$this->morphType, '=', $this->type],
])
->$aggregate($field);
}
/**
* 获取关联统计子查询.
*
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
$closure($this->query, $name);
}
return $this->query
->whereExp($this->morphKey, '=' . $this->parent->getTable() . '.' . $this->parent->getPk())
->where($this->morphType, '=', $this->type)
->fetchSql()
->$aggregate($field);
}
/**
* 多态一对多 关联模型预查询.
*
* @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->query);
}
$withLimit = $this->query->getOptions('limit');
if ($withLimit) {
$this->query->removeOption('limit');
}
$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 ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
continue;
}
$data[$key][] = $set;
}
return $data;
}
/**
* 保存(新增)当前关联数据对象
*
* @param array|Model $data 数据 可以使用数组 关联模型对象
* @param bool $replace 是否自动识别更新和写入
*
* @return Model|false
*/
public function save(array|Model $data, bool $replace = true)
{
$model = $this->make();
return $model->replace($replace)->save($data) ? $model : false;
}
/**
* 创建关联对象实例.
*
* @param array|Model $data
*
* @return Model
*/
public function make($data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
}
// 保存关联表数据
$pk = $this->parent->getPk();
$data[$this->morphKey] = $this->parent->$pk;
$data[$this->morphType] = $this->type;
return new $this->model($data);
}
/**
* 批量保存当前关联数据对象
*
* @param iterable $dataSet 数据集
* @param bool $replace 是否自动识别更新和写入
*
* @return array|false
*/
public function saveAll(iterable $dataSet, bool $replace = true)
{
$result = [];
foreach ($dataSet as $key => $data) {
$result[] = $this->save($data, $replace);
}
return empty($result) ? false : $result;
}
/**
* 获取多态关联外键.
*
* @return string
*/
public function getMorphKey()
{
return $this->morphKey;
}
/**
* 获取多态字段名.
*
* @return string
*/
public function getMorphType()
{
return $this->morphType;
}
/**
* 获取多态类型.
*
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
if (empty($this->baseQuery) && $this->parent->getData()) {
$pk = $this->parent->getPk();
$this->query->where([
[$this->morphKey, '=', $this->parent->$pk],
[$this->morphType, '=', $this->type],
]);
$this->baseQuery = true;
}
}
}
@@ -0,0 +1,371 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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\Model;
use think\model\Relation;
/**
* 多态一对一关联类.
*/
class MorphOne extends Relation
{
/**
* 多态关联外键.
*
* @var string
*/
protected $morphKey;
/**
* 多态字段.
*
* @var string
*/
protected $morphType;
/**
* 多态类型.
*
* @var string
*/
protected $type;
/**
* 绑定的关联属性.
*
* @var array
*/
protected $bindAttr = [];
/**
* 构造函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $morphKey 关联外键
* @param string $morphType 多态字段名
* @param string $type 多态类型
*/
public function __construct(Model $parent, string $model, string $morphKey, string $morphType, string $type)
{
$this->parent = $parent;
$this->model = $model;
$this->type = $type;
$this->morphKey = $morphKey;
$this->morphType = $morphType;
$this->query = (new $model())->db();
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param Closure $closure 闭包查询条件
*
* @return Model
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
if ($closure) {
$closure($this->query);
}
$this->baseQuery();
$relationModel = $this->query->relation($subRelation)->find();
if ($relationModel) {
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($this->parent, $relationModel);
}
$relationModel->setParent(clone $this->parent);
} else {
$default = $this->query->getOptions('default_model');
$relationModel = $this->getDefaultModel($default);
}
return $relationModel;
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null)
{
return $this->parent;
}
/**
* 根据关联条件查询当前模型.
*
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
{
throw new Exception('relation not support: hasWhere');
}
/**
* 预载入关联查询.
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$morphType = $this->morphType;
$morphKey = $this->morphKey;
$type = $this->type;
$range = [];
foreach ($resultSet as $result) {
$pk = $result->getPk();
// 获取关联外键列表
if (isset($result->$pk)) {
$range[] = $result->$pk;
}
}
if (!empty($range)) {
$data = $this->eagerlyMorphToOne([
[$morphKey, 'in', $range],
[$morphType, '=', $type],
], $subRelation, $closure, $cache);
$default = $this->query->getOptions('default_model');
$defaultModel = $this->getDefaultModel($default);
// 关联数据封装
foreach ($resultSet as $result) {
if (!isset($data[$result->$pk])) {
$relationModel = $defaultModel;
} else {
$relationModel = $data[$result->$pk];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($result, $relationModel);
} else {
// 设置关联属性
$result->setRelation($relation, $relationModel);
}
}
}
}
/**
* 预载入关联查询.
*
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
$pk = $result->getPk();
if (isset($result->$pk)) {
$pk = $result->$pk;
$data = $this->eagerlyMorphToOne([
[$this->morphKey, '=', $pk],
[$this->morphType, '=', $this->type],
], $subRelation, $closure, $cache);
if (isset($data[$pk])) {
$relationModel = $data[$pk];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
} else {
$default = $this->query->getOptions('default_model');
$relationModel = $this->getDefaultModel($default);
}
if (!empty($this->bindAttr)) {
// 绑定关联属性
$this->bindAttr($result, $relationModel);
} else {
// 设置关联属性
$result->setRelation($relation, $relationModel);
}
}
}
/**
* 多态一对一 关联模型预查询.
*
* @param array $where 关联预查询条件
* @param array $subRelation 子关联
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return array
*/
protected function eagerlyMorphToOne(array $where, array $subRelation = [], $closure = null, array $cache = []): array
{
// 预载入关联查询 支持嵌套预载入
if ($closure) {
$this->baseQuery = true;
$closure($this->query);
}
$list = $this->query
->where($where)
->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select();
$morphKey = $this->morphKey;
// 组装模型数据
$data = [];
foreach ($list as $set) {
$data[$set->$morphKey] = $set;
}
return $data;
}
/**
* 保存(新增)当前关联数据对象
*
* @param array|Model $data 数据 可以使用数组 关联模型对象
* @param bool $replace 是否自动识别更新和写入
*
* @return Model|false
*/
public function save(array|Model $data, bool $replace = true)
{
$model = $this->make();
return $model->replace($replace)->save($data) ? $model : false;
}
/**
* 创建关联对象实例.
*
* @param array|Model $data
*
* @return Model
*/
public function make(array|Model $data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
}
// 保存关联表数据
$pk = $this->parent->getPk();
$data[$this->morphKey] = $this->parent->$pk;
$data[$this->morphType] = $this->type;
return new $this->model($data);
}
/**
* 执行基础查询(进执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
if (empty($this->baseQuery) && $this->parent->getData()) {
$pk = $this->parent->getPk();
$this->query->where([
[$this->morphKey, '=', $this->parent->$pk],
[$this->morphType, '=', $this->type],
]);
$this->baseQuery = true;
}
}
/**
* 绑定关联表的属性到父模型属性.
*
* @param array $attr 要绑定的属性列表
*
* @return $this
*/
public function bind(array $attr)
{
$this->bindAttr = $attr;
return $this;
}
/**
* 获取绑定属性.
*
* @return array
*/
public function getBindAttr(): array
{
return $this->bindAttr;
}
/**
* 绑定关联属性到父模型.
*
* @param Model $result 父模型对象
* @param Model $model 关联模型对象
*
* @throws Exception
*
* @return void
*/
protected function bindAttr(Model $result, Model $model = null): void
{
foreach ($this->bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
$value = $result->getOrigin($key);
if (!is_null($value)) {
throw new Exception('bind attr has exists:' . $key);
}
$result->setAttr($key, $model?->$attr);
}
}
}
@@ -0,0 +1,395 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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\db\Query;
use think\helper\Str;
use think\Model;
use think\model\Relation;
/**
* 多态关联类.
*/
class MorphTo extends Relation
{
/**
* 多态关联外键.
*
* @var string
*/
protected $morphKey;
/**
* 多态字段.
*
* @var string
*/
protected $morphType;
/**
* 多态别名.
*
* @var array
*/
protected $alias = [];
/**
* 关联名.
*
* @var string
*/
protected $relation;
protected $queryCaller = [];
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $morphType 多态字段名
* @param string $morphKey 外键名
* @param array $alias 多态别名定义
* @param ?string $relation 关联名
*/
public function __construct(Model $parent, string $morphType, string $morphKey, array $alias = [], string $relation = null)
{
$this->parent = $parent;
$this->morphType = $morphType;
$this->morphKey = $morphKey;
$this->alias = $alias;
$this->relation = $relation;
}
/**
* 获取当前的关联模型类的实例.
*
* @return Model
*/
public function getModel(): Model
{
$morphType = $this->morphType;
$model = $this->parseModel($this->parent->$morphType);
return new $model();
}
/**
* 延迟获取关联数据.
*
* @param array $subRelation 子关联名
* @param ?Closure $closure 闭包查询条件
*
* @return Model
*/
public function getRelation(array $subRelation = [], Closure $closure = null)
{
$morphKey = $this->morphKey;
$morphType = $this->morphType;
// 多态模型
$model = $this->parseModel($this->parent->$morphType);
// 主键数据
$pk = $this->parent->$morphKey;
$relationModel = class_exists($model) ? $this->buildQuery((new $model())->relation($subRelation))->find($pk) : null;
if ($relationModel) {
$relationModel->setParent(clone $this->parent);
}
return $relationModel;
}
/**
* 根据关联条件查询当前模型.
*
* @param string $operator 比较操作符
* @param int $count 个数
* @param string $id 关联表的统计字段
* @param string $joinType JOIN类型
* @param Query $query Query对象
*
* @return Query
*/
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null)
{
return $this->parent;
}
/**
* 根据关联条件查询当前模型.
*
* @param mixed $where 查询条件(数组或者闭包)
* @param mixed $fields 字段
* @param string $joinType JOIN类型
* @param ?Query $query Query对象
*
* @return Query
*/
public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
{
$alias = class_basename($this->parent);
$types = $this->parent->distinct()->column($this->morphType);
$query = $query ?: $this->parent->db();
return $query->alias($alias)
->where(function (Query $query) use ($types, $where, $alias) {
foreach ($types as $type) {
if ($type) {
$query->whereExists(function (Query $query) use ($type, $where, $alias) {
$class = $this->parseModel($type);
/** @var Model $model */
$model = new $class();
$table = $model->getTable();
$query
->table($table)
->where($alias . '.' . $this->morphType, $type)
->whereRaw("`{$alias}`.`{$this->morphKey}`=`{$table}`.`{$model->getPk()}`")
->where($where);
}, 'OR');
}
}
});
}
/**
* 解析模型的完整命名空间.
*
* @param string $model 模型名(或者完整类名)
*
* @return Model
*/
protected function parseModel(string $model): string
{
if (isset($this->alias[$model])) {
$model = $this->alias[$model];
}
if (!str_contains($model, '\\')) {
$path = explode('\\', get_class($this->parent));
array_pop($path);
array_push($path, Str::studly($model));
$model = implode('\\', $path);
}
return $model;
}
/**
* 设置多态别名.
*
* @param array $alias 别名定义
*
* @return $this
*/
public function setAlias(array $alias)
{
$this->alias = $alias;
return $this;
}
/**
* 移除关联查询参数.
*
* @return $this
*/
public function removeOption(string $option = '')
{
return $this;
}
/**
* 预载入关联查询.
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param ?Closure $closure 闭包
* @param array $cache 关联缓存
*
* @throws Exception
*
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$morphKey = $this->morphKey;
$morphType = $this->morphType;
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (!empty($result->$morphKey)) {
$range[$result->$morphType][] = $result->$morphKey;
}
}
if (!empty($range)) {
foreach ($range as $key => $val) {
// 多态类型映射
$model = $this->parseModel($key);
$obj = new $model();
if (!is_null($closure)) {
$obj = $closure($obj);
}
$pk = $obj->getPk();
$list = $obj->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->select($val);
$data = [];
foreach ($list as $k => $vo) {
$data[$vo->$pk] = $vo;
}
foreach ($resultSet as $result) {
if ($key == $result->$morphType) {
// 关联模型
if (!isset($data[$result->$morphKey])) {
$relationModel = null;
} else {
$relationModel = $data[$result->$morphKey];
$relationModel->setParent(clone $result);
$relationModel->exists(true);
}
$result->setRelation($relation, $relationModel);
}
}
}
}
}
/**
* 预载入关联查询.
*
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param ?Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
{
// 多态类型映射
$model = $this->parseModel($result->{$this->morphType});
$this->eagerlyMorphToOne($model, $relation, $result, $subRelation, $cache);
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param ?Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
*
* @return int
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*')
{
}
/**
* 多态MorphTo 关联模型预查询.
*
* @param string $model 关联模型对象
* @param string $relation 关联名
* @param Model $result
* @param array $subRelation 子关联
* @param array $cache 关联缓存
*
* @return void
*/
protected function eagerlyMorphToOne(string $model, string $relation, Model $result, array $subRelation = [], array $cache = []): void
{
// 预载入关联查询 支持嵌套预载入
$pk = $this->parent->{$this->morphKey};
$data = null;
if (class_exists($model)) {
$data = (new $model())->with($subRelation)
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
->find($pk);
if ($data) {
$data->setParent(clone $result);
$data->exists(true);
}
}
$result->setRelation($relation, $data ?: null);
}
/**
* 添加关联数据.
*
* @param Model $model 关联模型对象
* @param string $type 多态类型
*
* @return Model
*/
public function associate(Model $model, string $type = ''): Model
{
$morphKey = $this->morphKey;
$morphType = $this->morphType;
$pk = $model->getPk();
$this->parent->setAttr($morphKey, $model->$pk);
$this->parent->setAttr($morphType, $type ?: get_class($model));
$this->parent->save();
return $this->parent->setRelation($this->relation, $model);
}
/**
* 注销关联数据.
*
* @return Model
*/
public function dissociate(): Model
{
$morphKey = $this->morphKey;
$morphType = $this->morphType;
$this->parent->setAttr($morphKey, null);
$this->parent->setAttr($morphType, null);
$this->parent->save();
return $this->parent->setRelation($this->relation, null);
}
protected function buildQuery(Query $query)
{
foreach ($this->queryCaller as $caller) {
call_user_func_array([$query, $caller[0]], $caller[1]);
}
return $query;
}
public function __call($method, $args)
{
$this->queryCaller[] = [$method, $args];
return $this;
}
}
@@ -0,0 +1,498 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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 Exception;
use think\db\BaseQuery as Query;
use think\db\Raw;
use think\Model;
use think\model\Pivot;
/**
* 多态多对多关联.
*/
class MorphToMany extends BelongsToMany
{
/**
* 多态关系的模型名映射别名的数组.
*
* @var array
*/
protected static $morphMap = [];
/**
* 多态字段名.
*
* @var string
*/
protected $morphType;
/**
* 多态模型名.
*
* @var string
*/
protected $morphClass;
/**
* 是否反向关联.
*
* @var bool
*/
protected $inverse;
/**
* 架构函数.
*
* @param Model $parent 上级模型对象
* @param string $model 模型名
* @param string $middle 中间表名/模型名
* @param string $morphKey 关联外键
* @param string $morphType 多态字段名
* @param string $localKey 当前模型关联键
* @param bool $inverse 反向关联
*/
public function __construct(Model $parent, string $model, string $middle, string $morphType, string $morphKey, string $localKey, bool $inverse = false)
{
$this->morphType = $morphType;
$this->inverse = $inverse;
$this->morphClass = $inverse ? $model : get_class($parent);
if (isset(static::$morphMap[$this->morphClass])) {
$this->morphClass = static::$morphMap[$this->morphClass];
}
$foreignKey = $inverse ? $morphKey : $localKey;
$localKey = $inverse ? $localKey : $morphKey;
parent::__construct($parent, $model, $middle, $foreignKey, $localKey);
}
/**
* 预载入关联查询(数据集).
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$pk = $resultSet[0]->getPk();
$range = [];
foreach ($resultSet as $result) {
// 获取关联外键列表
if (isset($result->$pk)) {
$range[] = $result->$pk;
}
}
if (!empty($range)) {
// 查询关联数据
$data = $this->eagerlyManyToMany([
['pivot.' . $this->localKey, 'in', $range],
['pivot.' . $this->morphType, '=', $this->morphClass],
], $subRelation, $closure, $cache);
// 关联数据封装
foreach ($resultSet as $result) {
if (!isset($data[$result->$pk])) {
$data[$result->$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$result->$pk], clone $this->parent));
}
}
}
/**
* 预载入关联查询(单个数据).
*
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
{
$pk = $result->getPk();
if (isset($result->$pk)) {
$pk = $result->$pk;
// 查询管理数据
$data = $this->eagerlyManyToMany([
['pivot.' . $this->localKey, '=', $pk],
['pivot.' . $this->morphType, '=', $this->morphClass],
], $subRelation, $closure, $cache);
// 关联数据封装
if (!isset($data[$pk])) {
$data[$pk] = [];
}
$result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
}
}
/**
* 关联统计
*
* @param Model $result 数据对象
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return int
*/
public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null)
{
$pk = $result->getPk();
if (!isset($result->$pk)) {
return 0;
}
if ($closure) {
$closure($this->query, $name);
}
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
['pivot.' . $this->localKey, '=', $result->$pk],
['pivot.' . $this->morphType, '=', $this->morphClass],
])->$aggregate($field);
}
/**
* 获取关联统计子查询.
*
* @param Closure $closure 闭包
* @param string $aggregate 聚合查询方法
* @param string $field 字段
* @param string $name 统计字段别名
*
* @return string
*/
public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
{
if ($closure) {
$closure($this->query, $name);
}
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
['pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->db(false)->getTable() . '.' . $this->parent->getPk())],
['pivot.' . $this->morphType, '=', $this->morphClass],
])->fetchSql()->$aggregate($field);
}
/**
* BELONGS TO MANY 关联查询.
*
* @param string $foreignKey 关联模型关联键
* @param string $localKey 当前模型关联键
* @param array $condition 关联查询条件
*
* @return Query
*/
protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query
{
// 关联查询封装
$tableName = $this->query->getTable();
$table = $this->pivot->db()->getTable();
$fields = $this->getQueryFields($tableName);
$query = $this->query
->field($fields)
->tableField(true, $table, 'pivot', 'pivot__');
if (empty($this->baseQuery)) {
$relationFk = $this->query->getPk();
$query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk)
->where($condition);
}
return $query;
}
/**
* 多对多 关联模型预查询.
*
* @param array $where 关联预查询条件
* @param array $subRelation 子关联
* @param Closure $closure 闭包
* @param array $cache 关联缓存
*
* @return array
*/
protected function eagerlyManyToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array
{
if ($closure) {
$closure($this->query);
}
$withLimit = $this->query->getOptions('limit');
if ($withLimit) {
$this->query->removeOption('limit');
}
// 预载入关联查询 支持嵌套预载入
$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 (str_contains($key, '__')) {
[$name, $attr] = explode('__', $key, 2);
if ('pivot' == $name) {
$pivot[$attr] = $val;
unset($set->$key);
}
}
}
$key = $pivot[$this->localKey];
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
continue;
}
$set->setRelation($this->pivotDataName, $this->newPivot($pivot));
$data[$key][] = $set;
}
return $data;
}
/**
* 附加关联的一个中间表数据.
*
* @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
* @param array $pivot 中间表额外数据
*
* @return array|Pivot
*/
public function attach($data, array $pivot = [])
{
if (is_array($data)) {
if (key($data) === 0) {
$id = $data;
} else {
// 保存关联表数据
$model = new $this->model();
$id = $model->insertGetId($data);
}
} elseif (is_numeric($data) || is_string($data)) {
// 根据关联表主键直接写入中间表
$id = $data;
} elseif ($data instanceof Model) {
// 根据关联表主键直接写入中间表
$id = $data->getKey();
}
if (!empty($id)) {
// 保存中间表数据
$pivot[$this->localKey] = $this->parent->getKey();
$pivot[$this->morphType] = $this->morphClass;
$ids = (array) $id;
$result = [];
foreach ($ids as $id) {
$pivot[$this->foreignKey] = $id;
$this->pivot->replace()
->exists(false)
->data([])
->save($pivot);
$result[] = $this->newPivot($pivot);
}
if (count($result) == 1) {
// 返回中间表模型对象
$result = $result[0];
}
return $result;
} else {
throw new Exception('miss relation data');
}
}
/**
* 判断是否存在关联数据.
*
* @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键
*
* @return Pivot|false
*/
public function attached($data)
{
if ($data instanceof Model) {
$id = $data->getKey();
} else {
$id = $data;
}
$pivot = $this->pivot
->where($this->localKey, $this->parent->getKey())
->where($this->morphType, $this->morphClass)
->where($this->foreignKey, $id)
->find();
return $pivot ?: false;
}
/**
* 解除关联的一个中间表数据.
*
* @param int|array $data 数据 可以使用关联对象的主键
* @param bool $relationDel 是否同时删除关联表数据
*
* @return int
*/
public function detach($data = null, bool $relationDel = false): int
{
if (is_array($data)) {
$id = $data;
} elseif (is_numeric($data) || is_string($data)) {
// 根据关联表主键直接写入中间表
$id = $data;
} elseif ($data instanceof Model) {
// 根据关联表主键直接写入中间表
$id = $data->getKey();
}
// 删除中间表数据
$pivot = [
[$this->localKey, '=', $this->parent->getKey()],
[$this->morphType, '=', $this->morphClass],
];
if (isset($id)) {
$pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id];
}
$result = $this->pivot->where($pivot)->delete();
// 删除关联表数据
if (isset($id) && $relationDel) {
$model = $this->model;
$model::destroy($id);
}
return $result;
}
/**
* 数据同步.
*
* @param array $ids
* @param bool $detaching
*
* @return array
*/
public function sync(array $ids, bool $detaching = true): array
{
$changes = [
'attached' => [],
'detached' => [],
'updated' => [],
];
$current = $this->pivot
->where($this->localKey, $this->parent->getKey())
->where($this->morphType, $this->morphClass)
->column($this->foreignKey);
$records = [];
foreach ($ids as $key => $value) {
if (!is_array($value)) {
$records[$value] = [];
} else {
$records[$key] = $value;
}
}
$detach = array_diff($current, array_keys($records));
if ($detaching && count($detach) > 0) {
$this->detach($detach);
$changes['detached'] = $detach;
}
foreach ($records as $id => $attributes) {
if (!in_array($id, $current)) {
$this->attach($id, $attributes);
$changes['attached'][] = $id;
} elseif (count($attributes) > 0 && $this->attach($id, $attributes)) {
$changes['updated'][] = $id;
}
}
return $changes;
}
/**
* 执行基础查询(仅执行一次).
*
* @return void
*/
protected function baseQuery(): void
{
if (empty($this->baseQuery)) {
$foreignKey = $this->foreignKey;
$localKey = $this->localKey;
// 关联查询
$this->belongsToManyQuery($foreignKey, $localKey, [
['pivot.' . $localKey, '=', $this->parent->getKey()],
['pivot.' . $this->morphType, '=', $this->morphClass],
]);
$this->baseQuery = true;
}
}
/**
* 设置或获取多态关系的模型名映射别名的数组.
*
* @param array|null $map
* @param bool $merge
*
* @return array
*/
public static function morphMap(array $map = null, $merge = true): array
{
if (is_array($map)) {
static::$morphMap = $merge && static::$morphMap
? $map + static::$morphMap : $map;
}
return static::$morphMap;
}
}
@@ -0,0 +1,365 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2023 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;
/**
* 一对一关联基础类.
*/
abstract class OneToOne extends Relation
{
/**
* JOIN类型.
*
* @var string
*/
protected $joinType = 'INNER';
/**
* 绑定的关联属性.
*
* @var array
*/
protected $bindAttr = [];
/**
* 关联名.
*
* @var string
*/
protected $relation;
/**
* 设置join类型.
*
* @param string $type JOIN类型
*
* @return $this
*/
public function joinType(string $type)
{
$this->joinType = $type;
return $this;
}
/**
* 预载入关联查询(JOIN方式).
*
* @param Query $query 查询对象
* @param string $relation 关联名
* @param mixed $field 关联字段
* @param string $joinType JOIN方式
* @param Closure $closure 闭包条件
* @param bool $first
*
* @return void
*/
public function eagerly(Query $query, string $relation, $field = true, string $joinType = '', Closure $closure = null, bool $first = false): void
{
$name = Str::snake(class_basename($this->parent));
if ($first) {
$table = $query->getTable();
$query->table([$table => $name]);
if ($query->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) {
$foreignKeyExp = $this->foreignKey;
if (!str_contains($foreignKeyExp, '.')) {
$foreignKeyExp = $name . '.' . $this->foreignKey;
}
$joinOn = $foreignKeyExp . '=' . $joinAlias . '.' . $this->localKey;
} else {
$foreignKeyExp = $this->foreignKey;
if (!str_contains($foreignKeyExp, '.')) {
$foreignKeyExp = $joinAlias . '.' . $this->foreignKey;
}
$joinOn = $name . '.' . $this->localKey . '=' . $foreignKeyExp;
}
if ($closure) {
// 执行闭包查询
$closure($query);
// 使用withField指定获取关联的字段
$withField = $this->query->getOptions('field');
if ($withField) {
$field = $withField;
}
}
$query->join([$joinTable => $joinAlias], $joinOn, $joinType)
->tableField($field, $joinTable, $joinAlias, $relation . '__');
}
/**
* 预载入关联查询(数据集).
*
* @param array $resultSet
* @param string $relation
* @param array $subRelation
* @param Closure $closure
*
* @return mixed
*/
abstract protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null);
/**
* 预载入关联查询(数据).
*
* @param Model $result
* @param string $relation
* @param array $subRelation
* @param Closure $closure
*
* @return mixed
*/
abstract protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], Closure $closure = null);
/**
* 预载入关联查询(数据集).
*
* @param array $resultSet 数据集
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @param bool $join 是否为JOIN方式
*
* @return void
*/
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = [], bool $join = false): void
{
if ($join) {
// 模型JOIN关联组装
foreach ($resultSet as $result) {
$this->match($this->model, $relation, $result);
}
} else {
// IN查询
$this->eagerlySet($resultSet, $relation, $subRelation, $closure, $cache);
}
}
/**
* 预载入关联查询(数据).
*
* @param Model $result 数据对象
* @param string $relation 当前关联名
* @param array $subRelation 子关联名
* @param Closure $closure 闭包
* @param array $cache 关联缓存
* @param bool $join 是否为JOIN方式
*
* @return void
*/
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = [], bool $join = false): void
{
if ($join) {
// 模型JOIN关联组装
$this->match($this->model, $relation, $result);
} else {
// IN查询
$this->eagerlyOne($result, $relation, $subRelation, $closure, $cache);
}
}
/**
* 保存(新增)当前关联数据对象
*
* @param array|Model $data 数据 可以使用数组 关联模型对象
* @param bool $replace 是否自动识别更新和写入
*
* @return Model|false
*/
public function save(array|Model $data, bool $replace = true)
{
$model = $this->make();
return $model->replace($replace)->save($data) ? $model : false;
}
/**
* 创建关联对象实例.
*
* @param array|Model $data
*
* @return Model
*/
public function make(array|Model $data = []): Model
{
if ($data instanceof Model) {
$data = $data->getData();
}
// 保存关联表数据
$data[$this->foreignKey] = $this->parent->{$this->localKey};
return new $this->model($data);
}
/**
* 绑定关联表的属性到父模型属性.
*
* @param array $attr 要绑定的属性列表
*
* @return $this
*/
public function bind(array $attr)
{
$this->bindAttr = $attr;
return $this;
}
/**
* 获取绑定属性.
*
* @return array
*/
public function getBindAttr(): array
{
return $this->bindAttr;
}
/**
* 一对一 关联模型预查询拼装.
*
* @param 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 (str_contains($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 (!empty($this->bindAttr)) {
$this->bindAttr($result, $relationModel);
}
} else {
$relationModel = null;
}
$result->setRelation($relation, $relationModel);
}
/**
* 绑定关联属性到父模型.
*
* @param Model $result 父模型对象
* @param Model $model 关联模型对象
*
* @throws Exception
*
* @return void
*/
protected function bindAttr(Model $result, Model $model = null): void
{
foreach ($this->bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
$value = $result->getOrigin($key);
if (!is_null($value)) {
throw new Exception('bind attr has exists:' . $key);
}
$result->setAttr($key, $model?->$attr);
}
}
/**
* 一对一 关联模型预查询(IN方式).
*
* @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->query);
}
$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;
}
}