SluggableBehavior.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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;
  9. use yii\base\InvalidConfigException;
  10. use yii\db\BaseActiveRecord;
  11. use yii\helpers\ArrayHelper;
  12. use yii\helpers\Inflector;
  13. use yii\validators\UniqueValidator;
  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 bool whether to skip slug generation if [[attribute]] is null or an empty string.
  97. * If true, the behaviour will not generate a new slug if [[attribute]] is null or an empty string.
  98. * @since 2.0.13
  99. */
  100. public $skipOnEmpty = false;
  101. /**
  102. * @var array configuration for slug uniqueness validator. Parameter 'class' may be omitted - by default
  103. * [[UniqueValidator]] will be used.
  104. * @see UniqueValidator
  105. */
  106. public $uniqueValidator = [];
  107. /**
  108. * @var callable slug unique value generator. It is used in case [[ensureUnique]] enabled and generated
  109. * slug is not unique. This should be a PHP callable with following signature:
  110. *
  111. * ```php
  112. * function ($baseSlug, $iteration, $model)
  113. * {
  114. * // return uniqueSlug
  115. * }
  116. * ```
  117. *
  118. * If not set unique slug will be generated adding incrementing suffix to the base slug.
  119. */
  120. public $uniqueSlugGenerator;
  121. /**
  122. * {@inheritdoc}
  123. */
  124. public function init()
  125. {
  126. parent::init();
  127. if (empty($this->attributes)) {
  128. $this->attributes = [BaseActiveRecord::EVENT_BEFORE_VALIDATE => $this->slugAttribute];
  129. }
  130. if ($this->attribute === null && $this->value === null) {
  131. throw new InvalidConfigException('Either "attribute" or "value" property must be specified.');
  132. }
  133. }
  134. /**
  135. * {@inheritdoc}
  136. */
  137. protected function getValue($event)
  138. {
  139. if (!$this->isNewSlugNeeded()) {
  140. return $this->owner->{$this->slugAttribute};
  141. }
  142. if ($this->attribute !== null) {
  143. $slugParts = [];
  144. foreach ((array) $this->attribute as $attribute) {
  145. $part = ArrayHelper::getValue($this->owner, $attribute);
  146. if ($this->skipOnEmpty && $this->isEmpty($part)) {
  147. return $this->owner->{$this->slugAttribute};
  148. }
  149. $slugParts[] = $part;
  150. }
  151. $slug = $this->generateSlug($slugParts);
  152. } else {
  153. $slug = parent::getValue($event);
  154. }
  155. return $this->ensureUnique ? $this->makeUnique($slug) : $slug;
  156. }
  157. /**
  158. * Checks whether the new slug generation is needed
  159. * This method is called by [[getValue]] to check whether the new slug generation is needed.
  160. * You may override it to customize checking.
  161. * @return bool
  162. * @since 2.0.7
  163. */
  164. protected function isNewSlugNeeded()
  165. {
  166. if (empty($this->owner->{$this->slugAttribute})) {
  167. return true;
  168. }
  169. if ($this->immutable) {
  170. return false;
  171. }
  172. if ($this->attribute === null) {
  173. return true;
  174. }
  175. foreach ((array) $this->attribute as $attribute) {
  176. if ($this->owner->isAttributeChanged($attribute)) {
  177. return true;
  178. }
  179. }
  180. return false;
  181. }
  182. /**
  183. * This method is called by [[getValue]] to generate the slug.
  184. * You may override it to customize slug generation.
  185. * The default implementation calls [[\yii\helpers\Inflector::slug()]] on the input strings
  186. * concatenated by dashes (`-`).
  187. * @param array $slugParts an array of strings that should be concatenated and converted to generate the slug value.
  188. * @return string the conversion result.
  189. */
  190. protected function generateSlug($slugParts)
  191. {
  192. return Inflector::slug(implode('-', $slugParts));
  193. }
  194. /**
  195. * This method is called by [[getValue]] when [[ensureUnique]] is true to generate the unique slug.
  196. * Calls [[generateUniqueSlug]] until generated slug is unique and returns it.
  197. * @param string $slug basic slug value
  198. * @return string unique slug
  199. * @see getValue
  200. * @see generateUniqueSlug
  201. * @since 2.0.7
  202. */
  203. protected function makeUnique($slug)
  204. {
  205. $uniqueSlug = $slug;
  206. $iteration = 0;
  207. while (!$this->validateSlug($uniqueSlug)) {
  208. $iteration++;
  209. $uniqueSlug = $this->generateUniqueSlug($slug, $iteration);
  210. }
  211. return $uniqueSlug;
  212. }
  213. /**
  214. * Checks if given slug value is unique.
  215. * @param string $slug slug value
  216. * @return bool whether slug is unique.
  217. */
  218. protected function validateSlug($slug)
  219. {
  220. /* @var $validator UniqueValidator */
  221. /* @var $model BaseActiveRecord */
  222. $validator = Yii::createObject(array_merge(
  223. [
  224. 'class' => UniqueValidator::className(),
  225. ],
  226. $this->uniqueValidator
  227. ));
  228. $model = clone $this->owner;
  229. $model->clearErrors();
  230. $model->{$this->slugAttribute} = $slug;
  231. $validator->validateAttribute($model, $this->slugAttribute);
  232. return !$model->hasErrors();
  233. }
  234. /**
  235. * Generates slug using configured callback or increment of iteration.
  236. * @param string $baseSlug base slug value
  237. * @param int $iteration iteration number
  238. * @return string new slug value
  239. * @throws \yii\base\InvalidConfigException
  240. */
  241. protected function generateUniqueSlug($baseSlug, $iteration)
  242. {
  243. if (is_callable($this->uniqueSlugGenerator)) {
  244. return call_user_func($this->uniqueSlugGenerator, $baseSlug, $iteration, $this->owner);
  245. }
  246. return $baseSlug . '-' . ($iteration + 1);
  247. }
  248. /**
  249. * Checks if $slugPart is empty string or null.
  250. *
  251. * @param string $slugPart One of attributes that is used for slug generation.
  252. * @return bool whether $slugPart empty or not.
  253. * @since 2.0.13
  254. */
  255. protected function isEmpty($slugPart)
  256. {
  257. return $slugPart === null || $slugPart === '';
  258. }
  259. }