* @license BSD */ namespace Domframework; /** * Allow to manage a linux Console to have a minimal but working text interface * When using this class, you must use the $console::echo method and not * display directely on screen * Like readline, but all in PHP * Manage the historical of the provided commands * Allow the user to use arrow keys and the shortcuts Ctrl+arrow, Ctrl+L, * Ctrl+U, Ctrl+W * To not allow the stop of the program by Ctrl+C, you can add * exec ("stty intr ^J"); * To update the window size when the terminal is resized, use after console * instanciation : * declare(ticks = 1); * pcntl_signal (SIGWINCH, function () use ($console) { * $console->updateTerminalSize (); * }); */ class Console { // PROPERTIES /** * Set the debug on if a filename is provided, or do not debug if false is * provided */ //private $debug = "/tmp/debug"; private $debug = false; /** * Save the initial stty value */ private $initSttyState; /** * Line Content */ private $lineContent = ""; /** * If true, display each char the user has pressed (echo mode) */ private $echoMode = true; /** * List of non printable chars in decimal. The non printable chars are not * displayed but are correctely captured */ private $nonWriteableChar = [1, 2, 3, 4, 6, 8, 9, 16, 18, 20, 21, 22, 23, 24, 25, 27, 127]; /** * The history list in an array */ private $history = []; /** * The history max size in entries */ private $historyMaxSize = 1000; /** * Set the completion keys. Not set by default */ private $completionKeys = false; /** * Set the function called when the completion char is called */ private $completionFunction = []; /** * Set the width of the terminal in chars */ private $termWidth; /** * Set the height of the terminal in chars */ private $termHeight; /** * Store the last cursor position in the last readline */ private $cursorPos = 1; /** * The constructor init the console. * Check if we have the rights to execute, if wa have in cli... */ public function __construct() { if (! function_exists("exec")) { throw $this->ConsoleException("No exec support in PHP"); } $this->initSttyState = exec("stty -g 2>/dev/null"); // Set the terminal to return the value each time a key is pressed. // Do not display anything, so we don't see the characters when the user is // deleting. // 'intr ^J' allow to redefine the interruption from Ctrl+C to Ctrl+J. It // allow to manage the Ctrl+C key to clean the entry exec("stty -echo -icanon min 1 time 0 2>/dev/null"); $this->updateTerminalSize(); } /** * Update the terminal size */ public function updateTerminalSize() { $this->termWidth = 80; $this->termHeight = 25; $termSize = exec("stty size 2>/dev/null", $null, $rc); if ($rc === 0) { list($termHeight, $termWidth) = explode(" ", $termSize); if (is_null($termWidth) || is_bool($termWidth)) { $this->termWidth = 80; } else { $this->termWidth = intval($termWidth); } if (is_null($termHeight) || is_bool($termHeight)) { $this->termHeight = 25; } else { $this->termHeight = intval($termHeight); } } } /** * The destructor return the terminal to initial state */ public function __destruct() { if ($this->initSttyState !== "") { exec("stty $this->initSttyState"); } $this->colorReset(); $this->textUnderline(false); $this->textBold(false); } /** * Each time a key is pressed by the user, display the value on screen (echo) */ public function setEcho() { $this->echoMode = true; } /** * Each time a key is pressed by the user, DO NOT display the value on screen * (echo disabled) */ public function unsetEcho() { $this->echoMode = false; } /** * Display a text on screen. Must be used before "readline" method because * the provided message will not be deleted by readline process. * @param string $message The message to display */ public function echo($message) { echo $message; $this->lineContent .= $message; } /** * Wait one valid character from the user. * The non printable chars are not displayed, nor returned * The ESC Sequences are skipped * @return the pressed char */ public function getc() { $char = $this->getKey(); while (in_array(ord($char), $this->nonWriteableChar, true)) { $char = $this->getKey(); } return $char; } /** * Wait one key pressed by the user. If the key pressed is an ESC sequence, * return this sequence * The non printable chars are not displayed, but are correctely returned * The UTF8 chars are return as multiple length chars * @return the pressed char */ public function getKey() { $char = fgetc(STDIN); if ($char === chr(27)) { $sequence = $char; $char2 = fgetc(STDIN); if (ord($char2) === 27) { // Sequence of ESC ESC } elseif (ord($char2) === 79) { // Start an ESC SS3 sequence // Like F2 Key $sequence .= $char2; $char = fgetc(STDIN); $sequence .= $char; } elseif (ord($char2) === 91 || ord($char2) === 93) { // Start an ESC CSI sequence. Do not display it, just return it. // The ESC squences are used to communicate the cursor keys, associated // with the Ctrl key, by example // ESC [ is followed by any number (including none) of "parameter bytes" // in the range 0x30–0x3F (ASCII 0–9:;<=>?), then by any number of // "intermediate bytes" in the range 0x20–0x2F (ASCII space and // !"#$%&'()*+,-./), then finally by a single "final byte" in the range // 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~).[14]:5.4 $sequence .= $char2; $char = fgetc(STDIN); while (ord($char) < 64 || ord($char) > 126) { $sequence .= $char; $char = fgetc(STDIN); } $sequence .= $char; } else { $this->consoleException("Invalid ESC seq : " . ord($char2)); } return $sequence; } // UTF Sequence : // - char1 < 128 : One char (like ascii) // - char1 >= 194 && char1 <= 223 : Two chars // - char1 >= 224 && char1 <= 239 : Three chars // - char1 >= 240 && char1 <= 244 : Four chars if (ord($char) < 128) { // One Char like ascii } elseif (ord($char) >= 194 && ord($char) <= 223) { // Two chars $char .= fgetc(STDIN); } elseif (ord($char) >= 224 && ord($char) <= 239) { // Three chars $char .= fgetc(STDIN) . fgetc(STDIN); } elseif (ord($char) >= 240 && ord($char) <= 244) { // Four chars $char .= fgetc(STDIN) . fgetc(STDIN) . fgetc(STDIN); } if ($this->echoMode && ! in_array(ord($char), $this->nonWriteableChar, true)) { echo $char; } return $char; } /** * Get the line of characters pressed by the user and return the result. * Stop when the user valid by \n. * Manage correctely the backspace, the Ctrl+W to remove word... * @param string $propo Preset the text for the user * @param boolean|string $stopperChar The chars to stop the analysis and * return the result * @return string The typed string */ public function readline($propo = "", $stopperChar = false) { // Gets can not delete chars before the call. Keep the prompt (if exists) if (! is_string($propo)) { $this->consoleException("Invalid proposition provided to readline"); } $prompt = $this->lineContent; $minLength = mb_strlen($this->lineContent) + 1; // Manage the history and a temporary buffer if the user has already type // something before calling the history $historyPos = count($this->history); $string = $propo; echo $string; $this->lineContent = $prompt . $string; // The cursor position from last char of line. $cursorPos = mb_strlen($this->lineContent) + 1; while (1) { $char = $this->getKey(); if ($stopperChar === false && $char === "\n") { // End of process without stopperChars $this->lineContent = ""; break; } if ( $stopperChar !== false && in_array($char, $this->mb_str_split($stopperChar), true) ) { // End of process with stopperChars $this->lineContent = ""; break; } if ( $this->completionKeys !== false && in_array($char, $this->mb_str_split($this->completionKeys), true) ) { // Manage autocompletion $this->debug("Autocompletion starting"); // Take the last part of the string without space or double quotes $pos = strrpos($string, " "); if ($pos === false) { // No space : put all in end $start = ""; $end = $string; } elseif ($pos === mb_strlen($string)) { // Last char is a space : put all in start $start = $string; $end = ""; } else { // Last char is not a space, end is the last word and start is the // begin to before last word $start = mb_substr($string, 0, $pos + 1); $end = mb_substr($string, $pos + 1); } $this->debug("Autocompletion : start='$start', end='$end'"); $completeArr = call_user_func( $this->completionFunction, self::tokenize($start) ); if (! is_array($completeArr)) { throw new \Exception("Autocompletion : return is not an array"); } $isAssoc = is_array($completeArr) && array_diff_key($completeArr, array_keys(array_keys($completeArr))); // Remove from completeArr the proposed values which doesn't match with // $end (invalid proposals) foreach ($completeArr as $key => $val) { if ($isAssoc) { $val = $key; } if (mb_substr($val, 0, mb_strlen($end)) !== $end) { unset($completeArr[$key]); } } if ( count($completeArr) === 1 && ($isAssoc && key($completeArr) !== "" || ! $isAssoc && reset($completeArr) !== "") ) { // One entry : add a space to put on the next $this->debug("Autocompletion : One entry not empty : " . "add it + ending space"); if ($isAssoc) { $string = $start . key($completeArr) . " "; } else { $string = $start . reset($completeArr) . " "; } } elseif (count($completeArr)) { // Multiple entries : display them to allow the user to choose $this->debug("Autocompletion : Multiple entries : display choices"); echo "\n"; // In associative array, the key is the possible answer to // autocompletion, and the value is the helper message // Get the largest key length to make a beautiful alignment // Get the smaller key length to found a affined answer $maxlen = 0; foreach ($completeArr as $key => $val) { $maxlen = max($maxlen, mb_strlen($key)); } $maxlen = $maxlen + 5; if ($isAssoc) { ksort($completeArr, SORT_NATURAL); } else { sort($completeArr, SORT_NATURAL); } foreach ($completeArr as $key => $val) { if ($isAssoc) { printf("%-{$maxlen}s %s\n", $key, $val); } elseif (trim($val) === "") { // TODO : Define the string to display for ending string echo "
\n"; } else { echo "$val\n"; } } if ($isAssoc) { $addChars = $this->shortestIdenticalValues( array_keys($completeArr) ); } else { $addChars = $this->shortestIdenticalValues($completeArr); } if ($addChars === "") { $addChars = $end; } $string = $start . $addChars; } else { $this->debug("Autocompletion : Zero entry : do not change"); $string = $start . $end; } if (is_array($completeArr) && count($completeArr)) { // If there were multiple suggestions displayed, rewrite the line $cursorPos = mb_strlen($prompt . $string) + 1; $this->rewriteLine($prompt . $string); $this->moveCursor($cursorPos); } $this->debug("Autocompletion : end '$prompt.$string'"); } elseif (ord($char) === 0) { // End of file $this->debug("Empty File entry : " . ord($char)); $string = chr(0); break; } elseif (ord($char) === 3) { // Abort (Ctrl+C) $this->debug("Abort Ctrl+C : " . ord($char)); $this->lineContent = ""; $string = ""; $this->clearLine(); echo "$prompt\n"; break; } elseif (ord($char) === 4) { // Logout (Ctrl+D) $this->debug("Logout Ctrl+D : " . ord($char)); $string = "exit\n"; $this->rewriteLine($prompt . $string); return $string; } elseif (ord($char) === 12) { // Refresh page (Ctrl+L) $this->debug("Refresh Ctrl+L : " . ord($char)); echo "\033[2J\033[;H\033c"; $cursorPos = mb_strlen($prompt . $string) + 1; $this->rewriteLine($prompt . $string); $this->moveCursor($cursorPos); } elseif (ord($char) === 21) { // Empty line from prompt to cursor (Ctrl+U) $this->debug("Empty line from prompt to cursor Ctrl+U : " . ord($char)); $string = mb_substr($string, $cursorPos - $minLength); $cursorPos = $minLength; $this->rewriteLine($prompt . $string); $this->moveCursor($cursorPos); } elseif (ord($char) === 23) { // Remove the last word (Ctrl+W) $this->debug("Remove the last word Ctrl+W : " . ord($char)); $tmp = mb_substr($string, 0, $cursorPos - $minLength); $end = mb_substr($string, $cursorPos - $minLength); $tmp = rtrim($tmp); $pos = mb_strrpos($tmp, " "); if ($pos !== false) { $pos++; } $string = mb_substr($string, 0, $pos) . $end; $cursorPos = $minLength + $pos; $this->rewriteLine($prompt . $string); $this->moveCursor($cursorPos); } elseif (ord($char) === 127 || ord($char) === 8) { // Remove the previous char (Backspace) $this->debug("Remove the previous char (Backspace) : " . ord($char)); if ($cursorPos <= $minLength) { continue; } $strArr = $this->mb_str_split($string); $cursorPos--; unset($strArr[$cursorPos - $minLength]); $string = implode($strArr); $this->rewriteLine($prompt . $string); $this->moveCursor($cursorPos); } elseif (ord($char[0]) === 27) { // ESC SEQUENCE $sequence = ""; foreach (str_split($char) as $key) { $sequence .= ord($key) . " "; } $this->debug("ESC SEQUENCE : $sequence"); if ($char === chr(27) . chr(91) . chr(49) . chr(59) . chr(53) . chr(67)) { // Cursor right + Ctrl : cursor jump by word $this->debug("Cursor right + Ctrl"); $tmp = mb_substr($string, $cursorPos - $minLength); $tmp = ltrim($tmp); $pos = strpos($tmp, " "); if ($pos !== false) { $cursorPos += $pos + 1 ; } else { $cursorPos = mb_strlen($prompt . $string) + 1; } $this->moveCursor($cursorPos); } elseif ($char === chr(27) . chr(91) . chr(49) . chr(59) . chr(53) . chr(68)) { // Cursor left + Ctrl : cursor jump by word $this->debug("Cursor left + Ctrl"); $tmp = mb_substr($string, 0, $cursorPos - $minLength); $tmp = rtrim($tmp); $pos = strrpos($tmp, " "); if ($pos !== false) { $pos++; } $cursorPos = $minLength + $pos; $this->moveCursor($cursorPos); } elseif ($char === chr(27) . chr(91) . chr(65)) { // Cursor up : display the previous history if defined $this->debug("Cursor up"); if (! isset($historyTmp)) { $historyTmp = $string; $historyTmpPos = $cursorPos; } if ($historyPos > 0) { $historyPos--; $slice = array_slice($this->history, $historyPos, 1); $string = reset($slice); $cursorPos = mb_strlen($prompt . $string) + 1; $this->rewriteLine($prompt . $string); $this->moveCursor($cursorPos); } } elseif ($char === chr(27) . chr(91) . chr(66)) { // Cursor down : display the next history if defined $this->debug("Cursor down"); if ($historyPos < count($this->history) - 1) { $historyPos++; $slice = array_slice($this->history, $historyPos, 1); $string = reset($slice); $cursorPos = mb_strlen($prompt . $string) + 1; } elseif (isset($historyTmp)) { $string = $historyTmp; $cursorPos = $historyTmpPos; unset($historyTmp); } $this->rewriteLine($prompt . $string); $this->moveCursor($cursorPos); } elseif ($char === chr(27) . chr(91) . chr(67)) { // Cursor right $this->debug("Cursor right"); if ($cursorPos <= mb_strlen($this->lineContent)) { $cursorPos++; $this->moveCursor($cursorPos); } } elseif ($char === chr(27) . chr(91) . chr(68)) { // Cursor left $this->debug("Cursor left"); if ($cursorPos > $minLength) { $cursorPos--; $this->moveCursor($cursorPos); } } elseif ($char === chr(27) . chr(91) . chr(70)) { // End key $this->debug("End key"); $cursorPos = $minLength + mb_strlen($string); $this->moveCursor($cursorPos); } elseif ($char === chr(27) . chr(91) . chr(72)) { // Home key $this->debug("Home key"); $cursorPos = $minLength; $this->moveCursor($cursorPos); } elseif ($char === chr(27) . chr(91) . chr(51) . chr(126)) { // Remove the char under the cursor (Delete) $this->debug("Delete key"); if ($cursorPos > mb_strlen($prompt . $string)) { continue; } $strArr = $this->mb_str_split($string); unset($strArr[$cursorPos - $minLength]); $string = implode($strArr); $this->rewriteLine($prompt . $string); $this->moveCursor($cursorPos); } } elseif (in_array(ord($char), $this->nonWriteableChar, true)) { // Non writeable char : skip it $this->debug("Non writeable char : " . ord($char)); } else { // Normal char : Add it to the string $this->debug("Normal char : " . ord($char)); $strArr = $this->mb_str_split($string); $firstArr = array_slice($strArr, 0, $cursorPos - $minLength); $lastArr = array_slice($strArr, $cursorPos - $minLength); $insertArr = [$char]; $strArr = array_merge($firstArr, $insertArr, $lastArr); $string = implode($strArr); $cursorPos++; $this->rewriteLine($prompt . $string); $this->moveCursor($cursorPos); } } $this->debug("End of readline '$string'"); return $string; } /** * Rewrite the line with the provided $text. * Delete all the old data * @param string $text The new text to use on line */ private function rewriteLine($text) { $this->debug("Call rewriteLine ($text)"); if ($this->echoMode) { $this->clearLine(); $this->lineContent = $text; echo $this->lineContent; $this->cursorPos = mb_strlen($this->lineContent); } } /** * Move the cursor on position $position. The first column is $cursorPos=1 * @param integer $cursorPos The new position on line */ private function moveCursor($cursorPos) { $this->debug("Call moveCursor ($cursorPos)"); if ($cursorPos < 1) { $this->consoleException("MoveCursor lesser than one : $cursorPos"); } if ($this->echoMode) { $oldLength = mb_strlen($this->lineContent); // 1. Calculate on which line the cursor is positionned $cursorLine = 1 + floor((-1 + $this->cursorPos) / $this->termWidth); // 2. Return the cursor to the first line for ($i = $cursorLine; $i > 1; $i--) { echo chr(27) . chr(91) . chr(49) . chr(65) . "\r"; } // 3. Down the cursor to the wanted line $wantedLine = ceil($cursorPos / $this->termWidth); if ($wantedLine > 1) { for ($i = 1; $i < $wantedLine; $i++) { echo "\r" . chr(27) . chr(91) . chr(49) . chr(66) . "\r"; } } // 4. Move the cursor on the last line $needMovePos = -1 + $cursorPos - ($wantedLine - 1) * $this->termWidth; echo "\r" . str_repeat(chr(27) . chr(91) . chr(67), $needMovePos); $this->cursorPos = $cursorPos; } } /** * Clear the existing line. */ public function clearLine() { $this->debug("Call clearLine"); $oldLength = mb_strlen($this->lineContent); // 1. Calculate on which line the cursor is positionned $cursorLine = 1 + floor((-1 + $this->cursorPos) / $this->termWidth); $lastLines = 1 + floor((1 + $oldLength) / $this->termWidth); $this->debug("==> clearLine : oldLength=$oldLength, " . "cursorLine=$cursorLine, lastLines=$lastLines"); for ($i = $cursorLine; $i < $lastLines; $i++) { $this->debug("==> clearLine : go Down (i=$i<$lastLines)"); echo "\033[1B"; } // 3. Remove the lines from lastLines to line 1 if ($lastLines > 1) { for ($i = $lastLines; $i > 1; $i--) { $this->debug("==> clearLine : Remove line up (i=$i<$lastLines)"); echo "\r\033[K\033[1A\r"; } } // 4. Clean the line 1 $this->debug("==> clearLine : Remove line 1"); echo "\r\033[K"; $this->lineContent = ""; $this->cursorPos = 1; } /** * Clear all the screen and remove the scroll of the screen */ public function clearScreen() { echo "\033[2J\033[;H\033c"; } /** * Get the terminal Height */ public function getTermHeight() { return $this->termHeight; } /** * Get the terminal Width */ public function getTermWidth() { return $this->termWidth; } /** * Call a specific function when a completion key is pressed * The function must get the partial text as first parameter, and must return * an array with the possibilities * If only one possibility is returned, the console will be immediately * updated. * @param string|bool $completionKeys The list of the completion keys. False * unset the method * @param callable $completionFunction The function called when one of the * completion keys is pressed. */ public function completeFunction($completionKeys, $completionFunction) { if (! is_string($completionKeys) && ! is_bool($completionKeys)) { $this->consoleException("Can not set the completionKeys : not a string"); } if ($completionKeys === true) { $this->consoleException("Can not set the completionKeys : not false"); } if (! is_callable($completionFunction)) { $this->consoleException("Can not set the completionFunction : " . "not a callable function"); } $this->completionKeys = $completionKeys; $this->completionFunction = $completionFunction; } /** * Get the actual history in memory */ public function getHistory() { return $this->history; } /** * Clear the history * This method do NOT write the empty history on disk */ public function clearHistory() { $this->history = []; return $this; } /** * Write the history to disk. * @param string $historyFile The history file where the history is stored */ public function writeHistory($historyFile) { if (file_exists($historyFile)) { if (! is_writeable($historyFile)) { $this->consoleException("History file '$historyFile' " . "is not writeable"); } $history = file_get_contents($historyFile); if ($history === false) { $this->consoleException("History file '$historyFile' can not be read"); } $historyArr = explode("\n", $history); if (! isset($historyArr[0]) || $historyArr[0] !== "__HISTORY__") { $this->consoleException("History file '$historyFile' " . "is not an history file : do not touch\n"); } } elseif (! file_exists(dirname($historyFile))) { $this->consoleException("History file '$historyFile' " . "can not be created: parent directory doesn't exists"); } elseif (! is_dir(dirname($historyFile))) { $this->consoleException("History file '$historyFile' " . "can not be created: parent directory is not a directory"); } file_put_contents($historyFile, "__HISTORY__\n"); $history = ""; foreach ($this->history as $time => $command) { $history .= "$time $command\n"; } file_put_contents($historyFile, $history, FILE_APPEND | LOCK_EX); return $this; } /** * Read the history from the disk * If the file doesn't exists, return an empty array * @param string $historyFile The history file where the history is stored * @return the read history with timestamp as key and command as value */ public function readHistory($historyFile) { if (! file_exists($historyFile)) { $this->history = []; return []; } if (! is_readable($historyFile)) { $this->consoleException("History file '$historyFile' can not be read"); } $history = file_get_contents($historyFile); if ($history === false) { $this->consoleException("History file '$historyFile' can not be read"); } $historyArr = explode("\n", $history); if (! isset($historyArr[0]) || $historyArr[0] !== "__HISTORY__") { $this->consoleException("History file '$historyFile' " . "is not an history file : do not touch\n"); } array_shift($historyArr); foreach ($historyArr as $line) { $tmp = @explode(" ", $line, 2); $time = (isset($tmp[0])) ? $tmp[0] : null; $command = (isset($tmp[1])) ? $tmp[1] : null; if ($time === null || $command === null || ! ctype_digit($time)) { continue; } $this->history[$time] = $command; } return $this->history; } /** * Add a new entry in history. * The new line can not be empty : it is not stored, but without error * This method do NOT write the history on disk : you must use writeHistory * @param string The new entry to add in history */ public function addHistory($line) { if (! is_string($line)) { $this->consoleException("Can not add line to history : " . "it is not a string"); } if (trim($line) === "") { return $this; } $this->history[time()] = $line; $this->history = array_slice( $this->history, -$this->historyMaxSize, null, true ); return $this; } /** * Get/Set the maximum number of entries in the history * If null, get the defined maximum number * @param integer|null $historyMaxSize The maximum number of entries */ public function historyMaxSize($historyMaxSize = null) { if ($historyMaxSize === null) { return $this->historyMaxSize; } if (intval($historyMaxSize) < 1) { $this->consoleException("Can not set the historyMaxSize : " . "negative value provided"); } $this->historyMaxSize = intval($historyMaxSize); } /** * Error management * @param string $message The message to throw in the exception */ public function consoleException($message) { throw new \Exception($message, 500); } /** * Set the text color * @param integer $colorNum The color number to use */ public function colorText($colorNum) { if (! is_int($colorNum)) { $this->consoleException("ColorNum provided to colorText is not an " . "integer"); } echo "\033[38;5;{$colorNum}m"; } /** * Set the background text color * @param integer $colorNum The color number to use */ public function colorBackgroundText($colorNum) { if (! is_int($colorNum)) { $this->consoleException("ColorNum provided to colorBackgroundText not " . "an integer"); } echo "\033[48;5;{$colorNum}m"; } /** * Reset the colors */ public function colorReset() { echo "\033[0m"; } /** * Underline the text * @param boolean $underline True to underline, false to remove the underline */ public function textUnderline($underline) { if ($underline === false) { $underline = 2; } else { $underline = ""; } echo "\033[{$underline}4m"; } /** * Bold the text * @param boolean $bold True to bold, false to remove the bold */ public function textBold($bold) { if ($bold === false) { $bold = 0; } else { $bold = 1; } echo "\033[{$bold}m"; } /** * Return true if the TTY is enabled, or false if the program is called from pipe */ public function isTTY() { return !! $this->initSttyState; } /** * Tokenize the provided line and aggragate if there is single or double * quotes. * Trim the spaces * @param string $line The line to tokenize * @return array The tokens */ public static function tokenize($line) { $tokens = []; $token = strtok(trim($line), ' '); while ($token) { // find double quoted tokens if ($token[0] == '"') { $token .= ' ' . strtok('"') . '"'; } // find single quoted tokens if ($token[0] == "'") { $token .= ' ' . strtok("'") . "'"; } $tokens[] = $token; $token = strtok(' '); } return $tokens; } /** * This function return an array with each char, but supports UTF-8 * @param string $string The string to explode * @param integer $split_length The number of chars in each split * @return array */ private function mb_str_split($string, $split_length = 1) { $res = []; for ($i = 0; $i < mb_strlen($string); $i += $split_length) { $res[] = mb_substr($string, $i, $split_length); } return $res; } /** * This function debug the data * @param mixed $data The data to store */ private function debug($data) { if ($this->debug === false) { return; } if (is_array($data) || is_bool($data)) { $data = var_export($data, true); } file_put_contents($this->debug, date("H:i:s") . " $data\n", FILE_APPEND); } /** * Look in the array which first chars of each possibilites are identical. * @param array $completeArr The values to examine * @return string the identical chars */ private function shortestIdenticalValues($completeArr) { if (! is_array($completeArr)) { return ""; } $minlen = 99999; foreach ($completeArr as $val) { $minlen = min($minlen, mb_strlen($val)); } $identicalString = ""; $sameCharLength = 1 ; while ($sameCharLength <= $minlen) { $tmp = ""; foreach ($completeArr as $val) { $part = mb_substr($val, 0, $sameCharLength); if ($tmp == "") { $tmp = $part; } if ($tmp !== $part) { break 2; } } $identicalString = $tmp; $sameCharLength++; } return $identicalString; } }