PageRenderTime 27ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/Core/Libs/Model/Behavior/SluggableBehavior.php

http://github.com/infinitas/infinitas
PHP | 342 lines | 243 code | 39 blank | 60 comment | 36 complexity | b9af836eb4052519bbebecf01931fe3d MD5 | raw file
  1. <?php
  2. /**
  3. * Sluggable Behavior class file.
  4. *
  5. * @filesource
  6. * @author Mariano Iglesias
  7. * @link http://cake-syrup.sourceforge.net/ingredients/sluggable-behavior/
  8. * @version $Revision: 36 $
  9. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  10. * @package Infinitas.Libs.Model.Behavior
  11. */
  12. /**
  13. * Model behavior to support generation of slugs for models.
  14. *
  15. * @package Infinitas.Libs.Model.Behavior
  16. */
  17. class SluggableBehavior extends ModelBehavior {
  18. /**
  19. * Contain settings indexed by model name.
  20. *
  21. * @var array
  22. */
  23. private $__settings = array();
  24. /**
  25. * Initiate behavior for the model using specified settings. Available settings:
  26. *
  27. * - label: (array | string, optional) set to the field name that contains the
  28. * string from where to generate the slug, or a set of field names to
  29. * concatenate for generating the slug. DEFAULTS TO: title
  30. *
  31. * - slug: (string, optional) name of the field name that holds generated slugs.
  32. * DEFAULTS TO: slug
  33. *
  34. * - separator: (string, optional) separator character / string to use for replacing
  35. * non alphabetic characters in generated slug. DEFAULTS TO: -
  36. *
  37. * - length: (integer, optional) maximum length the generated slug can have.
  38. * DEFAULTS TO: 100
  39. *
  40. * - overwrite: (boolean, optional) set to true if slugs should be re-generated when
  41. * updating an existing record. DEFAULTS TO: false
  42. *
  43. * @param object $Model Model using the behaviour
  44. * @param array $settings Settings to override for model.
  45. */
  46. public function setup(Model $Model, $settings = array()) {
  47. $default = array('label' => array('name'), 'slug' => 'slug', 'separator' => '-', 'overwrite' => false, 'translation' => null);
  48. if (!isset($this->__settings[$Model->alias])) {
  49. $this->__settings[$Model->alias] = $default;
  50. }
  51. $this->__settings[$Model->alias] = array_merge($this->__settings[$Model->alias], $settings);
  52. $field = $Model->schema($this->__settings[$Model->alias]['slug']);
  53. $this->__settings[$Model->alias]['length'] = $field['length'];
  54. if (!is_array($this->__settings[$Model->alias]['label'])) {
  55. $this->__settings[$Model->alias]['label'] = array($this->__settings[$Model->alias]['label']);
  56. }
  57. foreach ($this->__settings[$Model->alias]['label'] as $k => $field) {
  58. if (!$Model->hasField($field)) {
  59. unset($this->__settings[$Model->alias]['label'][$k]);
  60. }
  61. if ($field == $this->__settings[$Model->alias]['slug']) {
  62. throw new Exception(sprintf('Model "%s" can not slug its slug field "%s"', $Model->alias, $this->__settings[$Model->alias]['slug']));
  63. }
  64. }
  65. if (empty($this->__settings[$Model->alias]['label'])) {
  66. throw new Exception(sprintf('Model "%s" has no sluggable fields', $Model->alias));
  67. }
  68. if ($Model->_schema[$Model->displayField]['length'] > $Model->_schema[$this->__settings[$Model->alias]['slug']]['length']) {
  69. throw new Exception(sprintf(__d('libs', '%s slugs will be truncated, slug field too short'), $Model->alias));
  70. }
  71. }
  72. /**
  73. * Run before a model is saved, used to set up slug for model.
  74. *
  75. * @param object $Model Model about to be saved.
  76. *
  77. * @return boolean
  78. */
  79. public function beforeSave(Model $Model) {
  80. $return = parent::beforeSave($Model);
  81. $isBlank = false;
  82. if (isset($Model->data[$Model->alias][$this->__settings[$Model->alias]['slug']]) && empty($Model->data[$Model->alias][$this->__settings[$Model->alias]['slug']])) {
  83. $isBlank = true;
  84. }
  85. $shouldSlug = $Model->hasField($this->__settings[$Model->alias]['slug']) && (
  86. $isBlank ||
  87. $this->__settings[$Model->alias]['overwrite'] ||
  88. empty($Model->id)
  89. );
  90. if (!$shouldSlug) {
  91. return $return;
  92. }
  93. $label = '';
  94. foreach ($this->__settings[$Model->alias]['label'] as $field) {
  95. if (!empty($Model->data[$Model->alias][$field])) {
  96. $label .= !empty($label) ? ' ' : '' . $Model->data[$Model->alias][$field];
  97. }
  98. }
  99. if (empty($label)) {
  100. return $return;
  101. }
  102. $slug = $this->__slug($label, $this->__settings[$Model->alias]);
  103. $conditions = array(
  104. sprintf('%s.%s LIKE', $Model->alias, $this->__settings[$Model->alias]['slug']) => $slug . '%'
  105. );
  106. if (!empty($Model->id)) {
  107. $conditions[$Model->alias . '.' . $Model->primaryKey] = '!= ' . $Model->id;
  108. }
  109. $result = $Model->find(
  110. 'all',
  111. array(
  112. 'conditions' => $conditions,
  113. 'fields' => array(
  114. $Model->primaryKey,
  115. $this->__settings[$Model->alias]['slug']
  116. ),
  117. 'recursive' => - 1
  118. )
  119. );
  120. $sameUrls = null;
  121. if (!empty($result)) {
  122. $sameUrls = Set::extract($result, '{n}.' . $Model->alias . '.' . $this->__settings[$Model->alias]['slug']);
  123. }
  124. if (!empty($sameUrls)) {
  125. $begginingSlug = $slug;
  126. $index = 1;
  127. while ($index > 0) {
  128. if (!in_array($begginingSlug . $this->__settings[$Model->alias]['separator'] . $index, $sameUrls)) {
  129. $slug = $begginingSlug . $this->__settings[$Model->alias]['separator'] . $index;
  130. $index = - 1;
  131. }
  132. $index++;
  133. }
  134. }
  135. if (!empty($Model->whitelist) && !in_array($this->__settings[$Model->alias]['slug'], $Model->whitelist)) {
  136. $Model->whitelist[] = $this->__settings[$Model->alias]['slug'];
  137. }
  138. $Model->data[$Model->alias][$this->__settings[$Model->alias]['slug']] = $slug;
  139. return $return;
  140. }
  141. /**
  142. * Generate a slug for the given string using specified settings.
  143. *
  144. * @param string $string String from where to generate slug
  145. * @param array $settings Settings to use (looks for 'separator' and 'length')
  146. *
  147. * @return string
  148. */
  149. public function __slug($string, $settings) {
  150. if (!empty($settings['translation']) && is_array($settings['translation'])) {
  151. if (count($settings['translation']) >= 2 && count($settings['translation']) % 2 == 0) {
  152. for ($i = 0, $limiti = count($settings['translation']); $i < $limiti; $i += 2) {
  153. $from = $settings['translation'][$i];
  154. $to = $settings['translation'][$i + 1];
  155. if (is_string($from) && is_string($to)) {
  156. $string = strtr($string, $from, $to);
  157. }
  158. else {
  159. $string = str_replace($from, $to, $string);
  160. }
  161. }
  162. }
  163. else if (count($settings['translation']) == 1) {
  164. $string = strtr($string, $settings['translation'][0]);
  165. }
  166. $string = strtolower($string);
  167. }
  168. else if (!empty($settings['translation']) && is_string($settings['translation']) &&
  169. in_array(strtolower($settings['translation']), array('utf-8', 'iso-8859-1'))) {
  170. $translations = array(
  171. 'iso-8859-1' => array(
  172. chr(128) . chr(131) . chr(138) . chr(142) . chr(154) . chr(158)
  173. . chr(159) . chr(162) . chr(165) . chr(181) . chr(192) . chr(193) . chr(194)
  174. . chr(195) . chr(196) . chr(197) . chr(199) . chr(200) . chr(201) . chr(202)
  175. . chr(203) . chr(204) . chr(205) . chr(206) . chr(207) . chr(209) . chr(210)
  176. . chr(211) . chr(212) . chr(213) . chr(214) . chr(216) . chr(217) . chr(218)
  177. . chr(219) . chr(220) . chr(221) . chr(224) . chr(225) . chr(226) . chr(227)
  178. . chr(228) . chr(229) . chr(231) . chr(232) . chr(233) . chr(234) . chr(235)
  179. . chr(236) . chr(237) . chr(238) . chr(239) . chr(241) . chr(242) . chr(243)
  180. . chr(244) . chr(245) . chr(246) . chr(248) . chr(249) . chr(250) . chr(251)
  181. . chr(252) . chr(253) . chr(255),
  182. 'EfSZsz' . 'YcYuAAA' . 'AAACEEE' . 'EIIIINO' . 'OOOOOUU' . 'UUYaaaa' . 'aaceeee' . 'iiiinoo' . 'oooouuu' . 'uyy',
  183. array(chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254)),
  184. array('OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th')
  185. ),
  186. 'utf-8' => array(
  187. array(
  188. // Decompositions for Latin-1 Supplement
  189. chr(195) . chr(128) => 'A', chr(195) . chr(129) => 'A',
  190. chr(195) . chr(130) => 'A', chr(195) . chr(131) => 'A',
  191. chr(195) . chr(132) => 'A', chr(195) . chr(133) => 'A',
  192. chr(195) . chr(135) => 'C', chr(195) . chr(136) => 'E',
  193. chr(195) . chr(137) => 'E', chr(195) . chr(138) => 'E',
  194. chr(195) . chr(139) => 'E', chr(195) . chr(140) => 'I',
  195. chr(195) . chr(141) => 'I', chr(195) . chr(142) => 'I',
  196. chr(195) . chr(143) => 'I', chr(195) . chr(145) => 'N',
  197. chr(195) . chr(146) => 'O', chr(195) . chr(147) => 'O',
  198. chr(195) . chr(148) => 'O', chr(195) . chr(149) => 'O',
  199. chr(195) . chr(150) => 'O', chr(195) . chr(153) => 'U',
  200. chr(195) . chr(154) => 'U', chr(195) . chr(155) => 'U',
  201. chr(195) . chr(156) => 'U', chr(195) . chr(157) => 'Y',
  202. chr(195) . chr(159) => 's', chr(195) . chr(160) => 'a',
  203. chr(195) . chr(161) => 'a', chr(195) . chr(162) => 'a',
  204. chr(195) . chr(163) => 'a', chr(195) . chr(164) => 'a',
  205. chr(195) . chr(165) => 'a', chr(195) . chr(167) => 'c',
  206. chr(195) . chr(168) => 'e', chr(195) . chr(169) => 'e',
  207. chr(195) . chr(170) => 'e', chr(195) . chr(171) => 'e',
  208. chr(195) . chr(172) => 'i', chr(195) . chr(173) => 'i',
  209. chr(195) . chr(174) => 'i', chr(195) . chr(175) => 'i',
  210. chr(195) . chr(177) => 'n', chr(195) . chr(178) => 'o',
  211. chr(195) . chr(179) => 'o', chr(195) . chr(180) => 'o',
  212. chr(195) . chr(181) => 'o', chr(195) . chr(182) => 'o',
  213. chr(195) . chr(182) => 'o', chr(195) . chr(185) => 'u',
  214. chr(195) . chr(186) => 'u', chr(195) . chr(187) => 'u',
  215. chr(195) . chr(188) => 'u', chr(195) . chr(189) => 'y',
  216. chr(195) . chr(191) => 'y',
  217. // Decompositions for Latin Extended-A
  218. chr(196) . chr(128) => 'A', chr(196) . chr(129) => 'a',
  219. chr(196) . chr(130) => 'A', chr(196) . chr(131) => 'a',
  220. chr(196) . chr(132) => 'A', chr(196) . chr(133) => 'a',
  221. chr(196) . chr(134) => 'C', chr(196) . chr(135) => 'c',
  222. chr(196) . chr(136) => 'C', chr(196) . chr(137) => 'c',
  223. chr(196) . chr(138) => 'C', chr(196) . chr(139) => 'c',
  224. chr(196) . chr(140) => 'C', chr(196) . chr(141) => 'c',
  225. chr(196) . chr(142) => 'D', chr(196) . chr(143) => 'd',
  226. chr(196) . chr(144) => 'D', chr(196) . chr(145) => 'd',
  227. chr(196) . chr(146) => 'E', chr(196) . chr(147) => 'e',
  228. chr(196) . chr(148) => 'E', chr(196) . chr(149) => 'e',
  229. chr(196) . chr(150) => 'E', chr(196) . chr(151) => 'e',
  230. chr(196) . chr(152) => 'E', chr(196) . chr(153) => 'e',
  231. chr(196) . chr(154) => 'E', chr(196) . chr(155) => 'e',
  232. chr(196) . chr(156) => 'G', chr(196) . chr(157) => 'g',
  233. chr(196) . chr(158) => 'G', chr(196) . chr(159) => 'g',
  234. chr(196) . chr(160) => 'G', chr(196) . chr(161) => 'g',
  235. chr(196) . chr(162) => 'G', chr(196) . chr(163) => 'g',
  236. chr(196) . chr(164) => 'H', chr(196) . chr(165) => 'h',
  237. chr(196) . chr(166) => 'H', chr(196) . chr(167) => 'h',
  238. chr(196) . chr(168) => 'I', chr(196) . chr(169) => 'i',
  239. chr(196) . chr(170) => 'I', chr(196) . chr(171) => 'i',
  240. chr(196) . chr(172) => 'I', chr(196) . chr(173) => 'i',
  241. chr(196) . chr(174) => 'I', chr(196) . chr(175) => 'i',
  242. chr(196) . chr(176) => 'I', chr(196) . chr(177) => 'i',
  243. chr(196) . chr(178) => 'IJ', chr(196) . chr(179) => 'ij',
  244. chr(196) . chr(180) => 'J', chr(196) . chr(181) => 'j',
  245. chr(196) . chr(182) => 'K', chr(196) . chr(183) => 'k',
  246. chr(196) . chr(184) => 'k', chr(196) . chr(185) => 'L',
  247. chr(196) . chr(186) => 'l', chr(196) . chr(187) => 'L',
  248. chr(196) . chr(188) => 'l', chr(196) . chr(189) => 'L',
  249. chr(196) . chr(190) => 'l', chr(196) . chr(191) => 'L',
  250. chr(197) . chr(128) => 'l', chr(197) . chr(129) => 'L',
  251. chr(197) . chr(130) => 'l', chr(197) . chr(131) => 'N',
  252. chr(197) . chr(132) => 'n', chr(197) . chr(133) => 'N',
  253. chr(197) . chr(134) => 'n', chr(197) . chr(135) => 'N',
  254. chr(197) . chr(136) => 'n', chr(197) . chr(137) => 'N',
  255. chr(197) . chr(138) => 'n', chr(197) . chr(139) => 'N',
  256. chr(197) . chr(140) => 'O', chr(197) . chr(141) => 'o',
  257. chr(197) . chr(142) => 'O', chr(197) . chr(143) => 'o',
  258. chr(197) . chr(144) => 'O', chr(197) . chr(145) => 'o',
  259. chr(197) . chr(146) => 'OE', chr(197) . chr(147) => 'oe',
  260. chr(197) . chr(148) => 'R', chr(197) . chr(149) => 'r',
  261. chr(197) . chr(150) => 'R', chr(197) . chr(151) => 'r',
  262. chr(197) . chr(152) => 'R', chr(197) . chr(153) => 'r',
  263. chr(197) . chr(154) => 'S', chr(197) . chr(155) => 's',
  264. chr(197) . chr(156) => 'S', chr(197) . chr(157) => 's',
  265. chr(197) . chr(158) => 'S', chr(197) . chr(159) => 's',
  266. chr(197) . chr(160) => 'S', chr(197) . chr(161) => 's',
  267. chr(197) . chr(162) => 'T', chr(197) . chr(163) => 't',
  268. chr(197) . chr(164) => 'T', chr(197) . chr(165) => 't',
  269. chr(197) . chr(166) => 'T', chr(197) . chr(167) => 't',
  270. chr(197) . chr(168) => 'U', chr(197) . chr(169) => 'u',
  271. chr(197) . chr(170) => 'U', chr(197) . chr(171) => 'u',
  272. chr(197) . chr(172) => 'U', chr(197) . chr(173) => 'u',
  273. chr(197) . chr(174) => 'U', chr(197) . chr(175) => 'u',
  274. chr(197) . chr(176) => 'U', chr(197) . chr(177) => 'u',
  275. chr(197) . chr(178) => 'U', chr(197) . chr(179) => 'u',
  276. chr(197) . chr(180) => 'W', chr(197) . chr(181) => 'w',
  277. chr(197) . chr(182) => 'Y', chr(197) . chr(183) => 'y',
  278. chr(197) . chr(184) => 'Y', chr(197) . chr(185) => 'Z',
  279. chr(197) . chr(186) => 'z', chr(197) . chr(187) => 'Z',
  280. chr(197) . chr(188) => 'z', chr(197) . chr(189) => 'Z',
  281. chr(197) . chr(190) => 'z', chr(197) . chr(191) => 's',
  282. // Euro Sign
  283. chr(226) . chr(130) . chr(172) => 'E'
  284. )
  285. )
  286. );
  287. return $this->__slug($string, am($settings, array('translation' => $translations[$settings['translation']])));
  288. }
  289. $string = strtolower($string);
  290. $string = preg_replace('/[^a-z0-9_]/i', $settings['separator'], $string);
  291. $string = preg_replace(
  292. '/' . preg_quote($settings['separator']) . '[' . preg_quote($settings['separator']) . ']*/',
  293. $settings['separator'],
  294. $string
  295. );
  296. if (strlen($string) > $settings['length']) {
  297. $string = substr($string, 0, $settings['length']);
  298. }
  299. $string = preg_replace('/' . preg_quote($settings['separator']) . '$/', '', $string);
  300. $string = preg_replace('/^' . preg_quote($settings['separator']) . '/', '', $string);
  301. return $string;
  302. }
  303. }