BelongsToMany.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkPHP [ WE CAN DO IT JUST THINK ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: liu21st <liu21st@gmail.com>
  10. // +----------------------------------------------------------------------
  11. namespace think\model\relation;
  12. use Closure;
  13. use think\Collection;
  14. use think\db\BaseQuery as Query;
  15. use think\db\exception\DbException as Exception;
  16. use think\db\Raw;
  17. use think\helper\Str;
  18. use think\Model;
  19. use think\model\Pivot;
  20. use think\model\Relation;
  21. use think\Paginator;
  22. /**
  23. * 多对多关联类
  24. */
  25. class BelongsToMany extends Relation
  26. {
  27. /**
  28. * 中间表表名
  29. * @var string
  30. */
  31. protected $middle;
  32. /**
  33. * 中间表模型名称
  34. * @var string
  35. */
  36. protected $pivotName;
  37. /**
  38. * 中间表模型对象
  39. * @var Pivot
  40. */
  41. protected $pivot;
  42. /**
  43. * 中间表数据名称
  44. * @var string
  45. */
  46. protected $pivotDataName = 'pivot';
  47. /**
  48. * 架构函数
  49. * @access public
  50. * @param Model $parent 上级模型对象
  51. * @param string $model 模型名
  52. * @param string $middle 中间表/模型名
  53. * @param string $foreignKey 关联模型外键
  54. * @param string $localKey 当前模型关联键
  55. */
  56. public function __construct(Model $parent, string $model, string $middle, string $foreignKey, string $localKey)
  57. {
  58. $this->parent = $parent;
  59. $this->model = $model;
  60. $this->foreignKey = $foreignKey;
  61. $this->localKey = $localKey;
  62. if (false !== strpos($middle, '\\')) {
  63. $this->pivotName = $middle;
  64. $this->middle = class_basename($middle);
  65. } else {
  66. $this->middle = $middle;
  67. }
  68. $this->query = (new $model)->db();
  69. $this->pivot = $this->newPivot();
  70. }
  71. /**
  72. * 设置中间表模型
  73. * @access public
  74. * @param $pivot
  75. * @return $this
  76. */
  77. public function pivot(string $pivot)
  78. {
  79. $this->pivotName = $pivot;
  80. return $this;
  81. }
  82. /**
  83. * 设置中间表数据名称
  84. * @access public
  85. * @param string $name
  86. * @return $this
  87. */
  88. public function name(string $name)
  89. {
  90. $this->pivotDataName = $name;
  91. return $this;
  92. }
  93. /**
  94. * 实例化中间表模型
  95. * @access public
  96. * @param $data
  97. * @return Pivot
  98. * @throws Exception
  99. */
  100. protected function newPivot(array $data = []): Pivot
  101. {
  102. $class = $this->pivotName ?: Pivot::class;
  103. $pivot = new $class($data, $this->parent, $this->middle);
  104. if ($pivot instanceof Pivot) {
  105. return $pivot;
  106. } else {
  107. throw new Exception('pivot model must extends: \think\model\Pivot');
  108. }
  109. }
  110. /**
  111. * 合成中间表模型
  112. * @access protected
  113. * @param array|Collection|Paginator $models
  114. */
  115. protected function hydratePivot(iterable $models)
  116. {
  117. foreach ($models as $model) {
  118. $pivot = [];
  119. foreach ($model->getData() as $key => $val) {
  120. if (strpos($key, '__')) {
  121. list($name, $attr) = explode('__', $key, 2);
  122. if ('pivot' == $name) {
  123. $pivot[$attr] = $val;
  124. unset($model->$key);
  125. }
  126. }
  127. }
  128. $model->setRelation($this->pivotDataName, $this->newPivot($pivot));
  129. }
  130. }
  131. /**
  132. * 创建关联查询Query对象
  133. * @access protected
  134. * @return Query
  135. */
  136. protected function buildQuery(): Query
  137. {
  138. $foreignKey = $this->foreignKey;
  139. $localKey = $this->localKey;
  140. // 关联查询
  141. $pk = $this->parent->getPk();
  142. $condition = ['pivot.' . $localKey, '=', $this->parent->$pk];
  143. return $this->belongsToManyQuery($foreignKey, $localKey, [$condition]);
  144. }
  145. /**
  146. * 延迟获取关联数据
  147. * @access public
  148. * @param array $subRelation 子关联名
  149. * @param Closure $closure 闭包查询条件
  150. * @return Collection
  151. */
  152. public function getRelation(array $subRelation = [], Closure $closure = null): Collection
  153. {
  154. if ($closure) {
  155. $closure($this->getClosureType($closure));
  156. }
  157. $result = $this->buildQuery()
  158. ->relation($subRelation)
  159. ->select()
  160. ->setParent(clone $this->parent);
  161. $this->hydratePivot($result);
  162. return $result;
  163. }
  164. /**
  165. * 重载select方法
  166. * @access public
  167. * @param mixed $data
  168. * @return Collection
  169. */
  170. public function select($data = null): Collection
  171. {
  172. $result = $this->buildQuery()->select($data);
  173. $this->hydratePivot($result);
  174. return $result;
  175. }
  176. /**
  177. * 重载paginate方法
  178. * @access public
  179. * @param int|array $listRows
  180. * @param int|bool $simple
  181. * @param array $config
  182. * @return Paginator
  183. */
  184. public function paginate($listRows = null, $simple = false, $config = []): Paginator
  185. {
  186. $result = $this->buildQuery()->paginate($listRows, $simple, $config);
  187. $this->hydratePivot($result);
  188. return $result;
  189. }
  190. /**
  191. * 重载find方法
  192. * @access public
  193. * @param mixed $data
  194. * @return Model
  195. */
  196. public function find($data = null)
  197. {
  198. $result = $this->buildQuery()->find($data);
  199. if (!$result->isEmpty()) {
  200. $this->hydratePivot([$result]);
  201. }
  202. return $result;
  203. }
  204. /**
  205. * 查找多条记录 如果不存在则抛出异常
  206. * @access public
  207. * @param array|string|Query|\Closure $data
  208. * @return Collection
  209. */
  210. public function selectOrFail($data = null): Collection
  211. {
  212. return $this->buildQuery()->failException(true)->select($data);
  213. }
  214. /**
  215. * 查找单条记录 如果不存在则抛出异常
  216. * @access public
  217. * @param array|string|Query|\Closure $data
  218. * @return Model
  219. */
  220. public function findOrFail($data = null): Model
  221. {
  222. return $this->buildQuery()->failException(true)->find($data);
  223. }
  224. /**
  225. * 根据关联条件查询当前模型
  226. * @access public
  227. * @param string $operator 比较操作符
  228. * @param integer $count 个数
  229. * @param string $id 关联表的统计字段
  230. * @param string $joinType JOIN类型
  231. * @param Query $query Query对象
  232. * @return Model
  233. */
  234. public function has(string $operator = '>=', $count = 1, $id = '*', string $joinType = 'INNER', Query $query = null)
  235. {
  236. return $this->parent;
  237. }
  238. /**
  239. * 根据关联条件查询当前模型
  240. * @access public
  241. * @param mixed $where 查询条件(数组或者闭包)
  242. * @param mixed $fields 字段
  243. * @param string $joinType JOIN类型
  244. * @param Query $query Query对象
  245. * @return Query
  246. * @throws Exception
  247. */
  248. public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
  249. {
  250. throw new Exception('relation not support: hasWhere');
  251. }
  252. /**
  253. * 设置中间表的查询条件
  254. * @access public
  255. * @param string $field
  256. * @param string $op
  257. * @param mixed $condition
  258. * @return $this
  259. */
  260. public function wherePivot($field, $op = null, $condition = null)
  261. {
  262. $this->query->where('pivot.' . $field, $op, $condition);
  263. return $this;
  264. }
  265. /**
  266. * 预载入关联查询(数据集)
  267. * @access public
  268. * @param array $resultSet 数据集
  269. * @param string $relation 当前关联名
  270. * @param array $subRelation 子关联名
  271. * @param Closure $closure 闭包
  272. * @param array $cache 关联缓存
  273. * @return void
  274. */
  275. public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
  276. {
  277. $localKey = $this->localKey;
  278. $pk = $resultSet[0]->getPk();
  279. $range = [];
  280. foreach ($resultSet as $result) {
  281. // 获取关联外键列表
  282. if (isset($result->$pk)) {
  283. $range[] = $result->$pk;
  284. }
  285. }
  286. if (!empty($range)) {
  287. // 查询关联数据
  288. $data = $this->eagerlyManyToMany([
  289. ['pivot.' . $localKey, 'in', $range],
  290. ], $subRelation, $closure, $cache);
  291. // 关联数据封装
  292. foreach ($resultSet as $result) {
  293. if (!isset($data[$result->$pk])) {
  294. $data[$result->$pk] = [];
  295. }
  296. $result->setRelation($relation, $this->resultSetBuild($data[$result->$pk], clone $this->parent));
  297. }
  298. }
  299. }
  300. /**
  301. * 预载入关联查询(单个数据)
  302. * @access public
  303. * @param Model $result 数据对象
  304. * @param string $relation 当前关联名
  305. * @param array $subRelation 子关联名
  306. * @param Closure $closure 闭包
  307. * @param array $cache 关联缓存
  308. * @return void
  309. */
  310. public function eagerlyResult(Model $result, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void
  311. {
  312. $pk = $result->getPk();
  313. if (isset($result->$pk)) {
  314. $pk = $result->$pk;
  315. // 查询管理数据
  316. $data = $this->eagerlyManyToMany([
  317. ['pivot.' . $this->localKey, '=', $pk],
  318. ], $subRelation, $closure, $cache);
  319. // 关联数据封装
  320. if (!isset($data[$pk])) {
  321. $data[$pk] = [];
  322. }
  323. $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
  324. }
  325. }
  326. /**
  327. * 关联统计
  328. * @access public
  329. * @param Model $result 数据对象
  330. * @param Closure $closure 闭包
  331. * @param string $aggregate 聚合查询方法
  332. * @param string $field 字段
  333. * @param string $name 统计字段别名
  334. * @return integer
  335. */
  336. public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): float
  337. {
  338. $pk = $result->getPk();
  339. if (!isset($result->$pk)) {
  340. return 0;
  341. }
  342. $pk = $result->$pk;
  343. if ($closure) {
  344. $closure($this->getClosureType($closure), $name);
  345. }
  346. return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
  347. ['pivot.' . $this->localKey, '=', $pk],
  348. ])->$aggregate($field);
  349. }
  350. /**
  351. * 获取关联统计子查询
  352. * @access public
  353. * @param Closure $closure 闭包
  354. * @param string $aggregate 聚合查询方法
  355. * @param string $field 字段
  356. * @param string $name 统计字段别名
  357. * @return string
  358. */
  359. public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
  360. {
  361. if ($closure) {
  362. $closure($this->getClosureType($closure), $name);
  363. }
  364. return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
  365. [
  366. 'pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->db(false)->getTable() . '.' . $this->parent->getPk()),
  367. ],
  368. ])->fetchSql()->$aggregate($field);
  369. }
  370. /**
  371. * 多对多 关联模型预查询
  372. * @access protected
  373. * @param array $where 关联预查询条件
  374. * @param array $subRelation 子关联
  375. * @param Closure $closure 闭包
  376. * @param array $cache 关联缓存
  377. * @return array
  378. */
  379. protected function eagerlyManyToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array
  380. {
  381. if ($closure) {
  382. $closure($this->getClosureType($closure));
  383. }
  384. // 预载入关联查询 支持嵌套预载入
  385. $list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)
  386. ->with($subRelation)
  387. ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
  388. ->select();
  389. // 组装模型数据
  390. $data = [];
  391. foreach ($list as $set) {
  392. $pivot = [];
  393. foreach ($set->getData() as $key => $val) {
  394. if (strpos($key, '__')) {
  395. list($name, $attr) = explode('__', $key, 2);
  396. if ('pivot' == $name) {
  397. $pivot[$attr] = $val;
  398. unset($set->$key);
  399. }
  400. }
  401. }
  402. $key = $pivot[$this->localKey];
  403. if ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->withLimit) {
  404. continue;
  405. }
  406. $set->setRelation($this->pivotDataName, $this->newPivot($pivot));
  407. $data[$key][] = $set;
  408. }
  409. return $data;
  410. }
  411. /**
  412. * BELONGS TO MANY 关联查询
  413. * @access protected
  414. * @param string $foreignKey 关联模型关联键
  415. * @param string $localKey 当前模型关联键
  416. * @param array $condition 关联查询条件
  417. * @return Query
  418. */
  419. protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query
  420. {
  421. // 关联查询封装
  422. $tableName = $this->query->getTable();
  423. $table = $this->pivot->db()->getTable();
  424. $fields = $this->getQueryFields($tableName);
  425. if ($this->withLimit) {
  426. $this->query->limit($this->withLimit);
  427. }
  428. $query = $this->query
  429. ->field($fields)
  430. ->tableField(true, $table, 'pivot', 'pivot__');
  431. if (empty($this->baseQuery)) {
  432. $relationFk = $this->query->getPk();
  433. $query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk)
  434. ->where($condition);
  435. }
  436. return $query;
  437. }
  438. /**
  439. * 保存(新增)当前关联数据对象
  440. * @access public
  441. * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
  442. * @param array $pivot 中间表额外数据
  443. * @return array|Pivot
  444. */
  445. public function save($data, array $pivot = [])
  446. {
  447. // 保存关联表/中间表数据
  448. return $this->attach($data, $pivot);
  449. }
  450. /**
  451. * 批量保存当前关联数据对象
  452. * @access public
  453. * @param iterable $dataSet 数据集
  454. * @param array $pivot 中间表额外数据
  455. * @param bool $samePivot 额外数据是否相同
  456. * @return array|false
  457. */
  458. public function saveAll(iterable $dataSet, array $pivot = [], bool $samePivot = false)
  459. {
  460. $result = [];
  461. foreach ($dataSet as $key => $data) {
  462. if (!$samePivot) {
  463. $pivotData = $pivot[$key] ?? [];
  464. } else {
  465. $pivotData = $pivot;
  466. }
  467. $result[] = $this->attach($data, $pivotData);
  468. }
  469. return empty($result) ? false : $result;
  470. }
  471. /**
  472. * 附加关联的一个中间表数据
  473. * @access public
  474. * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
  475. * @param array $pivot 中间表额外数据
  476. * @return array|Pivot
  477. * @throws Exception
  478. */
  479. public function attach($data, array $pivot = [])
  480. {
  481. if (is_array($data)) {
  482. if (key($data) === 0) {
  483. $id = $data;
  484. } else {
  485. // 保存关联表数据
  486. $model = new $this->model;
  487. $id = $model->insertGetId($data);
  488. }
  489. } elseif (is_numeric($data) || is_string($data)) {
  490. // 根据关联表主键直接写入中间表
  491. $id = $data;
  492. } elseif ($data instanceof Model) {
  493. // 根据关联表主键直接写入中间表
  494. $relationFk = $data->getPk();
  495. $id = $data->$relationFk;
  496. }
  497. if (!empty($id)) {
  498. // 保存中间表数据
  499. $pk = $this->parent->getPk();
  500. $pivot[$this->localKey] = $this->parent->$pk;
  501. $ids = (array) $id;
  502. foreach ($ids as $id) {
  503. $pivot[$this->foreignKey] = $id;
  504. $this->pivot->replace()
  505. ->exists(false)
  506. ->data([])
  507. ->save($pivot);
  508. $result[] = $this->newPivot($pivot);
  509. }
  510. if (count($result) == 1) {
  511. // 返回中间表模型对象
  512. $result = $result[0];
  513. }
  514. return $result;
  515. } else {
  516. throw new Exception('miss relation data');
  517. }
  518. }
  519. /**
  520. * 判断是否存在关联数据
  521. * @access public
  522. * @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键
  523. * @return Pivot|false
  524. */
  525. public function attached($data)
  526. {
  527. if ($data instanceof Model) {
  528. $id = $data->getKey();
  529. } else {
  530. $id = $data;
  531. }
  532. $pivot = $this->pivot
  533. ->where($this->localKey, $this->parent->getKey())
  534. ->where($this->foreignKey, $id)
  535. ->find();
  536. return $pivot ?: false;
  537. }
  538. /**
  539. * 解除关联的一个中间表数据
  540. * @access public
  541. * @param integer|array $data 数据 可以使用关联对象的主键
  542. * @param bool $relationDel 是否同时删除关联表数据
  543. * @return integer
  544. */
  545. public function detach($data = null, bool $relationDel = false): int
  546. {
  547. if (is_array($data)) {
  548. $id = $data;
  549. } elseif (is_numeric($data) || is_string($data)) {
  550. // 根据关联表主键直接写入中间表
  551. $id = $data;
  552. } elseif ($data instanceof Model) {
  553. // 根据关联表主键直接写入中间表
  554. $relationFk = $data->getPk();
  555. $id = $data->$relationFk;
  556. }
  557. // 删除中间表数据
  558. $pk = $this->parent->getPk();
  559. $pivot = [];
  560. $pivot[] = [$this->localKey, '=', $this->parent->$pk];
  561. if (isset($id)) {
  562. $pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id];
  563. }
  564. $result = $this->pivot->where($pivot)->delete();
  565. // 删除关联表数据
  566. if (isset($id) && $relationDel) {
  567. $model = $this->model;
  568. $model::destroy($id);
  569. }
  570. return $result;
  571. }
  572. /**
  573. * 数据同步
  574. * @access public
  575. * @param array $ids
  576. * @param bool $detaching
  577. * @return array
  578. */
  579. public function sync(array $ids, bool $detaching = true): array
  580. {
  581. $changes = [
  582. 'attached' => [],
  583. 'detached' => [],
  584. 'updated' => [],
  585. ];
  586. $pk = $this->parent->getPk();
  587. $current = $this->pivot
  588. ->where($this->localKey, $this->parent->$pk)
  589. ->column($this->foreignKey);
  590. $records = [];
  591. foreach ($ids as $key => $value) {
  592. if (!is_array($value)) {
  593. $records[$value] = [];
  594. } else {
  595. $records[$key] = $value;
  596. }
  597. }
  598. $detach = array_diff($current, array_keys($records));
  599. if ($detaching && count($detach) > 0) {
  600. $this->detach($detach);
  601. $changes['detached'] = $detach;
  602. }
  603. foreach ($records as $id => $attributes) {
  604. if (!in_array($id, $current)) {
  605. $this->attach($id, $attributes);
  606. $changes['attached'][] = $id;
  607. } elseif (count($attributes) > 0 && $this->attach($id, $attributes)) {
  608. $changes['updated'][] = $id;
  609. }
  610. }
  611. return $changes;
  612. }
  613. }