Menu.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. <?php
  2. /**
  3. * Author: lf
  4. * Blog: https://blog.feehi.com
  5. * Email: job@feehi.com
  6. * Created at: 2017-03-15 21:16
  7. */
  8. namespace common\models;
  9. use Yii;
  10. use common\libs\FamilyTree;
  11. use common\helpers\FileDependencyHelper;
  12. use yii\helpers\ArrayHelper;
  13. use yii\behaviors\TimestampBehavior;
  14. use yii\helpers\Url;
  15. /**
  16. * This is the model class for table "{{%menu}}".
  17. *
  18. * @property string $id menu id
  19. * @property int $type menu type, frontend or backend
  20. * @property string $parent_id menu parent_id, if equals 0 represent first level menu
  21. * @property string $name menu name
  22. * @property string $url menu url, can be controller/action such as "site/index" or absolute uri like "https://www.feehi.com/tools/ip"
  23. * @property string $icon menu icon
  24. * @property string $sort menu sort
  25. * @property string $target html a tag target attribute, such as "_self" or "_blank"
  26. * @property integer is_absolute_url if 0, url will use the origin sample;if 1 url will be generate by Url::to(url)
  27. * @property integer $is_display if hidden this menu
  28. * @property string $created_at
  29. * @property string $updated_at
  30. */
  31. class Menu extends \yii\db\ActiveRecord
  32. {
  33. use FamilyTree;
  34. public $prefix_level_name;
  35. /** @var int $level menu level */
  36. public $level = null;
  37. /** @var int backend type menu */
  38. const TYPE_BACKEND = 0;
  39. /** @var int frontend type menu */
  40. const TYPE_FRONTEND = 1;
  41. /** @var int if menu hidden */
  42. const DISPLAY_YES = 1;
  43. const DISPLAY_NO = 0;
  44. /** @var string menu dependency file name */
  45. CONST MENU_CACHE_DEPENDENCY_FILE = "menu.txt";
  46. /**
  47. * get menu table name
  48. *
  49. * @return string
  50. */
  51. public static function tableName()
  52. {
  53. return '{{%menu}}';
  54. }
  55. /**
  56. * @inheritdoc
  57. */
  58. public function behaviors()
  59. {
  60. return [
  61. TimestampBehavior::className(),
  62. ];
  63. }
  64. /**
  65. * @inheritdoc
  66. */
  67. public function rules()
  68. {
  69. return [
  70. [['parent_id'], 'integer'],
  71. [['sort'], 'integer'],
  72. [['parent_id', 'sort'], 'default', 'value' => 0],
  73. [['sort'], 'compare', 'compareValue' => 0, 'operator' => '>='],
  74. [['is_display'], 'integer'],
  75. [['name', 'url', 'icon'], 'string', 'max' => 255],
  76. [['type', 'is_absolute_url'], 'in', 'range' => [0, 1]],
  77. [['target'], 'in', 'range' => ['_blank', '_self', '_parent', '_top']],
  78. [['name'], 'required'],
  79. ];
  80. }
  81. /**
  82. * @inheritdoc
  83. */
  84. public function attributeLabels()
  85. {
  86. return [
  87. 'id' => Yii::t('app', 'ID'),
  88. 'type' => Yii::t('app', 'Type'),
  89. 'parent_id' => Yii::t('app', 'Parent Id'),
  90. 'name' => Yii::t('app', 'Name'),
  91. 'url' => Yii::t('app', 'Url'),
  92. 'icon' => Yii::t('app', 'Icon'),
  93. 'sort' => Yii::t('app', 'Sort'),
  94. 'is_absolute_url' => Yii::t('app', 'Is Absolute Url'),
  95. 'target' => Yii::t('app', 'Target'),
  96. 'is_display' => Yii::t('app', 'Is Display'),
  97. 'created_at' => Yii::t('app', 'Created At'),
  98. 'updated_at' => Yii::t('app', 'Updated At'),
  99. ];
  100. }
  101. public function getItems()
  102. {
  103. return Menu::getMenus($this->type);
  104. }
  105. public function beforeSave($insert)
  106. {
  107. if (!$this->is_absolute_url) {
  108. $result = $this->convertRelativeUrlToJSONString();
  109. if (!$result) {
  110. return false;
  111. }
  112. }
  113. return parent::beforeSave($insert);
  114. }
  115. /**
  116. * get menu url
  117. *
  118. * @return string
  119. */
  120. public function getMenuUrl()
  121. {
  122. if ($this->is_absolute_url) {
  123. return $this->url;
  124. }
  125. $urlComponents = json_decode($this->url, true);
  126. if ($urlComponents === null) {
  127. //compatible old cms version
  128. $urlComponents[0] = $this->url;
  129. }
  130. return Url::to($urlComponents);
  131. }
  132. /**
  133. * convert relative url to json string
  134. * relative url format should like "/controller/action?p1=v1&p2=v2#fragment"
  135. * @return bool
  136. * @var string $urlDComponents will be encode to a json string for storage. when decode this json string can pass to Url::to($urlComponents) generate uri
  137. *
  138. */
  139. private function convertRelativeUrlToJSONString()
  140. {
  141. $urlComponents = [$this->url];
  142. if (strlen($this->url) > 0) {
  143. if (strpos($this->url, "/") !== 0) {
  144. $this->url = "/" . $this->url;
  145. }
  146. $urlComponents = [];
  147. $parsedUrl = parse_url($this->url);
  148. if (!isset($parsedUrl["path"]) || $parsedUrl["path"] === "") {
  149. $this->addError("url", Yii::t('app', 'Url is not a correct format. It should be like controller/action/?p1=v1&p2=v2'));
  150. return false;
  151. }
  152. $urlComponents[0] = $parsedUrl["path"];
  153. if (isset($parsedUrl["query"]) && $parsedUrl["query"] !== "") {
  154. parse_str($parsedUrl["query"], $query);
  155. if (!empty($query)) {
  156. $urlComponents = array_merge($urlComponents, $query);
  157. }
  158. }
  159. if (isset($parsedUrl["fragment"]) && $parsedUrl["fragment"] !== "") {
  160. $urlComponents["#"] = $parsedUrl["fragment"];
  161. }
  162. }
  163. $this->url = json_encode($urlComponents);
  164. if ($this->url === false) {
  165. $this->addError("url", Yii::t('app', 'Url is not a correct format. convert to json error. url components ' . print_r($urlComponents, true)));
  166. return false;
  167. }
  168. return true;
  169. }
  170. /**
  171. * convert json string to relative url
  172. * when edit this menu, should convert json string to the origin format for admin user edit
  173. *
  174. */
  175. public function convertJSONStringToRelativeUrl()
  176. {
  177. $urlComponents = json_decode($this->url, true);
  178. if( $urlComponents === null ){//compatible old version that stored not json format
  179. return $this->url;
  180. }
  181. $url = "";
  182. if (isset($urlComponents[0])) {
  183. $url .= $urlComponents[0];
  184. unset($urlComponents[0]);
  185. }
  186. $fragment = "";
  187. if (isset($urlComponents["#"])) {
  188. $fragment = "#" . $urlComponents["#"];
  189. unset($urlComponents["#"]);
  190. }
  191. if (!empty($urlComponents)) {
  192. $url .= "?" . urldecode(http_build_query($urlComponents)) . $fragment;
  193. }
  194. return $url;
  195. }
  196. /**
  197. * get menus
  198. *
  199. * @param null $menuType
  200. * @param null $isDisplay
  201. * @return array|\yii\db\ActiveRecord[]
  202. */
  203. public static function getMenus($menuType=null, $isDisplay=null)
  204. {
  205. $query = Menu::find()->orderBy(["sort"=>SORT_ASC]);
  206. if( $menuType !== null ){
  207. $query->andWhere(['type' => $menuType]);
  208. }
  209. if( $isDisplay !== null ){
  210. $query->andWhere(['is_display' => $isDisplay]);
  211. }
  212. $menus = $query->all();
  213. return $menus;
  214. }
  215. /**
  216. * validate
  217. *
  218. * @return bool
  219. */
  220. public function afterValidate()
  221. {
  222. if (!$this->getIsNewRecord()) {//if not create a new menu
  223. if ($this->id == $this->parent_id) {//cannot set menu to its own sub menu
  224. $this->addError('parent_id', Yii::t('app', 'Cannot be themselves sub'));
  225. return false;
  226. }
  227. $descendants = $this->getDescendants($this->id);
  228. $descendants = ArrayHelper::getColumn($descendants, 'id');
  229. if (in_array($this->parent_id, $descendants)) {//cannot set menu to its own descendants sub menu
  230. $this->addError('parent_id', Yii::t('app', 'Cannot be themselves descendants sub'));
  231. return false;
  232. }
  233. }
  234. }
  235. /**
  236. * check menu can be delete
  237. *
  238. * @return bool
  239. */
  240. public function beforeDelete()
  241. {
  242. $subs = $this->getDescendants($this->id);
  243. if (!empty($subs)) {
  244. $this->addError('id', Yii::t('app', 'Sub Menu exists, cannot be deleted'));
  245. return false;
  246. }
  247. return parent::beforeDelete();
  248. }
  249. public function getParent()
  250. {
  251. return $this->hasOne(self::className(), ['id' => 'parent_id']);
  252. }
  253. public function afterSave($insert, $changedAttributes)
  254. {
  255. $this->removeBackendMenuCache();
  256. parent::afterSave($insert, $changedAttributes);
  257. }
  258. public function afterDelete()
  259. {
  260. $this->removeBackendMenuCache();
  261. parent::afterDelete();
  262. }
  263. private function removeBackendMenuCache()
  264. {
  265. /** @var FileDependencyHelper $object */
  266. $object = Yii::createObject([
  267. 'class' => FileDependencyHelper::className(),
  268. 'fileName' => self::MENU_CACHE_DEPENDENCY_FILE,
  269. ]);
  270. $object->updateFile();
  271. }
  272. }