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.

216 lines
6.4KB

  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. If your attribute name is different, you may configure
  35. * the [[slugAttribute]] property like the following:
  36. *
  37. * ```php
  38. * public function behaviors()
  39. * {
  40. * return [
  41. * [
  42. * 'class' => SluggableBehavior::className(),
  43. * 'slugAttribute' => 'alias',
  44. * ],
  45. * ];
  46. * }
  47. * ```
  48. *
  49. * @author Alexander Kochetov <creocoder@gmail.com>
  50. * @author Paul Klimov <klimov.paul@gmail.com>
  51. * @since 2.0
  52. */
  53. class SluggableBehavior extends AttributeBehavior
  54. {
  55. /**
  56. * @var string the attribute that will receive the slug value
  57. */
  58. public $slugAttribute = 'slug';
  59. /**
  60. * @var string|array the attribute or list of attributes whose value will be converted into a slug
  61. */
  62. public $attribute;
  63. /**
  64. * @var string|callable the value that will be used as a slug. This can be an anonymous function
  65. * or an arbitrary value. If the former, the return value of the function will be used as a slug.
  66. * The signature of the function should be as follows,
  67. *
  68. * ```php
  69. * function ($event)
  70. * {
  71. * // return slug
  72. * }
  73. * ```
  74. */
  75. public $value;
  76. /**
  77. * @var boolean whether to generate a new slug if it has already been generated before.
  78. * If true, the behavior will not generate a new slug even if [[attribute]] is changed.
  79. * @since 2.0.2
  80. */
  81. public $immutable = false;
  82. /**
  83. * @var boolean whether to ensure generated slug value to be unique among owner class records.
  84. * If enabled behavior will validate slug uniqueness automatically. If validation fails it will attempt
  85. * generating unique slug value from based one until success.
  86. */
  87. public $ensureUnique = false;
  88. /**
  89. * @var array configuration for slug uniqueness validator. Parameter 'class' may be omitted - by default
  90. * [[UniqueValidator]] will be used.
  91. * @see UniqueValidator
  92. */
  93. public $uniqueValidator = [];
  94. /**
  95. * @var callable slug unique value generator. It is used in case [[ensureUnique]] enabled and generated
  96. * slug is not unique. This should be a PHP callable with following signature:
  97. *
  98. * ```php
  99. * function ($baseSlug, $iteration, $model)
  100. * {
  101. * // return uniqueSlug
  102. * }
  103. * ```
  104. *
  105. * If not set unique slug will be generated adding incrementing suffix to the base slug.
  106. */
  107. public $uniqueSlugGenerator;
  108. /**
  109. * @inheritdoc
  110. */
  111. public function init()
  112. {
  113. parent::init();
  114. if (empty($this->attributes)) {
  115. $this->attributes = [BaseActiveRecord::EVENT_BEFORE_VALIDATE => $this->slugAttribute];
  116. }
  117. if ($this->attribute === null && $this->value === null) {
  118. throw new InvalidConfigException('Either "attribute" or "value" property must be specified.');
  119. }
  120. }
  121. /**
  122. * @inheritdoc
  123. */
  124. protected function getValue($event)
  125. {
  126. $isNewSlug = true;
  127. if ($this->attribute !== null) {
  128. $attributes = (array) $this->attribute;
  129. /* @var $owner BaseActiveRecord */
  130. $owner = $this->owner;
  131. if (!empty($owner->{$this->slugAttribute})) {
  132. $isNewSlug = false;
  133. if (!$this->immutable) {
  134. foreach ($attributes as $attribute) {
  135. if ($owner->isAttributeChanged($attribute)) {
  136. $isNewSlug = true;
  137. break;
  138. }
  139. }
  140. }
  141. }
  142. if ($isNewSlug) {
  143. $slugParts = [];
  144. foreach ($attributes as $attribute) {
  145. $slugParts[] = $owner->{$attribute};
  146. }
  147. $slug = Inflector::slug(implode('-', $slugParts));
  148. } else {
  149. $slug = $owner->{$this->slugAttribute};
  150. }
  151. } else {
  152. $slug = parent::getValue($event);
  153. }
  154. if ($this->ensureUnique && $isNewSlug) {
  155. $baseSlug = $slug;
  156. $iteration = 0;
  157. while (!$this->validateSlug($slug)) {
  158. $iteration++;
  159. $slug = $this->generateUniqueSlug($baseSlug, $iteration);
  160. }
  161. }
  162. return $slug;
  163. }
  164. /**
  165. * Checks if given slug value is unique.
  166. * @param string $slug slug value
  167. * @return boolean whether slug is unique.
  168. */
  169. private function validateSlug($slug)
  170. {
  171. /* @var $validator UniqueValidator */
  172. /* @var $model BaseActiveRecord */
  173. $validator = Yii::createObject(array_merge(
  174. [
  175. 'class' => UniqueValidator::className()
  176. ],
  177. $this->uniqueValidator
  178. ));
  179. $model = clone $this->owner;
  180. $model->clearErrors();
  181. $model->{$this->slugAttribute} = $slug;
  182. $validator->validateAttribute($model, $this->slugAttribute);
  183. return !$model->hasErrors();
  184. }
  185. /**
  186. * Generates slug using configured callback or increment of iteration.
  187. * @param string $baseSlug base slug value
  188. * @param integer $iteration iteration number
  189. * @return string new slug value
  190. * @throws \yii\base\InvalidConfigException
  191. */
  192. private function generateUniqueSlug($baseSlug, $iteration)
  193. {
  194. if (is_callable($this->uniqueSlugGenerator)) {
  195. return call_user_func($this->uniqueSlugGenerator, $baseSlug, $iteration, $this->owner);
  196. } else {
  197. return $baseSlug . '-' . ($iteration + 1);
  198. }
  199. }
  200. }