commit
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace think\model\relation;
|
||||
|
||||
use Closure;
|
||||
use think\db\BaseQuery as Query;
|
||||
use think\helper\Str;
|
||||
use think\model\contract\Modelable as Model;
|
||||
|
||||
/**
|
||||
* BelongsTo关联类.
|
||||
*/
|
||||
class BelongsTo extends OneToOne
|
||||
{
|
||||
/**
|
||||
* 架构函数.
|
||||
*
|
||||
* @param Model $parent 上级模型对象
|
||||
* @param string $model 模型名
|
||||
* @param string $foreignKey 关联外键
|
||||
* @param string $localKey 关联主键
|
||||
* @param string $relation 关联名
|
||||
*/
|
||||
public function __construct(Model $parent, string $model, string $foreignKey, string $localKey, ?string $relation = null)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->model = $model;
|
||||
$this->foreignKey = $foreignKey;
|
||||
$this->localKey = $localKey;
|
||||
$this->query = (new $model())->db();
|
||||
$this->relation = $relation;
|
||||
|
||||
if (get_class($parent) == $model) {
|
||||
$this->selfRelation = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟获取关联数据.
|
||||
*
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包查询条件
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function getRelation(array $subRelation = [], ?Closure $closure = null)
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$foreignKey = $this->foreignKey;
|
||||
$relationModel = $this->query
|
||||
->removeWhereField($this->localKey)
|
||||
->where($this->localKey, $this->parent->$foreignKey)
|
||||
->relation($subRelation)
|
||||
->find();
|
||||
|
||||
if ($relationModel) {
|
||||
if (!empty($this->bindAttr)) {
|
||||
// 绑定关联属性
|
||||
$this->parent->bindRelationAttr($relationModel, $this->bindAttr);
|
||||
}
|
||||
} else {
|
||||
$default = $this->query->getOption('default_model');
|
||||
$relationModel = $this->getDefaultModel($default);
|
||||
}
|
||||
|
||||
return $relationModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建关联统计子查询.
|
||||
*
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 聚合字段别名
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationCountQuery(?Closure $closure = null, string $aggregate = 'count', string $field = 'id', &$name = ''): string
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
$alias = Str::snake(class_basename($this->model));
|
||||
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
|
||||
return $this->query
|
||||
->alias($alias)
|
||||
->whereExp($alias . '.' . $this->localKey, '=' . $this->parent->getTable(true) . '.' . $this->foreignKey)
|
||||
->fetchSql()
|
||||
->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联统计
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null)
|
||||
{
|
||||
$foreignKey = $this->foreignKey;
|
||||
|
||||
if (!isset($result->$foreignKey)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
return $this->query
|
||||
->where($this->localKey, '=', $result->$foreignKey)
|
||||
->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param string $operator 比较操作符
|
||||
* @param int $count 个数
|
||||
* @param string $id 关联表的统计字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', ?Query $query = null) : Query
|
||||
{
|
||||
$table = $this->query->getTable();
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
|
||||
if ($this->isSelfRelation() && $alias == $relation) {
|
||||
$relation .= '_';
|
||||
}
|
||||
|
||||
return $query->alias($alias)
|
||||
->whereExists(function ($query) use ($table, $alias, $relation) {
|
||||
$query->table([$table => $relation])
|
||||
->field($relation . '.' . $this->localKey)
|
||||
->whereColumn($alias . '.' . $this->foreignKey, $relation . '.' . $this->localKey);
|
||||
|
||||
$this->getRelationSoftDelete($query, $relation);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param mixed $where 查询条件(数组或者闭包)
|
||||
* @param mixed $fields 字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function hasWhere($where = [], $fields = null, string $joinType = '', ?Query $query = null, string $logic = '', string $relationAlias = ''): Query
|
||||
{
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$table = $this->query->getTable();
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
$fields = $this->getRelationQueryFields($fields, $alias);
|
||||
$relAlias = $relationAlias ?: $relation;
|
||||
|
||||
if ($this->isSelfRelation() && $alias == $relAlias) {
|
||||
$relAlias .= '_';
|
||||
}
|
||||
|
||||
$query->alias($alias)
|
||||
->via($alias)
|
||||
->field($fields)
|
||||
->join([$table => $relAlias], $alias . '.' . $this->foreignKey . '=' . $relAlias . '.' . $this->localKey, $joinType);
|
||||
|
||||
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据集).
|
||||
*
|
||||
* @param array $resultSet 数据集
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
$foreignKey = $this->foreignKey;
|
||||
|
||||
$range = [];
|
||||
foreach ($resultSet as $result) {
|
||||
// 获取关联外键列表
|
||||
if (isset($result->$foreignKey)) {
|
||||
$range[] = $result->$foreignKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($range)) {
|
||||
$this->query->removeWhereField($localKey);
|
||||
$default = $this->query->getOption('default_model');
|
||||
$defaultModel = $this->getDefaultModel($default);
|
||||
|
||||
$range = array_unique($range);
|
||||
$data = $this->eagerlyWhere([
|
||||
[$localKey, 'in', $range],
|
||||
], $localKey, $subRelation, $closure, $cache, count($range) > 1 ? true : false);
|
||||
|
||||
// 动态绑定参数
|
||||
$bindAttr = $this->query->getOption('bind_attr');
|
||||
if ($bindAttr) {
|
||||
$this->bind($bindAttr);
|
||||
}
|
||||
|
||||
// 关联数据封装
|
||||
foreach ($resultSet as $result) {
|
||||
// 关联模型
|
||||
if (!isset($data[$result->$foreignKey])) {
|
||||
$relationModel = $defaultModel;
|
||||
} else {
|
||||
$relationModel = $data[$result->$foreignKey];
|
||||
}
|
||||
|
||||
// 设置关联属性
|
||||
if (!empty($this->bindAttr) && $relationModel) {
|
||||
$result->bindRelationAttr($relationModel, $this->bindAttr, $relation);
|
||||
} else {
|
||||
$result->setRelation($relation, $relationModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据).
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
$foreignKey = $this->foreignKey;
|
||||
|
||||
$this->query->removeWhereField($localKey);
|
||||
|
||||
$data = $this->eagerlyWhere([
|
||||
[$localKey, '=', $result->$foreignKey],
|
||||
], $localKey, $subRelation, $closure, $cache);
|
||||
|
||||
// 关联模型
|
||||
if (!isset($data[$result->$foreignKey])) {
|
||||
$default = $this->query->getOption('default_model');
|
||||
$relationModel = $this->getDefaultModel($default);
|
||||
} else {
|
||||
$relationModel = $data[$result->$foreignKey];
|
||||
}
|
||||
|
||||
// 动态绑定参数
|
||||
$bindAttr = $this->query->getOption('bind_attr');
|
||||
if ($bindAttr) {
|
||||
$this->bind($bindAttr);
|
||||
}
|
||||
|
||||
// 设置关联属性
|
||||
if (!empty($this->bindAttr) && $relationModel) {
|
||||
$result->bindRelationAttr($relationModel, $this->bindAttr, $relation);
|
||||
} else {
|
||||
$result->setRelation($relation, $relationModel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加关联数据.
|
||||
*
|
||||
* @param Model $model关联模型对象
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function associate(Model $model): Model
|
||||
{
|
||||
$this->parent->set($this->foreignKey, $model->getKey());
|
||||
$this->parent->save();
|
||||
|
||||
return $this->parent->setRelation($this->relation, $model);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销关联数据.
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function dissociate(): Model
|
||||
{
|
||||
$foreignKey = $this->foreignKey;
|
||||
|
||||
$this->parent->set($foreignKey, null);
|
||||
$this->parent->save();
|
||||
|
||||
return $this->parent->setRelation($this->relation, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行基础查询(仅执行一次).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function baseQuery(): void
|
||||
{
|
||||
if (empty($this->baseQuery)) {
|
||||
if (isset($this->parent->{$this->foreignKey})) {
|
||||
// 关联查询带入关联条件
|
||||
$this->query->where($this->localKey, '=', $this->parent->{$this->foreignKey});
|
||||
}
|
||||
|
||||
$this->baseQuery = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,735 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace think\model\relation;
|
||||
|
||||
use Closure;
|
||||
use think\Collection;
|
||||
use think\db\BaseQuery as Query;
|
||||
use think\db\exception\DbException as Exception;
|
||||
use think\db\Raw;
|
||||
use think\helper\Str;
|
||||
use think\model\contract\Modelable as Model;
|
||||
use think\model\Pivot;
|
||||
use think\model\Relation;
|
||||
|
||||
/**
|
||||
* 多对多关联类.
|
||||
*/
|
||||
class BelongsToMany extends Relation
|
||||
{
|
||||
/**
|
||||
* 中间表表名.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $middle;
|
||||
|
||||
/**
|
||||
* 中间表模型名称.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $pivotName;
|
||||
|
||||
/**
|
||||
* 中间表模型对象
|
||||
*
|
||||
* @var Pivot
|
||||
*/
|
||||
protected $pivot;
|
||||
|
||||
/**
|
||||
* 中间表数据名称.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $pivotDataName = 'pivot';
|
||||
|
||||
/**
|
||||
* 绑定的关联属性.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $bindAttr = [];
|
||||
|
||||
/**
|
||||
* 架构函数.
|
||||
*
|
||||
* @param Model $parent 上级模型对象
|
||||
* @param string $model 模型名
|
||||
* @param string $middle 中间表/模型名
|
||||
* @param string $foreignKey 关联模型外键
|
||||
* @param string $localKey 当前模型关联键
|
||||
*/
|
||||
public function __construct(Model $parent, string $model, string $middle, string $foreignKey, string $localKey)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->model = $model;
|
||||
$this->foreignKey = $foreignKey;
|
||||
$this->localKey = $localKey;
|
||||
|
||||
if (str_contains($middle, '\\')) {
|
||||
$this->pivotName = $middle;
|
||||
$this->middle = Str::snake(class_basename($middle));
|
||||
} else {
|
||||
$this->middle = $middle;
|
||||
}
|
||||
|
||||
$this->query = (new $model())->db();
|
||||
$this->pivot = $this->newPivot();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置中间表模型.
|
||||
*
|
||||
* @param $pivot
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function pivot(string $pivot)
|
||||
{
|
||||
$this->pivotName = $pivot;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置中间表数据名称.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function name(string $name)
|
||||
{
|
||||
$this->pivotDataName = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定关联表的属性到父模型属性.
|
||||
*
|
||||
* @param array $attr 要绑定的属性列表
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function bind(array $attr)
|
||||
{
|
||||
$this->bindAttr = $attr;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实例化中间表模型.
|
||||
*
|
||||
* @param $data
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return Pivot
|
||||
*/
|
||||
protected function newPivot(array $data = []): Pivot
|
||||
{
|
||||
$class = $this->pivotName ?: Pivot::class;
|
||||
$pivot = new $class($data, $this->parent, $this->middle);
|
||||
|
||||
if ($pivot instanceof Pivot) {
|
||||
return $pivot;
|
||||
} else {
|
||||
throw new Exception('pivot model must extends: \think\model\Pivot');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟获取关联数据.
|
||||
*
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包查询条件
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRelation(array $subRelation = [], ?Closure $closure = null): Collection
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
return $this->relation($subRelation)->select();
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装Pivot模型.
|
||||
*
|
||||
* @param Model $result 模型对象
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function matchPivot(Model $result): array
|
||||
{
|
||||
$pivot = $result->getRelation('pivot');
|
||||
$bindAttr = $this->query->getOption('bind_attr');
|
||||
if (empty($bindAttr)) {
|
||||
$bindAttr = $this->bindAttr;
|
||||
}
|
||||
|
||||
foreach ($pivot as $attr => $val) {
|
||||
$pos = array_search($attr, $bindAttr);
|
||||
if (false !== $pos) {
|
||||
// 中间表属性绑定
|
||||
$key = !is_numeric($pos) ? $pos : $attr;
|
||||
if (null !== $result->getOrigin($key)) {
|
||||
throw new Exception('bind attr has exists:' . $attr);
|
||||
}
|
||||
$result->set($key, $val);
|
||||
}
|
||||
}
|
||||
|
||||
$result->setRelation($this->pivotDataName, $this->newPivot($pivot));
|
||||
|
||||
return $pivot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型
|
||||
* @access public
|
||||
* @param string $operator 比较操作符
|
||||
* @param integer $count 个数
|
||||
* @param string $id 关联表的统计字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query|null $query Query对象
|
||||
* @return Query
|
||||
*/
|
||||
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', ?Query $query = null): Query
|
||||
{
|
||||
$table = $this->query->getTable();
|
||||
$pivot = $this->pivot->getTable();
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
|
||||
if ('=' === $operator && 0 === $count) {
|
||||
return $query->alias($alias)
|
||||
->whereNotExists(function ($query) use ($pivot, $alias, $relation, $table) {
|
||||
$query->table([$pivot => 'pivot'])
|
||||
->field('pivot.' . $this->foreignKey)
|
||||
->join($table . ' ' . $relation, $relation . '.' . $this->query->getPk() . '= pivot.' . $this->foreignKey)
|
||||
->whereColumn($alias . '.' . $this->parent->getPk(), 'pivot.' . $this->localKey);
|
||||
|
||||
$this->getRelationSoftDelete($query, $relation);
|
||||
});
|
||||
}
|
||||
|
||||
$query->alias($alias)
|
||||
->field($model . '.*')
|
||||
->join([$pivot => 'pivot'], 'pivot.' . $this->localKey . '=' . $alias . '.' . $this->parent->getPk(), $joinType)
|
||||
->join($table . ' ' . $relation, $relation . '.' . $this->query->getPk() . '= pivot.' . $this->foreignKey, $joinType)
|
||||
->group($alias . '.' . $this->parent->getPk())
|
||||
->having('count(' . $id . ')' . $operator . $count);
|
||||
return $this->getRelationSoftDelete($query, $relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型
|
||||
* @access public
|
||||
* @param array|Closure $where 查询条件(数组或者闭包)
|
||||
* @param mixed $fields 字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query|null $query Query对象
|
||||
* @return Query
|
||||
*/
|
||||
public function hasWhere($where = [], $fields = null, string $joinType = '', ?Query $query = null, string $logic = '', string $relationAlias = ''): Query
|
||||
{
|
||||
$table = $this->query->getTable();
|
||||
$pivot = $this->pivot->getTable();
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
$fields = $this->getRelationQueryFields($fields, $alias);
|
||||
$relAlias = $relationAlias ?: $relation;
|
||||
|
||||
$query->alias($alias)
|
||||
->join([$pivot => 'pivot'], 'pivot.' . $this->localKey . '=' . $alias . '.' . $this->parent->getPk(), $joinType)
|
||||
->join([$table => $relAlias], $relAlias . '.' . $this->query->getPk() . '= pivot.' . $this->foreignKey, $joinType)
|
||||
->group($alias . '.' . $this->parent->getPk())
|
||||
->field($fields);
|
||||
|
||||
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置中间表的查询条件.
|
||||
*
|
||||
* @param string $field
|
||||
* @param string $op
|
||||
* @param mixed $condition
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function wherePivot($field, $op = null, $condition = null)
|
||||
{
|
||||
$this->query->where('pivot.' . $field, $op, $condition);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据集).
|
||||
*
|
||||
* @param array $resultSet 数据集
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
$pk = $resultSet[0]->getPk();
|
||||
$range = [];
|
||||
|
||||
foreach ($resultSet as $result) {
|
||||
// 获取关联外键列表
|
||||
if (isset($result->$pk)) {
|
||||
$range[] = $result->$pk;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($range)) {
|
||||
// 查询关联数据
|
||||
$range = array_unique($range);
|
||||
$data = $this->eagerlyManyToMany([
|
||||
['pivot.' . $localKey, 'in', $range],
|
||||
], $subRelation, $closure, $cache, count($range) > 1 ? true : false);
|
||||
|
||||
// 关联数据封装
|
||||
foreach ($resultSet as $result) {
|
||||
if (!isset($data[$result->$pk])) {
|
||||
$data[$result->$pk] = [];
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $this->resultSetBuild($data[$result->$pk]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(单个数据).
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResult(Model $result, string $relation, array $subRelation, ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$pk = $result->getPk();
|
||||
|
||||
if (is_string($pk) && isset($result->$pk)) {
|
||||
$pk = $result->$pk;
|
||||
// 查询管理数据
|
||||
$data = $this->eagerlyManyToMany([
|
||||
['pivot.' . $this->localKey, '=', $pk],
|
||||
], $subRelation, $closure, $cache);
|
||||
|
||||
// 关联数据封装
|
||||
if (!isset($data[$pk])) {
|
||||
$data[$pk] = [];
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $this->resultSetBuild($data[$pk]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联统计
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null)
|
||||
{
|
||||
$pk = $result->getPk();
|
||||
|
||||
if (!isset($result->$pk)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$pk = $result->$pk;
|
||||
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
|
||||
['pivot.' . $this->localKey, '=', $pk],
|
||||
])->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关联统计子查询.
|
||||
*
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationCountQuery(?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null) : string
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
$alias = Str::snake(class_basename($this->model));
|
||||
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
|
||||
if (!str_contains($field, '.')) {
|
||||
$field = $alias . '.' . $field;
|
||||
}
|
||||
|
||||
$this->query->alias($alias);
|
||||
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
|
||||
[
|
||||
'pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->getTable(true) . '.' . $this->parent->getPk()),
|
||||
],
|
||||
])->fetchSql()->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多对多 关联模型预查询.
|
||||
*
|
||||
* @param array $where 关联预查询条件
|
||||
* @param array $subRelation 子关联
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
* @param bool $collection 是否数据集查询
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function eagerlyManyToMany(array $where, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false) : array
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$withLimit = $this->query->getOption('limit');
|
||||
if ($withLimit && $collection) {
|
||||
$this->query->removeOption('limit');
|
||||
}
|
||||
|
||||
if ($this->isOneofMany) {
|
||||
// 仅获取一条关联数据
|
||||
if (!$collection) {
|
||||
$this->query->limit(1);
|
||||
} else {
|
||||
$withLimit = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 预载入关联查询 支持嵌套预载入
|
||||
$list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)
|
||||
->with($subRelation)
|
||||
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
|
||||
->lazy();
|
||||
|
||||
// 组装模型数据
|
||||
$data = [];
|
||||
foreach ($list as $set) {
|
||||
$pivot = $this->matchPivot($set);
|
||||
$key = $pivot[$this->localKey];
|
||||
|
||||
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[$key][] = $set;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* BELONGS TO MANY 关联查询.
|
||||
*
|
||||
* @param string $foreignKey 关联模型关联键
|
||||
* @param string $localKey 当前模型关联键
|
||||
* @param array $condition 关联查询条件
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query
|
||||
{
|
||||
// 关联查询封装
|
||||
if (empty($this->baseQuery)) {
|
||||
$tableName = $this->query->getTable(true);
|
||||
$table = $this->pivot->db()->getTable();
|
||||
$fields = $this->getQueryFields($tableName);
|
||||
|
||||
$this->query
|
||||
->field($fields)
|
||||
->tableField(true, $table, 'pivot', 'pivot__')
|
||||
->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $this->query->getPk())
|
||||
->where($condition);
|
||||
}
|
||||
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存(新增)当前关联数据对象
|
||||
*
|
||||
* @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
|
||||
* @param array $pivot 中间表额外数据
|
||||
*
|
||||
* @return array|Pivot
|
||||
*/
|
||||
public function save($data, array $pivot = [])
|
||||
{
|
||||
// 保存关联表/中间表数据
|
||||
return $this->attach($data, $pivot);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存当前关联数据对象
|
||||
*
|
||||
* @param iterable $dataSet 数据集
|
||||
* @param array $pivot 中间表额外数据
|
||||
* @param bool $samePivot 额外数据是否相同
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function saveAll(iterable $dataSet, array $pivot = [], bool $samePivot = false)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($dataSet as $key => $data) {
|
||||
if (!$samePivot) {
|
||||
$pivotData = $pivot[$key] ?? [];
|
||||
} else {
|
||||
$pivotData = $pivot;
|
||||
}
|
||||
|
||||
$result[] = $this->attach($data, $pivotData);
|
||||
}
|
||||
|
||||
return empty($result) ? false : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 附加关联的一个中间表数据.
|
||||
*
|
||||
* @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
|
||||
* @param array $pivot 中间表额外数据
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return array|Pivot
|
||||
*/
|
||||
public function attach($data, array $pivot = [])
|
||||
{
|
||||
if (is_array($data)) {
|
||||
if (key($data) === 0) {
|
||||
$id = $data;
|
||||
} else {
|
||||
// 保存关联表数据
|
||||
$model = new $this->model();
|
||||
$id = $model->insertGetId($data);
|
||||
}
|
||||
} elseif (is_numeric($data) || is_string($data)) {
|
||||
// 根据关联表主键直接写入中间表
|
||||
$id = $data;
|
||||
} elseif ($data instanceof Model) {
|
||||
// 根据关联表主键直接写入中间表
|
||||
$id = $data->getKey();
|
||||
}
|
||||
|
||||
if (!empty($id)) {
|
||||
// 保存中间表数据
|
||||
$pivot[$this->localKey] = $this->parent->getKey();
|
||||
|
||||
$ids = (array) $id;
|
||||
foreach ($ids as $id) {
|
||||
$pivot[$this->foreignKey] = $id;
|
||||
|
||||
$object = $this->newPivot();
|
||||
$object->replace()->save($pivot);
|
||||
$result[] = $object;
|
||||
}
|
||||
|
||||
if (count($result) == 1) {
|
||||
// 返回中间表模型对象
|
||||
$result = $result[0];
|
||||
}
|
||||
|
||||
return $result;
|
||||
} else {
|
||||
throw new Exception('miss relation data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否存在关联数据.
|
||||
*
|
||||
* @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键
|
||||
*
|
||||
* @return Pivot|false
|
||||
*/
|
||||
public function attached($data)
|
||||
{
|
||||
if ($data instanceof Model) {
|
||||
$id = $data->getKey();
|
||||
} else {
|
||||
$id = $data;
|
||||
}
|
||||
|
||||
$pivot = $this->pivot
|
||||
->where($this->localKey, $this->parent->getKey())
|
||||
->where($this->foreignKey, $id)
|
||||
->find();
|
||||
|
||||
return $pivot ?: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除关联的一个中间表数据.
|
||||
*
|
||||
* @param int|array $data 数据 可以使用关联对象的主键
|
||||
* @param bool $relationDel 是否同时删除关联表数据
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function detach($data = null, bool $relationDel = false): int
|
||||
{
|
||||
if (is_array($data)) {
|
||||
$id = $data;
|
||||
} elseif (is_numeric($data) || is_string($data)) {
|
||||
// 根据关联表主键直接写入中间表
|
||||
$id = $data;
|
||||
} elseif ($data instanceof Model) {
|
||||
// 根据关联表主键直接写入中间表
|
||||
$id = $data->getKey();
|
||||
}
|
||||
|
||||
// 删除中间表数据
|
||||
$pivot = [];
|
||||
$pivot[] = [$this->localKey, '=', $this->parent->getKey()];
|
||||
|
||||
if (isset($id)) {
|
||||
$pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id];
|
||||
}
|
||||
|
||||
$result = $this->newPivot()->where($pivot)->delete();
|
||||
|
||||
// 删除关联表数据
|
||||
if (isset($id) && $relationDel) {
|
||||
$model = $this->model;
|
||||
$model::destroy($id);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据同步.
|
||||
*
|
||||
* @param array $ids
|
||||
* @param bool $detaching
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function sync(array $ids, bool $detaching = true): array
|
||||
{
|
||||
$changes = [
|
||||
'attached' => [],
|
||||
'detached' => [],
|
||||
'updated' => [],
|
||||
];
|
||||
|
||||
$current = $this->pivot
|
||||
->where($this->localKey, $this->parent->getKey())
|
||||
->column($this->foreignKey);
|
||||
|
||||
$records = [];
|
||||
|
||||
foreach ($ids as $key => $value) {
|
||||
if (!is_array($value)) {
|
||||
$records[$value] = [];
|
||||
} else {
|
||||
$records[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$detach = array_diff($current, array_keys($records));
|
||||
|
||||
if ($detaching && count($detach) > 0) {
|
||||
$this->detach($detach);
|
||||
$changes['detached'] = $detach;
|
||||
}
|
||||
|
||||
foreach ($records as $id => $attributes) {
|
||||
if (!in_array($id, $current)) {
|
||||
$this->attach($id, $attributes);
|
||||
$changes['attached'][] = $id;
|
||||
} elseif (count($attributes) > 0) {
|
||||
$this->detach($id);
|
||||
$this->attach($id, $attributes);
|
||||
$changes['updated'][] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行基础查询(仅执行一次).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function baseQuery(): void
|
||||
{
|
||||
if (empty($this->baseQuery)) {
|
||||
$foreignKey = $this->foreignKey;
|
||||
$localKey = $this->localKey;
|
||||
|
||||
$this->query->filter(function ($result, $options) {
|
||||
$this->matchPivot($result);
|
||||
});
|
||||
|
||||
// 关联查询
|
||||
if (null === $this->parent->getKey()) {
|
||||
$condition = ['pivot.' . $localKey, 'exp', new Raw('=' . $this->parent->getTable(true) . '.' . $this->parent->getPk())];
|
||||
} else {
|
||||
$condition = ['pivot.' . $localKey, '=', $this->parent->getKey()];
|
||||
}
|
||||
|
||||
$this->belongsToManyQuery($foreignKey, $localKey, [$condition]);
|
||||
|
||||
$this->baseQuery = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace think\model\relation;
|
||||
|
||||
use Closure;
|
||||
use think\Collection;
|
||||
use think\db\BaseQuery as Query;
|
||||
use think\helper\Str;
|
||||
use think\model\contract\Modelable as Model;
|
||||
use think\model\Relation;
|
||||
|
||||
/**
|
||||
* 一对多关联类.
|
||||
*/
|
||||
class HasMany extends Relation
|
||||
{
|
||||
/**
|
||||
* 架构函数.
|
||||
*
|
||||
* @param Model $parent 上级模型对象
|
||||
* @param string $model 模型名
|
||||
* @param string $foreignKey 关联外键
|
||||
* @param string $localKey 当前模型主键
|
||||
*/
|
||||
public function __construct(Model $parent, string $model, string $foreignKey, string $localKey)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->model = $model;
|
||||
$this->foreignKey = $foreignKey;
|
||||
$this->localKey = $localKey;
|
||||
$this->query = (new $model())->db();
|
||||
|
||||
if (get_class($parent) == $model) {
|
||||
$this->selfRelation = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟获取关联数据.
|
||||
*
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包查询条件
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRelation(array $subRelation = [], ?Closure $closure = null): Collection
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
return $this->query
|
||||
->where($this->foreignKey, $this->parent->{$this->localKey})
|
||||
->relation($subRelation)
|
||||
->select();
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询.
|
||||
*
|
||||
* @param array $resultSet 数据集
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
$range = [];
|
||||
|
||||
foreach ($resultSet as $result) {
|
||||
// 获取关联外键列表
|
||||
if (isset($result->$localKey)) {
|
||||
$range[] = $result->$localKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($range)) {
|
||||
$range = array_unique($range);
|
||||
$data = $this->eagerlyOneToMany([
|
||||
[$this->foreignKey, 'in', $range],
|
||||
], $subRelation, $closure, $cache, count($range) > 1 ? true : false);
|
||||
|
||||
// 关联数据封装
|
||||
foreach ($resultSet as $result) {
|
||||
$pk = $result->$localKey;
|
||||
if (!isset($data[$pk])) {
|
||||
$data[$pk] = [];
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $this->resultSetBuild($data[$pk]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询.
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
|
||||
if (isset($result->$localKey)) {
|
||||
$pk = $result->$localKey;
|
||||
$data = $this->eagerlyOneToMany([
|
||||
[$this->foreignKey, '=', $pk],
|
||||
], $subRelation, $closure, $cache);
|
||||
|
||||
// 关联数据封装
|
||||
if (!isset($data[$pk])) {
|
||||
$data[$pk] = [];
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $this->resultSetBuild($data[$pk]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联统计
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null)
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
|
||||
if (!isset($result->$localKey)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
return $this->query
|
||||
->where($this->foreignKey, '=', $result->$localKey)
|
||||
->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建关联统计子查询.
|
||||
*
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationCountQuery(?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null) : string
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
$alias = Str::snake(class_basename($this->model));
|
||||
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
|
||||
return $this->query->alias($alias)
|
||||
->whereExp($alias . '.' . $this->foreignKey, '=' . $this->parent->getTable(true) . '.' . $this->localKey)
|
||||
->fetchSql()
|
||||
->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 一对多 关联模型预查询.
|
||||
*
|
||||
* @param array $where 关联预查询条件
|
||||
* @param array $subRelation 子关联
|
||||
* @param Closure $closure
|
||||
* @param array $cache 关联缓存
|
||||
* @param bool $collection 是否数据集查询
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function eagerlyOneToMany(array $where, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false) : array
|
||||
{
|
||||
$foreignKey = $this->foreignKey;
|
||||
|
||||
$this->query->removeWhereField($this->foreignKey);
|
||||
|
||||
// 预载入关联查询 支持嵌套预载入
|
||||
if ($closure) {
|
||||
$this->baseQuery = true;
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$withLimit = $this->query->getOption('limit');
|
||||
if ($withLimit && $collection) {
|
||||
$this->query->removeOption('limit');
|
||||
}
|
||||
|
||||
if ($this->isOneofMany) {
|
||||
if (!$collection) {
|
||||
$this->query->limit(1);
|
||||
} else {
|
||||
$withLimit = 1;
|
||||
}
|
||||
}
|
||||
|
||||
$list = $this->query
|
||||
->where($where)
|
||||
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
|
||||
->with($subRelation)
|
||||
->lazy();
|
||||
|
||||
// 组装模型数据
|
||||
$data = [];
|
||||
foreach ($list as $set) {
|
||||
$key = $set->$foreignKey;
|
||||
|
||||
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[$key][] = $set;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存(新增)当前关联数据对象
|
||||
*
|
||||
* @param array|Model $data 数据 可以使用数组 关联模型对象
|
||||
* @param bool $replace 是否自动识别更新和写入
|
||||
*
|
||||
* @return Model|false
|
||||
*/
|
||||
public function save(array | Model $data, bool $replace = true)
|
||||
{
|
||||
$model = $this->make();
|
||||
|
||||
return $model->replace($replace)->save($data) ? $model : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建关联对象实例.
|
||||
*
|
||||
* @param array|Model $data
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function make(array | Model $data = []): Model
|
||||
{
|
||||
if ($data instanceof Model) {
|
||||
$data = $data->getData();
|
||||
}
|
||||
|
||||
// 保存关联表数据
|
||||
$data[$this->foreignKey] = $this->parent->{$this->localKey};
|
||||
|
||||
return (new $this->model($data))->setSuffix($this->getModel()->getSuffix());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存当前关联数据对象
|
||||
*
|
||||
* @param iterable $dataSet 数据集
|
||||
* @param bool $replace 是否自动识别更新和写入
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function saveAll(iterable $dataSet, bool $replace = true)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($dataSet as $key => $data) {
|
||||
$result[] = $this->save($data, $replace);
|
||||
}
|
||||
|
||||
return empty($result) ? false : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param string $operator 比较操作符
|
||||
* @param int $count 个数
|
||||
* @param string $id 关联表的统计字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = 'INNER', ?Query $query = null): Query
|
||||
{
|
||||
$table = $this->query->getTable();
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
|
||||
return $query->alias($alias)
|
||||
->whereExists(function ($query) use ($alias, $id, $table, $operator, $count) {
|
||||
$table = $this->query->getTable();
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
|
||||
if ($this->isSelfRelation() && $alias == $relation) {
|
||||
$relation .= '_';
|
||||
}
|
||||
$query->table([$table => $relation])
|
||||
->field('count(' . $id . ') AS count')
|
||||
->whereColumn($relation . '.' . $this->foreignKey, $alias . '.' . $this->localKey)
|
||||
->having('count ' . $operator . ' ' . $count);
|
||||
|
||||
$this->getRelationSoftDelete($query, $relation);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param mixed $where 查询条件(数组或者闭包)
|
||||
* @param mixed $fields 字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function hasWhere($where = [], $fields = null, string $joinType = '', ?Query $query = null, string $logic = '', string $relationAlias = ''): Query
|
||||
{
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$table = $this->query->getTable();
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
$fields = $this->getRelationQueryFields($fields, $alias);
|
||||
$relAlias = $relationAlias ?: $relation;
|
||||
|
||||
if ($this->isSelfRelation() && $alias == $relAlias) {
|
||||
$relAlias .= '_';
|
||||
}
|
||||
|
||||
$query->alias($alias)
|
||||
->via($alias)
|
||||
->group($alias . '.' . $this->localKey)
|
||||
->field($fields)
|
||||
->join([$table => $relAlias], $alias . '.' . $this->localKey . '=' . $relAlias . '.' . $this->foreignKey, $joinType);
|
||||
|
||||
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行基础查询(仅执行一次).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function baseQuery(): void
|
||||
{
|
||||
if (empty($this->baseQuery)) {
|
||||
if (isset($this->parent->{$this->localKey})) {
|
||||
// 关联查询带入关联条件
|
||||
$this->query->where($this->foreignKey, '=', $this->parent->{$this->localKey});
|
||||
}
|
||||
|
||||
$this->baseQuery = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace think\model\relation;
|
||||
|
||||
use Closure;
|
||||
use think\Collection;
|
||||
use think\db\BaseQuery as Query;
|
||||
use think\helper\Str;
|
||||
use think\model\contract\Modelable as Model;
|
||||
use think\model\Relation;
|
||||
|
||||
/**
|
||||
* 远程一对多关联类.
|
||||
*/
|
||||
class HasManyThrough extends Relation
|
||||
{
|
||||
/**
|
||||
* 中间关联表外键.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $throughKey;
|
||||
|
||||
/**
|
||||
* 中间主键.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $throughPk;
|
||||
|
||||
/**
|
||||
* 中间表查询对象
|
||||
*
|
||||
* @var Query
|
||||
*/
|
||||
protected $through;
|
||||
|
||||
/**
|
||||
* 架构函数.
|
||||
*
|
||||
* @param Model $parent 上级模型对象
|
||||
* @param string $model 关联模型名
|
||||
* @param string $through 中间模型名
|
||||
* @param string $foreignKey 关联外键
|
||||
* @param string $throughKey 中间关联外键
|
||||
* @param string $localKey 当前模型主键
|
||||
* @param string $throughPk 中间模型主键
|
||||
*/
|
||||
public function __construct(Model $parent, string $model, string $through, string $foreignKey, string $throughKey, string $localKey, string $throughPk)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->model = $model;
|
||||
$this->through = (new $through())->db();
|
||||
$this->foreignKey = $foreignKey;
|
||||
$this->throughKey = $throughKey;
|
||||
$this->localKey = $localKey;
|
||||
$this->throughPk = $throughPk;
|
||||
$this->query = (new $model())->db();
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟获取关联数据.
|
||||
*
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包查询条件
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRelation(array $subRelation = [], ?Closure $closure = null)
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$this->baseQuery();
|
||||
|
||||
return $this->query->relation($subRelation)->select();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param string $operator 比较操作符
|
||||
* @param int $count 个数
|
||||
* @param string $id 关联表的统计字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
* @return Query
|
||||
*/
|
||||
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = 'INNER', ?Query $query = null): Query
|
||||
{
|
||||
// 子查询构建
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$table = $this->through->getTable();
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$relationTable = (new $this->model())->getTable();
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
|
||||
// 统计子查询
|
||||
$subQuery = $this->through
|
||||
->field('COUNT(' . $id . ')')
|
||||
->table($table)
|
||||
->join([$relationTable => $relation], $relation . '.' . $this->throughKey . '=' . $table . '.' . $this->throughPk, $joinType)
|
||||
->whereColumn($table . '.' . $this->throughPk, $model . '.' . $this->localKey);
|
||||
|
||||
$this->getRelationSoftDelete($subQuery, $relation);
|
||||
return $query->alias($alias)->where('(' . $subQuery->buildSql() . ') ' . $operator . ' ' . $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param mixed $where 查询条件(数组或者闭包)
|
||||
* @param mixed $fields 字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
* @return Query
|
||||
*/
|
||||
public function hasWhere($where = [], $fields = null, $joinType = '', ?Query $query = null, string $logic = '', string $relationAlias = ''): Query
|
||||
{
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$table = $this->through->getTable();
|
||||
$relationTable = (new $this->model())->getTable();
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
$relAlias = $relationAlias ?: $relation;
|
||||
|
||||
// EXISTS子查询
|
||||
$subQuery = $this->through
|
||||
->table($table)
|
||||
->join([$relationTable => $relAlias], $relAlias . '.' . $this->throughKey . '=' . $table . '.' . $this->throughPk, $joinType)
|
||||
->whereColumn($table . '.' . $this->throughPk, $alias . '.' . $this->localKey);
|
||||
|
||||
$this->getRelationSoftDelete($subQuery, $relAlias, $where, $logic);
|
||||
return $query->alias($alias)->whereExists($subQuery->buildSql());
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据集).
|
||||
*
|
||||
* @param array $resultSet 数据集
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
$foreignKey = $this->foreignKey;
|
||||
|
||||
$range = [];
|
||||
foreach ($resultSet as $result) {
|
||||
// 获取关联外键列表
|
||||
if (isset($result->$localKey)) {
|
||||
$range[] = $result->$localKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($range)) {
|
||||
$this->query->removeWhereField($foreignKey);
|
||||
$range = array_unique($range);
|
||||
$data = $this->eagerlyWhere([
|
||||
[$this->foreignKey, 'in', $range],
|
||||
], $foreignKey, $subRelation, $closure, $cache, count($range) > 1 ? true : false);
|
||||
|
||||
// 关联数据封装
|
||||
foreach ($resultSet as $result) {
|
||||
$pk = $result->$localKey;
|
||||
if (!isset($data[$pk])) {
|
||||
$data[$pk] = [];
|
||||
}
|
||||
|
||||
// 设置关联属性
|
||||
$result->setRelation($relation, $this->resultSetBuild($data[$pk]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据).
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
$foreignKey = $this->foreignKey;
|
||||
$pk = $result->$localKey;
|
||||
|
||||
$this->query->removeWhereField($foreignKey);
|
||||
|
||||
$data = $this->eagerlyWhere([
|
||||
[$foreignKey, '=', $pk],
|
||||
], $foreignKey, $subRelation, $closure, $cache);
|
||||
|
||||
// 关联数据封装
|
||||
if (!isset($data[$pk])) {
|
||||
$data[$pk] = [];
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $this->resultSetBuild($data[$pk]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联模型预查询.
|
||||
*
|
||||
* @param array $where 关联预查询条件
|
||||
* @param string $key 关联键名
|
||||
* @param array $subRelation 子关联
|
||||
* @param Closure $closure
|
||||
* @param array $cache 关联缓存
|
||||
* @param bool $collection 是否数据集查询
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function eagerlyWhere(array $where, string $key, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false): array
|
||||
{
|
||||
// 预载入关联查询 支持嵌套预载入
|
||||
$throughList = $this->through->where($where)->select();
|
||||
$keys = $throughList->column($this->throughPk, $this->throughPk);
|
||||
|
||||
if ($closure) {
|
||||
$this->baseQuery = true;
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$throughKey = $this->throughKey;
|
||||
|
||||
if ($this->baseQuery) {
|
||||
$throughKey = Str::snake(class_basename($this->model)) . '.' . $this->throughKey;
|
||||
}
|
||||
|
||||
$withLimit = $this->query->getOption('limit');
|
||||
if ($withLimit && $collection) {
|
||||
$this->query->removeOption('limit');
|
||||
}
|
||||
|
||||
if ($this->isOneofMany) {
|
||||
if (!$collection) {
|
||||
$this->query->limit(1);
|
||||
} else {
|
||||
$withLimit = 1;
|
||||
}
|
||||
}
|
||||
|
||||
$list = $this->query
|
||||
->where($throughKey, 'in', $keys)
|
||||
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
|
||||
->lazy();
|
||||
|
||||
// 组装模型数据
|
||||
$data = [];
|
||||
$keys = $throughList->column($this->foreignKey, $this->throughPk);
|
||||
|
||||
foreach ($list as $set) {
|
||||
$key = $keys[$set->{$this->throughKey}];
|
||||
|
||||
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[$key][] = $set;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联统计
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null)
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
|
||||
if (!isset($result->$localKey)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
$alias = Str::snake(class_basename($this->model));
|
||||
$alias = $this->query->getAlias() ?: $alias;
|
||||
$throughTable = $this->through->getTable();
|
||||
$pk = $this->throughPk;
|
||||
$throughKey = $this->throughKey;
|
||||
$modelTable = $this->parent->getTable();
|
||||
|
||||
if (!str_contains($field, '.')) {
|
||||
$field = $alias . '.' . $field;
|
||||
}
|
||||
|
||||
return $this->query
|
||||
->alias($alias)
|
||||
->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
|
||||
->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
|
||||
->where($throughTable . '.' . $this->foreignKey, $result->$localKey)
|
||||
->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建关联统计子查询.
|
||||
*
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationCountQuery(?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null) : string
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
$alias = Str::snake(class_basename($this->model));
|
||||
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
|
||||
$throughTable = $this->through->getTable();
|
||||
$pk = $this->throughPk;
|
||||
$throughKey = $this->throughKey;
|
||||
$modelTable = $this->parent->getTable();
|
||||
|
||||
if (!str_contains($field, '.')) {
|
||||
$field = $alias . '.' . $field;
|
||||
}
|
||||
|
||||
return $this->query
|
||||
->alias($alias)
|
||||
->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
|
||||
->whereColumn($throughTable . '.' . $this->foreignKey, $this->parent->getTable() . '.' . $this->localKey)
|
||||
->fetchSql()
|
||||
->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行基础查询(仅执行一次).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function baseQuery() : void
|
||||
{
|
||||
if (empty($this->baseQuery) && $this->parent->getData()) {
|
||||
$alias = Str::snake(class_basename($this->model));
|
||||
$throughTable = $this->through->getTable();
|
||||
$pk = $this->throughPk;
|
||||
$throughKey = $this->throughKey;
|
||||
$modelTable = $this->parent->getTable();
|
||||
$fields = $this->getQueryFields($alias);
|
||||
|
||||
$this->query
|
||||
->field($fields)
|
||||
->alias($alias)
|
||||
->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
|
||||
->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey});
|
||||
|
||||
$this->baseQuery = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace think\model\relation;
|
||||
|
||||
use Closure;
|
||||
use think\db\BaseQuery as Query;
|
||||
use think\helper\Str;
|
||||
use think\model\contract\Modelable as Model;
|
||||
|
||||
/**
|
||||
* HasOne 关联类.
|
||||
*/
|
||||
class HasOne extends OneToOne
|
||||
{
|
||||
/**
|
||||
* 架构函数.
|
||||
*
|
||||
* @param Model $parent 上级模型对象
|
||||
* @param string $model 模型名
|
||||
* @param string $foreignKey 关联外键
|
||||
* @param string $localKey 当前模型主键
|
||||
*/
|
||||
public function __construct(Model $parent, string $model, string $foreignKey, string $localKey)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->model = $model;
|
||||
$this->foreignKey = $foreignKey;
|
||||
$this->localKey = $localKey;
|
||||
$this->query = (new $model())->db();
|
||||
|
||||
if (get_class($parent) == $model) {
|
||||
$this->selfRelation = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟获取关联数据.
|
||||
*
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包查询条件
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function getRelation(array $subRelation = [], ?Closure $closure = null)
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
|
||||
if ($closure) {
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
// 判断关联类型执行查询
|
||||
$relationModel = $this->query
|
||||
->removeWhereField($this->foreignKey)
|
||||
->where($this->foreignKey, $this->parent->$localKey)
|
||||
->relation($subRelation)
|
||||
->find();
|
||||
|
||||
if ($relationModel) {
|
||||
if (!empty($this->bindAttr)) {
|
||||
// 绑定关联属性
|
||||
$this->parent->bindRelationAttr($relationModel, $this->bindAttr);
|
||||
}
|
||||
} else {
|
||||
$default = $this->query->getOption('default_model');
|
||||
$relationModel = $this->getDefaultModel($default);
|
||||
}
|
||||
|
||||
return $relationModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建关联统计子查询.
|
||||
*
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationCountQuery(?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null) : string
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
$alias = Str::snake(class_basename($this->model));
|
||||
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
|
||||
return $this->query
|
||||
->alias($alias)
|
||||
->whereExp($alias . '.' . $this->foreignKey, '=' . $this->parent->getTable(true) . '.' . $this->localKey)
|
||||
->fetchSql()
|
||||
->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联统计
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null)
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
|
||||
if (!isset($result->$localKey)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
return $this->query
|
||||
->where($this->foreignKey, '=', $result->$localKey)
|
||||
->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param string $operator 比较操作符
|
||||
* @param int $count 个数
|
||||
* @param string $id 关联表的统计字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', ?Query $query = null) : Query
|
||||
{
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$table = $this->query->getTable();
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
$method = (0 == $count && '=' == $operator) ? 'whereNotExists' : 'whereExists';
|
||||
|
||||
if ($this->isSelfRelation() && $alias == $relation) {
|
||||
$relation .= '_';
|
||||
}
|
||||
|
||||
return $query->alias($alias)->$method(function ($query) use ($table, $alias, $relation) {
|
||||
$query->table([$table => $relation])
|
||||
->field($relation . '.' . $this->foreignKey)
|
||||
->whereColumn($alias . '.' . $this->localKey, $relation . '.' . $this->foreignKey);
|
||||
$this->getRelationSoftDelete($query, $relation);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param mixed $where 查询条件(数组或者闭包)
|
||||
* @param mixed $fields 字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function hasWhere($where = [], $fields = null, string $joinType = '', ?Query $query = null, string $logic = '', string $relationAlias = ''): Query
|
||||
{
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$table = $this->query->getTable();
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
$fields = $this->getRelationQueryFields($fields, $alias);
|
||||
$relAlias = $relationAlias ?: $relation;
|
||||
|
||||
if ($this->isSelfRelation() && $alias == $relAlias) {
|
||||
$relAlias .= '_';
|
||||
}
|
||||
|
||||
$query->alias($alias)
|
||||
->via($alias)
|
||||
->field($fields)
|
||||
->join([$table => $relAlias], $alias . '.' . $this->localKey . '=' . $relAlias . '.' . $this->foreignKey, $joinType);
|
||||
|
||||
|
||||
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据集).
|
||||
*
|
||||
* @param array $resultSet 数据集
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
$foreignKey = $this->foreignKey;
|
||||
|
||||
$range = [];
|
||||
foreach ($resultSet as $result) {
|
||||
// 获取关联外键列表
|
||||
if (isset($result->$localKey)) {
|
||||
$range[] = $result->$localKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($range)) {
|
||||
$this->query->removeWhereField($foreignKey);
|
||||
$default = $this->query->getOption('default_model');
|
||||
$defaultModel = $this->getDefaultModel($default);
|
||||
|
||||
$range = array_unique($range);
|
||||
$data = $this->eagerlyWhere([
|
||||
[$foreignKey, 'in', $range],
|
||||
], $foreignKey, $subRelation, $closure, $cache, count($range) > 1 ? true : false);
|
||||
|
||||
// 动态绑定参数
|
||||
$bindAttr = $this->query->getOption('bind_attr');
|
||||
if ($bindAttr) {
|
||||
$this->bind($bindAttr);
|
||||
}
|
||||
|
||||
// 关联数据封装
|
||||
foreach ($resultSet as $result) {
|
||||
// 关联模型
|
||||
if (!isset($data[$result->$localKey])) {
|
||||
$relationModel = $defaultModel;
|
||||
} else {
|
||||
$relationModel = $data[$result->$localKey];
|
||||
}
|
||||
// 设置关联属性
|
||||
if (!empty($this->bindAttr) && $relationModel) {
|
||||
$result->bindRelationAttr($relationModel, $this->bindAttr, $relation);
|
||||
} else {
|
||||
$result->setRelation($relation, $relationModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据).
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
$foreignKey = $this->foreignKey;
|
||||
|
||||
$this->query->removeWhereField($foreignKey);
|
||||
|
||||
$data = $this->eagerlyWhere([
|
||||
[$foreignKey, '=', $result->$localKey],
|
||||
], $foreignKey, $subRelation, $closure, $cache);
|
||||
|
||||
// 关联模型
|
||||
if (!isset($data[$result->$localKey])) {
|
||||
$default = $this->query->getOption('default_model');
|
||||
$relationModel = $this->getDefaultModel($default);
|
||||
} else {
|
||||
$relationModel = $data[$result->$localKey];
|
||||
}
|
||||
|
||||
// 动态绑定参数
|
||||
$bindAttr = $this->query->getOption('bind_attr');
|
||||
if ($bindAttr) {
|
||||
$this->bind($bindAttr);
|
||||
}
|
||||
// 设置关联属性
|
||||
if (!empty($this->bindAttr) && $relationModel) {
|
||||
$result->bindRelationAttr($relationModel, $this->bindAttr, $relation);
|
||||
} else {
|
||||
$result->setRelation($relation, $relationModel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行基础查询(仅执行一次).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function baseQuery(): void
|
||||
{
|
||||
if (empty($this->baseQuery)) {
|
||||
if (isset($this->parent->{$this->localKey})) {
|
||||
// 关联查询带入关联条件
|
||||
$this->query->where($this->foreignKey, '=', $this->parent->{$this->localKey});
|
||||
}
|
||||
|
||||
$this->baseQuery = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace think\model\relation;
|
||||
|
||||
use Closure;
|
||||
use think\model\contract\Modelable as Model;
|
||||
|
||||
/**
|
||||
* 远程一对一关联类.
|
||||
*/
|
||||
class HasOneThrough extends HasManyThrough
|
||||
{
|
||||
/**
|
||||
* 延迟获取关联数据.
|
||||
*
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包查询条件
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function getRelation(array $subRelation = [], ?Closure $closure = null)
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$this->baseQuery();
|
||||
|
||||
$relationModel = $this->query->relation($subRelation)->find();
|
||||
|
||||
if ($relationModel) {
|
||||
} else {
|
||||
$default = $this->query->getOption('default_model');
|
||||
$relationModel = $this->getDefaultModel($default);
|
||||
}
|
||||
|
||||
return $relationModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据集).
|
||||
*
|
||||
* @param array $resultSet 数据集
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
$foreignKey = $this->foreignKey;
|
||||
|
||||
$range = [];
|
||||
foreach ($resultSet as $result) {
|
||||
// 获取关联外键列表
|
||||
if (isset($result->$localKey)) {
|
||||
$range[] = $result->$localKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($range)) {
|
||||
$this->query->removeWhereField($foreignKey);
|
||||
$default = $this->query->getOption('default_model');
|
||||
$defaultModel = $this->getDefaultModel($default);
|
||||
|
||||
$data = $this->eagerlyWhere([
|
||||
[$this->foreignKey, 'in', $range],
|
||||
], $foreignKey, $subRelation, $closure, $cache);
|
||||
|
||||
// 关联数据封装
|
||||
foreach ($resultSet as $result) {
|
||||
// 关联模型
|
||||
if (!isset($data[$result->$localKey])) {
|
||||
$relationModel = $defaultModel;
|
||||
} else {
|
||||
$relationModel = $data[$result->$localKey];
|
||||
}
|
||||
|
||||
// 设置关联属性
|
||||
$result->setRelation($relation, $relationModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据).
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$localKey = $this->localKey;
|
||||
$foreignKey = $this->foreignKey;
|
||||
|
||||
$this->query->removeWhereField($foreignKey);
|
||||
|
||||
$data = $this->eagerlyWhere([
|
||||
[$foreignKey, '=', $result->$localKey],
|
||||
], $foreignKey, $subRelation, $closure, $cache);
|
||||
|
||||
// 关联模型
|
||||
if (!isset($data[$result->$localKey])) {
|
||||
$default = $this->query->getOption('default_model');
|
||||
$relationModel = $this->getDefaultModel($default);
|
||||
} else {
|
||||
$relationModel = $data[$result->$localKey];
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $relationModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联模型预查询.
|
||||
*
|
||||
* @param array $where 关联预查询条件
|
||||
* @param string $key 关联键名
|
||||
* @param array $subRelation 子关联
|
||||
* @param Closure $closure
|
||||
* @param array $cache 关联缓存
|
||||
* @param bool $collection 是否数据集查询
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function eagerlyWhere(array $where, string $key, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false): array
|
||||
{
|
||||
// 预载入关联查询 支持嵌套预载入
|
||||
$keys = $this->through->where($where)->column($this->throughPk, $this->foreignKey);
|
||||
|
||||
if ($closure) {
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$list = $this->query
|
||||
->where($this->throughKey, 'in', $keys)
|
||||
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
|
||||
->select();
|
||||
|
||||
// 组装模型数据
|
||||
return array_map(function ($key) use ($list) {
|
||||
$set = $list->where($this->throughKey, '=', $key)->first();
|
||||
return $set ?: null;
|
||||
}, $keys);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace think\model\relation;
|
||||
|
||||
use Closure;
|
||||
use think\Collection;
|
||||
use think\db\BaseQuery as Query;
|
||||
use think\helper\Str;
|
||||
use think\model\contract\Modelable as Model;
|
||||
use think\model\Relation;
|
||||
|
||||
/**
|
||||
* 多态一对多关联.
|
||||
*/
|
||||
class MorphMany extends Relation
|
||||
{
|
||||
/**
|
||||
* 多态关联外键.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphKey;
|
||||
|
||||
/**
|
||||
* 多态字段名.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphType;
|
||||
|
||||
/**
|
||||
* 多态类型.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* 架构函数.
|
||||
*
|
||||
* @param Model $parent 上级模型对象
|
||||
* @param string $model 模型名
|
||||
* @param string $morphKey 关联外键
|
||||
* @param string $morphType 多态字段名
|
||||
* @param string $type 多态类型
|
||||
*/
|
||||
public function __construct(Model $parent, string $model, string $morphKey, string $morphType, string $type)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->model = $model;
|
||||
$this->type = $type;
|
||||
$this->morphKey = $morphKey;
|
||||
$this->morphType = $morphType;
|
||||
$this->query = (new $model())->db();
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟获取关联数据.
|
||||
*
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包查询条件
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRelation(array $subRelation = [], ?Closure $closure = null): Collection
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$this->baseQuery();
|
||||
|
||||
return $this->query->relation($subRelation)->select();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param string $operator 比较操作符
|
||||
* @param int $count 个数
|
||||
* @param string $id 关联表的统计字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', ?Query $query = null)
|
||||
{
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$table = $this->query->getTable();
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
|
||||
$query->alias($alias)
|
||||
->field($alias . '.*')
|
||||
->join([$table => $relation], $alias . '.' . $this->parent->getPk() . '=' . $relation . '.' . $this->morphKey)
|
||||
->where($relation . '.' . $this->morphType, '=', $this->type)
|
||||
->group($relation . '.' . $this->morphKey)
|
||||
->having('count(' . $id . ')' . $operator . $count);
|
||||
|
||||
return $this->getRelationSoftDelete($query, $relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param mixed $where 查询条件(数组或者闭包)
|
||||
* @param mixed $fields 字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function hasWhere($where = [], $fields = null, string $joinType = '', ?Query $query = null, string $logic = '', string $relationAlias = '')
|
||||
{
|
||||
$table = $this->query->getTable();
|
||||
$query = $query ?: $this->parent->db();
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
$fields = $this->getRelationQueryFields($fields, $alias);
|
||||
$relAlias = $relationAlias ?: $relation;
|
||||
|
||||
$query->alias($alias)
|
||||
->join([$table => $relAlias], $alias . '.' . $this->parent->getPk() . '=' . $relAlias . '.' . $this->morphKey, $joinType)
|
||||
->where($relAlias . '.' . $this->morphType, '=', $this->type)
|
||||
->group($relAlias . '.' . $this->morphKey)
|
||||
->field($fields);
|
||||
|
||||
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询.
|
||||
*
|
||||
* @param array $resultSet 数据集
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$morphType = $this->morphType;
|
||||
$morphKey = $this->morphKey;
|
||||
$type = $this->type;
|
||||
$range = [];
|
||||
|
||||
foreach ($resultSet as $result) {
|
||||
$pk = $result->getPk();
|
||||
// 获取关联外键列表
|
||||
if (isset($result->$pk)) {
|
||||
$range[] = $result->$pk;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($range)) {
|
||||
$where = [
|
||||
[$morphKey, 'in', array_unique($range)],
|
||||
[$morphType, '=', $type],
|
||||
];
|
||||
$data = $this->eagerlyMorphToMany($where, $subRelation, $closure, $cache, true);
|
||||
|
||||
// 关联数据封装
|
||||
foreach ($resultSet as $result) {
|
||||
if (!isset($data[$result->$pk])) {
|
||||
$data[$result->$pk] = [];
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $this->resultSetBuild($data[$result->$pk]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询.
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$pk = $result->getPk();
|
||||
|
||||
if (isset($result->$pk)) {
|
||||
$key = $result->$pk;
|
||||
$data = $this->eagerlyMorphToMany([
|
||||
[$this->morphKey, '=', $key],
|
||||
[$this->morphType, '=', $this->type],
|
||||
], $subRelation, $closure, $cache);
|
||||
|
||||
if (!isset($data[$key])) {
|
||||
$data[$key] = [];
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $this->resultSetBuild($data[$key]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联统计
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null)
|
||||
{
|
||||
$pk = $result->getPk();
|
||||
|
||||
if (!isset($result->$pk)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
return $this->query
|
||||
->where([
|
||||
[$this->morphKey, '=', $result->$pk],
|
||||
[$this->morphType, '=', $this->type],
|
||||
])
|
||||
->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关联统计子查询.
|
||||
*
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationCountQuery(?Closure $closure = null, string $aggregate = 'count', string $field = 'id', ? string &$name = null) : string
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
$alias = Str::snake(class_basename($this->model));
|
||||
$alias = $this->query->getAlias() ?: $alias . '_' . $aggregate;
|
||||
return $this->query
|
||||
->alias($alias)
|
||||
->whereColumn($alias . '.' . $this->morphKey, $this->parent->getTable(true) . '.' . $this->parent->getPk())
|
||||
->where($alias . '.' . $this->morphType, '=', $this->type)
|
||||
->fetchSql()
|
||||
->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多态一对多 关联模型预查询.
|
||||
*
|
||||
* @param array $where 关联预查询条件
|
||||
* @param array $subRelation 子关联
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
* @param bool $collection 是否数据集查询
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function eagerlyMorphToMany(array $where, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false) : array
|
||||
{
|
||||
// 预载入关联查询 支持嵌套预载入
|
||||
$this->query->removeOption('where');
|
||||
|
||||
if ($closure) {
|
||||
$this->baseQuery = true;
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$withLimit = $this->query->getOption('limit');
|
||||
if ($withLimit && $collection) {
|
||||
$this->query->removeOption('limit');
|
||||
}
|
||||
|
||||
if ($this->isOneofMany) {
|
||||
// 仅获取一条关联数据
|
||||
if (!$collection) {
|
||||
$this->query->limit(1);
|
||||
} else {
|
||||
$withLimit = 1;
|
||||
}
|
||||
}
|
||||
|
||||
$list = $this->query
|
||||
->where($where)
|
||||
->with($subRelation)
|
||||
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
|
||||
->lazy();
|
||||
|
||||
// 组装模型数据
|
||||
$data = [];
|
||||
$morphKey = $this->morphKey;
|
||||
foreach ($list as $set) {
|
||||
$key = $set->$morphKey;
|
||||
|
||||
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[$key][] = $set;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存(新增)当前关联数据对象
|
||||
*
|
||||
* @param array|Model $data 数据 可以使用数组 关联模型对象
|
||||
* @param bool $replace 是否自动识别更新和写入
|
||||
*
|
||||
* @return Model|false
|
||||
*/
|
||||
public function save(array | Model $data, bool $replace = true)
|
||||
{
|
||||
$model = $this->make();
|
||||
|
||||
return $model->replace($replace)->save($data) ? $model : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建关联对象实例.
|
||||
*
|
||||
* @param array|Model $data
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function make($data = []): Model
|
||||
{
|
||||
if ($data instanceof Model) {
|
||||
$data = $data->getData();
|
||||
}
|
||||
|
||||
// 保存关联表数据
|
||||
$pk = $this->parent->getPk();
|
||||
|
||||
$data[$this->morphKey] = $this->parent->$pk;
|
||||
$data[$this->morphType] = $this->type;
|
||||
|
||||
return (new $this->model($data))->setSuffix($this->getModel()->getSuffix());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存当前关联数据对象
|
||||
*
|
||||
* @param iterable $dataSet 数据集
|
||||
* @param bool $replace 是否自动识别更新和写入
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function saveAll(iterable $dataSet, bool $replace = true)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($dataSet as $key => $data) {
|
||||
$result[] = $this->save($data, $replace);
|
||||
}
|
||||
|
||||
return empty($result) ? false : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多态关联外键.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphKey()
|
||||
{
|
||||
return $this->morphKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多态字段名.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphType()
|
||||
{
|
||||
return $this->morphType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多态类型.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行基础查询(仅执行一次).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function baseQuery(): void
|
||||
{
|
||||
if (empty($this->baseQuery) && $this->parent->getData()) {
|
||||
$pk = $this->parent->getPk();
|
||||
|
||||
$this->query->where([
|
||||
[$this->morphKey, '=', $this->parent->$pk],
|
||||
[$this->morphType, '=', $this->type],
|
||||
]);
|
||||
|
||||
$this->baseQuery = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace think\model\relation;
|
||||
|
||||
use Closure;
|
||||
use think\db\BaseQuery as Query;
|
||||
use think\db\exception\DbException as Exception;
|
||||
use think\helper\Str;
|
||||
use think\model\contract\Modelable as Model;
|
||||
use think\model\Relation;
|
||||
|
||||
/**
|
||||
* 多态一对一关联类.
|
||||
*/
|
||||
class MorphOne extends Relation
|
||||
{
|
||||
/**
|
||||
* 多态关联外键.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphKey;
|
||||
|
||||
/**
|
||||
* 多态字段.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphType;
|
||||
|
||||
/**
|
||||
* 多态类型.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* 绑定的关联属性.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $bindAttr = [];
|
||||
|
||||
/**
|
||||
* 构造函数.
|
||||
*
|
||||
* @param Model $parent 上级模型对象
|
||||
* @param string $model 模型名
|
||||
* @param string $morphKey 关联外键
|
||||
* @param string $morphType 多态字段名
|
||||
* @param string $type 多态类型
|
||||
*/
|
||||
public function __construct(Model $parent, string $model, string $morphKey, string $morphType, string $type)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->model = $model;
|
||||
$this->type = $type;
|
||||
$this->morphKey = $morphKey;
|
||||
$this->morphType = $morphType;
|
||||
$this->query = (new $model())->db();
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟获取关联数据.
|
||||
*
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包查询条件
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function getRelation(array $subRelation = [], ?Closure $closure = null)
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$this->baseQuery();
|
||||
|
||||
$relationModel = $this->query->relation($subRelation)->find();
|
||||
|
||||
if ($relationModel) {
|
||||
if (!empty($this->bindAttr)) {
|
||||
// 绑定关联属性
|
||||
$this->bindAttr($this->parent, $relationModel);
|
||||
}
|
||||
} else {
|
||||
$default = $this->query->getOption('default_model');
|
||||
$relationModel = $this->getDefaultModel($default);
|
||||
}
|
||||
|
||||
return $relationModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param string $operator 比较操作符
|
||||
* @param int $count 个数
|
||||
* @param string $id 关联表的统计字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', ?Query $query = null)
|
||||
{
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$table = $this->query->getTable();
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
|
||||
$query->alias($alias)
|
||||
->field($alias . '.*')
|
||||
->join([$table => $relation], $alias . '.' . $this->parent->getPk() . '=' . $relation . '.' . $this->morphKey)
|
||||
->where($relation . '.' . $this->morphType, '=', $this->type)
|
||||
->group($relation . '.' . $this->morphKey)
|
||||
->having('count(' . $id . ')' . $operator . $count);
|
||||
|
||||
return $this->getRelationSoftDelete($query, $relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param mixed $where 查询条件(数组或者闭包)
|
||||
* @param mixed $fields 字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function hasWhere($where = [], $fields = null, string $joinType = '', ?Query $query = null, string $logic = '', string $relationAlias = '')
|
||||
{
|
||||
$table = $this->query->getTable();
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$relation = Str::snake(class_basename($this->model));
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
$fields = $this->getRelationQueryFields($fields, $alias);
|
||||
$relAlias = $relationAlias ?: $relation;
|
||||
|
||||
$query->alias($alias)
|
||||
->join([$table => $relAlias], $alias . '.' . $this->parent->getPk() . '=' . $relAlias . '.' . $this->morphKey, $joinType)
|
||||
->where($relAlias . '.' . $this->morphType, '=', $this->type)
|
||||
->group($relAlias . '.' . $this->morphKey)
|
||||
->field($fields);
|
||||
|
||||
return $this->getRelationSoftDelete($query, $relAlias, $where, $logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询.
|
||||
*
|
||||
* @param array $resultSet 数据集
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$morphType = $this->morphType;
|
||||
$morphKey = $this->morphKey;
|
||||
$type = $this->type;
|
||||
$range = [];
|
||||
|
||||
foreach ($resultSet as $result) {
|
||||
$pk = $result->getPk();
|
||||
// 获取关联外键列表
|
||||
if (isset($result->$pk)) {
|
||||
$range[] = $result->$pk;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($range)) {
|
||||
$data = $this->eagerlyMorphToOne([
|
||||
[$morphKey, 'in', $range],
|
||||
[$morphType, '=', $type],
|
||||
], $subRelation, $closure, $cache);
|
||||
|
||||
$default = $this->query->getOption('default_model');
|
||||
$defaultModel = $this->getDefaultModel($default);
|
||||
|
||||
// 关联数据封装
|
||||
foreach ($resultSet as $result) {
|
||||
if (!isset($data[$result->$pk])) {
|
||||
$relationModel = $defaultModel;
|
||||
} else {
|
||||
$relationModel = $data[$result->$pk];
|
||||
}
|
||||
|
||||
if (!empty($this->bindAttr)) {
|
||||
// 绑定关联属性
|
||||
$this->bindAttr($result, $relationModel);
|
||||
} else {
|
||||
// 设置关联属性
|
||||
$result->setRelation($relation, $relationModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询.
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$pk = $result->getPk();
|
||||
|
||||
if (isset($result->$pk)) {
|
||||
$pk = $result->$pk;
|
||||
$data = $this->eagerlyMorphToOne([
|
||||
[$this->morphKey, '=', $pk],
|
||||
[$this->morphType, '=', $this->type],
|
||||
], $subRelation, $closure, $cache);
|
||||
|
||||
if (isset($data[$pk])) {
|
||||
$relationModel = $data[$pk];
|
||||
} else {
|
||||
$default = $this->query->getOption('default_model');
|
||||
$relationModel = $this->getDefaultModel($default);
|
||||
}
|
||||
|
||||
if (!empty($this->bindAttr)) {
|
||||
// 绑定关联属性
|
||||
$this->bindAttr($result, $relationModel);
|
||||
} else {
|
||||
// 设置关联属性
|
||||
$result->setRelation($relation, $relationModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多态一对一 关联模型预查询.
|
||||
*
|
||||
* @param array $where 关联预查询条件
|
||||
* @param array $subRelation 子关联
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function eagerlyMorphToOne(array $where, array $subRelation = [], ?Closure $closure = null, array $cache = []): array
|
||||
{
|
||||
// 预载入关联查询 支持嵌套预载入
|
||||
if ($closure) {
|
||||
$this->baseQuery = true;
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$list = $this->query
|
||||
->where($where)
|
||||
->with($subRelation)
|
||||
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
|
||||
->lazy();
|
||||
|
||||
// 组装模型数据
|
||||
$data = [];
|
||||
$morphKey = $this->morphKey;
|
||||
foreach ($list as $set) {
|
||||
$data[$set->$morphKey] = $set;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存(新增)当前关联数据对象
|
||||
*
|
||||
* @param array|Model $data 数据 可以使用数组 关联模型对象
|
||||
* @param bool $replace 是否自动识别更新和写入
|
||||
*
|
||||
* @return Model|false
|
||||
*/
|
||||
public function save(array | Model $data, bool $replace = true)
|
||||
{
|
||||
$model = $this->make();
|
||||
|
||||
return $model->replace($replace)->save($data) ? $model : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建关联对象实例.
|
||||
*
|
||||
* @param array|Model $data
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function make(array | Model $data = []): Model
|
||||
{
|
||||
if ($data instanceof Model) {
|
||||
$data = $data->getData();
|
||||
}
|
||||
|
||||
// 保存关联表数据
|
||||
$pk = $this->parent->getPk();
|
||||
|
||||
$data[$this->morphKey] = $this->parent->$pk;
|
||||
$data[$this->morphType] = $this->type;
|
||||
|
||||
return (new $this->model($data))->setSuffix($this->getModel()->getSuffix());
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行基础查询(进执行一次).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function baseQuery(): void
|
||||
{
|
||||
if (empty($this->baseQuery) && $this->parent->getData()) {
|
||||
$pk = $this->parent->getPk();
|
||||
|
||||
$this->query->where([
|
||||
[$this->morphKey, '=', $this->parent->$pk],
|
||||
[$this->morphType, '=', $this->type],
|
||||
]);
|
||||
$this->baseQuery = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定关联表的属性到父模型属性.
|
||||
*
|
||||
* @param array $attr 要绑定的属性列表
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function bind(array $attr)
|
||||
{
|
||||
$this->bindAttr = $attr;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绑定属性.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getBindAttr(): array
|
||||
{
|
||||
return $this->bindAttr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定关联属性到父模型.
|
||||
*
|
||||
* @param Model $result 父模型对象
|
||||
* @param Model $model 关联模型对象
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function bindAttr(Model $result, ?Model $model = null): void
|
||||
{
|
||||
foreach ($this->bindAttr as $key => $attr) {
|
||||
$key = is_numeric($key) ? $attr : $key;
|
||||
$value = $result->getOrigin($key);
|
||||
|
||||
if (!is_null($value)) {
|
||||
throw new Exception('bind attr has exists:' . $key);
|
||||
}
|
||||
|
||||
$result->set($key, $model?->get($attr));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace think\model\relation;
|
||||
|
||||
use BackedEnum;
|
||||
use Closure;
|
||||
use think\db\exception\DbException as Exception;
|
||||
use think\db\Query;
|
||||
use think\helper\Str;
|
||||
use think\model\contract\Modelable as Model;
|
||||
use think\model\Relation;
|
||||
|
||||
/**
|
||||
* 多态关联类.
|
||||
*/
|
||||
class MorphTo extends Relation
|
||||
{
|
||||
/**
|
||||
* 多态关联外键.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphKey;
|
||||
|
||||
/**
|
||||
* 多态字段.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphType;
|
||||
|
||||
/**
|
||||
* 多态别名.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $alias = [];
|
||||
|
||||
/**
|
||||
* 关联名.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $relation;
|
||||
|
||||
protected $queryCaller = [];
|
||||
|
||||
/**
|
||||
* 架构函数.
|
||||
*
|
||||
* @param Model $parent 上级模型对象
|
||||
* @param string $morphType 多态字段名
|
||||
* @param string $morphKey 外键名
|
||||
* @param array $alias 多态别名定义
|
||||
* @param ?string $relation 关联名
|
||||
*/
|
||||
public function __construct(Model $parent, string $morphType, string $morphKey, array $alias = [], ?string $relation = null)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->morphType = $morphType;
|
||||
$this->morphKey = $morphKey;
|
||||
$this->alias = $alias;
|
||||
$this->relation = $relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前的关联模型类的实例.
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function getModel(): Model
|
||||
{
|
||||
$morphType = $this->morphType;
|
||||
$model = $this->parseModel($this->parent->$morphType);
|
||||
|
||||
return new $model();
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟获取关联数据.
|
||||
*
|
||||
* @param array $subRelation 子关联名
|
||||
* @param ?Closure $closure 闭包查询条件
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function getRelation(array $subRelation = [], ?Closure $closure = null)
|
||||
{
|
||||
$morphKey = $this->morphKey;
|
||||
$morphType = $this->morphType;
|
||||
|
||||
// 多态模型
|
||||
$model = $this->parseModel($this->parent->$morphType);
|
||||
|
||||
// 主键数据
|
||||
$pk = $this->parent->$morphKey;
|
||||
|
||||
return class_exists($model) ? $this->buildQuery((new $model())->relation($subRelation))->find($pk) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param string $operator 比较操作符
|
||||
* @param int $count 个数
|
||||
* @param string $id 关联表的统计字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', ?Query $query = null)
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联条件查询当前模型.
|
||||
*
|
||||
* @param mixed $where 查询条件(数组或者闭包)
|
||||
* @param mixed $fields 字段
|
||||
* @param string $joinType JOIN类型
|
||||
* @param ?Query $query Query对象
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function hasWhere($where = [], $fields = null, string $joinType = '', ?Query $query = null, string $logic = '')
|
||||
{
|
||||
$model = Str::snake(class_basename($this->parent));
|
||||
$types = $this->parent->distinct()->column($this->morphType);
|
||||
$query = $query ?: $this->parent->db();
|
||||
$alias = $query->getAlias() ?: $model;
|
||||
|
||||
return $query->alias($alias)
|
||||
->where(function (Query $query) use ($types, $where, $alias, $logic) {
|
||||
foreach ($types as $type) {
|
||||
if ($type) {
|
||||
$query->whereExists(function (Query $query) use ($type, $where, $alias, $logic) {
|
||||
$class = $this->parseModel($type);
|
||||
/** @var Model $model */
|
||||
$model = new $class();
|
||||
|
||||
$table = $model->getTable();
|
||||
$logic = 'OR' == $logic ? 'whereOr' : 'where';
|
||||
$query
|
||||
->table($table)
|
||||
->where($alias . '.' . $this->morphType, $type)
|
||||
->whereColumn($alias . '.' . $this->morphKey, $table . '.' . $model->getPk())
|
||||
->$logic($where);
|
||||
}, 'OR');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析模型的完整命名空间.
|
||||
*
|
||||
* @param string $model 模型名(或者完整类名)
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
protected function parseModel($model): string
|
||||
{
|
||||
if ($model instanceof BackedEnum) {
|
||||
$model = $model->value;
|
||||
}
|
||||
|
||||
if (isset($this->alias[$model])) {
|
||||
$model = $this->alias[$model];
|
||||
}
|
||||
|
||||
if (!str_contains($model, '\\')) {
|
||||
$path = explode('\\', get_class($this->parent));
|
||||
array_pop($path);
|
||||
array_push($path, Str::studly($model));
|
||||
$model = implode('\\', $path);
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置多态别名.
|
||||
*
|
||||
* @param array $alias 别名定义
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setAlias(array $alias)
|
||||
{
|
||||
$this->alias = $alias;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除关联查询参数.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removeOption(string $option = '')
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询.
|
||||
*
|
||||
* @param array $resultSet 数据集
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param ?Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$morphKey = $this->morphKey;
|
||||
$morphType = $this->morphType;
|
||||
$range = [];
|
||||
|
||||
foreach ($resultSet as $result) {
|
||||
// 获取关联外键列表
|
||||
if (!empty($result->$morphKey)) {
|
||||
$range[$result->$morphType][] = $result->$morphKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($range)) {
|
||||
foreach ($range as $key => $val) {
|
||||
// 多态类型映射
|
||||
$model = $this->parseModel($key);
|
||||
$data = [];
|
||||
if (class_exists($model)) {
|
||||
$obj = new $model();
|
||||
if (!is_null($closure)) {
|
||||
$obj = $closure($obj);
|
||||
}
|
||||
$pk = $obj->getPk();
|
||||
$list = $obj->with($subRelation)
|
||||
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
|
||||
->select($val);
|
||||
|
||||
foreach ($list as $k => $vo) {
|
||||
$data[$vo->$pk] = $vo;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($resultSet as $result) {
|
||||
if ($key == $result->$morphType) {
|
||||
// 关联模型
|
||||
if (!isset($data[$result->$morphKey])) {
|
||||
$relationModel = null;
|
||||
} else {
|
||||
$relationModel = $data[$result->$morphKey];
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $relationModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询.
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param ?Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
// 多态类型映射
|
||||
$model = $this->parseModel($result->{$this->morphType});
|
||||
|
||||
$this->eagerlyMorphToOne($model, $relation, $result, $subRelation, $cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联统计
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param ?Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = '*')
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 多态MorphTo 关联模型预查询.
|
||||
*
|
||||
* @param string $model 关联模型对象
|
||||
* @param string $relation 关联名
|
||||
* @param Model $result
|
||||
* @param array $subRelation 子关联
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function eagerlyMorphToOne(string $model, string $relation, Model $result, array $subRelation = [], array $cache = []): void
|
||||
{
|
||||
// 预载入关联查询 支持嵌套预载入
|
||||
$pk = $this->parent->{$this->morphKey};
|
||||
$data = null;
|
||||
|
||||
if (class_exists($model)) {
|
||||
$data = (new $model())->with($subRelation)
|
||||
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
|
||||
->find($pk);
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $data ?: null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加关联数据.
|
||||
*
|
||||
* @param Model $model 关联模型对象
|
||||
* @param string $type 多态类型
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function associate(Model $model, string $type = ''): Model
|
||||
{
|
||||
$morphKey = $this->morphKey;
|
||||
$morphType = $this->morphType;
|
||||
$pk = $model->getPk();
|
||||
|
||||
$this->parent->set($morphKey, $model->$pk);
|
||||
$this->parent->set($morphType, $type ?: get_class($model));
|
||||
$this->parent->save();
|
||||
|
||||
return $this->parent->setRelation($this->relation, $model);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销关联数据.
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function dissociate(): Model
|
||||
{
|
||||
$morphKey = $this->morphKey;
|
||||
$morphType = $this->morphType;
|
||||
|
||||
$this->parent->set($morphKey, null);
|
||||
$this->parent->set($morphType, null);
|
||||
$this->parent->save();
|
||||
|
||||
return $this->parent->setRelation($this->relation, null);
|
||||
}
|
||||
|
||||
protected function buildQuery(Query $query)
|
||||
{
|
||||
foreach ($this->queryCaller as $caller) {
|
||||
call_user_func_array([$query, $caller[0]], $caller[1]);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function __call($method, $args)
|
||||
{
|
||||
$this->queryCaller[] = [$method, $args];
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace think\model\relation;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use think\db\BaseQuery as Query;
|
||||
use think\db\Raw;
|
||||
use think\model\contract\Modelable as Model;
|
||||
use think\model\Pivot;
|
||||
|
||||
/**
|
||||
* 多态多对多关联.
|
||||
*/
|
||||
class MorphToMany extends BelongsToMany
|
||||
{
|
||||
/**
|
||||
* 多态关系的模型名映射别名的数组.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $morphMap = [];
|
||||
|
||||
/**
|
||||
* 多态字段名.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphType;
|
||||
|
||||
/**
|
||||
* 多态模型名.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $morphClass;
|
||||
|
||||
/**
|
||||
* 是否反向关联.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $inverse;
|
||||
|
||||
/**
|
||||
* 架构函数.
|
||||
*
|
||||
* @param Model $parent 上级模型对象
|
||||
* @param string $model 模型名
|
||||
* @param string $middle 中间表名/模型名
|
||||
* @param string $morphKey 关联外键
|
||||
* @param string $morphType 多态字段名
|
||||
* @param string $localKey 当前模型关联键
|
||||
* @param bool $inverse 反向关联
|
||||
*/
|
||||
public function __construct(Model $parent, string $model, string $middle, string $morphType, string $morphKey, string $localKey, bool $inverse = false)
|
||||
{
|
||||
$this->morphType = $morphType;
|
||||
$this->inverse = $inverse;
|
||||
$this->morphClass = $inverse ? $model : get_class($parent);
|
||||
|
||||
if (isset(static::$morphMap[$this->morphClass])) {
|
||||
$this->morphClass = static::$morphMap[$this->morphClass];
|
||||
}
|
||||
|
||||
$foreignKey = $inverse ? $morphKey : $localKey;
|
||||
$localKey = $inverse ? $localKey : $morphKey;
|
||||
|
||||
parent::__construct($parent, $model, $middle, $foreignKey, $localKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据集).
|
||||
*
|
||||
* @param array $resultSet 数据集
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$pk = $resultSet[0]->getPk();
|
||||
$range = [];
|
||||
|
||||
foreach ($resultSet as $result) {
|
||||
// 获取关联外键列表
|
||||
if (isset($result->$pk)) {
|
||||
$range[] = $result->$pk;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($range)) {
|
||||
// 查询关联数据
|
||||
$data = $this->eagerlyManyToMany([
|
||||
['pivot.' . $this->localKey, 'in', array_unique($range)],
|
||||
['pivot.' . $this->morphType, '=', $this->morphClass],
|
||||
], $subRelation, $closure, $cache, true);
|
||||
|
||||
// 关联数据封装
|
||||
foreach ($resultSet as $result) {
|
||||
if (!isset($data[$result->$pk])) {
|
||||
$data[$result->$pk] = [];
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $this->resultSetBuild($data[$result->$pk]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(单个数据).
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResult(Model $result, string $relation, array $subRelation, ?Closure $closure = null, array $cache = []): void
|
||||
{
|
||||
$pk = $result->getPk();
|
||||
|
||||
if (isset($result->$pk)) {
|
||||
$pk = $result->$pk;
|
||||
// 查询管理数据
|
||||
$data = $this->eagerlyManyToMany([
|
||||
['pivot.' . $this->localKey, '=', $pk],
|
||||
['pivot.' . $this->morphType, '=', $this->morphClass],
|
||||
], $subRelation, $closure, $cache);
|
||||
|
||||
// 关联数据封装
|
||||
if (!isset($data[$pk])) {
|
||||
$data[$pk] = [];
|
||||
}
|
||||
|
||||
$result->setRelation($relation, $this->resultSetBuild($data[$pk]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联统计
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function relationCount(Model $result, ?Closure $closure = null, string $aggregate = 'count', string $field = '*', ?string &$name = null)
|
||||
{
|
||||
$pk = $result->getPk();
|
||||
|
||||
if (!isset($result->$pk)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
|
||||
['pivot.' . $this->localKey, '=', $result->$pk],
|
||||
['pivot.' . $this->morphType, '=', $this->morphClass],
|
||||
])->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关联统计子查询.
|
||||
*
|
||||
* @param Closure $closure 闭包
|
||||
* @param string $aggregate 聚合查询方法
|
||||
* @param string $field 字段
|
||||
* @param string $name 统计字段别名
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRelationCountQuery(?Closure $closure = null, string $aggregate = 'count', string $field = '*', ?string &$name = null): string
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query, $name);
|
||||
}
|
||||
|
||||
return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
|
||||
['pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->getTable(true) . '.' . $this->parent->getPk())],
|
||||
['pivot.' . $this->morphType, '=', $this->morphClass],
|
||||
])->fetchSql()->$aggregate($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* BELONGS TO MANY 关联查询.
|
||||
*
|
||||
* @param string $foreignKey 关联模型关联键
|
||||
* @param string $localKey 当前模型关联键
|
||||
* @param array $condition 关联查询条件
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query
|
||||
{
|
||||
// 关联查询封装
|
||||
$tableName = $this->query->getTable();
|
||||
$table = $this->pivot->db()->getTable();
|
||||
$fields = $this->getQueryFields($tableName);
|
||||
|
||||
$query = $this->query
|
||||
->field($fields)
|
||||
->tableField(true, $table, 'pivot', 'pivot__');
|
||||
|
||||
if (empty($this->baseQuery)) {
|
||||
$relationFk = $this->query->getPk();
|
||||
$query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk)
|
||||
->where($condition);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多对多 关联模型预查询.
|
||||
*
|
||||
* @param array $where 关联预查询条件
|
||||
* @param array $subRelation 子关联
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
* @param bool $collection 是否数据集查询
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function eagerlyManyToMany(array $where, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false): array
|
||||
{
|
||||
if ($closure) {
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
$withLimit = $this->query->getOption('limit');
|
||||
if ($withLimit && $collection) {
|
||||
$this->query->removeOption('limit');
|
||||
}
|
||||
|
||||
if ($this->isOneofMany) {
|
||||
// 仅获取一条关联数据
|
||||
if (!$collection) {
|
||||
$this->query->limit(1);
|
||||
} else {
|
||||
$withLimit = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 预载入关联查询 支持嵌套预载入
|
||||
$list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)
|
||||
->with($subRelation)
|
||||
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
|
||||
->lazy();
|
||||
|
||||
// 组装模型数据
|
||||
$data = [];
|
||||
foreach ($list as $set) {
|
||||
$pivot = $set->getRelation('pivot');
|
||||
$key = $pivot[$this->localKey];
|
||||
|
||||
if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$set->setRelation($this->pivotDataName, $this->newPivot($pivot));
|
||||
|
||||
$data[$key][] = $set;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 附加关联的一个中间表数据.
|
||||
*
|
||||
* @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
|
||||
* @param array $pivot 中间表额外数据
|
||||
*
|
||||
* @return array|Pivot
|
||||
*/
|
||||
public function attach($data, array $pivot = [])
|
||||
{
|
||||
if (is_array($data)) {
|
||||
if (key($data) === 0) {
|
||||
$id = $data;
|
||||
} else {
|
||||
// 保存关联表数据
|
||||
$model = new $this->model();
|
||||
$id = $model->insertGetId($data);
|
||||
}
|
||||
} elseif (is_numeric($data) || is_string($data)) {
|
||||
// 根据关联表主键直接写入中间表
|
||||
$id = $data;
|
||||
} elseif ($data instanceof Model) {
|
||||
// 根据关联表主键直接写入中间表
|
||||
$id = $data->getKey();
|
||||
}
|
||||
|
||||
if (!empty($id)) {
|
||||
// 保存中间表数据
|
||||
$pivot[$this->localKey] = $this->parent->getKey();
|
||||
$pivot[$this->morphType] = $this->morphClass;
|
||||
|
||||
$result = [];
|
||||
foreach ((array) $ids as $id) {
|
||||
$pivot[$this->foreignKey] = $id;
|
||||
$object = $this->newPivot();
|
||||
$object->replace()->save($pivot);
|
||||
|
||||
$result[] = $object;
|
||||
}
|
||||
|
||||
if (count($result) == 1) {
|
||||
// 返回中间表模型对象
|
||||
$result = $result[0];
|
||||
}
|
||||
|
||||
return $result;
|
||||
} else {
|
||||
throw new Exception('miss relation data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否存在关联数据.
|
||||
*
|
||||
* @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键
|
||||
*
|
||||
* @return Pivot|false
|
||||
*/
|
||||
public function attached($data)
|
||||
{
|
||||
if ($data instanceof Model) {
|
||||
$id = $data->getKey();
|
||||
} else {
|
||||
$id = $data;
|
||||
}
|
||||
|
||||
$pivot = $this->pivot
|
||||
->where($this->localKey, $this->parent->getKey())
|
||||
->where($this->morphType, $this->morphClass)
|
||||
->where($this->foreignKey, $id)
|
||||
->find();
|
||||
|
||||
return $pivot ?: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除关联的一个中间表数据.
|
||||
*
|
||||
* @param int|array $data 数据 可以使用关联对象的主键
|
||||
* @param bool $relationDel 是否同时删除关联表数据
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function detach($data = null, bool $relationDel = false): int
|
||||
{
|
||||
if (is_array($data)) {
|
||||
$id = $data;
|
||||
} elseif (is_numeric($data) || is_string($data)) {
|
||||
// 根据关联表主键直接写入中间表
|
||||
$id = $data;
|
||||
} elseif ($data instanceof Model) {
|
||||
// 根据关联表主键直接写入中间表
|
||||
$id = $data->getKey();
|
||||
}
|
||||
|
||||
// 删除中间表数据
|
||||
$pivot = [
|
||||
[$this->localKey, '=', $this->parent->getKey()],
|
||||
[$this->morphType, '=', $this->morphClass],
|
||||
];
|
||||
|
||||
if (isset($id)) {
|
||||
$pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id];
|
||||
}
|
||||
|
||||
$result = $this->newPivot()->where($pivot)->delete();
|
||||
|
||||
// 删除关联表数据
|
||||
if (isset($id) && $relationDel) {
|
||||
$model = $this->model;
|
||||
$model::destroy($id);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据同步.
|
||||
*
|
||||
* @param array $ids
|
||||
* @param bool $detaching
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function sync(array $ids, bool $detaching = true): array
|
||||
{
|
||||
$changes = [
|
||||
'attached' => [],
|
||||
'detached' => [],
|
||||
'updated' => [],
|
||||
];
|
||||
|
||||
$current = $this->pivot
|
||||
->where($this->localKey, $this->parent->getKey())
|
||||
->where($this->morphType, $this->morphClass)
|
||||
->column($this->foreignKey);
|
||||
|
||||
$records = [];
|
||||
|
||||
foreach ($ids as $key => $value) {
|
||||
if (!is_array($value)) {
|
||||
$records[$value] = [];
|
||||
} else {
|
||||
$records[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$detach = array_diff($current, array_keys($records));
|
||||
|
||||
if ($detaching && count($detach) > 0) {
|
||||
$this->detach($detach);
|
||||
$changes['detached'] = $detach;
|
||||
}
|
||||
|
||||
foreach ($records as $id => $attributes) {
|
||||
if (!in_array($id, $current)) {
|
||||
$this->attach($id, $attributes);
|
||||
$changes['attached'][] = $id;
|
||||
} elseif (count($attributes) > 0) {
|
||||
$this->detach($id);
|
||||
$this->attach($id, $attributes);
|
||||
$changes['updated'][] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行基础查询(仅执行一次).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function baseQuery(): void
|
||||
{
|
||||
if (empty($this->baseQuery)) {
|
||||
$foreignKey = $this->foreignKey;
|
||||
$localKey = $this->localKey;
|
||||
|
||||
// 关联查询
|
||||
$this->belongsToManyQuery($foreignKey, $localKey, [
|
||||
['pivot.' . $localKey, '=', $this->parent->getKey()],
|
||||
['pivot.' . $this->morphType, '=', $this->morphClass],
|
||||
]);
|
||||
|
||||
$this->baseQuery = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置或获取多态关系的模型名映射别名的数组.
|
||||
*
|
||||
* @param array|null $map
|
||||
* @param bool $merge
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function morphMap(?array $map = null, $merge = true): array
|
||||
{
|
||||
if (is_array($map)) {
|
||||
static::$morphMap = $merge && static::$morphMap
|
||||
? $map + static::$morphMap : $map;
|
||||
}
|
||||
|
||||
return static::$morphMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2025 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace think\model\relation;
|
||||
|
||||
use Closure;
|
||||
use think\db\BaseQuery as Query;
|
||||
use think\db\exception\DbException as Exception;
|
||||
use think\db\exception\InvalidArgumentException;
|
||||
use think\helper\Str;
|
||||
use think\model\contract\Modelable as Model;
|
||||
use think\model\Relation;
|
||||
|
||||
/**
|
||||
* 一对一关联基础类.
|
||||
*/
|
||||
abstract class OneToOne extends Relation
|
||||
{
|
||||
/**
|
||||
* JOIN类型.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $joinType = 'INNER';
|
||||
|
||||
/**
|
||||
* 绑定的关联属性.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $bindAttr = [];
|
||||
|
||||
/**
|
||||
* 关联名.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $relation;
|
||||
|
||||
/**
|
||||
* 获取一对多关联的最新一条数据.
|
||||
*
|
||||
* @param string $field 排序字段
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function firstOfMany(string $field = '')
|
||||
{
|
||||
return $this->first($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一对多关联的最旧一条数据.
|
||||
*
|
||||
* @param string $field 排序字段
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function lastOfMany(string $field = '')
|
||||
{
|
||||
return $this->last($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置join类型.
|
||||
*
|
||||
* @param string $type JOIN类型
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function joinType(string $type)
|
||||
{
|
||||
$this->joinType = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(JOIN方式).
|
||||
*
|
||||
* @param Query $query 查询对象
|
||||
* @param string $relation 关联名
|
||||
* @param mixed $field 关联字段
|
||||
* @param string $joinType JOIN方式
|
||||
* @param Closure $closure 闭包条件
|
||||
* @param bool $first
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerly(Query $query, string $relation, $field = true, string $joinType = '', ?Closure $closure = null, bool $first = false): void
|
||||
{
|
||||
$name = Str::snake(class_basename($this->parent));
|
||||
|
||||
if ($first) {
|
||||
$table = $query->getTable();
|
||||
$query->table([$table => $name]);
|
||||
|
||||
if ($query->getOption('field')) {
|
||||
$masterField = $query->getOption('field');
|
||||
$query->removeOption('field');
|
||||
} else {
|
||||
$masterField = true;
|
||||
}
|
||||
|
||||
$query->tableField($masterField, $table, $name);
|
||||
}
|
||||
|
||||
// 预载入封装
|
||||
$joinTable = $this->query->getTable();
|
||||
$joinAlias = Str::snake($relation);
|
||||
$joinType = $joinType ?: $this->joinType;
|
||||
if (true !== $field) {
|
||||
$joinField = $field;
|
||||
} elseif ($this->query->getOption('field')) {
|
||||
$joinField = $this->query->getOption('field');
|
||||
} else {
|
||||
$joinField = $field;
|
||||
}
|
||||
|
||||
$query->via($joinAlias);
|
||||
|
||||
if ($this instanceof BelongsTo) {
|
||||
$foreignKeyExp = $this->foreignKey;
|
||||
|
||||
if (!str_contains($foreignKeyExp, '.')) {
|
||||
$foreignKeyExp = $name . '.' . $this->foreignKey;
|
||||
}
|
||||
|
||||
$joinOn = $foreignKeyExp . '=' . $joinAlias . '.' . $this->localKey;
|
||||
} else {
|
||||
$foreignKeyExp = $this->foreignKey;
|
||||
|
||||
if (!str_contains($foreignKeyExp, '.')) {
|
||||
$foreignKeyExp = $joinAlias . '.' . $this->foreignKey;
|
||||
}
|
||||
|
||||
$joinOn = $name . '.' . $this->localKey . '=' . $foreignKeyExp;
|
||||
}
|
||||
|
||||
if ($closure) {
|
||||
// 执行闭包查询
|
||||
$closure($query);
|
||||
|
||||
// 使用field指定获取关联的字段
|
||||
$withField = $query->getOption('field');
|
||||
if ($withField) {
|
||||
$joinField = $withField;
|
||||
}
|
||||
$query->removeOption('field');
|
||||
}
|
||||
|
||||
$query->join([$joinTable => $joinAlias], $joinOn, $joinType)
|
||||
->tableField($joinField, $joinTable, $joinAlias, $joinAlias . '__');
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据集).
|
||||
*
|
||||
* @param array $resultSet
|
||||
* @param string $relation
|
||||
* @param array $subRelation
|
||||
* @param Closure $closure
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], ?Closure $closure = null);
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据).
|
||||
*
|
||||
* @param Model $result
|
||||
* @param string $relation
|
||||
* @param array $subRelation
|
||||
* @param Closure $closure
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null);
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据集).
|
||||
*
|
||||
* @param array $resultSet 数据集
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
* @param bool $join 是否为JOIN方式
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $join = false): void
|
||||
{
|
||||
if ($join) {
|
||||
// 模型JOIN关联组装
|
||||
foreach ($resultSet as $result) {
|
||||
$this->match($this->model, $relation, $result);
|
||||
}
|
||||
} else {
|
||||
// IN查询
|
||||
$this->eagerlySet($resultSet, $relation, $subRelation, $closure, $cache);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预载入关联查询(数据).
|
||||
*
|
||||
* @param Model $result 数据对象
|
||||
* @param string $relation 当前关联名
|
||||
* @param array $subRelation 子关联名
|
||||
* @param Closure $closure 闭包
|
||||
* @param array $cache 关联缓存
|
||||
* @param bool $join 是否为JOIN方式
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function eagerlyResult(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $join = false): void
|
||||
{
|
||||
if ($join) {
|
||||
// 模型JOIN关联组装
|
||||
$this->match($this->model, $relation, $result);
|
||||
} else {
|
||||
// IN查询
|
||||
$this->eagerlyOne($result, $relation, $subRelation, $closure, $cache);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存(新增)当前关联数据对象
|
||||
*
|
||||
* @param array|Model $data 数据 可以使用数组 关联模型对象
|
||||
* @param bool $replace 是否自动识别更新和写入
|
||||
*
|
||||
* @return Model|false
|
||||
*/
|
||||
public function save(array | Model $data, bool $replace = true)
|
||||
{
|
||||
$model = $this->make();
|
||||
|
||||
return $model->replace($replace)->save($data) ? $model : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建关联对象实例.
|
||||
*
|
||||
* @param array|Model $data
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function make(array | Model $data = []): Model
|
||||
{
|
||||
if ($data instanceof Model) {
|
||||
$data = $data->getData();
|
||||
}
|
||||
|
||||
// 保存关联表数据
|
||||
$data[$this->foreignKey] = $this->parent->{$this->localKey};
|
||||
|
||||
return (new $this->model($data))->setSuffix($this->getModel()->getSuffix());
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定关联表的属性到父模型属性.
|
||||
*
|
||||
* @param array $attr 要绑定的属性列表
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function bind(array $attr)
|
||||
{
|
||||
$this->bindAttr = $attr;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一对一 关联模型预查询拼装.
|
||||
*
|
||||
* @param string $model 模型名称
|
||||
* @param string $relation 关联名
|
||||
* @param Model $result 模型对象实例
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function match(string $model, string $relation, Model $result): void
|
||||
{
|
||||
$data = $result->getRelation($relation);
|
||||
if (!empty($data)) {
|
||||
if ($this->bindAttr) {
|
||||
$result->bindRelationAttr($data, $this->bindAttr);
|
||||
} else {
|
||||
$relationModel = new $model($data);
|
||||
$result->setRelation($relation, $relationModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一对一 关联模型预查询(IN方式).
|
||||
*
|
||||
* @param array $where 关联预查询条件
|
||||
* @param string $key 关联键名
|
||||
* @param array $subRelation 子关联
|
||||
* @param Closure $closure
|
||||
* @param array $cache 关联缓存
|
||||
* @param bool $collection 是否数据集查询
|
||||
* @return array
|
||||
*/
|
||||
protected function eagerlyWhere(array $where, string $key, array $subRelation = [], ?Closure $closure = null, array $cache = [], bool $collection = false)
|
||||
{
|
||||
// 预载入关联查询 支持嵌套预载入
|
||||
if ($closure) {
|
||||
$this->baseQuery = true;
|
||||
$closure($this->query);
|
||||
}
|
||||
|
||||
if ($collection) {
|
||||
$this->query->removeOption('limit');
|
||||
} else {
|
||||
$this->query->limit(1);
|
||||
}
|
||||
|
||||
$list = $this->query
|
||||
->where($where)
|
||||
->with($subRelation)
|
||||
->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
|
||||
->lazy();
|
||||
|
||||
// 组装模型数据
|
||||
$data = [];
|
||||
foreach ($list as $set) {
|
||||
if (!isset($data[$set->$key])) {
|
||||
$data[$set->$key] = $set;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user