BelongsToMany.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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\db\Query;
  13. use think\Exception;
  14. use think\Loader;
  15. use think\Model;
  16. use think\model\Pivot;
  17. use think\model\Relation;
  18. class BelongsToMany extends Relation
  19. {
  20. // 中间表模型
  21. protected $middle;
  22. /**
  23. * 架构函数
  24. * @access public
  25. * @param Model $parent 上级模型对象
  26. * @param string $model 模型名
  27. * @param string $table 中间表名
  28. * @param string $foreignKey 关联模型外键
  29. * @param string $localKey 当前模型关联键
  30. */
  31. public function __construct(Model $parent, $model, $table, $foreignKey, $localKey)
  32. {
  33. $this->parent = $parent;
  34. $this->model = $model;
  35. $this->foreignKey = $foreignKey;
  36. $this->localKey = $localKey;
  37. $this->middle = $table;
  38. $this->query = (new $model)->db();
  39. }
  40. /**
  41. * 延迟获取关联数据
  42. * @param string $subRelation 子关联名
  43. * @param \Closure $closure 闭包查询条件
  44. * @return false|\PDOStatement|string|\think\Collection
  45. */
  46. public function getRelation($subRelation = '', $closure = null)
  47. {
  48. $foreignKey = $this->foreignKey;
  49. $localKey = $this->localKey;
  50. $middle = $this->middle;
  51. if ($closure) {
  52. call_user_func_array($closure, [& $this->query]);
  53. }
  54. // 关联查询
  55. $pk = $this->parent->getPk();
  56. $condition['pivot.' . $localKey] = $this->parent->$pk;
  57. $result = $this->belongsToManyQuery($middle, $foreignKey, $localKey, $condition)->relation($subRelation)->select();
  58. foreach ($result as $set) {
  59. $pivot = [];
  60. foreach ($set->getData() as $key => $val) {
  61. if (strpos($key, '__')) {
  62. list($name, $attr) = explode('__', $key, 2);
  63. if ('pivot' == $name) {
  64. $pivot[$attr] = $val;
  65. unset($set->$key);
  66. }
  67. }
  68. }
  69. $set->pivot = new Pivot($pivot, $this->middle);
  70. }
  71. return $result;
  72. }
  73. /**
  74. * 预载入关联查询(数据集)
  75. * @access public
  76. * @param array $resultSet 数据集
  77. * @param string $relation 当前关联名
  78. * @param string $subRelation 子关联名
  79. * @param \Closure $closure 闭包
  80. * @return void
  81. */
  82. public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
  83. {
  84. $localKey = $this->localKey;
  85. $foreignKey = $this->foreignKey;
  86. $pk = $resultSet[0]->getPk();
  87. $range = [];
  88. foreach ($resultSet as $result) {
  89. // 获取关联外键列表
  90. if (isset($result->$pk)) {
  91. $range[] = $result->$pk;
  92. }
  93. }
  94. if (!empty($range)) {
  95. // 查询关联数据
  96. $data = $this->eagerlyManyToMany([
  97. 'pivot.' . $localKey => [
  98. 'in',
  99. $range,
  100. ],
  101. ], $relation, $subRelation);
  102. // 关联属性名
  103. $attr = Loader::parseName($relation);
  104. // 关联数据封装
  105. foreach ($resultSet as $result) {
  106. if (!isset($data[$result->$pk])) {
  107. $data[$result->$pk] = [];
  108. }
  109. $result->setAttr($attr, $this->resultSetBuild($data[$result->$pk]));
  110. }
  111. }
  112. }
  113. /**
  114. * 预载入关联查询(单个数据)
  115. * @access public
  116. * @param Model $result 数据对象
  117. * @param string $relation 当前关联名
  118. * @param string $subRelation 子关联名
  119. * @param \Closure $closure 闭包
  120. * @return void
  121. */
  122. public function eagerlyResult(&$result, $relation, $subRelation, $closure)
  123. {
  124. $pk = $result->getPk();
  125. if (isset($result->$pk)) {
  126. $pk = $result->$pk;
  127. // 查询管理数据
  128. $data = $this->eagerlyManyToMany(['pivot.' . $this->localKey => $pk], $relation, $subRelation);
  129. // 关联数据封装
  130. if (!isset($data[$pk])) {
  131. $data[$pk] = [];
  132. }
  133. $result->setAttr(Loader::parseName($relation), $this->resultSetBuild($data[$pk]));
  134. }
  135. }
  136. /**
  137. * 关联统计
  138. * @access public
  139. * @param Model $result 数据对象
  140. * @param \Closure $closure 闭包
  141. * @return integer
  142. */
  143. public function relationCount($result, $closure)
  144. {
  145. $pk = $result->getPk();
  146. $count = 0;
  147. if (isset($result->$pk)) {
  148. $pk = $result->$pk;
  149. $count = $this->belongsToManyQuery($this->middle, $this->foreignKey, $this->localKey, ['pivot.' . $this->localKey => $pk])->count();
  150. }
  151. return $count;
  152. }
  153. /**
  154. * 获取关联统计子查询
  155. * @access public
  156. * @param \Closure $closure 闭包
  157. * @return string
  158. */
  159. public function getRelationCountQuery($closure)
  160. {
  161. return $this->belongsToManyQuery($this->middle, $this->foreignKey, $this->localKey, [
  162. 'pivot.' . $this->localKey => [
  163. 'exp',
  164. '=' . $this->parent->getTable() . '.' . $this->parent->getPk()
  165. ]
  166. ])->fetchSql()->count();
  167. }
  168. /**
  169. * 多对多 关联模型预查询
  170. * @access public
  171. * @param array $where 关联预查询条件
  172. * @param string $relation 关联名
  173. * @param string $subRelation 子关联
  174. * @return array
  175. */
  176. protected function eagerlyManyToMany($where, $relation, $subRelation = '')
  177. {
  178. // 预载入关联查询 支持嵌套预载入
  179. $list = $this->belongsToManyQuery($this->middle, $this->foreignKey, $this->localKey, $where)->with($subRelation)->select();
  180. // 组装模型数据
  181. $data = [];
  182. foreach ($list as $set) {
  183. $pivot = [];
  184. foreach ($set->getData() as $key => $val) {
  185. if (strpos($key, '__')) {
  186. list($name, $attr) = explode('__', $key, 2);
  187. if ('pivot' == $name) {
  188. $pivot[$attr] = $val;
  189. unset($set->$key);
  190. }
  191. }
  192. }
  193. $set->pivot = new Pivot($pivot, $this->middle);
  194. $data[$pivot[$this->localKey]][] = $set;
  195. }
  196. return $data;
  197. }
  198. /**
  199. * BELONGS TO MANY 关联查询
  200. * @access public
  201. * @param string $table 中间表名
  202. * @param string $foreignKey 关联模型关联键
  203. * @param string $localKey 当前模型关联键
  204. * @param array $condition 关联查询条件
  205. * @return Query
  206. */
  207. protected function belongsToManyQuery($table, $foreignKey, $localKey, $condition = [])
  208. {
  209. // 关联查询封装
  210. $tableName = $this->query->getTable();
  211. $relationFk = $this->query->getPk();
  212. return $this->query->field($tableName . '.*')
  213. ->field(true, false, $table, 'pivot', 'pivot__')
  214. ->join($table . ' pivot', 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk)
  215. ->where($condition);
  216. }
  217. /**
  218. * 保存(新增)当前关联数据对象
  219. * @access public
  220. * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
  221. * @param array $pivot 中间表额外数据
  222. * @return integer
  223. */
  224. public function save($data, array $pivot = [])
  225. {
  226. // 保存关联表/中间表数据
  227. return $this->attach($data, $pivot);
  228. }
  229. /**
  230. * 批量保存当前关联数据对象
  231. * @access public
  232. * @param array $dataSet 数据集
  233. * @param array $pivot 中间表额外数据
  234. * @param bool $samePivot 额外数据是否相同
  235. * @return integer
  236. */
  237. public function saveAll(array $dataSet, array $pivot = [], $samePivot = false)
  238. {
  239. $result = false;
  240. foreach ($dataSet as $key => $data) {
  241. if (!$samePivot) {
  242. $pivotData = isset($pivot[$key]) ? $pivot[$key] : [];
  243. } else {
  244. $pivotData = $pivot;
  245. }
  246. $result = $this->attach($data, $pivotData);
  247. }
  248. return $result;
  249. }
  250. /**
  251. * 附加关联的一个中间表数据
  252. * @access public
  253. * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
  254. * @param array $pivot 中间表额外数据
  255. * @return int
  256. * @throws Exception
  257. */
  258. public function attach($data, $pivot = [])
  259. {
  260. if (is_array($data)) {
  261. if (key($data) === 0) {
  262. $id = $data;
  263. } else {
  264. // 保存关联表数据
  265. $model = new $this->model;
  266. $model->save($data);
  267. $id = $model->getLastInsID();
  268. }
  269. } elseif (is_numeric($data) || is_string($data)) {
  270. // 根据关联表主键直接写入中间表
  271. $id = $data;
  272. } elseif ($data instanceof Model) {
  273. // 根据关联表主键直接写入中间表
  274. $relationFk = $data->getPk();
  275. $id = $data->$relationFk;
  276. }
  277. if ($id) {
  278. // 保存中间表数据
  279. $pk = $this->parent->getPk();
  280. $pivot[$this->localKey] = $this->parent->$pk;
  281. $ids = (array) $id;
  282. foreach ($ids as $id) {
  283. $pivot[$this->foreignKey] = $id;
  284. $result = $this->query->table($this->middle)->insert($pivot, true);
  285. }
  286. return $result;
  287. } else {
  288. throw new Exception('miss relation data');
  289. }
  290. }
  291. /**
  292. * 解除关联的一个中间表数据
  293. * @access public
  294. * @param integer|array $data 数据 可以使用关联对象的主键
  295. * @param bool $relationDel 是否同时删除关联表数据
  296. * @return integer
  297. */
  298. public function detach($data = null, $relationDel = false)
  299. {
  300. if (is_array($data)) {
  301. $id = $data;
  302. } elseif (is_numeric($data) || is_string($data)) {
  303. // 根据关联表主键直接写入中间表
  304. $id = $data;
  305. } elseif ($data instanceof Model) {
  306. // 根据关联表主键直接写入中间表
  307. $relationFk = $data->getPk();
  308. $id = $data->$relationFk;
  309. }
  310. // 删除中间表数据
  311. $pk = $this->parent->getPk();
  312. $pivot[$this->localKey] = $this->parent->$pk;
  313. if (isset($id)) {
  314. $pivot[$this->foreignKey] = is_array($id) ? ['in', $id] : $id;
  315. }
  316. $this->query->table($this->middle)->where($pivot)->delete();
  317. // 删除关联表数据
  318. if (isset($id) && $relationDel) {
  319. $model = $this->model;
  320. $model::destroy($id);
  321. }
  322. }
  323. /**
  324. * 执行基础查询(进执行一次)
  325. * @access protected
  326. * @return void
  327. */
  328. protected function baseQuery()
  329. {
  330. if (empty($this->baseQuery)) {
  331. $pk = $this->parent->getPk();
  332. $this->query->join($this->middle . ' pivot', 'pivot.' . $this->foreignKey . '=' . $this->query->getTable() . '.' . $this->query->getPk())->where('pivot.' . $this->localKey, $this->parent->$pk);
  333. $this->baseQuery = true;
  334. }
  335. }
  336. }