ModelTree.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. <?php
  2. namespace Dcat\Admin\Traits;
  3. use Dcat\Admin\Support\Helper;
  4. use Dcat\Admin\Tree;
  5. use Illuminate\Database\Eloquent\Builder;
  6. use Illuminate\Database\Eloquent\Model;
  7. use Illuminate\Support\Arr;
  8. use Illuminate\Support\Facades\Request;
  9. use Spatie\EloquentSortable\SortableTrait;
  10. /**
  11. * @property string $parentColumn
  12. * @property string $titleColumn
  13. * @property string $orderColumn
  14. * @property array $sortable
  15. */
  16. trait ModelTree
  17. {
  18. use SortableTrait;
  19. /**
  20. * @var array
  21. */
  22. protected static $branchOrder = [];
  23. /**
  24. * @var \Closure[]
  25. */
  26. protected $queryCallbacks = [];
  27. /**
  28. * Get children of current node.
  29. *
  30. * @return \Illuminate\Database\Eloquent\Relations\HasMany
  31. */
  32. public function children()
  33. {
  34. return $this->hasMany(static::class, $this->getParentColumn());
  35. }
  36. /**
  37. * Get parent of current node.
  38. *
  39. * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
  40. */
  41. public function parent()
  42. {
  43. return $this->belongsTo(static::class, $this->getParentColumn());
  44. }
  45. /**
  46. * @return string
  47. */
  48. public function getParentColumn()
  49. {
  50. return empty($this->parentColumn) ? 'parent_id' : $this->parentColumn;
  51. }
  52. /**
  53. * Set parent column.
  54. *
  55. * @param string $column
  56. */
  57. public function setParentColumn(string $column)
  58. {
  59. $this->parentColumn = $column;
  60. }
  61. /**
  62. * Get title column.
  63. *
  64. * @return string
  65. */
  66. public function getTitleColumn()
  67. {
  68. return empty($this->titleColumn) ? 'title' : $this->titleColumn;
  69. }
  70. /**
  71. * Set title column.
  72. *
  73. * @param string $column
  74. */
  75. public function setTitleColumn(string $column)
  76. {
  77. $this->titleColumn = $column;
  78. }
  79. /**
  80. * Get order column name.
  81. *
  82. * @return string
  83. */
  84. public function getOrderColumn()
  85. {
  86. return empty($this->orderColumn) ? 'order' : $this->orderColumn;
  87. }
  88. /**
  89. * Set order column.
  90. *
  91. * @param string $column
  92. */
  93. public function setOrderColumn(string $column)
  94. {
  95. $this->orderColumn = $column;
  96. }
  97. /**
  98. * Set query callback to model.
  99. *
  100. * @param \Closure|null $query
  101. *
  102. * @return $this
  103. */
  104. public function withQuery(\Closure $query = null)
  105. {
  106. $this->queryCallbacks[] = $query;
  107. return $this;
  108. }
  109. /**
  110. * Format data to tree like array.
  111. *
  112. * @return array
  113. */
  114. public function toTree(array $nodes = null)
  115. {
  116. if ($nodes === null) {
  117. $nodes = $this->allNodes();
  118. }
  119. return Helper::buildNestedArray(
  120. $nodes,
  121. 0,
  122. $this->getKeyName(),
  123. $this->getParentColumn()
  124. );
  125. }
  126. /**
  127. * Get all elements.
  128. *
  129. * @return mixed
  130. */
  131. public function allNodes()
  132. {
  133. return $this->callQueryCallbacks(new static())
  134. ->orderBy($this->getOrderColumn(), 'asc')
  135. ->get()
  136. ->toArray();
  137. }
  138. /**
  139. * @param $this $model
  140. *
  141. * @return $this|Builder
  142. */
  143. protected function callQueryCallbacks($model)
  144. {
  145. foreach ($this->queryCallbacks as $callback) {
  146. if ($callback) {
  147. $model = $callback($model);
  148. }
  149. }
  150. return $model;
  151. }
  152. /**
  153. * Set the order of branches in the tree.
  154. *
  155. * @param array $order
  156. *
  157. * @return void
  158. */
  159. protected static function setBranchOrder(array $order)
  160. {
  161. static::$branchOrder = array_flip(Arr::flatten($order));
  162. static::$branchOrder = array_map(function ($item) {
  163. return ++$item;
  164. }, static::$branchOrder);
  165. }
  166. /**
  167. * Save tree order from a tree like array.
  168. *
  169. * @param array $tree
  170. * @param int $parentId
  171. */
  172. public static function saveOrder($tree = [], $parentId = 0)
  173. {
  174. if (empty(static::$branchOrder)) {
  175. static::setBranchOrder($tree);
  176. }
  177. foreach ($tree as $branch) {
  178. $node = static::find($branch['id']);
  179. $node->{$node->getParentColumn()} = $parentId;
  180. $node->{$node->getOrderColumn()} = static::$branchOrder[$branch['id']];
  181. $node->save();
  182. if (isset($branch['children'])) {
  183. static::saveOrder($branch['children'], $branch['id']);
  184. }
  185. }
  186. }
  187. protected function determineOrderColumnName()
  188. {
  189. return $this->getOrderColumn();
  190. }
  191. public function moveOrderDown()
  192. {
  193. $orderColumnName = $this->determineOrderColumnName();
  194. $parentColumnName = $this->getParentColumn();
  195. $sameOrderModel = $this->getSameOrderModel('>');
  196. if ($sameOrderModel) {
  197. $this->$orderColumnName = $this->$orderColumnName + 1;
  198. $this->save();
  199. return $this;
  200. }
  201. $swapWithModel = $this->buildSortQuery()
  202. ->limit(1)
  203. ->ordered()
  204. ->where($orderColumnName, '>', $this->$orderColumnName)
  205. ->where($parentColumnName, $this->$parentColumnName)
  206. ->first();
  207. if (! $swapWithModel) {
  208. return false;
  209. }
  210. return $this->swapOrderWithModel($swapWithModel);
  211. }
  212. public function moveOrderUp()
  213. {
  214. $orderColumnName = $this->determineOrderColumnName();
  215. $parentColumnName = $this->getParentColumn();
  216. $swapWithModel = $this->buildSortQuery()
  217. ->limit(1)
  218. ->ordered('desc')
  219. ->where($orderColumnName, '<', $this->$orderColumnName)
  220. ->where($parentColumnName, $this->$parentColumnName)
  221. ->first();
  222. if ($swapWithModel) {
  223. return $this->swapOrderWithModel($swapWithModel);
  224. }
  225. $sameOrderModel = $this->getSameOrderModel('<');
  226. if (! $sameOrderModel) {
  227. return false;
  228. }
  229. $sameOrderModel->$orderColumnName = $sameOrderModel->$orderColumnName + 1;
  230. $sameOrderModel->save();
  231. return $this;
  232. }
  233. protected function getSameOrderModel(string $operator = '<')
  234. {
  235. $orderColumnName = $this->determineOrderColumnName();
  236. $parentColumnName = $this->getParentColumn();
  237. return $this->buildSortQuery()
  238. ->limit(1)
  239. ->orderBy($orderColumnName)
  240. ->orderBy($this->getKeyName())
  241. ->where($this->getKeyName(), $operator, $this->getKey())
  242. ->where($orderColumnName, $this->$orderColumnName)
  243. ->where($parentColumnName, $this->$parentColumnName)
  244. ->first();
  245. }
  246. public function moveToStart()
  247. {
  248. $parentColumnName = $this->getParentColumn();
  249. $firstModel = $this->buildSortQuery()
  250. ->limit(1)
  251. ->ordered()
  252. ->where($parentColumnName, $this->$parentColumnName)
  253. ->first();
  254. if ($firstModel->id === $this->id) {
  255. return $this;
  256. }
  257. $orderColumnName = $this->determineOrderColumnName();
  258. $this->$orderColumnName = $firstModel->$orderColumnName;
  259. $this->save();
  260. $this->buildSortQuery()->where($this->getKeyName(), '!=', $this->id)->increment($orderColumnName);
  261. return $this;
  262. }
  263. /**
  264. * Get options for Select field in form.
  265. *
  266. * @param \Closure|null $closure
  267. * @param string $rootText
  268. *
  269. * @return array
  270. */
  271. public static function selectOptions(\Closure $closure = null, $rootText = null)
  272. {
  273. $rootText = $rootText ?: admin_trans_label('root');
  274. $options = (new static())->withQuery($closure)->buildSelectOptions();
  275. return collect($options)->prepend($rootText, 0)->all();
  276. }
  277. /**
  278. * Build options of select field in form.
  279. *
  280. * @param array $nodes
  281. * @param int $parentId
  282. * @param string $prefix
  283. *
  284. * @return array
  285. */
  286. protected function buildSelectOptions(array $nodes = [], $parentId = 0, $prefix = '')
  287. {
  288. $prefix = $prefix ?: str_repeat('&nbsp;', 6);
  289. $options = [];
  290. if (empty($nodes)) {
  291. $nodes = $this->allNodes();
  292. }
  293. $titleColumn = $this->getTitleColumn();
  294. $parentColumn = $this->getParentColumn();
  295. foreach ($nodes as $node) {
  296. $node[$titleColumn] = $prefix.'&nbsp;'.$node[$titleColumn];
  297. if ($node[$parentColumn] == $parentId) {
  298. $children = $this->buildSelectOptions($nodes, $node[$this->getKeyName()], $prefix.$prefix);
  299. $options[$node[$this->getKeyName()]] = $node[$titleColumn];
  300. if ($children) {
  301. $options += $children;
  302. }
  303. }
  304. }
  305. return $options;
  306. }
  307. /**
  308. * {@inheritdoc}
  309. */
  310. public function delete()
  311. {
  312. $this->where($this->getParentColumn(), $this->getKey())->delete();
  313. return parent::delete();
  314. }
  315. /**
  316. * {@inheritdoc}
  317. */
  318. protected static function boot()
  319. {
  320. parent::boot();
  321. static::saving(function (Model $branch) {
  322. $parentColumn = $branch->getParentColumn();
  323. if (
  324. $branch->getKey()
  325. && Request::has($parentColumn)
  326. && Request::input($parentColumn) == $branch->getKey()
  327. ) {
  328. throw new \Exception(trans('admin.parent_select_error'));
  329. }
  330. if (Request::has('_order')) {
  331. $order = Request::input('_order');
  332. Request::offsetUnset('_order');
  333. Tree::make(new static())->saveOrder($order);
  334. return false;
  335. }
  336. return $branch;
  337. });
  338. }
  339. }