SluggableBehavior.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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|null the attribute or list of attributes whose value will be converted into a slug
  66. * or `null` meaning that the `$value` property will be used to generate a slug.
  67. */
  68. public $attribute;
  69. /**
  70. * @var callable|string|null the value that will be used as a slug. This can be an anonymous function
  71. * or an arbitrary value or null. If the former, the return value of the function will be used as a slug.
  72. * If `null` then the `$attribute` property will be used to generate a slug.
  73. * The signature of the function should be as follows,
  74. *
  75. * ```php
  76. * function ($event)
  77. * {
  78. * // return slug
  79. * }
  80. * ```
  81. */
  82. public $value;
  83. /**
  84. * @var bool whether to generate a new slug if it has already been generated before.
  85. * If true, the behavior will not generate a new slug even if [[attribute]] is changed.
  86. * @since 2.0.2
  87. */
  88. public $immutable = false;
  89. /**
  90. * @var bool whether to ensure generated slug value to be unique among owner class records.
  91. * If enabled behavior will validate slug uniqueness automatically. If validation fails it will attempt
  92. * generating unique slug value from based one until success.
  93. */
  94. public $ensureUnique = false;
  95. /**
  96. * @var array configuration for slug uniqueness validator. Parameter 'class' may be omitted - by default
  97. * [[UniqueValidator]] will be used.
  98. * @see UniqueValidator
  99. */
  100. public $uniqueValidator = [];
  101. /**
  102. * @var callable slug unique value generator. It is used in case [[ensureUnique]] enabled and generated
  103. * slug is not unique. This should be a PHP callable with following signature:
  104. *
  105. * ```php
  106. * function ($baseSlug, $iteration, $model)
  107. * {
  108. * // return uniqueSlug
  109. * }
  110. * ```
  111. *
  112. * If not set unique slug will be generated adding incrementing suffix to the base slug.
  113. */
  114. public $uniqueSlugGenerator;
  115. /**
  116. * @inheritdoc
  117. */
  118. public function init()
  119. {
  120. parent::init();
  121. if (empty($this->attributes)) {
  122. $this->attributes = [BaseActiveRecord::EVENT_BEFORE_VALIDATE => $this->slugAttribute];
  123. }
  124. if ($this->attribute === null && $this->value === null) {
  125. throw new InvalidConfigException('Either "attribute" or "value" property must be specified.');
  126. }
  127. }
  128. /**
  129. * @inheritdoc
  130. */
  131. protected function getValue($event)
  132. {
  133. if (!$this->isNewSlugNeeded()) {
  134. return $this->owner->{$this->slugAttribute};
  135. }
  136. if ($this->attribute !== null) {
  137. $slugParts = [];
  138. foreach ((array) $this->attribute as $attribute) {
  139. $slugParts[] = ArrayHelper::getValue($this->owner, $attribute);
  140. }
  141. $slug = $this->generateSlug($slugParts);
  142. } else {
  143. $slug = parent::getValue($event);
  144. }
  145. return $this->ensureUnique ? $this->makeUnique($slug) : $slug;
  146. }
  147. /**
  148. * Checks whether the new slug generation is needed
  149. * This method is called by [[getValue]] to check whether the new slug generation is needed.
  150. * You may override it to customize checking.
  151. * @return bool
  152. * @since 2.0.7
  153. */
  154. protected function isNewSlugNeeded()
  155. {
  156. if (empty($this->owner->{$this->slugAttribute})) {
  157. return true;
  158. }
  159. if ($this->immutable) {
  160. return false;
  161. }
  162. if ($this->attribute === null) {
  163. return true;
  164. }
  165. foreach ((array)$this->attribute as $attribute) {
  166. if ($this->owner->isAttributeChanged($attribute)) {
  167. return true;
  168. }
  169. }
  170. return false;
  171. }
  172. /**
  173. * This method is called by [[getValue]] to generate the slug.
  174. * You may override it to customize slug generation.
  175. * The default implementation calls [[\yii\helpers\Inflector::slug()]] on the input strings
  176. * concatenated by dashes (`-`).
  177. * @param array $slugParts an array of strings that should be concatenated and converted to generate the slug value.
  178. * @return string the conversion result.
  179. */
  180. protected function generateSlug($slugParts)
  181. {
  182. return Inflector::slug(implode('-', $slugParts));
  183. }
  184. /**
  185. * This method is called by [[getValue]] when [[ensureUnique]] is true to generate the unique slug.
  186. * Calls [[generateUniqueSlug]] until generated slug is unique and returns it.
  187. * @param string $slug basic slug value
  188. * @return string unique slug
  189. * @see getValue
  190. * @see generateUniqueSlug
  191. * @since 2.0.7
  192. */
  193. protected function makeUnique($slug)
  194. {
  195. $uniqueSlug = $slug;
  196. $iteration = 0;
  197. while (!$this->validateSlug($uniqueSlug)) {
  198. $iteration++;
  199. $uniqueSlug = $this->generateUniqueSlug($slug, $iteration);
  200. }
  201. return $uniqueSlug;
  202. }
  203. /**
  204. * Checks if given slug value is unique.
  205. * @param string $slug slug value
  206. * @return bool whether slug is unique.
  207. */
  208. protected function validateSlug($slug)
  209. {
  210. /* @var $validator UniqueValidator */
  211. /* @var $model BaseActiveRecord */
  212. $validator = Yii::createObject(array_merge(
  213. [
  214. 'class' => UniqueValidator::className(),
  215. ],
  216. $this->uniqueValidator
  217. ));
  218. $model = clone $this->owner;
  219. $model->clearErrors();
  220. $model->{$this->slugAttribute} = $slug;
  221. $validator->validateAttribute($model, $this->slugAttribute);
  222. return !$model->hasErrors();
  223. }
  224. /**
  225. * Generates slug using configured callback or increment of iteration.
  226. * @param string $baseSlug base slug value
  227. * @param int $iteration iteration number
  228. * @return string new slug value
  229. * @throws \yii\base\InvalidConfigException
  230. */
  231. protected function generateUniqueSlug($baseSlug, $iteration)
  232. {
  233. if (is_callable($this->uniqueSlugGenerator)) {
  234. return call_user_func($this->uniqueSlugGenerator, $baseSlug, $iteration, $this->owner);
  235. }
  236. return $baseSlug . '-' . ($iteration + 1);
  237. }
  238. }