Repository: briannesbitt/Carbon (php)

Issue:
Serialization of 'Carbon\Carbon@anonymous' is not allowed (Trait Mixin)
<!--
    🛑 DON'T REMOVE ME.
    This issue template applies to all
      - bug reports,
      - feature proposals,
      - and documentation requests

    Having all those informations will allow us to know exactly
    what you expect and answer you faster and precisely (answer
    that matches your Carbon version, PHP version and usage).
    
    Note: Comments between <!- - and - -> won't appear in the final
    issue (See [Preview] tab).
-->
Hello,

I encountered an issue with the following code:
<!--
    ⚠️ Your code below (not the code from inside Carbon library)
    Be sure it's dependency-free and enough to reproduce the issue
    when we run it on:
    https://play.phpsandbox.io/embed/nesbot/carbon
-->
```php
<?php

trait CarbonTimezoneTrait
{
    public function toUserTz(): Carbon
    {
        return $this->timezone('America/Chicago'); // from db, but you get the point
    }
}

Carbon::mixin(CarbonTimezoneTrait::class);

unserialize(
    serialize(
        Carbon::now()->toUserTz(),
    ),
);
```

Carbon version: **2.66.0**
<!--
    ⚠️ Run the command `composer show nesbot/carbon` to get "versions"
-->

PHP version: **8.1**

<!--
    Use `echo phpversion();`
    to get PHP version.

    Some issues can depends on your context, settings,
    macros, timezone, language. So to be sure the code
    you give is enough to reproduce your bug, try it
    first in:
    https://play.phpsandbox.io/embed/nesbot/carbon?input=%3C%3Fphp%0A%0Aecho%20Carbon%3A%3Aparse('2018-06-01')-%3EsubDay()-%3Emonth%3B

    You can use the [Options] button to change the version
    then when you get the bug with this editor, you can use
    the [Export] button, copy the link of the opened tab,
    then paste it in the issue. Then we can immediatly get
    your issue.
-->


I expected to get a carbon instance back, but I actually get:

```
Serialization of 'Carbon\Carbon@anonymous' is not allowed
```
<!--
    If you did not succeed to get the same result in
    https://play.phpsandbox.io/embed/nesbot/carbon?input=%3C%3Fphp%0A%0Aecho%20Carbon%3A%3Aparse('2018-06-01')%3B then precise the
    result you get.
-->

Note, that the following does work, so this appears to be an issue with the trait mixin:

```php
<?php

use Closure;

class CarbonTimezoneMixin
{
    public function toUserTz(): Closure
    {
        return static function () {
            return self::this()->timezone('America/Chicago');
        };
    }
}

Carbon::mixin(new CarbonTimezoneMixin());

unserialize(
    serialize(
        Carbon::now()->toUserTz(),
    ),
);
```
Thanks!


Pull request:
Restore type of mixin results using $this
Fix #2778

Patch:
diff --git a/phpstan.neon b/phpstan.neon
index 62c809ff..628fa91e 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -16,7 +16,9 @@ parameters:
         - '#^Call to an undefined static method#'
         - '#^Unsafe usage of new static\(\)\.$#'
         - '#^Method Carbon\\Carbon(Interface|Immutable)?::(add|sub)[A-Z][A-Za-z]+\(\) invoked with 1 parameter, 0 required\.$#'
-        - '#^Call to an undefined method Carbon\\Carbon(Interface|Immutable)?::(super|noThis)\(\)\.$#'
+        - '#^Call to an undefined method Carbon\\Carbon(Interface|Immutable)?::(super|noThis|toAppTz)\(\)\.$#'
+        - '#^Call to an undefined method Carbon\\CarbonInterval::andAgain\(\)\.$#'
+        - '#^Call to an undefined method Carbon\\CarbonPeriod::(oneMoreDay|endNextDay)\(\)\.$#'
         - '#^Call to an undefined method DateTimeInterface::setTimezone\(\)\.$#'
         - '#should return (\S*)(static|\$this)\(Carbon\\CarbonPeriod\) but returns Carbon\\CarbonPeriod\.$#'
         - '#should return (\S*)(static|\$this)\(Carbon\\Carbon\) but returns Carbon\\Carbon(Interface)?\.$#'
diff --git a/src/Carbon/Traits/Mixin.php b/src/Carbon/Traits/Mixin.php
index 4db010eb..f880a8e4 100644
--- a/src/Carbon/Traits/Mixin.php
+++ b/src/Carbon/Traits/Mixin.php
@@ -11,6 +11,9 @@
 
 namespace Carbon\Traits;
 
+use Carbon\CarbonInterface;
+use Carbon\CarbonInterval;
+use Carbon\CarbonPeriod;
 use Closure;
 use Generator;
 use ReflectionClass;
@@ -99,11 +102,13 @@ trait Mixin
     {
         $context = eval(self::getAnonymousClassCodeForTrait($trait));
         $className = \get_class($context);
+        $baseClass = static::class;
 
         foreach (self::getMixableMethods($context) as $name) {
             $closureBase = Closure::fromCallable([$context, $name]);
 
-            static::macro($name, function (...$parameters) use ($closureBase, $className) {
+            static::macro($name, function (...$parameters) use ($closureBase, $className, $baseClass) {
+                $downContext = isset($this) ? ($this) : new $baseClass();
                 $context = isset($this) ? $this->cast($className) : new $className();
 
                 try {
@@ -116,7 +121,36 @@ trait Mixin
                 // in case of errors not converted into exceptions
                 $closure = $closure ?: $closureBase;
 
-                return $closure(...$parameters);
+                $result = $closure(...$parameters);
+
+                if (!($result instanceof $className)) {
+                    return $result;
+                }
+
+                if ($downContext instanceof CarbonInterface && $result instanceof CarbonInterface) {
+                    return $downContext
+                        ->setTimezone($result->getTimezone())
+                        ->modify($result->format('Y-m-d H:i:s.u'))
+                        ->settings($result->getSettings());
+                }
+
+                if ($downContext instanceof CarbonInterval && $result instanceof CarbonInterval) {
+                    $downContext->copyProperties($result);
+                    self::copyStep($downContext, $result);
+                    self::copyNegativeUnits($downContext, $result);
+
+                    return $downContext->settings($result->getSettings());
+                }
+
+                if ($downContext instanceof CarbonPeriod && $result instanceof CarbonPeriod) {
+                    return $downContext
+                        ->setDates($result->getStartDate(), $result->getEndDate())
+                        ->setRecurrences($result->getRecurrences())
+                        ->setOptions($result->getOptions())
+                        ->settings($result->getSettings());
+                }
+
+                return $result;
             });
         }
     }


Test patch:
diff --git a/tests/Carbon/MacroTest.php b/tests/Carbon/MacroTest.php
index af100aab..6362fc8a 100644
--- a/tests/Carbon/MacroTest.php
+++ b/tests/Carbon/MacroTest.php
@@ -283,4 +283,20 @@ class MacroTest extends AbstractTestCaseWithOldNow
 
         $this->assertSame('Europe/Paris', $now->format('e'));
     }
+
+    public function testSerializationAfterTraitChaining()
+    {
+        require_once __DIR__.'/../Fixtures/CarbonTimezoneTrait.php';
+
+        Carbon::mixin(CarbonTimezoneTrait::class);
+        Carbon::setTestNow('2023-05-24 14:49');
+
+        $date = Carbon::toAppTz(false, 'Europe/Paris');
+
+        $this->assertSame('2023-05-24 16:49 Europe/Paris', unserialize(serialize($date))->format('Y-m-d H:i e'));
+
+        $date = Carbon::parse('2023-06-12 11:49')->toAppTz(false, 'Europe/Paris');
+
+        $this->assertSame('2023-06-12 13:49 Europe/Paris', unserialize(serialize($date))->format('Y-m-d H:i e'));
+    }
 }
diff --git a/tests/CarbonImmutable/MacroTest.php b/tests/CarbonImmutable/MacroTest.php
index 78fe56df..7e93efd2 100644
--- a/tests/CarbonImmutable/MacroTest.php
+++ b/tests/CarbonImmutable/MacroTest.php
@@ -151,4 +151,20 @@ class MacroTest extends AbstractTestCaseWithOldNow
 
         $this->assertSame('Europe/Paris', $now->format('e'));
     }
+
+    public function testSerializationAfterTraitChaining()
+    {
+        require_once __DIR__.'/../Fixtures/CarbonTimezoneTrait.php';
+
+        Carbon::mixin(CarbonTimezoneTrait::class);
+        Carbon::setTestNow('2023-05-24 14:49');
+
+        $date = Carbon::toAppTz(false, 'Europe/Paris');
+
+        $this->assertSame('2023-05-24 16:49 Europe/Paris', unserialize(serialize($date))->format('Y-m-d H:i e'));
+
+        $date = Carbon::parse('2023-06-12 11:49')->toAppTz(false, 'Europe/Paris');
+
+        $this->assertSame('2023-06-12 13:49 Europe/Paris', unserialize(serialize($date))->format('Y-m-d H:i e'));
+    }
 }
diff --git a/tests/CarbonInterval/Fixtures/MixinTrait.php b/tests/CarbonInterval/Fixtures/MixinTrait.php
new file mode 100644
index 00000000..968281d9
--- /dev/null
+++ b/tests/CarbonInterval/Fixtures/MixinTrait.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * This file is part of the Carbon package.
+ *
+ * (c) Brian Nesbitt <brian@nesbot.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Tests\CarbonInterval\Fixtures;
+
+trait MixinTrait
+{
+    public function andAgain()
+    {
+        return $this->times(2);
+    }
+}
diff --git a/tests/CarbonInterval/MacroTest.php b/tests/CarbonInterval/MacroTest.php
index d55df5f2..4b6077b7 100644
--- a/tests/CarbonInterval/MacroTest.php
+++ b/tests/CarbonInterval/MacroTest.php
@@ -16,6 +16,7 @@ namespace Tests\CarbonInterval;
 use Carbon\CarbonInterval;
 use Tests\AbstractTestCaseWithOldNow;
 use Tests\CarbonInterval\Fixtures\Mixin;
+use Tests\CarbonInterval\Fixtures\MixinTrait;
 
 class MacroTest extends AbstractTestCaseWithOldNow
 {
@@ -130,4 +131,16 @@ class MacroTest extends AbstractTestCaseWithOldNow
 
         $this->assertSame('6 hours', $interval->doMultiply()->forHumans());
     }
+
+    public function testMixinInstance()
+    {
+        include_once __DIR__.'/Fixtures/MixinTrait.php';
+        CarbonInterval::mixin(MixinTrait::class);
+
+        $input = CarbonInterval::days(2);
+        $result = $input->andAgain();
+
+        $this->assertSame('4 days', $result->forHumans());
+        $this->assertSame($input, $result);
+    }
 }
diff --git a/tests/CarbonPeriod/Fixtures/MacroableClass.php b/tests/CarbonPeriod/Fixtures/MacroableClass.php
new file mode 100644
index 00000000..a209d3f5
--- /dev/null
+++ b/tests/CarbonPeriod/Fixtures/MacroableClass.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * This file is part of the Carbon package.
+ *
+ * (c) Brian Nesbitt <brian@nesbot.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Tests\CarbonPeriod\Fixtures;
+
+use BadMethodCallException;
+use Carbon\Traits\Macro;
+use Closure;
+
+class MacroableClass
+{
+    use Macro;
+
+    public $endDate;
+
+    protected $localMacros = [];
+
+    public function getEndDate()
+    {
+        return $this->endDate;
+    }
+
+    public function setEndDate($endDate)
+    {
+        $this->endDate = $endDate;
+
+        return $this;
+    }
+
+    public function cast($className)
+    {
+        $new = new $className();
+
+        return $new->setEndDate($this->endDate);
+    }
+
+    public function __call($method, $parameters)
+    {
+        $macro = static::$globalMacros[$method] ?? null;
+
+        if ($macro instanceof Closure) {
+            return $macro->call($this);
+        }
+
+        throw new BadMethodCallException("$method not found");
+    }
+}
diff --git a/tests/CarbonPeriod/Fixtures/MixinTrait.php b/tests/CarbonPeriod/Fixtures/MixinTrait.php
new file mode 100644
index 00000000..797922af
--- /dev/null
+++ b/tests/CarbonPeriod/Fixtures/MixinTrait.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * This file is part of the Carbon package.
+ *
+ * (c) Brian Nesbitt <brian@nesbot.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Tests\CarbonPeriod\Fixtures;
+
+trait MixinTrait
+{
+    public function oneMoreDay()
+    {
+        return $this->setEndDate($this->endNextDay());
+    }
+
+    public function endNextDay()
+    {
+        return $this->getEndDate()->addDay();
+    }
+}
diff --git a/tests/CarbonPeriod/MacroTest.php b/tests/CarbonPeriod/MacroTest.php
index 17375d9d..df9f07e7 100644
--- a/tests/CarbonPeriod/MacroTest.php
+++ b/tests/CarbonPeriod/MacroTest.php
@@ -16,9 +16,12 @@ namespace Tests\CarbonPeriod;
 use BadMethodCallException;
 use Carbon\Carbon;
 use Carbon\CarbonPeriod;
+use Carbon\CarbonPeriodImmutable;
 use ReflectionClass;
 use Tests\AbstractTestCase;
+use Tests\CarbonPeriod\Fixtures\MacroableClass;
 use Tests\CarbonPeriod\Fixtures\Mixin;
+use Tests\CarbonPeriod\Fixtures\MixinTrait;
 
 class MacroTest extends AbstractTestCase
 {
@@ -205,4 +208,39 @@ class MacroTest extends AbstractTestCase
 
         $this->assertEquals(Carbon::tomorrow(), $period->getStartDate());
     }
+
+    public function testMixinInstance()
+    {
+        require_once __DIR__.'/Fixtures/MixinTrait.php';
+        require_once __DIR__.'/Fixtures/MacroableClass.php';
+
+        $periodClass = $this->periodClass;
+        $periodClass::mixin(MixinTrait::class);
+
+        $period = $periodClass::create('2023-06-10', '2023-06-12');
+        $result = $period->oneMoreDay();
+        $expectedEnd = $this->periodClass === CarbonPeriodImmutable::class
+            ? '2023-06-12'
+            : '2023-06-13';
+
+        $this->assertSame('Every 1 day from 2023-06-10 to 2023-06-13', (string) $result);
+        $this->assertSame("Every 1 day from 2023-06-10 to $expectedEnd", (string) $period);
+        $this->assertSame('2023-06-14', $result->endNextDay()->format('Y-m-d'));
+        $this->assertSame($this->periodClass === CarbonPeriodImmutable::class
+            ? '2023-06-13'
+            : '2023-06-14', $period->endNextDay()->format('Y-m-d'));
+
+        MacroableClass::mixin(MixinTrait::class);
+
+        $obj = new MacroableClass();
+        $result = $obj
+            ->setEndDate(Carbon::parse('2023-06-01'))
+            ->oneMoreDay();
+        $endDate = $result->getEndDate();
+
+        $this->assertInstanceOf(MacroableClass::class, $result);
+        $this->assertNotSame(MacroableClass::class, \get_class($result));
+        $this->assertSame(Carbon::class, \get_class($endDate));
+        $this->assertSame('2023-06-02', $endDate->format('Y-m-d'));
+    }
 }
diff --git a/tests/Fixtures/CarbonTimezoneTrait.php b/tests/Fixtures/CarbonTimezoneTrait.php
index 85779c39..99ea07e9 100644
--- a/tests/Fixtures/CarbonTimezoneTrait.php
+++ b/tests/Fixtures/CarbonTimezoneTrait.php
@@ -18,7 +18,7 @@ trait CarbonTimezoneTrait
     public function toAppTz(bool $shift = false, string $tz = 'UTC'): CarbonInterface
     {
         return $shift
-            ? self::this()->shiftTimezone($tz)
-            : self::this()->timezone($tz);
+            ? $this->shiftTimezone($tz)
+            : $this->timezone($tz);
     }
 }