Repository: briannesbitt/Carbon (php)

Issue:
Unserialize from Carbon v2 objects
Related to #3002

Running with Carbon v2:
```php
echo serialize(Carbon::parse('-2 months')->diffAsCarbonInterval());
```

We obtain:
```
O:21:"Carbon\CarbonInterval":22:{s:1:"y";i:0;s:1:"m";i:2;s:1:"d";i:0;s:1:"h";i:0;s:1:"i";i:0;s:1:"s";i:0;s:1:"f";d:5.4E-5;s:6:"invert";i:0;s:4:"days";b:0;s:11:"from_string";b:0;s:9:" * tzName";N;s:7:" * step";N;s:22:" * localMonthsOverflow";N;s:21:" * localYearsOverflow";N;s:25:" * localStrictModeEnabled";N;s:24:" * localHumanDiffOptions";N;s:22:" * localToStringFormat";N;s:18:" * localSerializer";N;s:14:" * localMacros";N;s:21:" * localGenericMacros";N;s:22:" * localFormatFunction";N;s:18:" * localTranslator";N;}
```

Then when running in Carbon v3:
```php
unserialize($string); // $string being the serialized data above
```

It chokes on `tzName` (which has been renamed `timezoneSetting`)

Carbon version: **3.3.1**

PHP version: **8.3**

Pull request description:
Unserialize carbon interval created by v2
Fix #3015

Patch:
diff --git a/src/Carbon/CarbonInterval.php b/src/Carbon/CarbonInterval.php
index d09d04e3..2c642fee 100644
--- a/src/Carbon/CarbonInterval.php
+++ b/src/Carbon/CarbonInterval.php
@@ -1428,6 +1428,10 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
                     break;
 
                 default:
+                    if (str_starts_with($key, ' * ')) {
+                        return $this->setSetting(substr($key, 3), $value);
+                    }
+
                     if ($this->localStrictModeEnabled ?? Carbon::isStrictModeEnabled()) {
                         throw new UnknownSetterException($key);
                     }
@@ -3340,4 +3344,40 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
             $this->rawInterval = null;
         }
     }
+
+    /** @return $this */
+    private function setSetting(string $setting, mixed $value): self
+    {
+        switch ($setting) {
+            case 'timezoneSetting':
+                return $value === null ? $this : $this->setTimezone($value);
+
+            case 'step':
+                $this->setStep($value);
+
+                return $this;
+
+            case 'localMonthsOverflow':
+                return $value === null ? $this : $this->settings(['monthOverflow' => $value]);
+
+            case 'localYearsOverflow':
+                return $value === null ? $this : $this->settings(['yearOverflow' => $value]);
+
+            case 'localStrictModeEnabled':
+            case 'localHumanDiffOptions':
+            case 'localToStringFormat':
+            case 'localSerializer':
+            case 'localMacros':
+            case 'localGenericMacros':
+            case 'localFormatFunction':
+            case 'localTranslator':
+                $this->$setting = $value;
+
+                return $this;
+
+            default:
+                // Drop unknown settings
+                return $this;
+        }
+    }
 }


Test patch:
diff --git a/tests/Language/TranslatorTest.php b/tests/Language/TranslatorTest.php
index 308082c4..2a42c4fb 100644
--- a/tests/Language/TranslatorTest.php
+++ b/tests/Language/TranslatorTest.php
@@ -108,4 +108,13 @@ class TranslatorTest extends AbstractTestCase
 
         $this->assertEquals('zh', unserialize(serialize($translator))->getLocale());
     }
+
+    public function testUnserializeV2Object()
+    {
+        $interval = unserialize(<<<'EOS'
+            O:21:"Carbon\CarbonInterval":22:{s:1:"y";i:0;s:1:"m";i:2;s:1:"d";i:0;s:1:"h";i:0;s:1:"i";i:0;s:1:"s";i:0;s:1:"f";d:5.4E-5;s:6:"invert";i:0;s:4:"days";b:0;s:11:"from_string";b:0;s:9:" * tzName";N;s:7:" * step";N;s:22:" * localMonthsOverflow";N;s:21:" * localYearsOverflow";N;s:25:" * localStrictModeEnabled";N;s:24:" * localHumanDiffOptions";N;s:22:" * localToStringFormat";N;s:18:" * localSerializer";N;s:14:" * localMacros";N;s:21:" * localGenericMacros";N;s:22:" * localFormatFunction";N;s:18:" * localTranslator";N;}
+            EOS);
+
+        $this->assertCarbonInterval($interval, 0, 2, 0, 0, 0, 0, 54);
+    }
 }