ClosureTableBehavior.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. <?php
  2. /**
  3. * 重庆赤晓店信息科技有限公司
  4. * https://www.chixiaodian.com
  5. * Copyright (c) 2023 赤店商城 All rights reserved.
  6. */
  7. namespace app\librarys\shareTree\behaviors;
  8. use app\librarys\shareTree\exceptions\LogicException;
  9. use app\librarys\shareTree\models\AbstractTreePath;
  10. use Yii;
  11. use yii\base\Behavior;
  12. use yii\base\Component;
  13. use yii\base\InvalidConfigException;
  14. use yii\db\ActiveQuery;
  15. use yii\db\ActiveRecord;
  16. use yii\helpers\ArrayHelper;
  17. /**
  18. * @property ActiveRecord $owner
  19. */
  20. class ClosureTableBehavior extends Behavior
  21. {
  22. /** @var int 如果有子节点,默认删除类型不会删除元素 */
  23. public const DELETION_TYPE_0 = 0;
  24. /** @var int 移除元素,首先移动在元素的父节点下的子节点 */
  25. public const DELETION_TYPE_1 = 1;
  26. /** @var string 负责嵌套树的类的名称 */
  27. public $treePathModelClass;
  28. /** @var string 保存父类的字段名 */
  29. public $ownerParentIdAttribute = 'parent_id';
  30. /** @var int 元素删除类型 */
  31. public $deletionType = self::DELETION_TYPE_0;
  32. /** @var int|null old parent id */
  33. protected $oldParentId;
  34. /**
  35. * @inheritDoc
  36. *
  37. * @param Component $owner
  38. * @throws InvalidConfigException
  39. */
  40. public function attach($owner): void
  41. {
  42. if (empty($this->treePathModelClass) || !is_string($this->treePathModelClass)) {
  43. throw new InvalidConfigException('在ClosureTableBehavior中没有配置树路径表名');
  44. }
  45. parent::attach($owner);
  46. }
  47. /**
  48. * @inheritDoc
  49. */
  50. public function events(): array
  51. {
  52. return [
  53. ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert',
  54. ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate',
  55. ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate',
  56. ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
  57. ];
  58. }
  59. /**
  60. * 新增数据重新计算路径
  61. *
  62. * @throws InvalidConfigException
  63. */
  64. public function afterInsert(): void
  65. {
  66. if ($this->owner->getAttribute($this->ownerParentIdAttribute)) {
  67. $this->addTreePathOwnerToParents();
  68. }
  69. $this->addTreePathOwnerToOwner();
  70. }
  71. /**
  72. * 创建父子关系.
  73. *
  74. * @throws InvalidConfigException
  75. */
  76. protected function addTreePathOwnerToParents(): void
  77. {
  78. foreach ($this->getTreePathParents() as $treePath) {
  79. $this->saveTreePathModel(
  80. $treePath->parent_id,
  81. $this->owner->id,
  82. $this->owner->getAttribute($this->ownerParentIdAttribute),
  83. $treePath->parent_level,
  84. $treePath->child_level + 1
  85. );
  86. }
  87. }
  88. /**
  89. * 返回节点树路径关系
  90. *
  91. * @return array
  92. */
  93. protected function getTreePathParents(): array
  94. {
  95. $primaryKey = $this->owner->getAttribute($this->ownerParentIdAttribute);
  96. if ($primaryKey === 0) {
  97. return [];
  98. }
  99. return $this->treePathModelClass::find()
  100. ->byChildId($primaryKey)
  101. ->orderBy('child_level')
  102. ->all();
  103. }
  104. /**
  105. * 保存树路径
  106. *
  107. * @param int $parentId
  108. * @param int $childId
  109. * @param int|null $nearestParentId
  110. * @param int $parentLevel
  111. * @param int $childLevel
  112. * @return bool
  113. * @throws InvalidConfigException
  114. */
  115. protected function saveTreePathModel(
  116. int $parentId,
  117. int $childId,
  118. ?int $nearestParentId,
  119. ?int $parentLevel,
  120. int $childLevel
  121. ): bool {
  122. $model = $this->addTreePathModelObject();
  123. $model->parent_id = $parentId;
  124. $model->child_id = $childId;
  125. $model->nearest_parent_id = $nearestParentId;
  126. $model->parent_level = $parentLevel;
  127. $model->child_level = $childLevel;
  128. return $model->save();
  129. }
  130. /**
  131. * 创建树路径模型对象
  132. *
  133. * @return AbstractTreePath
  134. * @throws InvalidConfigException
  135. */
  136. protected function addTreePathModelObject(): AbstractTreePath
  137. {
  138. return Yii::createObject($this->treePathModelClass);
  139. }
  140. /**
  141. * 创建 owner-owner 关系.
  142. *
  143. * @throws InvalidConfigException
  144. */
  145. protected function addTreePathOwnerToOwner(): void
  146. {
  147. $parentLevel = $this->getParentLevel();
  148. $this->saveTreePathModel(
  149. $this->owner->id,
  150. $this->owner->id,
  151. $this->owner->getAttribute($this->ownerParentIdAttribute),
  152. $parentLevel + 1,
  153. $parentLevel + 1
  154. );
  155. }
  156. /**
  157. * 返回父级别
  158. *
  159. * @return int
  160. */
  161. protected function getParentLevel(): ?int
  162. {
  163. if (empty($this->owner->getAttribute($this->ownerParentIdAttribute))) {
  164. return 0;
  165. }
  166. return $this->treePathModelClass::find()
  167. ->select('parent_level')
  168. ->byParentId($this->owner->getAttribute($this->ownerParentIdAttribute))
  169. ->byChildId($this->owner->getAttribute($this->ownerParentIdAttribute))
  170. ->scalar();
  171. }
  172. /**
  173. * 更新之前验证
  174. *
  175. * @throws LogicException
  176. */
  177. public function beforeUpdate(): void
  178. {
  179. $this->oldParentId = $this->owner->oldAttributes[$this->ownerParentIdAttribute] ?? 0;
  180. if ($this->owner->getAttribute($this->ownerParentIdAttribute) !== 0) {
  181. if ($this->hasChilds()) {
  182. throw new LogicException('你不能把parent移动到child下边');
  183. }
  184. }
  185. }
  186. /**
  187. * 验证父节点下是否有子节点
  188. *
  189. * @return bool
  190. */
  191. private function hasChilds(): bool
  192. {
  193. return (bool)$this->treePathModelClass::find()
  194. ->byParentId($this->owner->id)
  195. ->byChildId($this->owner->getAttribute($this->ownerParentIdAttribute))
  196. ->count();
  197. }
  198. /**
  199. * 删除之前检查父节点
  200. *
  201. * @throws LogicException
  202. */
  203. public function beforeDelete(): void
  204. {
  205. switch ($this->deletionType) {
  206. case self::DELETION_TYPE_1:
  207. $this->deletionType1();
  208. break;
  209. case self::DELETION_TYPE_0:
  210. default:
  211. $this->deletionType0();
  212. break;
  213. }
  214. }
  215. /**
  216. * 如果有子节点,删除时不会删除当前节点
  217. */
  218. protected function deletionType0(): void
  219. {
  220. if ($this->childs()->count()) {
  221. throw new LogicException('你不能删除包含子集的元素');
  222. }
  223. $this->removeTreePathByIds($this->owner->id);
  224. }
  225. /**
  226. * 移除元素,首先移动在元素的父节点下的子节点
  227. */
  228. protected function deletionType1(): void
  229. {
  230. foreach ($this->childs(1)->all() as $child) {
  231. $child->setAttribute(
  232. $this->ownerParentIdAttribute,
  233. $this->owner->getAttribute($this->ownerParentIdAttribute)
  234. );
  235. $child->save();
  236. }
  237. $this->removeTreePathByIds($this->owner->id);
  238. }
  239. /**
  240. * 在更新父节点后更新树路径
  241. */
  242. public function afterUpdate(): void
  243. {
  244. if ($this->oldParentId != $this->owner->getAttribute($this->ownerParentIdAttribute)) {
  245. $this->rebuildTreePath();
  246. }
  247. }
  248. /**
  249. * 获取元素所有子节点
  250. *
  251. * @param bool|false $withParent
  252. * @param int|null $depth
  253. * @param bool|false $eagerLoading
  254. * @return ActiveQuery
  255. */
  256. public function childs(?int $depth = null, bool $withParent = false, bool $eagerLoading = false): ActiveQuery
  257. {
  258. return $this->owner::find()
  259. ->childs($this->owner->id, $withParent, $depth, $eagerLoading)
  260. ->orderBy(['treePathsChild.child_level' => SORT_ASC]);
  261. }
  262. /**
  263. * 添加树路径
  264. *
  265. * @throws InvalidConfigException
  266. */
  267. public function addTreePath(): void
  268. {
  269. $this->addTreePathOwnerToParents();
  270. $this->addTreePathOwnerToOwner();
  271. }
  272. /**
  273. * 重建树路径
  274. *
  275. * @throws InvalidConfigException
  276. */
  277. public function rebuildTreePath(): void
  278. {
  279. $childs = $this->childs()->all();
  280. $this->removeTreePathByIds($this->owner->id);
  281. $this->addTreePath();
  282. if ($childs) {
  283. $this->removeTreePathByIds(ArrayHelper::map($childs, 'id', 'id'));
  284. foreach ($childs as $child) {
  285. $child->addTreePath();
  286. }
  287. }
  288. }
  289. /**
  290. * 删除树路径 by ids
  291. */
  292. /**
  293. * @param array|int $ids
  294. */
  295. protected function removeTreePathByIds($ids): void
  296. {
  297. $this->treePathModelClass::deleteAll(
  298. [
  299. 'child_id' => $ids,
  300. ]
  301. );
  302. }
  303. }