diff --git a/CHANGELOG.md b/CHANGELOG.md index b3c4738..a5550f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v0.2.3-dev + +**2022-??-?? — [Diff](https://github.com/maciejczyzewski/bottomline/compare/0.2.2...0.2.3) — [Docs](https://maciejczyzewski.github.io/bottomline/)** + +* Updates to the `__::slug()` function, + - Optimized to be more performant when given plain, old ASCII text + - Has a workaround for [a bug in PHP 8.1](https://github.com/php/php-src/issues/7898) that would return an empty string + ## v0.2.2 **2022-01-07 — [Diff](https://github.com/maciejczyzewski/bottomline/compare/0.2.1...0.2.2) — [Docs](https://maciejczyzewski.github.io/bottomline/)** diff --git a/src/__/functions/slug.php b/src/__/functions/slug.php index 6d54425..9e7ec66 100644 --- a/src/__/functions/slug.php +++ b/src/__/functions/slug.php @@ -2,6 +2,88 @@ namespace functions; +class _StringOps +{ + private static $hasMultibyteSupport; + private $needsMultibyteSupport; + + public function __construct($string) + { + $this->needsMultibyteSupport = preg_match('/[^\x00-\x7F]/', $string) === 1; + $this->assertMultibyte(); + } + + public function needsMultibyteSupport() + { + return $this->needsMultibyteSupport; + } + + /** + * @return string + */ + public function smart_substr() + { + $args = func_get_args(); + $fxn = $this->needsMultibyteSupport ? 'mb_substr' : 'substr'; + + if (!$this->needsMultibyteSupport) { + array_pop($args); + } + + return call_user_func_array($fxn, $args); + } + + /** + * @return int + */ + public function smart_strlen() + { + $args = func_get_args(); + + if ($this->needsMultibyteSupport) { + call_user_func_array('mb_strlen', $args); + } + + return strlen($args[0]); + } + + /** + * @return string + */ + public function smart_strtolower() + { + $args = func_get_args(); + + if ($this->needsMultibyteSupport) { + return call_user_func_array('mb_strtolower', $args); + } + + return strtolower($args[0]); + } + + private function assertMultibyte() + { + if (!$this->needsMultibyteSupport) { + return; + } + + if ($this->hasMultibyteSupport()) { + return; + } + + throw new \RuntimeException('The `mbstring` extension is not available and is required.'); + } + + private function hasMultibyteSupport() + { + if (self::$hasMultibyteSupport === null) { + self::$hasMultibyteSupport = extension_loaded('mbstring'); + } + + return self::$hasMultibyteSupport; + } +} + /** * Create a web friendly URL slug from a string. * @@ -33,8 +115,25 @@ */ function slug($str, array $options = []) { - // Make sure string is in UTF-8 and strip invalid UTF-8 characters - $str = \mb_convert_encoding((string)$str, 'UTF-8', \mb_list_encodings()); + if (!is_string($str)) { + throw new \InvalidArgumentException('The $str argument expends a string.'); + } + + $ops = new _StringOps($str); + + // Let's not waste resources if we don't need to do multibyte string processing + if ($ops->needsMultibyteSupport()) { + // Make sure string is in UTF-8 and strip invalid UTF-8 characters + /** @var false|string $encodedString */ + $encodedString = \mb_convert_encoding($str, 'UTF-8', \mb_list_encodings()); + + // PHP 8.1.0 has a bug where $encodedString can be an empty string, so + // we only want to override `$str` if we have a good value. + // https://github.com/php/php-src/issues/7898 + if ($encodedString !== '' && $encodedString !== false) { + $str = $encodedString; + } + } $defaults = [ 'delimiter' => '-', @@ -66,11 +165,11 @@ function slug($str, array $options = []) // Truncate slug to max. characters if ($options['limit']) { - $str = \mb_substr($str, 0, ($options['limit'] ?: \mb_strlen($str, 'UTF-8')), 'UTF-8'); + $str = $ops->smart_substr($str, 0, ($options['limit'] ?: $ops->smart_strlen($str, 'UTF-8')), 'UTF-8'); } // Remove delimiter from ends $str = \trim($str, $options['delimiter']); - return $options['lowercase'] ? \mb_strtolower($str, 'UTF-8') : $str; + return $options['lowercase'] ? $ops->smart_strtolower($str, 'UTF-8') : $str; } diff --git a/tests/__/Functions/SlugTest.php b/tests/__/Functions/SlugTest.php index 9509566..6e45aca 100644 --- a/tests/__/Functions/SlugTest.php +++ b/tests/__/Functions/SlugTest.php @@ -9,15 +9,19 @@ class SlugTest extends TestCase { - public function testSlug() + public function testSlugWithUtf8() { - // Arrange - $a = 'Jakieś zdanie z dużą ilością obcych znaków!'; + $input = 'Jakieś zdanie z dużą ilością obcych znaków!'; + $actual = __::slug($input); - // Act - $x = __::slug($a); + $this->assertEquals('jakies-zdanie-z-duza-iloscia-obcych-znakow', $actual); + } + + public function testSlugWithAscii() + { + $input = 'Hello World!'; + $actual = __::slug($input); - // Assert - $this->assertEquals('jakies-zdanie-z-duza-iloscia-obcych-znakow', $x); + $this->assertEquals('hello-world', $actual); } }