BelongsToMany.php 18 KB

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