PositionBehavior.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. <?php
  2. namespace common\behaviors;
  3. use yii\base\Behavior;
  4. use yii\base\ModelEvent;
  5. use yii\db\BaseActiveRecord;
  6. /**
  7. * PositionBehavior allows managing custom order for the records in the database.
  8. * Behavior uses the specific integer field of the database entity to set up position index.
  9. * Due to this the database entity, which the model refers to, must contain field [[positionAttribute]].
  10. *
  11. * ```php
  12. * class Item extends ActiveRecord
  13. * {
  14. * public function behaviors()
  15. * {
  16. * return [
  17. * 'positionBehavior' => [
  18. * 'class' => PositionBehavior::className(),
  19. * 'positionAttribute' => 'position',
  20. * ],
  21. * ];
  22. * }
  23. * }
  24. * ```
  25. *
  26. * @property BaseActiveRecord $owner owner ActiveRecord instance.
  27. *
  28. * @author Paul Klimov <klimov.paul@gmail.com>
  29. * @since 1.0
  30. */
  31. class PositionBehavior extends Behavior
  32. {
  33. /**
  34. * @var string name owner attribute, which will store position value.
  35. * This attribute should be an integer.
  36. */
  37. public $positionAttribute = 'position';
  38. /**
  39. * @var array list of owner attribute names, which values split records into the groups,
  40. * which should have their own positioning.
  41. * Example: `['group_id', 'category_id']`
  42. */
  43. public $groupAttributes = [];
  44. /**
  45. * @var integer position value, which should be applied to the model on its save.
  46. * Internal usage only.
  47. */
  48. private $positionOnSave;
  49. /**
  50. * Moves owner record by one position towards the start of the list.
  51. * @return boolean movement successful.
  52. */
  53. public function movePrev()
  54. {
  55. $positionAttribute = $this->positionAttribute;
  56. /* @var $previousRecord BaseActiveRecord */
  57. $previousRecord = $this->owner->find()
  58. ->andWhere($this->createGroupConditionAttributes())
  59. ->andWhere([$positionAttribute => ($this->owner->$positionAttribute - 1)])
  60. ->one();
  61. if (empty($previousRecord)) {
  62. return false;
  63. }
  64. $previousRecord->updateAttributes([
  65. $positionAttribute => $this->owner->$positionAttribute
  66. ]);
  67. $this->owner->updateAttributes([
  68. $positionAttribute => $this->owner->$positionAttribute - 1
  69. ]);
  70. return true;
  71. }
  72. /**
  73. * Moves owner record by one position towards the end of the list.
  74. * @return boolean movement successful.
  75. */
  76. public function moveNext()
  77. {
  78. $positionAttribute = $this->positionAttribute;
  79. /* @var $nextRecord BaseActiveRecord */
  80. $nextRecord = $this->owner->find()
  81. ->andWhere($this->createGroupConditionAttributes())
  82. ->andWhere([$positionAttribute => ($this->owner->$positionAttribute + 1)])
  83. ->one();
  84. if (empty($nextRecord)) {
  85. return false;
  86. }
  87. $nextRecord->updateAttributes([
  88. $positionAttribute => $this->owner->$positionAttribute
  89. ]);
  90. $this->owner->updateAttributes([
  91. $positionAttribute => $this->owner->getAttribute($positionAttribute) + 1
  92. ]);
  93. return true;
  94. }
  95. /**
  96. * Moves owner record to the start of the list.
  97. * @return boolean movement successful.
  98. */
  99. public function moveFirst()
  100. {
  101. $positionAttribute = $this->positionAttribute;
  102. if ($this->owner->$positionAttribute == 1) {
  103. return false;
  104. }
  105. $this->owner->updateAllCounters(
  106. [
  107. $positionAttribute => +1
  108. ],
  109. [
  110. 'and',
  111. $this->createGroupConditionAttributes(),
  112. ['<', $positionAttribute, $this->owner->$positionAttribute]
  113. ]
  114. );
  115. $this->owner->updateAttributes([
  116. $positionAttribute => 1
  117. ]);
  118. return true;
  119. }
  120. /**
  121. * Moves owner record to the end of the list.
  122. * @return boolean movement successful.
  123. */
  124. public function moveLast()
  125. {
  126. $positionAttribute = $this->positionAttribute;
  127. $recordsCount = $this->countGroupRecords();
  128. if ($this->owner->getAttribute($positionAttribute) == $recordsCount) {
  129. return false;
  130. }
  131. $this->owner->updateAllCounters(
  132. [
  133. $positionAttribute => -1
  134. ],
  135. [
  136. 'and',
  137. $this->createGroupConditionAttributes(),
  138. ['>', $positionAttribute, $this->owner->$positionAttribute]
  139. ]
  140. );
  141. $this->owner->updateAttributes([
  142. $positionAttribute => $recordsCount
  143. ]);
  144. return true;
  145. }
  146. /**
  147. * Moves owner record to the specific position.
  148. * If specified position exceeds the total number of records,
  149. * owner will be moved to the end of the list.
  150. * @param integer $position number of the new position.
  151. * @return boolean movement successful.
  152. */
  153. public function moveToPosition($position)
  154. {
  155. if (!is_numeric($position) || $position < 1) {
  156. return false;
  157. }
  158. $positionAttribute = $this->positionAttribute;
  159. $oldRecord = $this->owner->findOne($this->owner->getPrimaryKey());
  160. $oldRecordPosition = $oldRecord->$positionAttribute;
  161. if ($oldRecordPosition == $position) {
  162. return true;
  163. }
  164. if ($position < $oldRecordPosition) {
  165. // Move Up:
  166. $this->owner->updateAllCounters(
  167. [
  168. $positionAttribute => +1
  169. ],
  170. [
  171. 'and',
  172. $this->createGroupConditionAttributes(),
  173. ['>=', $positionAttribute, $position],
  174. ['<', $positionAttribute, $oldRecord->$positionAttribute],
  175. ]
  176. );
  177. $this->owner->updateAttributes([
  178. $positionAttribute => $position
  179. ]);
  180. return true;
  181. } else {
  182. // Move Down:
  183. $recordsCount = $this->countGroupRecords();
  184. if ($position >= $recordsCount) {
  185. return $this->moveLast();
  186. }
  187. $this->owner->updateAllCounters(
  188. [
  189. $positionAttribute => -1
  190. ],
  191. [
  192. 'and',
  193. $this->createGroupConditionAttributes(),
  194. ['>', $positionAttribute, $oldRecord->$positionAttribute],
  195. ['<=', $positionAttribute, $position],
  196. ]
  197. );
  198. $this->owner->updateAttributes([
  199. $positionAttribute => $position
  200. ]);
  201. return true;
  202. }
  203. }
  204. /**
  205. * Creates array of group attributes with their values.
  206. * @see groupAttributes
  207. * @return array attribute conditions.
  208. */
  209. protected function createGroupConditionAttributes()
  210. {
  211. $condition = [];
  212. if (!empty($this->groupAttributes)) {
  213. foreach ($this->groupAttributes as $attribute) {
  214. $condition[$attribute] = $this->owner->$attribute;
  215. }
  216. }
  217. return $condition;
  218. }
  219. /**
  220. * Finds the number of records which belongs to the group of the owner.
  221. * @see groupAttributes
  222. * @return integer records count.
  223. */
  224. protected function countGroupRecords()
  225. {
  226. $query = $this->owner->find();
  227. if (!empty($this->groupAttributes)) {
  228. $query->andWhere($this->createGroupConditionAttributes());
  229. }
  230. return $query->count();
  231. }
  232. // Events :
  233. /**
  234. * @inheritdoc
  235. */
  236. public function events()
  237. {
  238. return [
  239. BaseActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert',
  240. BaseActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate',
  241. BaseActiveRecord::EVENT_AFTER_INSERT => 'afterSave',
  242. BaseActiveRecord::EVENT_AFTER_UPDATE => 'afterSave',
  243. BaseActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
  244. ];
  245. }
  246. /**
  247. * Handles owner 'beforeInsert' owner event, preparing its positioning.
  248. * @param ModelEvent $event event instance.
  249. */
  250. public function beforeInsert($event)
  251. {
  252. $positionAttribute = $this->positionAttribute;
  253. if ($this->owner->$positionAttribute > 0) {
  254. $this->positionOnSave = $this->owner->$positionAttribute;
  255. }
  256. $this->owner->$positionAttribute = $this->countGroupRecords() + 1;
  257. }
  258. /**
  259. * Handles owner 'beforeInsert' owner event, preparing its possible re-positioning.
  260. * @param ModelEvent $event event instance.
  261. */
  262. public function beforeUpdate($event)
  263. {
  264. $positionAttribute = $this->positionAttribute;
  265. $isNewGroup = false;
  266. foreach ($this->groupAttributes as $groupAttribute) {
  267. if ($this->owner->isAttributeChanged($groupAttribute, false)) {
  268. $isNewGroup = true;
  269. break;
  270. }
  271. }
  272. if ($isNewGroup) {
  273. $oldRecord = $this->owner->findOne($this->owner->getPrimaryKey());
  274. $oldRecord->moveLast();
  275. $this->positionOnSave = $this->owner->$positionAttribute;
  276. $this->owner->$positionAttribute = $this->countGroupRecords() + 1;
  277. } else {
  278. if ($this->owner->isAttributeChanged($positionAttribute, false)) {
  279. $this->positionOnSave = $this->owner->$positionAttribute;
  280. $this->owner->$positionAttribute = $this->owner->getOldAttribute($positionAttribute);
  281. }
  282. }
  283. }
  284. /**
  285. * This event raises after owner inserted or updated.
  286. * It applies previously set [[positionOnSave]].
  287. * This event supports other functionality.
  288. * @param ModelEvent $event event instance.
  289. */
  290. public function afterSave($event)
  291. {
  292. if ($this->positionOnSave !== null) {
  293. $this->moveToPosition($this->positionOnSave);
  294. }
  295. $this->positionOnSave = null;
  296. }
  297. /**
  298. * Handles owner 'beforeDelete' owner event, moving it to the end of the list before deleting.
  299. * @param ModelEvent $event event instance.
  300. */
  301. public function beforeDelete($event)
  302. {
  303. $this->moveLast();
  304. }
  305. }