From 33300cfd7cae90c120de08b972625bd4b6ce52ca Mon Sep 17 00:00:00 2001
From: Vladimir Jimenez <allejo@me.com>
Date: Sat, 8 Jan 2022 22:58:07 -0800
Subject: [PATCH 1/2] slug(): partial optimizations + fix PHP 8.1

---
 src/__/functions/slug.php       | 107 ++++++++++++++++++++++++++++++--
 tests/__/Functions/SlugTest.php |  18 +++---
 2 files changed, 114 insertions(+), 11 deletions(-)

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);
     }
 }

From c299a185e8774a0d89b4f72b3395ddae438e66c8 Mon Sep 17 00:00:00 2001
From: Vladimir Jimenez <allejo@me.com>
Date: Sat, 8 Jan 2022 23:22:18 -0800
Subject: [PATCH 2/2] Update CHANGELOG with __::slug() updates

---
 CHANGELOG.md | 8 ++++++++
 1 file changed, 8 insertions(+)

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/)**