framework icon indicating copy to clipboard operation
framework copied to clipboard

Cache::many(), Object __unserialize not working correctly with Redis and SERIALIZER_IGBINARY

Open SanderSander opened this issue 3 months ago • 0 comments

Laravel Version

12.26.4

PHP Version

8.4.8

Database Driver & Version

igbinary version => 3.2.16

Description

When using Redis as the cache store and enabling SERIALIZER_IGBINARY, unserializing nested objects from the cache leads to inconsistent behavior: the unserialize process returns the wrong objects when Cache::many() is called. In the example below, instead of receiving an instance of NestedClass, an instance of RootClass is returned.

This issue does not occur when the default serializer is used; it happens only when SERIALIZER_IGBINARY is active.

I'm not certain whether this is an issue with Redis, the igbinary extension, or Laravel's Redis driver.

Example Output (Works as Expected: Default Serializer)

php artisan test-cache
"loaded in unserialize" // routes/console.php:27
NestedClass^ {#806
  +name: "classC"
} // routes/console.php:27
"simple get" // routes/console.php:59
RootClass^ {#805
  +content: "base"
  +classC: NestedClass^ {#806
    +name: "classC"
  }
} // routes/console.php:59
"loaded in unserialize" // routes/console.php:27
NestedClass^ {#809
  +name: "classC"
} // routes/console.php:27
"loaded in unserialize" // routes/console.php:27
NestedClass^ {#811
  +name: "classC"
} // routes/console.php:27
"loaded in unserialize" // routes/console.php:27
NestedClass^ {#815
  +name: "classC"
} // routes/console.php:27
array:3 [
  "instance" => RootClass^ {#805
    +content: "base"
    +classC: NestedClass^ {#809
      +name: "classC"
    }
  }
  "instance-1" => RootClass^ {#808
    +content: "base"
    +classC: NestedClass^ {#811
      +name: "classC"
    }
  }
  "instance-2" => RootClass^ {#813
    +content: "base"
    +classC: NestedClass^ {#815
      +name: "classC"
    }
  }
] // routes/console.php:63

Example Output (Error with SERIALIZER_IGBINARY)

php artisan test-cache
"loaded in unserialize" // routes/console.php:27
NestedClass^ {#806
  +name: "classC"
} // routes/console.php:27
"simple get" // routes/console.php:59
RootClass^ {#805
  +content: "base"
  +classC: NestedClass^ {#806
    +name: "classC"
  }
} // routes/console.php:59
"loaded in unserialize" // routes/console.php:27
NestedClass^ {#807
  +name: "classC"
} // routes/console.php:27
"loaded in unserialize" // routes/console.php:27
RootClass^ {#813
  +content: "base"
  +classC: NestedClass^ {#807
    +name: "classC"
  }
} // routes/console.php:27

   TypeError 

  Cannot assign RootClass to property RootClass::$classC of type NestedClass

  at routes/console.php:28
     24▕     {
     25▕         $this->content = $data['content'];
     26▕         $classC = Cache::get('classC');
     27▕         dump('loaded in unserialize', $classC);
  ➜  28▕         $this->classC = $classC;
     29▕     }
     30▕ }
     31▕
     32▕ class NestedClass {

  1   [internal]:0
      RootClass::__unserialize()
      +7 vendor frames 

  9   routes/console.php:26
      Illuminate\Support\Facades\Facade::__callStatic()

Steps To Reproduce

class RootClass {

    public function __construct(
        public string      $content,
        public NestedClass $classC,
    ) { }

    public function __serialize(): array
    {
        return [
            'content' => $this->content,
        ];
    }

    public function __unserialize(array $data): void
    {
        $this->content = $data['content'];
        $classC = Cache::get('classC');
        dump('loaded in unserialize', $classC);
        $this->classC = $classC;
    }
}

class NestedClass {
    public function __construct(
        public string $name,
    ) {}

    public function __serialize(): array
    {
        return ['name' => $this->name];
    }

    public function __unserialize(array $data): void
    {
        $this->name = $data['name'];
    }
}


Artisan::command('test-cache', function () {
   $classC = new NestedClass('classC');
   Cache::put('classC', $classC);

   $instance = new RootClass('base', $classC);
   Cache::put('instance', $instance);
   Cache::put('instance-1', $instance);
   Cache::put('instance-2', $instance);

   // This works fine
   dump('simple get', Cache::get('instance-1'));

   // This works with the default serializer but not with SERIALIZER_IGBINARY
   $back = Cache::many(['instance', 'instance-1', 'instance-2']);
   dd($back);
});

SanderSander avatar Sep 01 '25 09:09 SanderSander