You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

254 line
7.7KB

  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\behaviors;
  8. use yii\base\InvalidConfigException;
  9. use yii\db\BaseActiveRecord;
  10. use yii\helpers\Inflector;
  11. use yii\validators\UniqueValidator;
  12. use Yii;
  13. /**
  14. * SluggableBehavior automatically fills the specified attribute with a value that can be used a slug in a URL.
  15. *
  16. * To use SluggableBehavior, insert the following code to your ActiveRecord class:
  17. *
  18. * ```php
  19. * use yii\behaviors\SluggableBehavior;
  20. *
  21. * public function behaviors()
  22. * {
  23. * return [
  24. * [
  25. * 'class' => SluggableBehavior::className(),
  26. * 'attribute' => 'title',
  27. * // 'slugAttribute' => 'slug',
  28. * ],
  29. * ];
  30. * }
  31. * ```
  32. *
  33. * By default, SluggableBehavior will fill the `slug` attribute with a value that can be used a slug in a URL
  34. * when the associated AR object is being validated.
  35. *
  36. * Because attribute values will be set automatically by this behavior, they are usually not user input and should therefore
  37. * not be validated, i.e. the `slug` attribute should not appear in the [[\yii\base\Model::rules()|rules()]] method of the model.
  38. *
  39. * If your attribute name is different, you may configure the [[slugAttribute]] property like the following:
  40. *
  41. * ```php
  42. * public function behaviors()
  43. * {
  44. * return [
  45. * [
  46. * 'class' => SluggableBehavior::className(),
  47. * 'slugAttribute' => 'alias',
  48. * ],
  49. * ];
  50. * }
  51. * ```
  52. *
  53. * @author Alexander Kochetov <creocoder@gmail.com>
  54. * @author Paul Klimov <klimov.paul@gmail.com>
  55. * @since 2.0
  56. */
  57. class SluggableBehavior extends AttributeBehavior
  58. {
  59. /**
  60. * @var string the attribute that will receive the slug value
  61. */
  62. public $slugAttribute = 'slug';
  63. /**
  64. * @var string|array the attribute or list of attributes whose value will be converted into a slug
  65. */
  66. public $attribute;
  67. /**
  68. * @var string|callable the value that will be used as a slug. This can be an anonymous function
  69. * or an arbitrary value. If the former, the return value of the function will be used as a slug.
  70. * The signature of the function should be as follows,
  71. *
  72. * ```php
  73. * function ($event)
  74. * {
  75. * // return slug
  76. * }
  77. * ```
  78. */
  79. public $value;
  80. /**
  81. * @var boolean whether to generate a new slug if it has already been generated before.
  82. * If true, the behavior will not generate a new slug even if [[attribute]] is changed.
  83. * @since 2.0.2
  84. */
  85. public $immutable = false;
  86. /**
  87. * @var boolean whether to ensure generated slug value to be unique among owner class records.
  88. * If enabled behavior will validate slug uniqueness automatically. If validation fails it will attempt
  89. * generating unique slug value from based one until success.
  90. */
  91. public $ensureUnique = false;
  92. /**
  93. * @var array configuration for slug uniqueness validator. Parameter 'class' may be omitted - by default
  94. * [[UniqueValidator]] will be used.
  95. * @see UniqueValidator
  96. */
  97. public $uniqueValidator = [];
  98. /**
  99. * @var callable slug unique value generator. It is used in case [[ensureUnique]] enabled and generated
  100. * slug is not unique. This should be a PHP callable with following signature:
  101. *
  102. * ```php
  103. * function ($baseSlug, $iteration, $model)
  104. * {
  105. * // return uniqueSlug
  106. * }
  107. * ```
  108. *
  109. * If not set unique slug will be generated adding incrementing suffix to the base slug.
  110. */
  111. public $uniqueSlugGenerator;
  112. /**
  113. * @inheritdoc
  114. */
  115. public function init()
  116. {
  117. parent::init();
  118. if (empty($this->attributes)) {
  119. $this->attributes = [BaseActiveRecord::EVENT_BEFORE_VALIDATE => $this->slugAttribute];
  120. }
  121. if ($this->attribute === null && $this->value === null) {
  122. throw new InvalidConfigException('Either "attribute" or "value" property must be specified.');
  123. }
  124. }
  125. /**
  126. * @inheritdoc
  127. */
  128. protected function getValue($event)
  129. {
  130. if ($this->attribute !== null) {
  131. if ($this->isNewSlugNeeded()) {
  132. $slugParts = [];
  133. foreach ((array) $this->attribute as $attribute) {
  134. $slugParts[] = $this->owner->{$attribute};
  135. }
  136. $slug = $this->generateSlug($slugParts);
  137. } else {
  138. return $this->owner->{$this->slugAttribute};
  139. }
  140. } else {
  141. $slug = parent::getValue($event);
  142. }
  143. return $this->ensureUnique ? $this->makeUnique($slug) : $slug;
  144. }
  145. /**
  146. * Checks whether the new slug generation is needed
  147. * This method is called by [[getValue]] to check whether the new slug generation is needed.
  148. * You may override it to customize checking.
  149. * @return boolean
  150. * @since 2.0.7
  151. */
  152. protected function isNewSlugNeeded()
  153. {
  154. if (empty($this->owner->{$this->slugAttribute})) {
  155. return true;
  156. }
  157. if ($this->immutable) {
  158. return false;
  159. }
  160. foreach ((array)$this->attribute as $attribute) {
  161. if ($this->owner->isAttributeChanged($attribute)) {
  162. return true;
  163. }
  164. }
  165. return false;
  166. }
  167. /**
  168. * This method is called by [[getValue]] to generate the slug.
  169. * You may override it to customize slug generation.
  170. * The default implementation calls [[\yii\helpers\Inflector::slug()]] on the input strings
  171. * concatenated by dashes (`-`).
  172. * @param array $slugParts an array of strings that should be concatenated and converted to generate the slug value.
  173. * @return string the conversion result.
  174. */
  175. protected function generateSlug($slugParts)
  176. {
  177. return Inflector::slug(implode('-', $slugParts));
  178. }
  179. /**
  180. * This method is called by [[getValue]] when [[ensureUnique]] is true to generate the unique slug.
  181. * Calls [[generateUniqueSlug]] until generated slug is unique and returns it.
  182. * @param string $slug basic slug value
  183. * @return string unique slug
  184. * @see getValue
  185. * @see generateUniqueSlug
  186. * @since 2.0.7
  187. */
  188. protected function makeUnique($slug)
  189. {
  190. $uniqueSlug = $slug;
  191. $iteration = 0;
  192. while (!$this->validateSlug($uniqueSlug)) {
  193. $iteration++;
  194. $uniqueSlug = $this->generateUniqueSlug($slug, $iteration);
  195. }
  196. return $uniqueSlug;
  197. }
  198. /**
  199. * Checks if given slug value is unique.
  200. * @param string $slug slug value
  201. * @return boolean whether slug is unique.
  202. */
  203. protected function validateSlug($slug)
  204. {
  205. /* @var $validator UniqueValidator */
  206. /* @var $model BaseActiveRecord */
  207. $validator = Yii::createObject(array_merge(
  208. [
  209. 'class' => UniqueValidator::className(),
  210. ],
  211. $this->uniqueValidator
  212. ));
  213. $model = clone $this->owner;
  214. $model->clearErrors();
  215. $model->{$this->slugAttribute} = $slug;
  216. $validator->validateAttribute($model, $this->slugAttribute);
  217. return !$model->hasErrors();
  218. }
  219. /**
  220. * Generates slug using configured callback or increment of iteration.
  221. * @param string $baseSlug base slug value
  222. * @param integer $iteration iteration number
  223. * @return string new slug value
  224. * @throws \yii\base\InvalidConfigException
  225. */
  226. protected function generateUniqueSlug($baseSlug, $iteration)
  227. {
  228. if (is_callable($this->uniqueSlugGenerator)) {
  229. return call_user_func($this->uniqueSlugGenerator, $baseSlug, $iteration, $this->owner);
  230. }
  231. return $baseSlug . '-' . ($iteration + 1);
  232. }
  233. }