luyadev/yii-helpers

View on GitHub
src/helpers/StringHelper.php

Summary

Maintainability
B
6 hrs
Test Coverage
A
99%
<?php

namespace luya\yii\helpers;

use yii\helpers\BaseStringHelper;

/**
 * Helper methods when dealing with Strings.
 *
 * Extends the {{yii\helpers\StringHelper}} class by some useful functions like:
 *
 * + {{luya\yii\helpers\StringHelper::typeCast()}}
 * + {{luya\yii\helpers\StringHelper::isFloat()}}
 * + {{luya\yii\helpers\StringHelper::replaceFirst()}}
 * + {{luya\yii\helpers\StringHelper::contains()}}
 * + {{luya\yii\helpers\StringHelper::startsWithWildcard()}}
 * + {{luya\yii\helpers\StringHelper::typeCastNumeric()}}
 *
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.0
 */
class StringHelper extends BaseStringHelper
{
    /**
     * Convert a YouTube link to an embeddable video URL.
     *
     * If the given input URL is invalid, `false` is returned.
     *
     * @param string $url
     * @return string|boolean
     * @see https://stackoverflow.com/a/48130447
     */
    public static function toYouTubeEmbed($url)
    {
        preg_match('%(?:youtube(?:-nocookie)?\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([^"&?/ ]{11})%i', $url, $match);

        if (isset($match[1])) {
            return 'https://www.youtube.com/embed/' . $match[1];
        }

        return false;
    }
    /**
     * TypeCast a string to its specific types.
     *
     * Arrays will passed to the {{luya\yii\helpers\ArrayHelper::typeCast()}} class.
     *
     * @param mixed $string The input string to type cast. Arrays will be passed to {{luya\yii\helpers\ArrayHelper::typeCast()}}.
     * @return mixed The new type casted value, if the input is an array the output is the typecasted array.
     */
    public static function typeCast($string)
    {
        if (is_numeric($string)) {
            return static::typeCastNumeric($string);
        } elseif (is_array($string)) {
            return ArrayHelper::typeCast($string);
        }

        return $string;
    }

    /**
     * String Wildcard Check.
     *
     * Checks whether a strings starts with the wildcard symbol and compares the string before the wild card symbol `*`
     * with the string provided. If there is NO wildcard symbol it always returns `false`.
     *
     *
     * @param string $string The string which should be checked with $with comparator
     * @param string $with The with string which must end with the wildcard symbol `*` e.g. `foo*` would match string `foobar`.
     * @param boolean $caseSensitive Whether to compare the starts with string as case-sensitive or not, defaults to `true`.
     * @return boolean Whether the string starts with the wildcard marked string or not, if no wildcard symbol is contained.
     * in the $with it always returns `false`.
     */
    public static function startsWithWildcard($string, $with, $caseSensitive = true)
    {
        if (substr($with, -1) != "*") {
            return false;
        }

        return self::startsWith($string, rtrim($with, '*'), $caseSensitive);
    }



    /**
     * See if filter conditions match the given value.
     *
     * Example filter conditions:
     *
     * + `cms_*` matches everything starting with "cms_".
     * + `cms_*,admin_*` matches booth cms_* and admin_* tables.
     * + `!cms_*` matches all not start with "cms_"
     * + `!cms_*,!admin_*` matches all not starting with "cms_" and not starting with "admin_"
     * + `cms_*,!admin_*` matches all start with "cms_" but not start with "admin_"
     *
     * Only first match is relevant:
     *
     * + "cms_*,!admin_*,admin_*" include all cms_* tables but exclude all admin_* tables (last match has no effect)
     * + "cms_*,admin_*,!admin_*" include all cms_* and admin_* tables (last match has no effect)
     *
     * Example using condition string:
     *
     * ```php
     * filterMatch('hello', 'he*'); // true
     * filterMatch('hello', 'ho,he*'); // true
     * filterMatch('hello', ['ho', 'he*']); // true
     * ```
     *
     * @param $value The value on which the filter conditions should be applied on.
     * @param array|string $conditions An array of filter conditions, if a string is given he will be exploded by commas.
     * @param boolean $caseSensitive Whether to match value even when lower/upper case is not correct/same.
     * @return bool Returns `true` if one of the given filter conditions matches.
     */
    public static function filterMatch($value, $conditions, $caseSensitive = true)
    {
        if (is_scalar($conditions)) {
            $conditions = self::explode($conditions, ",", true, true);
        }

        foreach ($conditions as $condition) {
            $isMatch = true;
            // negate
            if (substr($condition, 0, 1) == "!") {
                $isMatch = false;
                $condition = substr($condition, 1);
            }
            if ($caseSensitive) {
                $condition = strtolower($condition);
                $value = strtolower($value);
            }
            if ($condition == $value || self::startsWithWildcard($value, $condition)) {
                return $isMatch;
            }
        }

        return false;
    }

    /**
     * TypeCast a numeric value to float or integer.
     *
     * If the given value is not a numeric or float value it will be returned as it is. In order to find out whether it's float
     * or not use {{luya\yii\helpers\StringHelper::isFloat()}}.
     *
     * @param mixed $value The given value to parse.
     * @return mixed Returns the original value if not numeric or integer, float casted value.
     */
    public static function typeCastNumeric($value)
    {
        if (!self::isFloat($value)) {
            return $value;
        }

        if (intval($value) == $value) {
            return (int) $value;
        }

        return (float) $value;
    }

    /**
     * Checks whether a string is a float value.
     *
     * Compared to `is_float()` function of PHP, it only ensures whether the input variable is type float.
     *
     * @param mixed $value The value to check whether it's float or not.
     * @return boolean Whether it's a float value or not.
     */
    public static function isFloat($value)
    {
        if (is_float($value)) {
            return true;
        }

        if (!is_array($value) && preg_match('/^\d+\.$/', $value)) {
            // ordinal number of the form cardinal number followed by point, e.g. "24."
            return false;
        }

        return ($value == (string)(float) $value);
    }

    /**
     * Replace only the first occurrence found inside the string.
     *
     * The replace first method is *case-sensitive*.
     *
     * ```php
     * StringHelper::replaceFirst('abc', '123', 'abc abc abc'); // returns "123 abc abc"
     * ```
     *
     * @param string $search Search string to look for.
     * @param string $replace Replacement value for the first found occurrence.
     * @param string $subject The string you want to look up to replace the first element.
     * @return mixed Replaced string
     */
    public static function replaceFirst($search, $replace, $subject)
    {
        return preg_replace('/'.preg_quote($search, '/').'/', $replace, $subject, 1);
    }

    /**
     * Check whether a char or word exists in a string or not.
     *
     * This method is case-sensitive. The need can be an array with multiple chars or words who
     * are going to look up in the haystack string.
     *
     * If an array of needle words is provided the $strict parameter defines whether all need keys must be found
     * in the string to get the `true` response or if just one of the keys are found the response is already `true`.
     *
     * ```php
     * if (StringHelper::contains('foo', 'the foo bar Bar'')) {
     *    echo "yes!";
     * }
     * ```
     *
     * check if one of the given needles exists:
     *
     * ```php
     * if (StringHelper::contains(['jungle', 'hell0], 'Welcome to the jungle!)) {
     *    echo "yes!";
     * }
     * ```
     *
     * @param string|array $needle The char or word to find in the $haystack. Can be an array to multi find words or char in the string.
     * @param string $haystack The haystack where the $needle string should be looked up. A string or phrase with words.
     * @param boolean $strict If an array of needles is provided the $strict parameter defines whether all keys must be found ($strict = `true`) or just one result must be found ($strict = `false`).
     * @return boolean If an array of values is provided the response may change depending on $findAll.
     */
    public static function contains($needle, $haystack, $strict = false)
    {
        $needles = (array) $needle;

        $state = false;

        foreach ($needles as $item) {
            $state = (strpos($haystack, (string) $item) !== false);

            if ($strict && !$state) {
                return false;
            }

            if (!$strict && $state) {
                return true;
            }
        }

        return $state;
    }

    /**
     * "Minify" html content.
     *
     * + remove space
     * + remove tabs
     * + remove newlines
     * + remove HTML comments
     *
     * @param string $content The content to minify.
     * @param array $options Optional arguments to provide for minification:
     * - comments: boolean, where HTML comments should be removed or not, defaults to `false`.
     * @return mixed Returns the minified content.
     */
    public static function minify($content, array $options = [])
    {
        $min = preg_replace(['/[\n\r]/', '/\>[^\S ]+/s', '/[^\S ]+\</s', '/(\s)+/s', ], ['', '>', '<', '\\1'], trim($content));
        $min = str_replace(['> <'], ['><'], $min);

        if (ArrayHelper::getValue($options, 'comments', false)) {
            $min = preg_replace('/<!--(.*)-->/Uis', '', $min);
        }

        return $min;
    }

    /**
     * Cut the given word/string from the content. It's truncated to the left side and to the right side of the word.
     *
     * An example of how a sentence is cutted:
     *
     * ```php
     * $cut = StringHelper::truncateMiddle('the quick fox jumped over the lazy dog', 'jumped', 12);
     * echo $cut; // ..e quick fox jumped over the la..
     * ```
     *
     * @param string $content The content to cut the words from.
     * @param string $word The word which should be in the middle of the string.
     * @param integer $length The amount of chars to cut on the left and right side from the word.
     * @param string $affix The chars which should be used for prefix and suffix when string is cutted.
     * @param boolean $caseSensitive Whether the search word in the string even when lower/upper case is not correct.
     */
    public static function truncateMiddle($content, $word, $length, $affix = '..', $caseSensitive = false)
    {
        $content = strip_tags($content);
        $array = self::mb_str_split($content);
        $first = mb_strpos($caseSensitive ? $content : mb_strtolower($content), $caseSensitive ? $word : mb_strtolower($word));

        // we could not find any match, therefore use casual truncate method.
        if ($first === false) {
            // as the length value in truncate middle stands for to the left and to the right, we multiply this value with 2
            return self::truncate($content, ($length * 2), $affix);
        }

        $last = $first + mb_strlen($word);

        // left and right array chars from word
        $left = array_slice($array, 0, $first, true);
        $right = array_slice($array, $last, null, true);
        $middle = array_splice($array, $first, mb_strlen($word));

        // string before
        $before = (count($left) > $length) ? $affix.implode("", array_slice($left, -$length)) : implode("", $left);
        $after = (count($right) > $length) ? implode("", array_slice($right, 0, $length)) . $affix : implode("", $right);

        return $before . implode("", $middle) . $after;
    }

    /**
     * Highlight a word within a content.
     *
     * Since version 1.0.14 it's possible to provide an array with words to highlight.
     *
     * > This function IS NOT case-sensitive!
     *
     * ```php
     * StringHelper::highlightWord('Hello John!', 'john');
     * ```
     *
     * The above example would return `Hello <b>John</b>!`.
     *
     * @param string $content The content to find the word.
     * @param string|array $word The word to find within the content. It can be an array. If a word exists already in the list of words, this one will be stripped. e.g. `['test', 'testfoobar']` would remove `test` from the list as it exists in `testfoobar`.
     * @param string $markup The markup used wrap the word to highlight.
     */
    public static function highlightWord($content, $word, $markup = '<b>%s</b>')
    {
        $transliterateContent = Inflector::transliterate($content);
        $highlights = [];

        $words = array_unique((array) $word);

        // if there are multiple words, we need to ensure the same part of a word does not exists twice
        // otherwise this can generate wrong highlight results like a highlight inside of a highlight.
        if (count($words) > 1) {
            foreach ($words as $wordIndex => $word) {
                $inArrayIndex = preg_grep('/'.preg_quote($word, '/').'/', $words);
                if ((is_countable($inArrayIndex) ? count($inArrayIndex) : 0) > 1) {
                    unset($words[$wordIndex]);
                }
            }
        }

        foreach ($words as $word) {
            // search in content
            preg_match_all("/".preg_quote($word, '/')."+/i", $content, $matches);
            foreach ($matches[0] as $word) {
                $highlights[] = $word;
                // if the word is covered already, do not process further in foreach and break here
                break;
            }

            // search in transliterated content if not yet breaked from previous results
            preg_match_all("/".preg_quote($word, '/')."+/i", $transliterateContent, $matches);
            foreach ($matches[0] as $word) {
                $highlights[] = self::sliceTransliteratedWord($word, $transliterateContent, $content);
            }
        }

        // highlight all results in text with [[$word]]
        foreach (array_unique($highlights) as $highlight) {
            $content = str_replace($highlight, '[['.$highlight.']]', $content);
        }

        preg_match_all('/\[\[(.*?)\]\]/', $content, $matches, PREG_SET_ORDER);

        $searchReplace = [];
        foreach ($matches as $match) {
            if (!array_key_exists($match[0], $searchReplace)) {
                $searchReplace[$match[0]] = sprintf($markup, $match[1]);
            }
        }

        foreach ($searchReplace as $search => $replace) {
            $content = str_replace($search, $replace, $content);
        }

        return $content;
    }

    /**
     * Search a word within a transliterated text and cut out the original word in the original text.
     *
     * For example when you search for the transliterated word in text and want to return the original:
     *
     * ```php
     * StringHelper::sliceTransliteratedWord('frederic', 'Hello frederic', 'Hello fréderic');
     * ```
     *
     * The above example would return `fréderic`
     *
     * @param string $word
     * @param string $transliteratedText
     * @param string $originalText
     * @return string
     * @since 1.1.0
     */
    public static function sliceTransliteratedWord($word, $transliteratedText, $originalText)
    {
        return mb_substr($originalText, mb_strpos($transliteratedText, $word), mb_strlen($word));
    }

    /**
     * Multibyte-safe `str_split()` function.
     *
     * @param string $string The string to split into an array.
     * @param integer $length The length of the chars to cut.
     * @see https://www.php.net/manual/de/function.str-split.php#115703
     */
    public static function mb_str_split($string, $length = 1)
    {
        return preg_split('/(.{'.$length.'})/us', $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
    }

    /**
     * Check whether a value is numeric or not.
     *
     * There are situations where `is_numeric()` does not provide the desired result,
     * like for example `is_numeric('3e30')` would return true, as `e` can be considered
     * as exponential operator.
     *
     * Therefore this function checks with regex whether values or 0-9 if strict is enabled,
     * which is default behavior.
     *
     * @param mixed $value The value to check.
     * @param boolean $strict
     * @return boolean
     */
    public static function isNummeric($value, $strict = true)
    {
        if (!is_scalar($value)) {
            return false;
        }

        if (is_bool($value)) {
            return false;
        }

        if ($strict) {
            return preg_match('/^[0-9]+$/', $value) == 1 ? true : false;
        }

        return is_numeric($value);
    }

    /**
     * Templating a string with Variables
     *
     * The variables should be declared as `{{username}}` while the variables array key should contain `username`.
     *
     * Usage example:
     *
     * ```php
     * $content = StringHelper::template('<p>{{ name }}</p>', ['name' => 'John']);
     *
     * // output: <p>John</p>
     * ```
     *
     * If a variable is not found, the original curly brackets will be returned.
     *
     * @param string $template The template to parse. The template may contain double curly brackets variables.
     * @param array $variables The variables which should be available in the template.
     * @param boolean $removeEmpty Whether variables in double curly brackets should be removed, even the have not be assigned by $variables array.
     * @param string $leftDelimiter The delimiter for the variable on the left, default is `{{` {@since 1.2.0}
     * @param string $rightDelimiter The delimiter for the variable on the right, default is `}}` {@since 1.2.0}
     * @return string
     */
    public static function template($template, array $variables = [], $removeEmpty = false, $leftDelimiter = '{{', $rightDelimiter = '}}')
    {
        preg_match_all("/$leftDelimiter(.*?)$rightDelimiter/", $template, $matches, PREG_SET_ORDER);

        if (empty($matches)) {
            return $template;
        }

        foreach ($matches as $match) {
            $exposedVariableName = trim($match[1]);
            if (array_key_exists($exposedVariableName, $variables)) {
                $template = str_replace($match[0], (string) $variables[$exposedVariableName], $template);
            } elseif ($removeEmpty) {
                $template = str_replace($match[0], '', $template);
            }
        }

        return $template;
    }

    /**
     * Convert a text with different separators to an array.
     *
     * It's very common to use separators when working with user input, for example a list of domains separated by commas. Therefore
     * this function will use common separators the generate an array from a text string.
     *
     * Explodes the string by: "Newline", ";", ","
     *
     * + newline
     * + comma
     * + point comma
     *
     * @param string $text A text which contains a list of items separated by separators like commas.
     * @return array
     */
    public static function textList($text, array $separators = [PHP_EOL, "\n", "\r", "\n\r", ";", ","])
    {
        return StringHelper::explode(str_replace($separators, ';', $text), ";", true, true);
    }
}