diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index e4c9871045..564e9d313f 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -13,6 +13,7 @@ Yii Framework 2 Change Log - Enh #4040: Added `$viewFile` and `$params` to the `EVENT_BEFORE_RENDER` and `EVENT_AFTER_RENDER` events for `View` (qiangxue) - Enh #4275: Added `removeChildren()` to `yii\rbac\ManagerInterface` and implementations (samdark) - Enh: Added `yii\base\Application::loadedModules` (qiangxue) +- Enh #5316: Added `startsWith()` and `endsWith()` to `yii\helpers\StringHelper`. Methods are binary-safe, multibyte-safe and optionally case-insensitive (armab) - Chg #2037: Dropped the support for using `yii\base\Module` as concrete module classes (qiangxue) diff --git a/framework/helpers/BaseStringHelper.php b/framework/helpers/BaseStringHelper.php index d04d0fb3ec..bc14a896c9 100644 --- a/framework/helpers/BaseStringHelper.php +++ b/framework/helpers/BaseStringHelper.php @@ -7,6 +7,8 @@ namespace yii\helpers; +use Yii; + /** * BaseStringHelper provides concrete implementation for [[StringHelper]]. * @@ -100,8 +102,8 @@ class BaseStringHelper */ public static function truncate($string, $length, $suffix = '...', $encoding = null) { - if (mb_strlen($string, $encoding ?: \Yii::$app->charset) > $length) { - return trim(mb_substr($string, 0, $length, $encoding ?: \Yii::$app->charset)) . $suffix; + if (mb_strlen($string, $encoding ?: Yii::$app->charset) > $length) { + return trim(mb_substr($string, 0, $length, $encoding ?: Yii::$app->charset)) . $suffix; } else { return $string; } @@ -124,4 +126,50 @@ class BaseStringHelper return $string; } } + + /** + * Check if given string starts with specified substring. + * Binary and multibyte safe. + * + * @param string $string Input string + * @param string $with Part to search + * @param boolean $caseSensitive Case sensitive search. Default is true. + * @return boolean Returns true if first input starts with second input, false otherwise + */ + public static function startsWith($string, $with, $caseSensitive = true) + { + if (!$bytes = static::byteLength($with)) { + return true; + } + if ($caseSensitive) { + return strncmp($string, $with, $bytes) === 0; + } else { + return mb_strtolower(mb_substr($string, 0, $bytes, '8bit'), Yii::$app->charset) === mb_strtolower($with, Yii::$app->charset); + } + } + + /** + * Check if given string ends with specified substring. + * Binary and multibyte safe. + * + * @param string $string + * @param string $with + * @param boolean $caseSensitive Case sensitive search. Default is true. + * @return boolean Returns true if first input ends with second input, false otherwise + */ + public static function endsWith($string, $with, $caseSensitive = true) + { + if (!$bytes = static::byteLength($with)) { + return true; + } + if ($caseSensitive) { + // Warning check, see http://php.net/manual/en/function.substr-compare.php#refsect1-function.substr-compare-returnvalues + if (static::byteLength($string) < $bytes) { + return false; + } + return substr_compare($string, $with, -$bytes, $bytes) === 0; + } else { + return mb_strtolower(mb_substr($string, -$bytes, null, '8bit'), Yii::$app->charset) === mb_strtolower($with, Yii::$app->charset); + } + } } diff --git a/tests/unit/framework/helpers/StringHelperTest.php b/tests/unit/framework/helpers/StringHelperTest.php index 33d075b47c..6b5580af32 100644 --- a/tests/unit/framework/helpers/StringHelperTest.php +++ b/tests/unit/framework/helpers/StringHelperTest.php @@ -114,4 +114,109 @@ class StringHelperTest extends TestCase $this->assertEquals('это тестовая multibyte!!!', StringHelper::truncateWords('это тестовая multibyte строка', 3, '!!!')); $this->assertEquals('это строка с неожиданными...', StringHelper::truncateWords('это строка с неожиданными пробелами', 4)); } + + /** + * @dataProvider providerStartsWith + */ + public function testStartsWith($result, $string, $with) + { + // case sensitive version check + $this->assertSame($result, StringHelper::startsWith($string, $with)); + // case insensitive version check + $this->assertSame($result, StringHelper::startsWith($string, $with, false)); + } + + /** + * Rules that should work the same for case-sensitive and case-insensitive `startsWith()` + */ + public function providerStartsWith() + { + return [ + // positive check + [true, '', ''], + [true, '', null], + [true, 'string', ''], + [true, ' string', ' '], + [true, 'abc', 'abc'], + [true, 'Bürger', 'Bürger'], + [true, '我Я multibyte', '我Я'], + [true, 'Qנטשופ צרכנות', 'Qנ'], + [true, 'ไทย.idn.icann.org', 'ไ'], + [true, '!?+', "\x21\x3F"], + [true, "\x21?+", '!?'], + // false-positive check + [false, '', ' '], + [false, ' ', ' '], + [false, 'Abc', 'Abcde'], + [false, 'abc', 'abe'], + [false, 'abc', 'b'], + [false, 'abc', 'c'], + ]; + } + + public function testStartsWithCaseSensitive() + { + $this->assertFalse(StringHelper::startsWith('Abc', 'a')); + $this->assertFalse(StringHelper::startsWith('üЯ multibyte', 'Üя multibyte')); + } + + public function testStartsWithCaseInsensitive() + { + $this->assertTrue(StringHelper::startsWith('sTrInG', 'StRiNg', false)); + $this->assertTrue(StringHelper::startsWith('CaSe', 'cAs', false)); + $this->assertTrue(StringHelper::startsWith('HTTP://BÜrger.DE/', 'http://bürger.de', false)); + $this->assertTrue(StringHelper::startsWith('üЯйΨB', 'ÜяЙΨ', false)); + } + + /** + * @dataProvider providerEndsWith + */ + public function testEndsWith($result, $string, $with) + { + // case sensitive version check + $this->assertSame($result, StringHelper::endsWith($string, $with)); + // case insensitive version check + $this->assertSame($result, StringHelper::endsWith($string, $with, false)); + } + + /** + * Rules that should work the same for case-sensitive and case-insensitive `endsWith()` + */ + public function providerEndsWith() + { + return [ + // positive check + [true, '', ''], + [true, '', null], + [true, 'string', ''], + [true, 'string ', ' '], + [true, 'string', 'g'], + [true, 'abc', 'abc'], + [true, 'Bürger', 'Bürger'], + [true, 'Я multibyte строка我!', ' строка我!'], + [true, '+!?', "\x21\x3F"], + [true, "+\x21?", "!\x3F"], + [true, 'נטשופ צרכנות', 'ת'], + // false-positive check + [false, '', ' '], + [false, ' ', ' '], + [false, 'aaa', 'aaaa'], + [false, 'abc', 'abe'], + [false, 'abc', 'a'], + [false, 'abc', 'b'], + ]; + } + + public function testEndsWithCaseSensitive() + { + $this->assertFalse(StringHelper::endsWith('string', 'G')); + $this->assertFalse(StringHelper::endsWith('multibyte строка', 'А')); + } + + public function testEndsWithCaseInsensitive() + { + $this->assertTrue(StringHelper::endsWith('sTrInG', 'StRiNg', false)); + $this->assertTrue(StringHelper::endsWith('string', 'nG', false)); + $this->assertTrue(StringHelper::endsWith('BüЯйΨ', 'ÜяЙΨ', false)); + } }