Repository: laravel/framework (php)

Issue:
Serialization fails for typed custom Eloquent Collections that are empty
- Laravel Version: 9.25.1
- PHP Version: 8.1.6
- Database Driver & Version: MySQL 8.0.27

### Description:

When queueing a job that has a **custom** Eloquent Collection as a typed property, and it holds an empty collection, (un)serializing fails with a Type Error, since it tries to assign the base Eloquent Collection to the property:

```php
class SomeJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public OrderCollection $orders;

    public function __construct(OrderCollection $orders)
    {
        $this->orders = $orders;
    }

    public function handle()
    {
        //
    }
}

$job = new SomeJob(new OrderCollection());

$serialized = serialize($job)

unserialize($serialized)

```

Result:

`TypeError : Cannot assign Illuminate\Database\Eloquent\Collection to property SomeJob::$orders of type App\Eloquent\OrderCollection`

### Steps To Reproduce:

I've added a failing test to prove the bug is there:
https://github.com/hxnk/framework/commit/ac6ed8ba058ae007c73e352c332fd4fb1070c1df

Pull request description:
[9.x] Fix empty collection class serialization
This fixes an issue where restoring a custom Eloquent collection wouldn't be of the same type. We should save the class type in the ModelIdentifier class to achieve this.

Fixes #43732


Patch:
diff --git a/src/Illuminate/Broadcasting/BroadcastManager.php b/src/Illuminate/Broadcasting/BroadcastManager.php
index 90f6823469..616f99c6dc 100644
--- a/src/Illuminate/Broadcasting/BroadcastManager.php
+++ b/src/Illuminate/Broadcasting/BroadcastManager.php
@@ -168,13 +168,19 @@ public function queue($event)
             $queue = $event->queue;
         }
 
-        if ($this->mustBeUniqueAndCannotAcquireLock($event)) {
-            return;
+        $broadcastEvent = new BroadcastEvent(clone $event);
+
+        if ($event instanceof ShouldBeUnique) {
+            $broadcastEvent = new UniqueBroadcastEvent(clone $event);
+
+            if ($this->mustBeUniqueAndCannotAcquireLock($broadcastEvent)) {
+                return;
+            }
         }
 
         $this->app->make('queue')
-                ->connection($event->connection ?? null)
-                ->pushOn($queue, new BroadcastEvent(clone $event));
+            ->connection($event->connection ?? null)
+            ->pushOn($queue, $broadcastEvent);
     }
 
     /**
@@ -185,10 +191,6 @@ public function queue($event)
      */
     protected function mustBeUniqueAndCannotAcquireLock($event)
     {
-        if (! $event instanceof ShouldBeUnique) {
-            return false;
-        }
-
         return ! (new UniqueLock(
             method_exists($event, 'uniqueVia')
                 ? $event->uniqueVia()
diff --git a/src/Illuminate/Broadcasting/UniqueBroadcastEvent.php b/src/Illuminate/Broadcasting/UniqueBroadcastEvent.php
new file mode 100644
index 0000000000..83c752df08
--- /dev/null
+++ b/src/Illuminate/Broadcasting/UniqueBroadcastEvent.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Illuminate\Broadcasting;
+
+use Illuminate\Container\Container;
+use Illuminate\Contracts\Cache\Repository;
+use Illuminate\Contracts\Queue\ShouldBeUnique;
+
+class UniqueBroadcastEvent extends BroadcastEvent implements ShouldBeUnique
+{
+    /**
+     * The unique lock identifier.
+     *
+     * @var mixed
+     */
+    public $uniqueId;
+
+    /**
+     * The number of seconds the unique lock should be maintained.
+     *
+     * @var int
+     */
+    public $uniqueFor;
+
+    /**
+     * Create a new event instance.
+     *
+     * @param  mixed  $event
+     * @return void
+     */
+    public function __construct($event)
+    {
+        $this->uniqueId = get_class($event);
+
+        if (method_exists($event, 'uniqueId')) {
+            $this->uniqueId .= $event->uniqueId();
+        } elseif (property_exists($event, 'uniqueId')) {
+            $this->uniqueId .= $event->uniqueId;
+        }
+
+        if (method_exists($event, 'uniqueFor')) {
+            $this->uniqueFor = $event->uniqueFor();
+        } elseif (property_exists($event, 'uniqueFor')) {
+            $this->uniqueFor = $event->uniqueFor;
+        }
+
+        parent::__construct($event);
+    }
+
+    /**
+     * Resolve the cache implementation that should manage the event's uniqueness.
+     *
+     * @return \Illuminate\Contracts\Cache\Repository
+     */
+    public function uniqueVia()
+    {
+        return method_exists($this->event, 'uniqueVia')
+                ? $this->event->uniqueVia()
+                : Container::getInstance()->make(Repository::class);
+    }
+}
diff --git a/src/Illuminate/Broadcasting/composer.json b/src/Illuminate/Broadcasting/composer.json
index f0736a37da..1f38b07af8 100644
--- a/src/Illuminate/Broadcasting/composer.json
+++ b/src/Illuminate/Broadcasting/composer.json
@@ -19,6 +19,7 @@
         "psr/log": "^1.0|^2.0|^3.0",
         "illuminate/bus": "^9.0",
         "illuminate/collections": "^9.0",
+        "illuminate/container": "^9.0",
         "illuminate/contracts": "^9.0",
         "illuminate/queue": "^9.0",
         "illuminate/support": "^9.0"
diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php
index c7f56f20ed..e6cf820592 100644
--- a/src/Illuminate/Collections/Enumerable.php
+++ b/src/Illuminate/Collections/Enumerable.php
@@ -982,7 +982,7 @@ public function sortDesc($options = SORT_REGULAR);
     /**
      * Sort the collection using the given callback.
      *
-     * @param  array<array-key, (callable(TValue, TKey): mixed)|array<array-key, string>|(callable(TValue, TKey): mixed)|string  $callback
+     * @param  array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}>|(callable(TValue, TKey): mixed)|string  $callback
      * @param  int  $options
      * @param  bool  $descending
      * @return static
@@ -992,7 +992,7 @@ public function sortBy($callback, $options = SORT_REGULAR, $descending = false);
     /**
      * Sort the collection in descending order using the given callback.
      *
-     * @param  array<array-key, (callable(TValue, TKey): mixed)|array<array-key, string>|(callable(TValue, TKey): mixed)|string  $callback
+     * @param  array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}>|(callable(TValue, TKey): mixed)|string  $callback
      * @param  int  $options
      * @return static
      */
diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php
index a337991d74..738989a266 100644
--- a/src/Illuminate/Collections/LazyCollection.php
+++ b/src/Illuminate/Collections/LazyCollection.php
@@ -1296,7 +1296,7 @@ public function sortDesc($options = SORT_REGULAR)
     /**
      * Sort the collection using the given callback.
      *
-     * @param  array<array-key, (callable(TValue, TKey): mixed)|array<array-key, string>|(callable(TValue, TKey): mixed)|string  $callback
+     * @param  array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}>|(callable(TValue, TKey): mixed)|string  $callback
      * @param  int  $options
      * @param  bool  $descending
      * @return static
@@ -1309,7 +1309,7 @@ public function sortBy($callback, $options = SORT_REGULAR, $descending = false)
     /**
      * Sort the collection in descending order using the given callback.
      *
-     * @param  array<array-key, (callable(TValue, TKey): mixed)|array<array-key, string>|(callable(TValue, TKey): mixed)|string  $callback
+     * @param  array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}>|(callable(TValue, TKey): mixed)|string  $callback
      * @param  int  $options
      * @return static
      */
diff --git a/src/Illuminate/Contracts/Database/ModelIdentifier.php b/src/Illuminate/Contracts/Database/ModelIdentifier.php
index 9893d280ef..aacd18c079 100644
--- a/src/Illuminate/Contracts/Database/ModelIdentifier.php
+++ b/src/Illuminate/Contracts/Database/ModelIdentifier.php
@@ -34,6 +34,13 @@ class ModelIdentifier
      */
     public $connection;
 
+    /**
+     * The class name of the model collection.
+     *
+     * @var string|null
+     */
+    public $collectionClass;
+
     /**
      * Create a new model identifier.
      *
@@ -50,4 +57,17 @@ public function __construct($class, $id, array $relations, $connection)
         $this->relations = $relations;
         $this->connection = $connection;
     }
+
+    /**
+     * Specify the collection class that should be used when serializing / restoring collections.
+     *
+     * @param  string|null  $collectionClass
+     * @return $this
+     */
+    public function useCollectionClass(?string $collectionClass)
+    {
+        $this->collectionClass = $collectionClass;
+
+        return $this;
+    }
 }
diff --git a/src/Illuminate/Database/Connectors/SqlServerConnector.php b/src/Illuminate/Database/Connectors/SqlServerConnector.php
index caefa68469..c9fbbe7331 100755
--- a/src/Illuminate/Database/Connectors/SqlServerConnector.php
+++ b/src/Illuminate/Database/Connectors/SqlServerConnector.php
@@ -160,6 +160,10 @@ protected function getSqlSrvDsn(array $config)
             $arguments['LoginTimeout'] = $config['login_timeout'];
         }
 
+        if (isset($config['authentication'])) {
+            $arguments['Authentication'] = $config['authentication'];
+        }
+
         return $this->buildConnectString('sqlsrv', $arguments);
     }
 
diff --git a/src/Illuminate/Http/Concerns/InteractsWithInput.php b/src/Illuminate/Http/Concerns/InteractsWithInput.php
index f5673db504..c420aad7fc 100644
--- a/src/Illuminate/Http/Concerns/InteractsWithInput.php
+++ b/src/Illuminate/Http/Concerns/InteractsWithInput.php
@@ -562,7 +562,7 @@ protected function retrieveItem($source, $key, $default)
      * Dump the request items and end the script.
      *
      * @param  mixed  $keys
-     * @return void
+     * @return never
      */
     public function dd(...$keys)
     {
diff --git a/src/Illuminate/Queue/Jobs/Job.php b/src/Illuminate/Queue/Jobs/Job.php
index 373d48f3c1..d5464946b7 100755
--- a/src/Illuminate/Queue/Jobs/Job.php
+++ b/src/Illuminate/Queue/Jobs/Job.php
@@ -302,7 +302,7 @@ public function timeout()
      */
     public function retryUntil()
     {
-        return $this->payload()['retryUntil'] ?? $this->payload()['timeoutAt'] ?? null;
+        return $this->payload()['retryUntil'] ?? null;
     }
 
     /**
diff --git a/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php b/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php
index bf10754f9d..748c34590e 100644
--- a/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php
+++ b/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php
@@ -20,11 +20,15 @@ trait SerializesAndRestoresModelIdentifiers
     protected function getSerializedPropertyValue($value)
     {
         if ($value instanceof QueueableCollection) {
-            return new ModelIdentifier(
+            return (new ModelIdentifier(
                 $value->getQueueableClass(),
                 $value->getQueueableIds(),
                 $value->getQueueableRelations(),
                 $value->getQueueableConnection()
+            ))->useCollectionClass(
+                ($collectionClass = get_class($value)) !== EloquentCollection::class
+                    ? $collectionClass
+                    : null
             );
         }
 
@@ -66,7 +70,9 @@ protected function getRestoredPropertyValue($value)
     protected function restoreCollection($value)
     {
         if (! $value->class || count($value->id) === 0) {
-            return new EloquentCollection;
+            return ! is_null($value->collectionClass ?? null)
+                ? new $value->collectionClass
+                : new EloquentCollection;
         }
 
         $collection = $this->getQueryForModelRestoration(
diff --git a/src/Illuminate/Support/Facades/Vite.php b/src/Illuminate/Support/Facades/Vite.php
index 25afa06374..052beee9b1 100644
--- a/src/Illuminate/Support/Facades/Vite.php
+++ b/src/Illuminate/Support/Facades/Vite.php
@@ -6,9 +6,9 @@
  * @method static string useCspNonce(?string $nonce = null)
  * @method static string|null cspNonce()
  * @method static string asset(string $asset, string|null $buildDirectory)
- * @method static \Illuminte\Foundation\Vite useIntegrityKey(string|false $key)
- * @method static \Illuminte\Foundation\Vite useScriptTagAttributes(callable|array $callback)
- * @method static \Illuminte\Foundation\Vite useStyleTagAttributes(callable|array $callback)
+ * @method static \Illuminate\Foundation\Vite useIntegrityKey(string|false $key)
+ * @method static \Illuminate\Foundation\Vite useScriptTagAttributes(callable|array $callback)
+ * @method static \Illuminate\Foundation\Vite useStyleTagAttributes(callable|array $callback)
  *
  * @see \Illuminate\Foundation\Vite
  */


Test patch:
diff --git a/src/Illuminate/Testing/Fluent/Concerns/Debugging.php b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php
index f51d119074..e655556cf9 100644
--- a/src/Illuminate/Testing/Fluent/Concerns/Debugging.php
+++ b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php
@@ -21,7 +21,7 @@ public function dump(string $prop = null): self
      * Dumps the given props and exits.
      *
      * @param  string|null  $prop
-     * @return void
+     * @return never
      */
     public function dd(string $prop = null): void
     {
diff --git a/tests/Integration/Broadcasting/BroadcastManagerTest.php b/tests/Integration/Broadcasting/BroadcastManagerTest.php
index 30297ebe61..8ab47fb655 100644
--- a/tests/Integration/Broadcasting/BroadcastManagerTest.php
+++ b/tests/Integration/Broadcasting/BroadcastManagerTest.php
@@ -3,6 +3,7 @@
 namespace Illuminate\Tests\Integration\Broadcasting;
 
 use Illuminate\Broadcasting\BroadcastEvent;
+use Illuminate\Broadcasting\UniqueBroadcastEvent;
 use Illuminate\Contracts\Broadcasting\ShouldBeUnique;
 use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
 use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
@@ -43,10 +44,10 @@ public function testUniqueEventsCanBeBroadcast()
 
         Broadcast::queue(new TestEventUnique);
 
-        Bus::assertNotDispatched(BroadcastEvent::class);
-        Queue::assertPushed(BroadcastEvent::class);
+        Bus::assertNotDispatched(UniqueBroadcastEvent::class);
+        Queue::assertPushed(UniqueBroadcastEvent::class);
 
-        $lockKey = 'laravel_unique_job:'.TestEventUnique::class;
+        $lockKey = 'laravel_unique_job:'.UniqueBroadcastEvent::class.TestEventUnique::class;
         $this->assertFalse($this->app->get(Cache::class)->lock($lockKey, 10)->get());
     }
 }
diff --git a/tests/Integration/Queue/ModelSerializationTest.php b/tests/Integration/Queue/ModelSerializationTest.php
index 30b45af36d..e0d83da081 100644
--- a/tests/Integration/Queue/ModelSerializationTest.php
+++ b/tests/Integration/Queue/ModelSerializationTest.php
@@ -314,9 +314,21 @@ public function test_model_serialization_structure()
         $serialized = serialize(new ModelSerializationParentAccessibleTestClass($user, $user, $user));
 
         $this->assertSame(
-            'O:78:"Illuminate\\Tests\\Integration\\Queue\\ModelSerializationParentAccessibleTestClass":2:{s:4:"user";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":4:{s:5:"class";s:61:"Illuminate\\Tests\\Integration\\Queue\\ModelSerializationTestUser";s:2:"id";i:1;s:9:"relations";a:0:{}s:10:"connection";s:7:"testing";}s:8:"'."\0".'*'."\0".'user2";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":4:{s:5:"class";s:61:"Illuminate\\Tests\\Integration\\Queue\\ModelSerializationTestUser";s:2:"id";i:1;s:9:"relations";a:0:{}s:10:"connection";s:7:"testing";}}', $serialized
+            'O:78:"Illuminate\\Tests\\Integration\\Queue\\ModelSerializationParentAccessibleTestClass":2:{s:4:"user";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":5:{s:5:"class";s:61:"Illuminate\\Tests\\Integration\\Queue\\ModelSerializationTestUser";s:2:"id";i:1;s:9:"relations";a:0:{}s:10:"connection";s:7:"testing";s:15:"collectionClass";N;}s:8:"'."\0".'*'."\0".'user2";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":5:{s:5:"class";s:61:"Illuminate\\Tests\\Integration\\Queue\\ModelSerializationTestUser";s:2:"id";i:1;s:9:"relations";a:0:{}s:10:"connection";s:7:"testing";s:15:"collectionClass";N;}}', $serialized
         );
     }
+
+    public function test_serialization_types_empty_custom_eloquent_collection()
+    {
+        $class = new ModelSerializationTypedCustomCollectionTestClass(
+            new ModelSerializationTestCustomUserCollection());
+
+        $serialized = serialize($class);
+
+        unserialize($serialized);
+
+        $this->assertTrue(true);
+    }
 }
 
 trait TraitBootsAndInitializersTest
@@ -352,6 +364,18 @@ class ModelSerializationTestCustomUserCollection extends Collection
     //
 }
 
+class ModelSerializationTypedCustomCollectionTestClass
+{
+    use SerializesModels;
+
+    public ModelSerializationTestCustomUserCollection $collection;
+
+    public function __construct(ModelSerializationTestCustomUserCollection $collection)
+    {
+        $this->collection = $collection;
+    }
+}
+
 class ModelSerializationTestCustomUser extends Model
 {
     public $table = 'users';