PageRenderTime 68ms CodeModel.GetById 33ms app.highlight 30ms RepoModel.GetById 1ms app.codeStats 0ms

/models/behaviors/sluggable.php

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