PageRenderTime 47ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/Model/Behavior/SluggableBehavior.php

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