PageRenderTime 43ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/models/behaviors/sluggable.php

https://github.com/lecterror/neutrinocms
PHP | 398 lines | 274 code | 42 blank | 82 comment | 33 complexity | 36d6da6a078d5ecb1db21c13e3e18d37 MD5 | raw file
Possible License(s): AGPL-1.0
  1. <?php
  2. /* SVN FILE: $Id$ */
  3. /**
  4. * Sluggable Behavior class file.
  5. *
  6. * @filesource
  7. * @author Mariano Iglesias
  8. * @link http://cake-syrup.sourceforge.net/ingredients/sluggable-behavior/
  9. * @version $Revision$
  10. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  11. * @package app
  12. * @subpackage app.models.behaviors
  13. */
  14. /**
  15. * Model behavior to support generation of slugs for models.
  16. *
  17. * @package app
  18. * @subpackage app.models.behaviors
  19. */
  20. class SluggableBehavior extends ModelBehavior {
  21. /**
  22. * Contain settings indexed by model name.
  23. *
  24. * @var array
  25. * @access private
  26. */
  27. var $__settings = array();
  28. /**
  29. * Initiate behavior for the model using specified settings. Available settings:
  30. *
  31. * - label: (array | string, optional) set to the field name that contains the
  32. * string from where to generate the slug, or a set of field names to
  33. * concatenate for generating the slug. DEFAULTS TO: title
  34. *
  35. * - real: (boolean, optional) if set to true then field names defined in
  36. * label must exist in the database table. DEFAULTS TO: true
  37. *
  38. * - slug: (string, optional) name of the field name that holds generated slugs.
  39. * DEFAULTS TO: slug
  40. *
  41. * - separator: (string, optional) separator character / string to use for replacing
  42. * non alphabetic characters in generated slug. DEFAULTS TO: -
  43. *
  44. * - length: (integer, optional) maximum length the generated slug can have.
  45. * DEFAULTS TO: 100
  46. *
  47. * - overwrite: (boolean, optional) set to true if slugs should be re-generated when
  48. * updating an existing record. DEFAULTS TO: false
  49. *
  50. * @param object $Model Model using the behaviour
  51. * @param array $settings Settings to override for model.
  52. * @access public
  53. */
  54. function setup(&$Model, $settings = array()) {
  55. $default = array('real' => true, 'label' => array('title'), 'slug' => 'slug', 'separator' => '-', 'length' => 100, 'overwrite' => false, 'translation' => null);
  56. if (!isset($this->__settings[$Model->alias])) {
  57. $this->__settings[$Model->alias] = $default;
  58. }
  59. $this->__settings[$Model->alias] = array_merge($this->__settings[$Model->alias], ife(is_array($settings), $settings, array()));
  60. }
  61. /**
  62. * Run before a model is saved, used to set up slug for model.
  63. *
  64. * @param object $Model Model about to be saved.
  65. * @return boolean true if save should proceed, false otherwise
  66. * @access public
  67. */
  68. function beforeSave(&$Model) {
  69. $return = parent::beforeSave($Model);
  70. // Make label fields an array
  71. if (!is_array($this->__settings[$Model->alias]['label'])) {
  72. $this->__settings[$Model->alias]['label'] = array($this->__settings[$Model->alias]['label']);
  73. }
  74. // Make sure all label fields are available
  75. if ($this->__settings[$Model->alias]['real']) {
  76. foreach($this->__settings[$Model->alias]['label'] as $field) {
  77. if (!$Model->hasField($field)) {
  78. return $return;
  79. }
  80. }
  81. }
  82. // See if we should be generating a slug
  83. if ((!$this->__settings[$Model->alias]['real'] || $Model->hasField($this->__settings[$Model->alias]['slug'])) && ($this->__settings[$Model->alias]['overwrite'] || empty($Model->id))) {
  84. // Build label out of data in label fields, if available, or using a default slug otherwise
  85. $label = '';
  86. foreach($this->__settings[$Model->alias]['label'] as $field) {
  87. if (!empty($Model->data[$Model->alias][$field])) {
  88. $label .= ife(!empty($label), ' ', '') . $Model->data[$Model->alias][$field];
  89. }
  90. }
  91. // Keep on going only if we've got something to slug
  92. if (!empty($label)) {
  93. // Get the slug
  94. $slug = $this->__slug($label, $this->__settings[$Model->alias]);
  95. // Look for slugs that start with the same slug we've just generated
  96. $conditions = array($Model->alias . '.' . $this->__settings[$Model->alias]['slug'] . ' LIKE' => $slug . '%');
  97. if (!empty($Model->id)) {
  98. $conditions[$Model->alias . '.' . $Model->primaryKey . ' !='] = $Model->id;
  99. }
  100. $result = $Model->find('all', array('conditions' => $conditions, 'fields' => array($Model->primaryKey, $this->__settings[$Model->alias]['slug']), 'recursive' => -1));
  101. $sameUrls = null;
  102. if (!empty($result)) {
  103. $sameUrls = Set::extract($result, '{n}.' . $Model->alias . '.' . $this->__settings[$Model->alias]['slug']);
  104. }
  105. // If we have collissions
  106. if (!empty($sameUrls)) {
  107. $begginingSlug = $slug;
  108. $index = 1;
  109. // Attach an ending incremental number until we find a free slug
  110. while($index > 0) {
  111. if (!in_array($begginingSlug . $this->__settings[$Model->alias]['separator'] . $index, $sameUrls)) {
  112. $slug = $begginingSlug . $this->__settings[$Model->alias]['separator'] . $index;
  113. $index = -1;
  114. }
  115. $index++;
  116. }
  117. }
  118. // Now set the slug as part of the model data to be saved, making sure that
  119. // we are on the white list of fields to be saved
  120. if (!empty($Model->whitelist) && !in_array($this->__settings[$Model->alias]['slug'], $Model->whitelist)) {
  121. $Model->whitelist[] = $this->__settings[$Model->alias]['slug'];
  122. }
  123. $Model->data[$Model->alias][$this->__settings[$Model->alias]['slug']] = $slug;
  124. }
  125. }
  126. return $return;
  127. }
  128. /**
  129. * Generate a slug for the given string using specified settings.
  130. *
  131. * @param string $string String from where to generate slug
  132. * @param array $settings Settings to use (looks for 'separator' and 'length')
  133. * @return string Slug for given string
  134. * @access private
  135. */
  136. function __slug($string, $settings) {
  137. if (!empty($settings['translation']) && is_array($settings['translation'])) {
  138. // Run user-defined translation tables
  139. if (count($settings['translation']) >= 2 && count($settings['translation']) % 2 == 0) {
  140. for($i=0, $limiti=count($settings['translation']); $i < $limiti; $i+=2) {
  141. $from = $settings['translation'][$i];
  142. $to = $settings['translation'][$i + 1];
  143. if (is_string($from) && is_string($to)) {
  144. $string = strtr($string, $from, $to);
  145. } else {
  146. $string = str_replace($from, $to, $string);
  147. }
  148. }
  149. } else if (count($settings['translation']) == 1) {
  150. $string = strtr($string, $settings['translation'][0]);
  151. }
  152. $string = strtolower($string);
  153. } else if (!empty($settings['translation']) && is_string($settings['translation']) && in_array(strtolower($settings['translation']), array('utf-8', 'iso-8859-1'))) {
  154. // Run pre-defined translation tables
  155. $translations = array(
  156. 'iso-8859-1' => array(
  157. chr(128).chr(131).chr(138).chr(142).chr(154).chr(158)
  158. .chr(159).chr(162).chr(165).chr(181).chr(192).chr(193).chr(194)
  159. .chr(195).chr(196).chr(197).chr(199).chr(200).chr(201).chr(202)
  160. .chr(203).chr(204).chr(205).chr(206).chr(207).chr(209).chr(210)
  161. .chr(211).chr(212).chr(213).chr(214).chr(216).chr(217).chr(218)
  162. .chr(219).chr(220).chr(221).chr(224).chr(225).chr(226).chr(227)
  163. .chr(228).chr(229).chr(231).chr(232).chr(233).chr(234).chr(235)
  164. .chr(236).chr(237).chr(238).chr(239).chr(241).chr(242).chr(243)
  165. .chr(244).chr(245).chr(246).chr(248).chr(249).chr(250).chr(251)
  166. .chr(252).chr(253).chr(255),
  167. 'EfSZsz' . 'YcYuAAA' . 'AAACEEE' . 'EIIIINO' . 'OOOOOUU' . 'UUYaaaa' . 'aaceeee' . 'iiiinoo' . 'oooouuu' . 'uyy',
  168. array(chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254)),
  169. array('OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th')
  170. ),
  171. 'utf-8' => array(
  172. array(
  173. // Decompositions for Latin-1 Supplement
  174. chr(195).chr(128) => 'A', chr(195).chr(129) => 'A',
  175. chr(195).chr(130) => 'A', chr(195).chr(131) => 'A',
  176. chr(195).chr(132) => 'A', chr(195).chr(133) => 'A',
  177. chr(195).chr(135) => 'C', chr(195).chr(136) => 'E',
  178. chr(195).chr(137) => 'E', chr(195).chr(138) => 'E',
  179. chr(195).chr(139) => 'E', chr(195).chr(140) => 'I',
  180. chr(195).chr(141) => 'I', chr(195).chr(142) => 'I',
  181. chr(195).chr(143) => 'I', chr(195).chr(145) => 'N',
  182. chr(195).chr(146) => 'O', chr(195).chr(147) => 'O',
  183. chr(195).chr(148) => 'O', chr(195).chr(149) => 'O',
  184. chr(195).chr(150) => 'O', chr(195).chr(153) => 'U',
  185. chr(195).chr(154) => 'U', chr(195).chr(155) => 'U',
  186. chr(195).chr(156) => 'U', chr(195).chr(157) => 'Y',
  187. chr(195).chr(159) => 's', chr(195).chr(160) => 'a',
  188. chr(195).chr(161) => 'a', chr(195).chr(162) => 'a',
  189. chr(195).chr(163) => 'a', chr(195).chr(164) => 'a',
  190. chr(195).chr(165) => 'a', chr(195).chr(167) => 'c',
  191. chr(195).chr(168) => 'e', chr(195).chr(169) => 'e',
  192. chr(195).chr(170) => 'e', chr(195).chr(171) => 'e',
  193. chr(195).chr(172) => 'i', chr(195).chr(173) => 'i',
  194. chr(195).chr(174) => 'i', chr(195).chr(175) => 'i',
  195. chr(195).chr(177) => 'n', chr(195).chr(178) => 'o',
  196. chr(195).chr(179) => 'o', chr(195).chr(180) => 'o',
  197. chr(195).chr(181) => 'o', chr(195).chr(182) => 'o',
  198. chr(195).chr(182) => 'o', chr(195).chr(185) => 'u',
  199. chr(195).chr(186) => 'u', chr(195).chr(187) => 'u',
  200. chr(195).chr(188) => 'u', chr(195).chr(189) => 'y',
  201. chr(195).chr(191) => 'y',
  202. // Decompositions for Latin Extended-A
  203. chr(196).chr(128) => 'A', chr(196).chr(129) => 'a',
  204. chr(196).chr(130) => 'A', chr(196).chr(131) => 'a',
  205. chr(196).chr(132) => 'A', chr(196).chr(133) => 'a',
  206. chr(196).chr(134) => 'C', chr(196).chr(135) => 'c',
  207. chr(196).chr(136) => 'C', chr(196).chr(137) => 'c',
  208. chr(196).chr(138) => 'C', chr(196).chr(139) => 'c',
  209. chr(196).chr(140) => 'C', chr(196).chr(141) => 'c',
  210. chr(196).chr(142) => 'D', chr(196).chr(143) => 'd',
  211. chr(196).chr(144) => 'D', chr(196).chr(145) => 'd',
  212. chr(196).chr(146) => 'E', chr(196).chr(147) => 'e',
  213. chr(196).chr(148) => 'E', chr(196).chr(149) => 'e',
  214. chr(196).chr(150) => 'E', chr(196).chr(151) => 'e',
  215. chr(196).chr(152) => 'E', chr(196).chr(153) => 'e',
  216. chr(196).chr(154) => 'E', chr(196).chr(155) => 'e',
  217. chr(196).chr(156) => 'G', chr(196).chr(157) => 'g',
  218. chr(196).chr(158) => 'G', chr(196).chr(159) => 'g',
  219. chr(196).chr(160) => 'G', chr(196).chr(161) => 'g',
  220. chr(196).chr(162) => 'G', chr(196).chr(163) => 'g',
  221. chr(196).chr(164) => 'H', chr(196).chr(165) => 'h',
  222. chr(196).chr(166) => 'H', chr(196).chr(167) => 'h',
  223. chr(196).chr(168) => 'I', chr(196).chr(169) => 'i',
  224. chr(196).chr(170) => 'I', chr(196).chr(171) => 'i',
  225. chr(196).chr(172) => 'I', chr(196).chr(173) => 'i',
  226. chr(196).chr(174) => 'I', chr(196).chr(175) => 'i',
  227. chr(196).chr(176) => 'I', chr(196).chr(177) => 'i',
  228. chr(196).chr(178) => 'IJ',chr(196).chr(179) => 'ij',
  229. chr(196).chr(180) => 'J', chr(196).chr(181) => 'j',
  230. chr(196).chr(182) => 'K', chr(196).chr(183) => 'k',
  231. chr(196).chr(184) => 'k', chr(196).chr(185) => 'L',
  232. chr(196).chr(186) => 'l', chr(196).chr(187) => 'L',
  233. chr(196).chr(188) => 'l', chr(196).chr(189) => 'L',
  234. chr(196).chr(190) => 'l', chr(196).chr(191) => 'L',
  235. chr(197).chr(128) => 'l', chr(197).chr(129) => 'L',
  236. chr(197).chr(130) => 'l', chr(197).chr(131) => 'N',
  237. chr(197).chr(132) => 'n', chr(197).chr(133) => 'N',
  238. chr(197).chr(134) => 'n', chr(197).chr(135) => 'N',
  239. chr(197).chr(136) => 'n', chr(197).chr(137) => 'N',
  240. chr(197).chr(138) => 'n', chr(197).chr(139) => 'N',
  241. chr(197).chr(140) => 'O', chr(197).chr(141) => 'o',
  242. chr(197).chr(142) => 'O', chr(197).chr(143) => 'o',
  243. chr(197).chr(144) => 'O', chr(197).chr(145) => 'o',
  244. chr(197).chr(146) => 'OE',chr(197).chr(147) => 'oe',
  245. chr(197).chr(148) => 'R',chr(197).chr(149) => 'r',
  246. chr(197).chr(150) => 'R',chr(197).chr(151) => 'r',
  247. chr(197).chr(152) => 'R',chr(197).chr(153) => 'r',
  248. chr(197).chr(154) => 'S',chr(197).chr(155) => 's',
  249. chr(197).chr(156) => 'S',chr(197).chr(157) => 's',
  250. chr(197).chr(158) => 'S',chr(197).chr(159) => 's',
  251. chr(197).chr(160) => 'S', chr(197).chr(161) => 's',
  252. chr(197).chr(162) => 'T', chr(197).chr(163) => 't',
  253. chr(197).chr(164) => 'T', chr(197).chr(165) => 't',
  254. chr(197).chr(166) => 'T', chr(197).chr(167) => 't',
  255. chr(197).chr(168) => 'U', chr(197).chr(169) => 'u',
  256. chr(197).chr(170) => 'U', chr(197).chr(171) => 'u',
  257. chr(197).chr(172) => 'U', chr(197).chr(173) => 'u',
  258. chr(197).chr(174) => 'U', chr(197).chr(175) => 'u',
  259. chr(197).chr(176) => 'U', chr(197).chr(177) => 'u',
  260. chr(197).chr(178) => 'U', chr(197).chr(179) => 'u',
  261. chr(197).chr(180) => 'W', chr(197).chr(181) => 'w',
  262. chr(197).chr(182) => 'Y', chr(197).chr(183) => 'y',
  263. chr(197).chr(184) => 'Y', chr(197).chr(185) => 'Z',
  264. chr(197).chr(186) => 'z', chr(197).chr(187) => 'Z',
  265. chr(197).chr(188) => 'z', chr(197).chr(189) => 'Z',
  266. chr(197).chr(190) => 'z', chr(197).chr(191) => 's',
  267. // Russian symbols (ISO 9-95)
  268. chr(208).chr(129) => 'YO',
  269. chr(208).chr(132) => 'E',
  270. chr(208).chr(134) => 'I',
  271. chr(208).chr(135) => 'YI',
  272. chr(208).chr(144) => 'A',
  273. chr(208).chr(145) => 'B',
  274. chr(208).chr(146) => 'V',
  275. chr(208).chr(147) => 'G',
  276. chr(208).chr(148) => 'D',
  277. chr(208).chr(149) => 'E',
  278. chr(208).chr(150) => 'ZH',
  279. chr(208).chr(151) => 'Z',
  280. chr(208).chr(152) => 'I',
  281. chr(208).chr(153) => 'Y',
  282. chr(208).chr(154) => 'K',
  283. chr(208).chr(155) => 'L',
  284. chr(208).chr(156) => 'M',
  285. chr(208).chr(157) => 'N',
  286. chr(208).chr(158) => 'O',
  287. chr(208).chr(159) => 'P',
  288. chr(208).chr(160) => 'R',
  289. chr(208).chr(161) => 'S',
  290. chr(208).chr(162) => 'T',
  291. chr(208).chr(163) => 'U',
  292. chr(208).chr(164) => 'F',
  293. chr(208).chr(165) => 'H',
  294. chr(208).chr(166) => 'TS',
  295. chr(208).chr(167) => 'CH',
  296. chr(208).chr(168) => 'SH',
  297. chr(208).chr(169) => 'SCH',
  298. chr(208).chr(171) => 'YI',
  299. chr(208).chr(173) => 'E',
  300. chr(208).chr(174) => 'YU',
  301. chr(208).chr(175) => 'YA',
  302. chr(208).chr(176) => 'a',
  303. chr(208).chr(177) => 'b',
  304. chr(208).chr(178) => 'v',
  305. chr(208).chr(179) => 'g',
  306. chr(208).chr(180) => 'd',
  307. chr(208).chr(181) => 'e',
  308. chr(208).chr(182) => 'zh',
  309. chr(208).chr(183) => 'z',
  310. chr(208).chr(184) => 'i',
  311. chr(208).chr(185) => 'y',
  312. chr(208).chr(186) => 'k',
  313. chr(208).chr(187) => 'l',
  314. chr(208).chr(188) => 'm',
  315. chr(208).chr(189) => 'n',
  316. chr(208).chr(190) => 'o',
  317. chr(208).chr(191) => 'p',
  318. chr(209).chr(128) => 'r',
  319. chr(209).chr(129) => 's',
  320. chr(209).chr(130) => 't',
  321. chr(209).chr(131) => 'u',
  322. chr(209).chr(132) => 'f',
  323. chr(209).chr(133) => 'h',
  324. chr(209).chr(134) => 'ts',
  325. chr(209).chr(135) => 'ch',
  326. chr(209).chr(136) => 'sh',
  327. chr(209).chr(137) => 'sch',
  328. chr(209).chr(139) => 'yi',
  329. chr(209).chr(141) => 'e',
  330. chr(209).chr(142) => 'yu',
  331. chr(209).chr(143) => 'ya',
  332. chr(209).chr(145) => 'yo',
  333. chr(209).chr(148) => 'e',
  334. chr(209).chr(150) => 'i',
  335. chr(209).chr(151) => 'yi',
  336. chr(210).chr(144) => 'G',
  337. chr(210).chr(145) => 'g',
  338. // Euro Sign
  339. chr(226).chr(130).chr(172) => 'E'
  340. )
  341. )
  342. );
  343. return $this->__slug($string, array_merge($settings, array('translation' => $translations[$settings['translation']])));
  344. }
  345. $string = strtolower($string);
  346. $string = preg_replace('/[^a-z0-9_]/i', $settings['separator'], $string);
  347. $string = preg_replace('/' . preg_quote($settings['separator']) . '[' . preg_quote($settings['separator']) . ']*/', $settings['separator'], $string);
  348. if (strlen($string) > $settings['length']) {
  349. $string = substr($string, 0, $settings['length']);
  350. }
  351. $string = preg_replace('/' . preg_quote($settings['separator']) . '$/', '', $string);
  352. $string = preg_replace('/^' . preg_quote($settings['separator']) . '/', '', $string);
  353. return $string;
  354. }
  355. }
  356. ?>