BelongsToMany.php 17 KB

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