SluggableBehavior.php 7.8 KB

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