String.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. <?php
  2. /**
  3. * String handling methods.
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package Cake.Utility
  16. * @since CakePHP(tm) v 1.2.0.5551
  17. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  18. */
  19. /**
  20. * String handling methods.
  21. *
  22. *
  23. * @package Cake.Utility
  24. */
  25. class String {
  26. /**
  27. * Generate a random UUID
  28. *
  29. * @see http://www.ietf.org/rfc/rfc4122.txt
  30. * @return RFC 4122 UUID
  31. */
  32. public static function uuid() {
  33. $node = env('SERVER_ADDR');
  34. if (strpos($node, ':') !== false) {
  35. if (substr_count($node, '::')) {
  36. $node = str_replace(
  37. '::', str_repeat(':0000', 8 - substr_count($node, ':')) . ':', $node
  38. );
  39. }
  40. $node = explode(':', $node);
  41. $ipSix = '';
  42. foreach ($node as $id) {
  43. $ipSix .= str_pad(base_convert($id, 16, 2), 16, 0, STR_PAD_LEFT);
  44. }
  45. $node = base_convert($ipSix, 2, 10);
  46. if (strlen($node) < 38) {
  47. $node = null;
  48. } else {
  49. $node = crc32($node);
  50. }
  51. } elseif (empty($node)) {
  52. $host = env('HOSTNAME');
  53. if (empty($host)) {
  54. $host = env('HOST');
  55. }
  56. if (!empty($host)) {
  57. $ip = gethostbyname($host);
  58. if ($ip === $host) {
  59. $node = crc32($host);
  60. } else {
  61. $node = ip2long($ip);
  62. }
  63. }
  64. } elseif ($node !== '127.0.0.1') {
  65. $node = ip2long($node);
  66. } else {
  67. $node = null;
  68. }
  69. if (empty($node)) {
  70. $node = crc32(Configure::read('Security.salt'));
  71. }
  72. if (function_exists('hphp_get_thread_id')) {
  73. $pid = hphp_get_thread_id();
  74. } elseif (function_exists('zend_thread_id')) {
  75. $pid = zend_thread_id();
  76. } else {
  77. $pid = getmypid();
  78. }
  79. if (!$pid || $pid > 65535) {
  80. $pid = mt_rand(0, 0xfff) | 0x4000;
  81. }
  82. list($timeMid, $timeLow) = explode(' ', microtime());
  83. return sprintf(
  84. "%08x-%04x-%04x-%02x%02x-%04x%08x", (int)$timeLow, (int)substr($timeMid, 2) & 0xffff,
  85. mt_rand(0, 0xfff) | 0x4000, mt_rand(0, 0x3f) | 0x80, mt_rand(0, 0xff), $pid, $node
  86. );
  87. }
  88. /**
  89. * Tokenizes a string using $separator, ignoring any instance of $separator that appears between
  90. * $leftBound and $rightBound
  91. *
  92. * @param string $data The data to tokenize
  93. * @param string $separator The token to split the data on.
  94. * @param string $leftBound The left boundary to ignore separators in.
  95. * @param string $rightBound The right boundary to ignore separators in.
  96. * @return array Array of tokens in $data.
  97. */
  98. public static function tokenize($data, $separator = ',', $leftBound = '(', $rightBound = ')') {
  99. if (empty($data) || is_array($data)) {
  100. return $data;
  101. }
  102. $depth = 0;
  103. $offset = 0;
  104. $buffer = '';
  105. $results = array();
  106. $length = strlen($data);
  107. $open = false;
  108. while ($offset <= $length) {
  109. $tmpOffset = -1;
  110. $offsets = array(
  111. strpos($data, $separator, $offset),
  112. strpos($data, $leftBound, $offset),
  113. strpos($data, $rightBound, $offset)
  114. );
  115. for ($i = 0; $i < 3; $i++) {
  116. if ($offsets[$i] !== false && ($offsets[$i] < $tmpOffset || $tmpOffset == -1)) {
  117. $tmpOffset = $offsets[$i];
  118. }
  119. }
  120. if ($tmpOffset !== -1) {
  121. $buffer .= substr($data, $offset, ($tmpOffset - $offset));
  122. if (!$depth && $data{$tmpOffset} == $separator) {
  123. $results[] = $buffer;
  124. $buffer = '';
  125. } else {
  126. $buffer .= $data{$tmpOffset};
  127. }
  128. if ($leftBound != $rightBound) {
  129. if ($data{$tmpOffset} == $leftBound) {
  130. $depth++;
  131. }
  132. if ($data{$tmpOffset} == $rightBound) {
  133. $depth--;
  134. }
  135. } else {
  136. if ($data{$tmpOffset} == $leftBound) {
  137. if (!$open) {
  138. $depth++;
  139. $open = true;
  140. } else {
  141. $depth--;
  142. }
  143. }
  144. }
  145. $offset = ++$tmpOffset;
  146. } else {
  147. $results[] = $buffer . substr($data, $offset);
  148. $offset = $length + 1;
  149. }
  150. }
  151. if (empty($results) && !empty($buffer)) {
  152. $results[] = $buffer;
  153. }
  154. if (!empty($results)) {
  155. return array_map('trim', $results);
  156. }
  157. return array();
  158. }
  159. /**
  160. * Replaces variable placeholders inside a $str with any given $data. Each key in the $data array
  161. * corresponds to a variable placeholder name in $str.
  162. * Example: `String::insert(':name is :age years old.', array('name' => 'Bob', '65'));`
  163. * Returns: Bob is 65 years old.
  164. *
  165. * Available $options are:
  166. *
  167. * - before: The character or string in front of the name of the variable placeholder (Defaults to `:`)
  168. * - after: The character or string after the name of the variable placeholder (Defaults to null)
  169. * - escape: The character or string used to escape the before character / string (Defaults to `\`)
  170. * - format: A regex to use for matching variable placeholders. Default is: `/(?<!\\)\:%s/`
  171. * (Overwrites before, after, breaks escape / clean)
  172. * - clean: A boolean or array with instructions for String::cleanInsert
  173. *
  174. * @param string $str A string containing variable placeholders
  175. * @param string $data A key => val array where each key stands for a placeholder variable name
  176. * to be replaced with val
  177. * @param string $options An array of options, see description above
  178. * @return string
  179. */
  180. public static function insert($str, $data, $options = array()) {
  181. $defaults = array(
  182. 'before' => ':', 'after' => null, 'escape' => '\\', 'format' => null, 'clean' => false
  183. );
  184. $options += $defaults;
  185. $format = $options['format'];
  186. $data = (array)$data;
  187. if (empty($data)) {
  188. return ($options['clean']) ? String::cleanInsert($str, $options) : $str;
  189. }
  190. if (!isset($format)) {
  191. $format = sprintf(
  192. '/(?<!%s)%s%%s%s/',
  193. preg_quote($options['escape'], '/'),
  194. str_replace('%', '%%', preg_quote($options['before'], '/')),
  195. str_replace('%', '%%', preg_quote($options['after'], '/'))
  196. );
  197. }
  198. if (strpos($str, '?') !== false && is_numeric(key($data))) {
  199. $offset = 0;
  200. while (($pos = strpos($str, '?', $offset)) !== false) {
  201. $val = array_shift($data);
  202. $offset = $pos + strlen($val);
  203. $str = substr_replace($str, $val, $pos, 1);
  204. }
  205. return ($options['clean']) ? String::cleanInsert($str, $options) : $str;
  206. }
  207. asort($data);
  208. $dataKeys = array_keys($data);
  209. $hashKeys = array_map('crc32', $dataKeys);
  210. $tempData = array_combine($dataKeys, $hashKeys);
  211. krsort($tempData);
  212. foreach ($tempData as $key => $hashVal) {
  213. $key = sprintf($format, preg_quote($key, '/'));
  214. $str = preg_replace($key, $hashVal, $str);
  215. }
  216. $dataReplacements = array_combine($hashKeys, array_values($data));
  217. foreach ($dataReplacements as $tmpHash => $tmpValue) {
  218. $tmpValue = (is_array($tmpValue)) ? '' : $tmpValue;
  219. $str = str_replace($tmpHash, $tmpValue, $str);
  220. }
  221. if (!isset($options['format']) && isset($options['before'])) {
  222. $str = str_replace($options['escape'] . $options['before'], $options['before'], $str);
  223. }
  224. return ($options['clean']) ? String::cleanInsert($str, $options) : $str;
  225. }
  226. /**
  227. * Cleans up a String::insert() formatted string with given $options depending on the 'clean' key in
  228. * $options. The default method used is text but html is also available. The goal of this function
  229. * is to replace all whitespace and unneeded markup around placeholders that did not get replaced
  230. * by String::insert().
  231. *
  232. * @param string $str
  233. * @param string $options
  234. * @return string
  235. * @see String::insert()
  236. */
  237. public static function cleanInsert($str, $options) {
  238. $clean = $options['clean'];
  239. if (!$clean) {
  240. return $str;
  241. }
  242. if ($clean === true) {
  243. $clean = array('method' => 'text');
  244. }
  245. if (!is_array($clean)) {
  246. $clean = array('method' => $options['clean']);
  247. }
  248. switch ($clean['method']) {
  249. case 'html':
  250. $clean = array_merge(array(
  251. 'word' => '[\w,.]+',
  252. 'andText' => true,
  253. 'replacement' => '',
  254. ), $clean);
  255. $kleenex = sprintf(
  256. '/[\s]*[a-z]+=(")(%s%s%s[\s]*)+\\1/i',
  257. preg_quote($options['before'], '/'),
  258. $clean['word'],
  259. preg_quote($options['after'], '/')
  260. );
  261. $str = preg_replace($kleenex, $clean['replacement'], $str);
  262. if ($clean['andText']) {
  263. $options['clean'] = array('method' => 'text');
  264. $str = String::cleanInsert($str, $options);
  265. }
  266. break;
  267. case 'text':
  268. $clean = array_merge(array(
  269. 'word' => '[\w,.]+',
  270. 'gap' => '[\s]*(?:(?:and|or)[\s]*)?',
  271. 'replacement' => '',
  272. ), $clean);
  273. $kleenex = sprintf(
  274. '/(%s%s%s%s|%s%s%s%s)/',
  275. preg_quote($options['before'], '/'),
  276. $clean['word'],
  277. preg_quote($options['after'], '/'),
  278. $clean['gap'],
  279. $clean['gap'],
  280. preg_quote($options['before'], '/'),
  281. $clean['word'],
  282. preg_quote($options['after'], '/')
  283. );
  284. $str = preg_replace($kleenex, $clean['replacement'], $str);
  285. break;
  286. }
  287. return $str;
  288. }
  289. /**
  290. * Wraps text to a specific width, can optionally wrap at word breaks.
  291. *
  292. * ### Options
  293. *
  294. * - `width` The width to wrap to. Defaults to 72
  295. * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true.
  296. * - `indent` String to indent with. Defaults to null.
  297. * - `indentAt` 0 based index to start indenting at. Defaults to 0.
  298. *
  299. * @param string $text Text the text to format.
  300. * @param array|integer $options Array of options to use, or an integer to wrap the text to.
  301. * @return string Formatted text.
  302. */
  303. public static function wrap($text, $options = array()) {
  304. if (is_numeric($options)) {
  305. $options = array('width' => $options);
  306. }
  307. $options += array('width' => 72, 'wordWrap' => true, 'indent' => null, 'indentAt' => 0);
  308. if ($options['wordWrap']) {
  309. $wrapped = wordwrap($text, $options['width'], "\n");
  310. } else {
  311. $wrapped = trim(chunk_split($text, $options['width'] - 1, "\n"));
  312. }
  313. if (!empty($options['indent'])) {
  314. $chunks = explode("\n", $wrapped);
  315. for ($i = $options['indentAt'], $len = count($chunks); $i < $len; $i++) {
  316. $chunks[$i] = $options['indent'] . $chunks[$i];
  317. }
  318. $wrapped = implode("\n", $chunks);
  319. }
  320. return $wrapped;
  321. }
  322. /**
  323. * Highlights a given phrase in a text. You can specify any expression in highlighter that
  324. * may include the \1 expression to include the $phrase found.
  325. *
  326. * ### Options:
  327. *
  328. * - `format` The piece of html with that the phrase will be highlighted
  329. * - `html` If true, will ignore any HTML tags, ensuring that only the correct text is highlighted
  330. * - `regex` a custom regex rule that is ued to match words, default is '|$tag|iu'
  331. *
  332. * @param string $text Text to search the phrase in
  333. * @param string $phrase The phrase that will be searched
  334. * @param array $options An array of html attributes and options.
  335. * @return string The highlighted text
  336. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/text.html#TextHelper::highlight
  337. */
  338. public static function highlight($text, $phrase, $options = array()) {
  339. if (empty($phrase)) {
  340. return $text;
  341. }
  342. $default = array(
  343. 'format' => '<span class="highlight">\1</span>',
  344. 'html' => false,
  345. 'regex' => "|%s|iu"
  346. );
  347. $options = array_merge($default, $options);
  348. extract($options);
  349. if (is_array($phrase)) {
  350. $replace = array();
  351. $with = array();
  352. foreach ($phrase as $key => $segment) {
  353. $segment = '(' . preg_quote($segment, '|') . ')';
  354. if ($html) {
  355. $segment = "(?![^<]+>)$segment(?![^<]+>)";
  356. }
  357. $with[] = (is_array($format)) ? $format[$key] : $format;
  358. $replace[] = sprintf($options['regex'], $segment);
  359. }
  360. return preg_replace($replace, $with, $text);
  361. }
  362. $phrase = '(' . preg_quote($phrase, '|') . ')';
  363. if ($html) {
  364. $phrase = "(?![^<]+>)$phrase(?![^<]+>)";
  365. }
  366. return preg_replace(sprintf($options['regex'], $phrase), $format, $text);
  367. }
  368. /**
  369. * Strips given text of all links (<a href=....)
  370. *
  371. * @param string $text Text
  372. * @return string The text without links
  373. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/text.html#TextHelper::stripLinks
  374. */
  375. public static function stripLinks($text) {
  376. return preg_replace('|<a\s+[^>]+>|im', '', preg_replace('|<\/a>|im', '', $text));
  377. }
  378. /**
  379. * Truncates text starting from the end.
  380. *
  381. * Cuts a string to the length of $length and replaces the first characters
  382. * with the ellipsis if the text is longer than length.
  383. *
  384. * ### Options:
  385. *
  386. * - `ellipsis` Will be used as Beginning and prepended to the trimmed string
  387. * - `exact` If false, $text will not be cut mid-word
  388. *
  389. * @param string $text String to truncate.
  390. * @param integer $length Length of returned string, including ellipsis.
  391. * @param array $options An array of options.
  392. * @return string Trimmed string.
  393. */
  394. public static function tail($text, $length = 100, $options = array()) {
  395. $default = array(
  396. 'ellipsis' => '...', 'exact' => true
  397. );
  398. $options = array_merge($default, $options);
  399. extract($options);
  400. if (!function_exists('mb_strlen')) {
  401. class_exists('Multibyte');
  402. }
  403. if (mb_strlen($text) <= $length) {
  404. return $text;
  405. }
  406. $truncate = mb_substr($text, mb_strlen($text) - $length + mb_strlen($ellipsis));
  407. if (!$exact) {
  408. $spacepos = mb_strpos($truncate, ' ');
  409. $truncate = $spacepos === false ? '' : trim(mb_substr($truncate, $spacepos));
  410. }
  411. return $ellipsis . $truncate;
  412. }
  413. /**
  414. * Truncates text.
  415. *
  416. * Cuts a string to the length of $length and replaces the last characters
  417. * with the ellipsis if the text is longer than length.
  418. *
  419. * ### Options:
  420. *
  421. * - `ellipsis` Will be used as Ending and appended to the trimmed string (`ending` is deprecated)
  422. * - `exact` If false, $text will not be cut mid-word
  423. * - `html` If true, HTML tags would be handled correctly
  424. *
  425. * @param string $text String to truncate.
  426. * @param integer $length Length of returned string, including ellipsis.
  427. * @param array $options An array of html attributes and options.
  428. * @return string Trimmed string.
  429. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/text.html#TextHelper::truncate
  430. */
  431. public static function truncate($text, $length = 100, $options = array()) {
  432. $default = array(
  433. 'ellipsis' => '...', 'exact' => true, 'html' => false
  434. );
  435. if (isset($options['ending'])) {
  436. $default['ellipsis'] = $options['ending'];
  437. } elseif (!empty($options['html']) && Configure::read('App.encoding') == 'UTF-8') {
  438. $default['ellipsis'] = "\xe2\x80\xa6";
  439. }
  440. $options = array_merge($default, $options);
  441. extract($options);
  442. if (!function_exists('mb_strlen')) {
  443. class_exists('Multibyte');
  444. }
  445. if ($html) {
  446. if (mb_strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
  447. return $text;
  448. }
  449. $totalLength = mb_strlen(strip_tags($ellipsis));
  450. $openTags = array();
  451. $truncate = '';
  452. preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER);
  453. foreach ($tags as $tag) {
  454. if (!preg_match('/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/s', $tag[2])) {
  455. if (preg_match('/<[\w]+[^>]*>/s', $tag[0])) {
  456. array_unshift($openTags, $tag[2]);
  457. } elseif (preg_match('/<\/([\w]+)[^>]*>/s', $tag[0], $closeTag)) {
  458. $pos = array_search($closeTag[1], $openTags);
  459. if ($pos !== false) {
  460. array_splice($openTags, $pos, 1);
  461. }
  462. }
  463. }
  464. $truncate .= $tag[1];
  465. $contentLength = mb_strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $tag[3]));
  466. if ($contentLength + $totalLength > $length) {
  467. $left = $length - $totalLength;
  468. $entitiesLength = 0;
  469. if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $tag[3], $entities, PREG_OFFSET_CAPTURE)) {
  470. foreach ($entities[0] as $entity) {
  471. if ($entity[1] + 1 - $entitiesLength <= $left) {
  472. $left--;
  473. $entitiesLength += mb_strlen($entity[0]);
  474. } else {
  475. break;
  476. }
  477. }
  478. }
  479. $truncate .= mb_substr($tag[3], 0 , $left + $entitiesLength);
  480. break;
  481. } else {
  482. $truncate .= $tag[3];
  483. $totalLength += $contentLength;
  484. }
  485. if ($totalLength >= $length) {
  486. break;
  487. }
  488. }
  489. } else {
  490. if (mb_strlen($text) <= $length) {
  491. return $text;
  492. }
  493. $truncate = mb_substr($text, 0, $length - mb_strlen($ellipsis));
  494. }
  495. if (!$exact) {
  496. $spacepos = mb_strrpos($truncate, ' ');
  497. if ($html) {
  498. $truncateCheck = mb_substr($truncate, 0, $spacepos);
  499. $lastOpenTag = mb_strrpos($truncateCheck, '<');
  500. $lastCloseTag = mb_strrpos($truncateCheck, '>');
  501. if ($lastOpenTag > $lastCloseTag) {
  502. preg_match_all('/<[\w]+[^>]*>/s', $truncate, $lastTagMatches);
  503. $lastTag = array_pop($lastTagMatches[0]);
  504. $spacepos = mb_strrpos($truncate, $lastTag) + mb_strlen($lastTag);
  505. }
  506. $bits = mb_substr($truncate, $spacepos);
  507. preg_match_all('/<\/([a-z]+)>/', $bits, $droppedTags, PREG_SET_ORDER);
  508. if (!empty($droppedTags)) {
  509. if (!empty($openTags)) {
  510. foreach ($droppedTags as $closingTag) {
  511. if (!in_array($closingTag[1], $openTags)) {
  512. array_unshift($openTags, $closingTag[1]);
  513. }
  514. }
  515. } else {
  516. foreach ($droppedTags as $closingTag) {
  517. $openTags[] = $closingTag[1];
  518. }
  519. }
  520. }
  521. }
  522. $truncate = mb_substr($truncate, 0, $spacepos);
  523. }
  524. $truncate .= $ellipsis;
  525. if ($html) {
  526. foreach ($openTags as $tag) {
  527. $truncate .= '</' . $tag . '>';
  528. }
  529. }
  530. return $truncate;
  531. }
  532. /**
  533. * Extracts an excerpt from the text surrounding the phrase with a number of characters on each side
  534. * determined by radius.
  535. *
  536. * @param string $text String to search the phrase in
  537. * @param string $phrase Phrase that will be searched for
  538. * @param integer $radius The amount of characters that will be returned on each side of the founded phrase
  539. * @param string $ellipsis Ending that will be appended
  540. * @return string Modified string
  541. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/text.html#TextHelper::excerpt
  542. */
  543. public static function excerpt($text, $phrase, $radius = 100, $ellipsis = '...') {
  544. if (empty($text) || empty($phrase)) {
  545. return self::truncate($text, $radius * 2, array('ellipsis' => $ellipsis));
  546. }
  547. $append = $prepend = $ellipsis;
  548. $phraseLen = mb_strlen($phrase);
  549. $textLen = mb_strlen($text);
  550. $pos = mb_strpos(mb_strtolower($text), mb_strtolower($phrase));
  551. if ($pos === false) {
  552. return mb_substr($text, 0, $radius) . $ellipsis;
  553. }
  554. $startPos = $pos - $radius;
  555. if ($startPos <= 0) {
  556. $startPos = 0;
  557. $prepend = '';
  558. }
  559. $endPos = $pos + $phraseLen + $radius;
  560. if ($endPos >= $textLen) {
  561. $endPos = $textLen;
  562. $append = '';
  563. }
  564. $excerpt = mb_substr($text, $startPos, $endPos - $startPos);
  565. $excerpt = $prepend . $excerpt . $append;
  566. return $excerpt;
  567. }
  568. /**
  569. * Creates a comma separated list where the last two items are joined with 'and', forming natural English
  570. *
  571. * @param array $list The list to be joined
  572. * @param string $and The word used to join the last and second last items together with. Defaults to 'and'
  573. * @param string $separator The separator used to join all the other items together. Defaults to ', '
  574. * @return string The glued together string.
  575. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/text.html#TextHelper::toList
  576. */
  577. public static function toList($list, $and = 'and', $separator = ', ') {
  578. if (count($list) > 1) {
  579. return implode($separator, array_slice($list, null, -1)) . ' ' . $and . ' ' . array_pop($list);
  580. }
  581. return array_pop($list);
  582. }
  583. }