diff --git a/composer.lock b/composer.lock index 90a2b1758..f690b9b5c 100644 --- a/composer.lock +++ b/composer.lock @@ -721,16 +721,16 @@ }, { "name": "topthink/think-orm", - "version": "v2.0.29", + "version": "v2.0.30", "source": { "type": "git", "url": "https://github.com/top-think/think-orm.git", - "reference": "37cde31af3725cf5460ad8dc18032326b88e6310" + "reference": "b6f61fc243974a25cb6914b84a529b373a717626" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/top-think/think-orm/zipball/37cde31af3725cf5460ad8dc18032326b88e6310", - "reference": "37cde31af3725cf5460ad8dc18032326b88e6310", + "url": "https://api.github.com/repos/top-think/think-orm/zipball/b6f61fc243974a25cb6914b84a529b373a717626", + "reference": "b6f61fc243974a25cb6914b84a529b373a717626", "shasum": "", "mirrors": [ { @@ -768,7 +768,7 @@ "database", "orm" ], - "time": "2019-12-23T13:50:01+00:00" + "time": "2020-01-02T09:09:50+00:00" }, { "name": "topthink/think-template", diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 000000000..112ce7b6f --- /dev/null +++ b/nbproject/project.properties @@ -0,0 +1,7 @@ +include.path=${php.global.include.path} +php.version=PHP_74 +source.encoding=UTF-8 +src.dir=. +tags.asp=false +tags.short=false +web.root=. diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 000000000..52118b3e6 --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,9 @@ + + + org.netbeans.modules.php.project + + + thinkadmin + + + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 6dff9464e..a2350a4e2 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -442,6 +442,7 @@ return array( 'think\\model\\relation\\MorphMany' => $vendorDir . '/topthink/think-orm/src/model/relation/MorphMany.php', 'think\\model\\relation\\MorphOne' => $vendorDir . '/topthink/think-orm/src/model/relation/MorphOne.php', 'think\\model\\relation\\MorphTo' => $vendorDir . '/topthink/think-orm/src/model/relation/MorphTo.php', + 'think\\model\\relation\\MorphToMany' => $vendorDir . '/topthink/think-orm/src/model/relation/MorphToMany.php', 'think\\model\\relation\\OneToOne' => $vendorDir . '/topthink/think-orm/src/model/relation/OneToOne.php', 'think\\paginator\\driver\\Bootstrap' => $vendorDir . '/topthink/think-orm/src/paginator/driver/Bootstrap.php', 'think\\response\\File' => $vendorDir . '/topthink/framework/src/think/response/File.php', diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index bdb39119d..1fd46421b 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -575,6 +575,7 @@ class ComposerStaticInit2b1316f37dd8fe5c4c25969e0b842e8e 'think\\model\\relation\\MorphMany' => __DIR__ . '/..' . '/topthink/think-orm/src/model/relation/MorphMany.php', 'think\\model\\relation\\MorphOne' => __DIR__ . '/..' . '/topthink/think-orm/src/model/relation/MorphOne.php', 'think\\model\\relation\\MorphTo' => __DIR__ . '/..' . '/topthink/think-orm/src/model/relation/MorphTo.php', + 'think\\model\\relation\\MorphToMany' => __DIR__ . '/..' . '/topthink/think-orm/src/model/relation/MorphToMany.php', 'think\\model\\relation\\OneToOne' => __DIR__ . '/..' . '/topthink/think-orm/src/model/relation/OneToOne.php', 'think\\paginator\\driver\\Bootstrap' => __DIR__ . '/..' . '/topthink/think-orm/src/paginator/driver/Bootstrap.php', 'think\\response\\File' => __DIR__ . '/..' . '/topthink/framework/src/think/response/File.php', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 498e3ed89..a85cbe4e8 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -738,17 +738,17 @@ }, { "name": "topthink/think-orm", - "version": "v2.0.29", - "version_normalized": "2.0.29.0", + "version": "v2.0.30", + "version_normalized": "2.0.30.0", "source": { "type": "git", "url": "https://github.com/top-think/think-orm.git", - "reference": "37cde31af3725cf5460ad8dc18032326b88e6310" + "reference": "b6f61fc243974a25cb6914b84a529b373a717626" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/top-think/think-orm/zipball/37cde31af3725cf5460ad8dc18032326b88e6310", - "reference": "37cde31af3725cf5460ad8dc18032326b88e6310", + "url": "https://api.github.com/repos/top-think/think-orm/zipball/b6f61fc243974a25cb6914b84a529b373a717626", + "reference": "b6f61fc243974a25cb6914b84a529b373a717626", "shasum": "", "mirrors": [ { @@ -764,7 +764,7 @@ "psr/simple-cache": "^1.0", "topthink/think-helper": "^3.1" }, - "time": "2019-12-23T13:50:01+00:00", + "time": "2020-01-02T09:09:50+00:00", "type": "library", "installation-source": "dist", "autoload": { diff --git a/vendor/services.php b/vendor/services.php index cd6f10d8b..cf22079cc 100644 --- a/vendor/services.php +++ b/vendor/services.php @@ -1,5 +1,5 @@ 'think\\app\\Service', diff --git a/vendor/topthink/think-orm/src/db/PDOConnection.php b/vendor/topthink/think-orm/src/db/PDOConnection.php index 482e75998..0d23b832f 100644 --- a/vendor/topthink/think-orm/src/db/PDOConnection.php +++ b/vendor/topthink/think-orm/src/db/PDOConnection.php @@ -906,7 +906,8 @@ abstract class PDOConnection extends Connection implements ConnectionInterface return 0; } - $query->parseOptions(); + $options = $query->parseOptions(); + $replace = !empty($options['replace']); if (0 === $limit && count($dataSet) >= 5000) { $limit = 1000; @@ -921,7 +922,7 @@ abstract class PDOConnection extends Connection implements ConnectionInterface $count = 0; foreach ($array as $item) { - $sql = $this->builder->insertAll($query, $item); + $sql = $this->builder->insertAll($query, $item, $replace); $count += $this->execute($query, $sql, $query->getBind()); } @@ -935,7 +936,7 @@ abstract class PDOConnection extends Connection implements ConnectionInterface return $count; } - $sql = $this->builder->insertAll($query, $dataSet); + $sql = $this->builder->insertAll($query, $dataSet, $replace); return $this->execute($query, $sql, $query->getBind()); } diff --git a/vendor/topthink/think-orm/src/db/connector/Mongo.php b/vendor/topthink/think-orm/src/db/connector/Mongo.php index 4eb631b4e..809246f7e 100644 --- a/vendor/topthink/think-orm/src/db/connector/Mongo.php +++ b/vendor/topthink/think-orm/src/db/connector/Mongo.php @@ -23,6 +23,7 @@ use MongoDB\Driver\Exception\RuntimeException; use MongoDB\Driver\Manager; use MongoDB\Driver\Query as MongoQuery; use MongoDB\Driver\ReadPreference; +use MongoDB\Driver\WriteConcern; use think\db\BaseQuery; use think\db\builder\Mongo as Builder; use think\db\Connection; @@ -41,6 +42,8 @@ class Mongo extends Connection implements ConnectionInterface protected $typeMap = 'array'; protected $mongo; // MongoDb Object protected $cursor; // MongoCursor Object + protected $session_uuid; // sessions会话列表当前会话数组key 随机生成 + protected $sessions = []; // 会话列表 // 数据库连接参数配置 protected $config = [ @@ -265,7 +268,14 @@ class Mongo extends Connection implements ConnectionInterface $readPreference = $options['readPreference'] ?? null; $this->queryStartTime = microtime(true); - $this->cursor = $this->mongo->executeQuery($namespace, $mongoQuery, $readPreference); + if ($session = $this->getSession()) { + $this->cursor = $this->mongo->executeQuery($namespace, $query, [ + 'readPreference' => is_null($readPreference) ? new ReadPreference(ReadPreference::RP_PRIMARY) : $readPreference, + 'session' => $session + ]); + } else { + $this->cursor = $this->mongo->executeQuery($namespace, $mongoQuery, $readPreference); + } // SQL监控 if (!empty($this->config['trigger_sql'])) { @@ -351,7 +361,14 @@ class Mongo extends Connection implements ConnectionInterface $writeConcern = $options['writeConcern'] ?? null; $this->queryStartTime = microtime(true); - $writeResult = $this->mongo->executeBulkWrite($namespace, $bulk, $writeConcern); + if ($session = $this->getSession()) { + $writeResult = $this->mongo->executeBulkWrite($namespace, $bulk, [ + 'session' => $session, + 'writeConcern' => is_null($writeConcern) ? new WriteConcern(1) : $writeConcern + ]); + } else { + $writeResult = $this->mongo->executeBulkWrite($namespace, $bulk, $writeConcern); + } // SQL监控 if (!empty($this->config['trigger_sql'])) { @@ -403,7 +420,14 @@ class Mongo extends Connection implements ConnectionInterface $this->queryStr = 'db.' . $this->queryStr; } - $this->cursor = $this->mongo->executeCommand($dbName, $command, $readPreference); + if ($session = $this->getSession()) { + $this->cursor = $this->mongo->executeCommand($dbName, $command, [ + 'readPreference' => is_null($readPreference) ? new ReadPreference(ReadPreference::RP_PRIMARY) : $readPreference, + 'session' => $session + ]); + } else { + $this->cursor = $this->mongo->executeCommand($dbName, $command, $readPreference); + } // SQL监控 if (!empty($this->config['trigger_sql'])) { @@ -1033,7 +1057,23 @@ class Mongo extends Connection implements ConnectionInterface * @throws \Throwable */ public function transaction(callable $callback) - {} + { + $this->startTrans(); + try { + $result = null; + if (is_callable($callback)) { + $result = call_user_func_array($callback, [$this]); + } + $this->commit(); + return $result; + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } catch (\Throwable $e) { + $this->rollback(); + throw $e; + } + } /** * 启动事务 @@ -1043,7 +1083,13 @@ class Mongo extends Connection implements ConnectionInterface * @throws \Exception */ public function startTrans() - {} + { + $this->initConnect(true); + $this->session_uuid = uniqid(); + $this->sessions[$this->session_uuid] = $this->getMongo()->startSession(); + + $this->sessions[$this->session_uuid]->startTransaction([]); + } /** * 用于非自动提交状态下面的查询提交 @@ -1052,7 +1098,12 @@ class Mongo extends Connection implements ConnectionInterface * @throws PDOException */ public function commit() - {} + { + if ($session = $this->getSession()) { + $session->commitTransaction(); + $this->setLastSession(); + } + } /** * 事务回滚 @@ -1061,6 +1112,40 @@ class Mongo extends Connection implements ConnectionInterface * @throws PDOException */ public function rollback() - {} + { + if ($session = $this->getSession()) { + $session->abortTransaction(); + $this->setLastSession(); + } + } + /** + * 结束当前会话,设置上一个会话为当前会话 + * @author klinson + */ + protected function setLastSession() + { + if ($session = $this->getSession()) { + $session->endSession(); + unset($this->sessions[$this->session_uuid]); + if (empty($this->sessions)) { + $this->session_uuid = null; + } else { + end($this->sessions); + $this->session_uuid = key($this->sessions); + } + } + } + + /** + * 获取当前会话 + * @return \MongoDB\Driver\Session|null + * @author klinson + */ + public function getSession() + { + return ($this->session_uuid && isset($this->sessions[$this->session_uuid])) + ? $this->sessions[$this->session_uuid] + : null; + } } diff --git a/vendor/topthink/think-orm/src/model/concern/Attribute.php b/vendor/topthink/think-orm/src/model/concern/Attribute.php index 5c0eea75a..a02dc3fb7 100644 --- a/vendor/topthink/think-orm/src/model/concern/Attribute.php +++ b/vendor/topthink/think-orm/src/model/concern/Attribute.php @@ -411,7 +411,7 @@ trait Attribute if (empty($param)) { $value = (float) $value; } else { - $value = (float) number_format($value, $param, '.', ''); + $value = (float) number_format($value, (int) $param, '.', ''); } break; case 'boolean': @@ -576,7 +576,7 @@ trait Attribute if (empty($param)) { $value = (float) $value; } else { - $value = (float) number_format($value, $param, '.', ''); + $value = (float) number_format($value, (int) $param, '.', ''); } break; case 'boolean': diff --git a/vendor/topthink/think-orm/src/model/concern/RelationShip.php b/vendor/topthink/think-orm/src/model/concern/RelationShip.php index 7b0d4a76a..483922813 100644 --- a/vendor/topthink/think-orm/src/model/concern/RelationShip.php +++ b/vendor/topthink/think-orm/src/model/concern/RelationShip.php @@ -28,6 +28,7 @@ use think\model\relation\HasOneThrough; use think\model\relation\MorphMany; use think\model\relation\MorphOne; use think\model\relation\MorphTo; +use think\model\relation\MorphToMany; use think\model\relation\OneToOne; /** @@ -617,6 +618,65 @@ trait RelationShip return new MorphTo($this, $morphType, $foreignKey, $alias, $relation); } + /** + * MORPH TO MANY关联定义 + * @access public + * @param string $model 模型名 + * @param string $middle 中间表名/模型名 + * @param string|array $morph 多态字段信息 + * @param string $localKey 当前模型关联键 + * @return MorphToMany + */ + public function morphToMany(string $model, string $middle, $morph = null, string $localKey = null): MorphToMany + { + if (is_null($morph)) { + $morph = $middle; + } + + // 记录当前关联信息 + if (is_array($morph)) { + [$morphType, $morphKey] = $morph; + } else { + $morphType = $morph . '_type'; + $morphKey = $morph . '_id'; + } + + $model = $this->parseModel($model); + $name = Str::snake(class_basename($model)); + $localKey = $localKey ?: $this->getForeignKey($name); + + return new MorphToMany($this, $model, $middle, $morphType, $morphKey, $localKey); + } + + /** + * MORPH BY MANY关联定义 + * @access public + * @param string $model 模型名 + * @param string $middle 中间表名/模型名 + * @param string|array $morph 多态字段信息 + * @param string $foreignKey 关联外键 + * @return MorphToMany + */ + public function morphByMany(string $model, string $middle, $morph = null, string $foreignKey = null): MorphToMany + { + if (is_null($morph)) { + $morph = $middle; + } + + // 记录当前关联信息 + if (is_array($morph)) { + [$morphType, $morphKey] = $morph; + } else { + $morphType = $morph . '_type'; + $morphKey = $morph . '_id'; + } + + $model = $this->parseModel($model); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + + return new MorphToMany($this, $model, $middle, $morphType, $morphKey, $foreignKey, true); + } + /** * 解析模型的完整命名空间 * @access protected diff --git a/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php b/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php index eccbfe9c5..69888c90e 100644 --- a/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php +++ b/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php @@ -157,9 +157,7 @@ class BelongsToMany extends Relation $localKey = $this->localKey; // 关联查询 - $pk = $this->parent->getPk(); - - $condition = ['pivot.' . $localKey, '=', $this->parent->$pk]; + $condition = ['pivot.' . $localKey, '=', $this->parent->getKey()]; return $this->belongsToManyQuery($foreignKey, $localKey, [$condition]); } @@ -454,7 +452,6 @@ class BelongsToMany extends Relation } } } - $key = $pivot[$this->localKey]; if ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->withLimit) { @@ -562,16 +559,14 @@ class BelongsToMany extends Relation $id = $data; } elseif ($data instanceof Model) { // 根据关联表主键直接写入中间表 - $relationFk = $data->getPk(); - $id = $data->$relationFk; + $id = $data->getKey(); } if (!empty($id)) { // 保存中间表数据 - $pk = $this->parent->getPk(); - $pivot[$this->localKey] = $this->parent->$pk; - $ids = (array) $id; + $pivot[$this->localKey] = $this->parent->getKey(); + $ids = (array) $id; foreach ($ids as $id) { $pivot[$this->foreignKey] = $id; $this->pivot->replace() @@ -630,14 +625,12 @@ class BelongsToMany extends Relation $id = $data; } elseif ($data instanceof Model) { // 根据关联表主键直接写入中间表 - $relationFk = $data->getPk(); - $id = $data->$relationFk; + $id = $data->getKey(); } // 删除中间表数据 - $pk = $this->parent->getPk(); $pivot = []; - $pivot[] = [$this->localKey, '=', $this->parent->$pk]; + $pivot[] = [$this->localKey, '=', $this->parent->getKey()]; if (isset($id)) { $pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id]; @@ -669,10 +662,8 @@ class BelongsToMany extends Relation 'updated' => [], ]; - $pk = $this->parent->getPk(); - $current = $this->pivot - ->where($this->localKey, $this->parent->$pk) + ->where($this->localKey, $this->parent->getKey()) ->column($this->foreignKey); $records = []; diff --git a/vendor/topthink/think-orm/src/model/relation/MorphToMany.php b/vendor/topthink/think-orm/src/model/relation/MorphToMany.php new file mode 100644 index 000000000..7b8438b99 --- /dev/null +++ b/vendor/topthink/think-orm/src/model/relation/MorphToMany.php @@ -0,0 +1,474 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\BaseQuery as Query; +use think\db\Raw; +use think\helper\Str; +use think\Model; +use think\model\Relation; + +/** + * 多态多对多关联 + */ +class MorphToMany extends BelongsToMany +{ + + /** + * 多态关联外键 + * @var string + */ + protected $morphKey; + + /** + * 多态字段名 + * @var string + */ + protected $morphType; + + /** + * 多态模型名 + * @var string + */ + protected $morphClass; + + /** + * 是否反向关联 + * @var bool + */ + protected $inverse; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $middle 中间表名/模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $localKey 当前模型关联键 + * @param bool $inverse 反向关联 + */ + public function __construct(Model $parent, string $model, string $middle, string $morphType, string $morphKey, string $localKey, bool $inverse = false) + { + $this->morphKey = $morphKey; + $this->morphType = $morphType; + $this->inverse = $inverse; + $this->morphClass = $inverse ? $model : get_class($parent); + + parent::__construct($parent, $model, $middle, $morphKey, $localKey); + } + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void + { + $pk = $resultSet[0]->getPk(); + $range = []; + + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + // 查询关联数据 + $data = $this->eagerlyManyToMany([ + ['pivot.' . $this->morphKey, 'in', $range], + ['pivot.' . $this->morphType, '=', $this->morphClass], + ], $subRelation, $closure, $cache); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + $result->setRelation($relation, $this->resultSetBuild($data[$result->$pk], clone $this->parent)); + } + } + } + + /** + * 预载入关联查询(单个数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResult(Model $result, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void + { + $pk = $result->getPk(); + + if (isset($result->$pk)) { + $pk = $result->$pk; + // 查询管理数据 + $data = $this->eagerlyManyToMany([ + ['pivot.' . $this->morphKey, '=', $pk], + ['pivot.' . $this->morphType, '=', $this->morphClass], + ], $subRelation, $closure, $cache); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent)); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): float + { + $pk = $result->getPk(); + + if (!isset($result->$pk)) { + return 0; + } + + $pk = $result->$pk; + + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + return $this->belongsToManyQuery($this->morphKey, $this->localKey, [ + ['pivot.' . ($this->inverse ? $this->localKey : $this->morphKey), '=', $pk], + ['pivot.' . $this->morphType, '=', $this->morphClass], + ])->$aggregate($field); + } + + /** + * 获取关联统计子查询 + * @access public + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return string + */ + public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string + { + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + return $this->belongsToManyQuery($this->morphKey, $this->localKey, [ + ['pivot.' . ($this->inverse ? $this->localKey : $this->morphKey), 'exp', new Raw('=' . $this->parent->db(false)->getTable() . '.' . $this->parent->getPk())], + ['pivot.' . $this->morphType, '=', $this->morphClass], + ])->fetchSql()->$aggregate($field); + } + + /** + * BELONGS TO MANY 关联查询 + * @access protected + * @param string $foreignKey 关联模型关联键 + * @param string $localKey 当前模型关联键 + * @param array $condition 关联查询条件 + * @return Query + */ + protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query + { + // 关联查询封装 + $tableName = $this->query->getTable(); + $table = $this->pivot->db()->getTable(); + $fields = $this->getQueryFields($tableName); + + if ($this->withLimit) { + $this->query->limit($this->withLimit); + } + + $query = $this->query + ->field($fields) + ->tableField(true, $table, 'pivot', 'pivot__'); + + if (empty($this->baseQuery)) { + $relationFk = $this->query->getPk(); + $query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk) + ->where($condition); + } + + return $query; + } + + /** + * 多对多 关联模型预查询 + * @access protected + * @param array $where 关联预查询条件 + * @param array $subRelation 子关联 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return array + */ + protected function eagerlyManyToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array + { + if ($closure) { + $closure($this->getClosureType($closure)); + } + + // 预载入关联查询 支持嵌套预载入 + $list = $this->belongsToManyQuery($this->morphKey, $this->localKey, $where) + ->with($subRelation) + ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null) + ->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $pivot = []; + foreach ($set->getData() as $key => $val) { + if (strpos($key, '__')) { + [$name, $attr] = explode('__', $key, 2); + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($set->$key); + } + } + } + + $key = $pivot[$this->morphKey]; + + if ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->withLimit) { + continue; + } + + $set->setRelation($this->pivotDataName, $this->newPivot($pivot)); + + $data[$key][] = $set; + } + + return $data; + } + + /** + * 附加关联的一个中间表数据 + * @access public + * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return array|Pivot + * @throws Exception + */ + public function attach($data, array $pivot = []) + { + if (is_array($data)) { + if (key($data) === 0) { + $id = $data; + } else { + // 保存关联表数据 + $model = new $this->model; + $id = $model->insertGetId($data); + } + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $id = $data->getKey(); + } + + if (!empty($id)) { + // 保存中间表数据 + $pivot[$this->inverse ? $this->localKey : $this->morphKey] = $this->parent->getKey(); + + $pivot[$this->morphType] = $this->morphClass; + $ids = (array) $id; + + foreach ($ids as $id) { + $pivot[$this->inverse ? $this->morphKey : $this->localKey] = $id; + + $this->pivot->replace() + ->exists(false) + ->data([]) + ->save($pivot); + $result[] = $this->newPivot($pivot); + } + + if (count($result) == 1) { + // 返回中间表模型对象 + $result = $result[0]; + } + + return $result; + } else { + throw new Exception('miss relation data'); + } + } + + /** + * 判断是否存在关联数据 + * @access public + * @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键 + * @return Pivot|false + */ + public function attached($data) + { + if ($data instanceof Model) { + $id = $data->getKey(); + } else { + $id = $data; + } + + $pivot = $this->pivot + ->where($this->inverse ? $this->localKey : $this->morphKey, $this->parent->getKey()) + ->where($this->morphType, $this->morphClass) + ->where($this->inverse ? $this->morphKey : $this->localKey, $id) + ->find(); + + return $pivot ?: false; + } + + /** + * 解除关联的一个中间表数据 + * @access public + * @param integer|array $data 数据 可以使用关联对象的主键 + * @param bool $relationDel 是否同时删除关联表数据 + * @return integer + */ + public function detach($data = null, bool $relationDel = false): int + { + if (is_array($data)) { + $id = $data; + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $id = $data->getKey(); + } + + // 删除中间表数据 + $pivot = [ + [$this->inverse ? $this->localKey : $this->morphKey, '=', $this->parent->getKey()], + [$this->morphType, '=', $this->morphClass], + ]; + + if (isset($id)) { + $pivot[] = [$this->inverse ? $this->morphKey : $this->localKey, is_array($id) ? 'in' : '=', $id]; + } + + $result = $this->pivot->where($pivot)->delete(); + + // 删除关联表数据 + if (isset($id) && $relationDel) { + $model = $this->model; + $model::destroy($id); + } + + return $result; + } + + /** + * 数据同步 + * @access public + * @param array $ids + * @param bool $detaching + * @return array + */ + public function sync(array $ids, bool $detaching = true): array + { + $changes = [ + 'attached' => [], + 'detached' => [], + 'updated' => [], + ]; + + $current = $this->pivot + ->where($this->inverse ? $this->localKey : $this->morphKey, $this->parent->getKey()) + ->where($this->morphType, $this->morphClass) + ->column($this->inverse ? $this->morphKey : $this->localKey); + + $records = []; + + foreach ($ids as $key => $value) { + if (!is_array($value)) { + $records[$value] = []; + } else { + $records[$key] = $value; + } + } + + $detach = array_diff($current, array_keys($records)); + + if ($detaching && count($detach) > 0) { + $this->detach($detach); + $changes['detached'] = $detach; + } + + foreach ($records as $id => $attributes) { + if (!in_array($id, $current)) { + $this->attach($id, $attributes); + $changes['attached'][] = $id; + } elseif (count($attributes) > 0 && $this->attach($id, $attributes)) { + $changes['updated'][] = $id; + } + } + + return $changes; + } + + /** + * 创建关联查询Query对象 + * @access protected + * @return Query + */ + protected function buildQuery(): Query + { + // 关联查询 + $condition = [ + ['pivot.' . $this->morphKey, '=', $this->parent->getKey()], + ['pivot.' . $this->morphType, '=', $this->morphClass], + ]; + + return $this->belongsToManyQuery($this->morphKey, $this->localKey, $condition); + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery(): void + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $this->query->where([ + [$this->morphKey, '=', $this->parent->getKey()], + [$this->morphType, '=', $this->morphClass], + ]); + + $this->baseQuery = true; + } + } + +}