SluggableBehavior.php 8.9 KB

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